Merge remote-tracking branch 'origin/main'

This commit is contained in:
2025-04-25 14:07:16 +08:00
145 changed files with 5541 additions and 38 deletions

1
.obsidian/app.json vendored
View File

@ -9,6 +9,7 @@
"alwaysUpdateLinks": true,
"showUnsupportedFiles": true,
"pdfExportSettings": {
"includeName": true,
"pageSize": "Letter",
"landscape": false,
"margin": "0",

View File

@ -6,5 +6,6 @@
"file-explorer-note-count",
"obsidian-git",
"better-export-pdf",
"hidden-folder-obsidian"
"hidden-folder-obsidian",
"automatic-table-of-contents"
]

View File

@ -39,6 +39,6 @@
"repelStrength": 10,
"linkStrength": 1,
"linkDistance": 250,
"scale": 0.7132754626224443,
"scale": 0.5087618855792604,
"close": true
}

View File

@ -136,5 +136,14 @@
],
"key": "["
}
],
"automatic-table-of-contents:insert-automatic-table-of-contents-docs": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "D"
}
]
}

View File

@ -0,0 +1,17 @@
---
name: Bug report
about: Create a report to help improve the plugin
title: ''
labels: bug
assignees: johansatge
---
_Hello! Please follow the checklist below when filling a bug report_
- [ ] Check in the readme if the bug is linked to a documented limitation
- [ ] Describe what the bug is
- [ ] Check if the bug can be reproduced in the _Obsidian Sandbox_ vault, or with other plugins disabled
- [ ] Paste a Markdown block that helps reproducing the described issue
- [ ] Upload a screenshot with the rendered result if relevant
- [ ] Describe the environment (OS, Obsidian version, installed plugins)

View File

@ -0,0 +1,15 @@
---
name: Feature request
about: Suggest an idea for this plugin
title: ''
labels: enhancement
assignees: johansatge
---
_Hello! Please follow the checklist below when filling a feature request_
- [ ] Check in the readme if your feature request is linked to a documented limitation
- [ ] Is your feature request related to a problem?
- [ ] Describe the solution you'd like
- [ ] Provide a Markdown block that helps describing your request, if relevant

View File

@ -0,0 +1,27 @@
name: test
on: push
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 1
strategy:
matrix:
node-version: [18.x]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v3
with:
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
path: node_modules
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: npm ci
- name: Run tests
run: npm test

View File

@ -0,0 +1,2 @@
# https://github.com/pjeby/hot-reload
.hotreload

View File

@ -0,0 +1,21 @@
# Contributing
First off, thanks for taking the time to contribute! :tada:
## Bugs and questions
Report bugs, ask questions, and request features using [GitHub Issues](https://github.com/johansatge/obsidian-automatic-table-of-contents/issues).
When submitting a bug report, do not hesitate to be as exhaustive as possible, by adding:
* A quick summary of the bug
* The expected and actual behavior
* The platform you are using (operating system, Obsidian version...)
* A Markdown sample
* Any other information that you think would be useful
## Working on the project
When contributing to this repository, please create an issue first, so the change you wish to make can be discussed with the other maintainers.
When you submit code changes, your submissions are understood to be under the same license that covers the project. Feel free to contact the maintainers if that's a concern.

View File

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright © 2023 Johan Satgé
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,118 @@
# Obsidian Automatic Table Of Contents
[![Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22automatic-table-of-contents%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json)](https://obsidian.md/plugins?search=automatic%20table%20of%20contents)
[![Version](https://img.shields.io/github/v/release/johansatge/obsidian-automatic-table-of-contents)](https://github.com/johansatge/obsidian-automatic-table-of-contents/releases)
[![Test](https://github.com/johansatge/obsidian-automatic-table-of-contents/actions/workflows/test.yml/badge.svg)](https://github.com/johansatge/obsidian-automatic-table-of-contents/actions)
> An Obsidian plugin to create a table of contents in a note, that updates itself when the note changes
---
![demo](images/demo.gif)
- [Installation](#installation)
- [From Obsidian (easiest)](#from-obsidian-easiest)
- [From git](#from-git)
- [From source](#from-source)
- [Usage and options](#usage-and-options)
- [Limitations and known bugs](#limitations-and-known-bugs)
- [Publish a new version](#publish-a-new-version)
- [Changelog](#changelog)
- [License](#license)
- [Contributing](#contributing)
## Installation
### From Obsidian (easiest)
Install the plugin from the [Community plugins](https://obsidian.md/plugins?search=automatic%20table%20of%20contents) section in the app settings.
### From git
Clone the plugin in your `.obsidian/plugins` directory:
```shell
cd /path/to/your/vault/.obsidian/plugins
git clone git@github.com:johansatge/obsidian-automatic-table-of-contents.git
```
### From source
Download the [latest release](https://github.com/johansatge/obsidian-automatic-table-of-contents/releases) and unzip it in the `.obsidian/plugins/automatic-table-of-contents` directory.
## Usage and options
Insert a codeblock with the `table-of-contents` (or its short version `toc`) syntax.
````
```table-of-contents
option1: value1
option2: value2
```
````
Alternatively, two commands are available in the command palette:
- Insert table of contents
- Insert table of contents (with available options)
The following options are available:
| Option | Default value | Description |
| --- | --- | --- |
| `title` | _None_ | Title to display before the table of contents (supports Markdown) |
| `style` | `nestedList` | Table of contents style (can be `nestedList`, `nestedOrderedList` or `inlineFirstLevel`) |
| `minLevel` | `0` | Include headings from the specified level (`0` for no limit) |
| `maxLevel` | `0` | Include headings up to the specified level (`0` for no limit) |
| `includeLinks` | `true` | Make headings clickable |
| `debugInConsole` | `false` | Print debug info in Obsidian console |
## Limitations and known bugs
- The table of contents may not be generated correctly if the document doesn't implement a correct titles hierarchy (level 1 title then level 3 without a level 2 in between, for instance)
- HTML & markdown that may be in the document headings are stripped when `includeLinks: true` (see [#24](https://github.com/johansatge/obsidian-automatic-table-of-contents/issues/24) & [#27](https://github.com/johansatge/obsidian-automatic-table-of-contents/issues/27))
- LaTeX equations are not rendered correctly when `includeLinks: true` (see [#13](https://github.com/johansatge/obsidian-automatic-table-of-contents/issues/13))
- Links might not behave correctly if the same title is present several times on the page (see [#57](https://github.com/johansatge/obsidian-automatic-table-of-contents/issues/57))
## Publish a new version
- Push a commit with the new version number as message with:
- The relevant changelog in `README.md`
- The new version number in `manifest.json`
- Tag the commit with the version number
- Publish a [new GitHub release](https://github.com/johansatge/obsidian-automatic-table-of-contents/releases/new) with:
- The version number as title
- The changelog from `README.md` as description
- `main.js` and `manifest.json` as attachments
- _Set as the latest release_ checked
## Changelog
This project uses [semver](http://semver.org/).
| Version | Date | Notes |
| --- | --- | --- |
| `1.5.1` | 2025-01-18 | Rename command for readability (fix #53) |
| `1.5.0` | 2024-11-24 | Add `hideWhenEmpty` option (fix #51) |
| `1.4.0` | 2024-05-19 | Add `nestedOrderedList` style ([@bjtho08](https://github.com/bjtho08)) (fix #41) |
| `1.3.3` | 2024-05-16 | Compute the right min level when `style:inlineFirstLevel` (fix #39) |
| `1.3.2` | 2024-02-18 | Harden headings stripping |
| `1.3.1` | 2024-02-18 | Strip headings for readability (fix #24, #27) |
| `1.3.0` | 2024-02-17 | Introduce `title` option (fix #5, #32) |
| `1.2.0` | 2024-01-19 | Introduce `toc` shorthand trigger (fix #19) |
| `1.1.0` | 2024-01-03 | Introduce `minLevel` option ([@ras0q](https://github.com/ras0q)) (fix #11) |
| `1.0.6` | 2023-11-02 | Escape special characters (fix #3) |
| `1.0.5` | 2023-11-01 | Fix plugin activation on mobile (fix #17) |
| `1.0.4` | 2023-10-31 | Support pages with no first level headings (fix #6) |
| `1.0.3` | 2023-09-30 | Fix readme |
| `1.0.2` | 2023-09-25 | Fix output sometimes displaying `undefined` headings |
| `1.0.1` | 2023-09-09 | Fix reference to global `App` instance |
| `1.0.0` | 2023-08-27 | Initial version |
## License
This project is released under the [MIT License](LICENSE).
## Contributing
Bug reports and feature requests are welcome! More details in the [contribution guidelines](CONTRIBUTING.md).

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -0,0 +1,301 @@
let Plugin = class {}
let MarkdownRenderer = {}
let MarkdownRenderChild = class {}
let htmlToMarkdown = (html) => html
if (isObsidian()) {
const obsidian = require('obsidian')
Plugin = obsidian.Plugin
MarkdownRenderer = obsidian.MarkdownRenderer
MarkdownRenderChild = obsidian.MarkdownRenderChild
htmlToMarkdown = obsidian.htmlToMarkdown
}
const codeblockId = 'table-of-contents'
const codeblockIdShort = 'toc'
const availableOptions = {
title: {
type: 'string',
default: '',
comment: '',
},
style: {
type: 'value',
default: 'nestedList',
values: ['nestedList', 'nestedOrderedList', 'inlineFirstLevel'],
comment: 'TOC style (nestedList|nestedOrderedList|inlineFirstLevel)',
},
minLevel: {
type: 'number',
default: 0,
comment: 'Include headings from the specified level',
},
maxLevel: {
type: 'number',
default: 0,
comment: 'Include headings up to the specified level',
},
includeLinks: {
type: 'boolean',
default: true,
comment: 'Make headings clickable',
},
hideWhenEmpty: {
type: 'boolean',
default: false,
comment: 'Hide TOC if no headings are found'
},
debugInConsole: {
type: 'boolean',
default: false,
comment: 'Print debug info in Obsidian console',
},
}
class ObsidianAutomaticTableOfContents extends Plugin {
async onload() {
const handler = (sourceText, element, context) => {
context.addChild(new Renderer(this.app, element, context.sourcePath, sourceText))
}
this.registerMarkdownCodeBlockProcessor(codeblockId, handler)
this.registerMarkdownCodeBlockProcessor(codeblockIdShort, handler)
this.addCommand({
id: 'insert-automatic-table-of-contents',
name: 'Insert table of contents',
editorCallback: onInsertToc,
})
this.addCommand({
id: 'insert-automatic-table-of-contents-docs',
name: 'Insert table of contents (with available options)',
editorCallback: onInsertTocWithDocs,
})
}
}
function onInsertToc(editor) {
const markdown = '```' + codeblockId + '\n```'
editor.replaceRange(markdown, editor.getCursor())
}
function onInsertTocWithDocs(editor) {
let markdown = ['```' + codeblockId]
Object.keys(availableOptions).forEach((optionName) => {
const option = availableOptions[optionName]
const comment = option.comment.length > 0 ? ` # ${option.comment}` : ''
markdown.push(`${optionName}: ${option.default}${comment}`)
})
markdown.push('```')
editor.replaceRange(markdown.join('\n'), editor.getCursor())
}
class Renderer extends MarkdownRenderChild {
constructor(app, element, sourcePath, sourceText) {
super(element)
this.app = app
this.element = element
this.sourcePath = sourcePath
this.sourceText = sourceText
}
// Render on load
onload() {
this.render()
this.registerEvent(this.app.metadataCache.on('changed', this.onMetadataChange.bind(this)))
}
// Render on file change
onMetadataChange() {
this.render()
}
render() {
try {
const options = parseOptionsFromSourceText(this.sourceText)
if (options.debugInConsole) debug('Options', options)
const metadata = this.app.metadataCache.getCache(this.sourcePath)
const headings = metadata && metadata.headings ? metadata.headings : []
if (options.debugInConsole) debug('Headings', headings)
const markdown = getMarkdownFromHeadings(headings, options)
if (options.debugInConsole) debug('Markdown', markdown)
this.element.empty()
MarkdownRenderer.renderMarkdown(markdown, this.element, this.sourcePath, this)
} catch(error) {
const readableError = `_💥 Could not render table of contents (${error.message})_`
MarkdownRenderer.renderMarkdown(readableError, this.element, this.sourcePath, this)
}
}
}
function getMarkdownFromHeadings(headings, options) {
const markdownHandlersByStyle = {
nestedList: getMarkdownNestedListFromHeadings,
nestedOrderedList: getMarkdownNestedOrderedListFromHeadings,
inlineFirstLevel: getMarkdownInlineFirstLevelFromHeadings,
}
let titleMarkdown = ''
if (options.title && options.title.length > 0) {
titleMarkdown += options.title + '\n'
}
const markdownHeadings = markdownHandlersByStyle[options.style](headings, options)
if (markdownHeadings === null) {
if (options.hideWhenEmpty) {
return ''
}
return titleMarkdown + '_Table of contents: no headings found_'
}
return titleMarkdown + markdownHeadings
}
function getMarkdownNestedListFromHeadings(headings, options) {
return getMarkdownListFromHeadings(headings, false, options)
}
function getMarkdownNestedOrderedListFromHeadings(headings, options) {
return getMarkdownListFromHeadings(headings, true, options)
}
function getMarkdownListFromHeadings(headings, isOrdered, options) {
const prefix = isOrdered ? '1.' : '-'
const lines = []
const minLevel = options.minLevel > 0
? options.minLevel
: Math.min(...headings.map((heading) => heading.level))
headings.forEach((heading) => {
if (heading.level < minLevel) return
if (options.maxLevel > 0 && heading.level > options.maxLevel) return
lines.push(`${'\t'.repeat(heading.level - minLevel)}${prefix} ${getMarkdownHeading(heading, options)}`)
})
return lines.length > 0 ? lines.join('\n') : null
}
function getMarkdownInlineFirstLevelFromHeadings(headings, options) {
const minLevel = options.minLevel > 0
? options.minLevel
: Math.min(...headings.map((heading) => heading.level))
const items = headings
.filter((heading) => heading.level === minLevel)
.map((heading) => {
return getMarkdownHeading(heading, options)
})
return items.length > 0 ? items.join(' | ') : null
}
function getMarkdownHeading(heading, options) {
const stripMarkdown = (text) => {
text = text.replaceAll('*', '').replaceAll('_', '').replaceAll('`', '')
text = text.replaceAll('==', '').replaceAll('~~', '')
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Strip markdown links
return text
}
const stripHtml = (text) => stripMarkdown(htmlToMarkdown(text))
const stripWikilinks = (text, isForLink) => {
// Strip [[link|text]] format
// For the text part of the final link we only keep "text"
// For the link part we need the text + link
// Example: "# Some [[file.md|heading]]" must be translated to "[[#Some file.md heading|Some heading]]"
text = text.replace(/\[\[([^\]]+)\|([^\]]+)\]\]/g, isForLink ? '$1 $2' : '$2')
text = text.replace(/\[\[([^\]]+)\]\]/g, '$1') // Strip [[link]] format
// Replace malformed links & reserved wikilinks chars
text = text.replaceAll('[[', '').replaceAll('| ', isForLink ? '' : '- ').replaceAll('|', isForLink ? ' ' : '-')
return text
}
const stripTags = (text) => text.replaceAll('#', '')
if (options.includeLinks) {
// Remove markdown, HTML & wikilinks from text for readability, as they are not rendered in a wikilink
let text = heading.heading
text = stripMarkdown(text)
text = stripHtml(text)
text = stripWikilinks(text, false)
// Remove wikilinks & tags from link or it won't be clickable (on the other hand HTML & markdown must stay)
let link = heading.heading
link = stripWikilinks(link, true)
link = stripTags(link)
// Return wiklink style link
return `[[#${link}|${text}]]`
// Why not markdown links? Because even if it looks like the text part would have a better compatibility
// with complex headings (as it would support HTML, markdown, etc) the link part is messy,
// because it requires some encoding that looks buggy and undocumented; official docs state the link must be URL encoded
// (https://help.obsidian.md/Linking+notes+and+files/Internal+links#Supported+formats+for+internal+links)
// but it doesn't work properly, example: "## Some <em>heading</em> with simple HTML" must be encoded as:
// [Some <em>heading</em> with simple HTML](#Some%20<em>heading</em>%20with%20simpler%20HTML)
// and not
// [Some <em>heading</em> with simple HTML](#Some%20%3Cem%3Eheading%3C%2Fem%3E%20with%20simpler%20HTML)
// Also it won't be clickable at all if the heading contains #tags or more complex HTML
// (example: ## Some <em style="background: red">heading</em> #with-a-tag)
// (unless there is a way to encode these use cases that I didn't find)
}
return heading.heading
}
function parseOptionsFromSourceText(sourceText = '') {
const options = {}
Object.keys(availableOptions).forEach((option) => {
options[option] = availableOptions[option].default
})
sourceText.split('\n').forEach((line) => {
const option = parseOptionFromSourceLine(line)
if (option !== null) {
options[option.name] = option.value
}
})
return options
}
function parseOptionFromSourceLine(line) {
const matches = line.match(/([a-zA-Z0-9._ ]+):(.*)/)
if (line.startsWith('#') || !matches) return null
const possibleName = matches[1].trim()
const optionParams = availableOptions[possibleName]
let possibleValue = matches[2].trim()
if (!optionParams || optionParams.type !== 'string') {
// Strip comments from values except for strings (as a string may contain markdown)
possibleValue = possibleValue.replace(/#[^#]*$/, '').trim()
}
const valueError = new Error(`Invalid value for \`${possibleName}\``)
if (optionParams && optionParams.type === 'number') {
const value = parseInt(possibleValue)
if (value < 0) throw valueError
return { name: possibleName, value }
}
if (optionParams && optionParams.type === 'boolean') {
if (!['true', 'false'].includes(possibleValue)) throw valueError
return { name: possibleName, value: possibleValue === 'true' }
}
if (optionParams && optionParams.type === 'value') {
if (!optionParams.values.includes(possibleValue)) throw valueError
return { name: possibleName, value: possibleValue }
}
if (optionParams && optionParams.type === 'string') {
return { name: possibleName, value: possibleValue }
}
return null
}
function debug(type, data) {
console.log(...[
`%cAutomatic Table Of Contents %c${type}:\n`,
'color: orange; font-weight: bold',
'font-weight: bold',
data,
])
}
function isObsidian() {
if (typeof process !== 'object') {
return true // Obsidian mobile doesn't have a global process object
}
return !process.env || !process.env.JEST_WORKER_ID // Jest runtime is not Obsidian
}
if (isObsidian()) {
module.exports = ObsidianAutomaticTableOfContents
} else {
module.exports = {
parseOptionsFromSourceText,
getMarkdownFromHeadings,
}
}

View File

@ -0,0 +1,10 @@
{
"id": "automatic-table-of-contents",
"name": "Automatic Table Of Contents",
"version": "1.5.1",
"minAppVersion": "1.3.0",
"description": "Create a table of contents in a note, that updates itself when the note changes",
"author": "Johan Satgé",
"authorUrl": "https://github.com/johansatge",
"isDesktopOnly": false
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
{
"private": true,
"scripts": {
"test": "jest --config=test/jest.config.js"
},
"devDependencies": {
"jest": "^29.7.0"
}
}

View File

@ -0,0 +1,218 @@
const {
getMarkdownFromHeadings,
parseOptionsFromSourceText,
} = require('../main.js')
const testStandardHeadings = [
{ heading: 'Title 1 level 1', level: 1 },
{ heading: 'Title 1 level 2', level: 2 },
{ heading: 'Title 1 level 3', level: 3 },
{ heading: 'Title 2 level 1', level: 1 },
{ heading: 'Title 3 level 1', level: 1 },
{ heading: 'Title 3 level 2', level: 2 },
]
const testHeadingsWithoutFirstLevel = [
{ heading: 'Title 1 level 2', level: 2 },
{ heading: 'Title 1 level 3', level: 3 },
{ heading: 'Title 1 level 4', level: 4 },
{ heading: 'Title 2 level 2', level: 2 },
{ heading: 'Title 3 level 2', level: 2 },
{ heading: 'Title 3 level 3', level: 3 },
]
const testHeadingsWithSpecialChars = [
{ heading: 'Title 1 `level 1` {with special chars}, **bold**, _italic_, #a-tag, ==highlighted== and ~~strikethrough~~ text', level: 1 },
{ heading: 'Title 1 level 2 <em style="color: black">with HTML</em>', level: 2 },
{ heading: 'Title 1 level 2 [[wikilink1]] [[wikilink2|wikitext2]] [mdlink](https://mdurl)', level: 2 },
{ heading: 'Title 1 level 2 [[malformedlink a pi|pe | and [other chars]', level: 2 },
]
describe('Headings', () => {
test('Returns default message if no headings', () => {
const options = parseOptionsFromSourceText('')
const md = getMarkdownFromHeadings([], options)
expect(md).toContain('no headings found')
})
test('Returns empty TOC if no headings & option enabled', () => {
const options = parseOptionsFromSourceText('')
options.hideWhenEmpty = true
const md = getMarkdownFromHeadings([], options)
expect(md).toEqual('')
})
test('Returns indented list with links', () => {
const options = parseOptionsFromSourceText('')
const md = getMarkdownFromHeadings(testStandardHeadings, options)
const expectedMd = sanitizeMd(`
- [[#Title 1 level 1|Title 1 level 1]]
- [[#Title 1 level 2|Title 1 level 2]]
- [[#Title 1 level 3|Title 1 level 3]]
- [[#Title 2 level 1|Title 2 level 1]]
- [[#Title 3 level 1|Title 3 level 1]]
- [[#Title 3 level 2|Title 3 level 2]]
`)
expect(md).toEqual(expectedMd)
})
test('Returns indented list with links if no first level', () => {
const options = parseOptionsFromSourceText('')
const md = getMarkdownFromHeadings(testHeadingsWithoutFirstLevel, options)
const expectedMd = sanitizeMd(`
- [[#Title 1 level 2|Title 1 level 2]]
- [[#Title 1 level 3|Title 1 level 3]]
- [[#Title 1 level 4|Title 1 level 4]]
- [[#Title 2 level 2|Title 2 level 2]]
- [[#Title 3 level 2|Title 3 level 2]]
- [[#Title 3 level 3|Title 3 level 3]]
`)
expect(md).toEqual(expectedMd)
})
test('Returns indented ordered list with links', () => {
const options = parseOptionsFromSourceText('')
options.style = 'nestedOrderedList'
const md = getMarkdownFromHeadings(testStandardHeadings, options)
const expectedMd = sanitizeMd(`
1. [[#Title 1 level 1|Title 1 level 1]]
1. [[#Title 1 level 2|Title 1 level 2]]
1. [[#Title 1 level 3|Title 1 level 3]]
1. [[#Title 2 level 1|Title 2 level 1]]
1. [[#Title 3 level 1|Title 3 level 1]]
1. [[#Title 3 level 2|Title 3 level 2]]
`)
expect(md).toEqual(expectedMd)
})
test('Returns indented list with min level', () => {
const options = parseOptionsFromSourceText('')
options.minLevel = 2
const md = getMarkdownFromHeadings(testStandardHeadings, options)
const expectedMd = sanitizeMd(`
- [[#Title 1 level 2|Title 1 level 2]]
- [[#Title 1 level 3|Title 1 level 3]]
- [[#Title 3 level 2|Title 3 level 2]]
`)
expect(md).toEqual(expectedMd)
})
test('Returns indented list with max level', () => {
const options = parseOptionsFromSourceText('')
options.maxLevel = 2
const md = getMarkdownFromHeadings(testStandardHeadings, options)
const expectedMd = sanitizeMd(`
- [[#Title 1 level 1|Title 1 level 1]]
- [[#Title 1 level 2|Title 1 level 2]]
- [[#Title 2 level 1|Title 2 level 1]]
- [[#Title 3 level 1|Title 3 level 1]]
- [[#Title 3 level 2|Title 3 level 2]]
`)
expect(md).toEqual(expectedMd)
})
test('Returns indented list without links', () => {
const options = parseOptionsFromSourceText('')
options.includeLinks = false
const md = getMarkdownFromHeadings(testStandardHeadings, options)
const expectedMd = sanitizeMd(`
- Title 1 level 1
- Title 1 level 2
- Title 1 level 3
- Title 2 level 1
- Title 3 level 1
- Title 3 level 2
`)
expect(md).toEqual(expectedMd)
})
test('Returns indented list with title', () => {
const options = parseOptionsFromSourceText('')
options.title = '# My TOC'
const md = getMarkdownFromHeadings(testStandardHeadings, options)
const expectedMd = sanitizeMd(`
# My TOC
- [[#Title 1 level 1|Title 1 level 1]]
- [[#Title 1 level 2|Title 1 level 2]]
- [[#Title 1 level 3|Title 1 level 3]]
- [[#Title 2 level 1|Title 2 level 1]]
- [[#Title 3 level 1|Title 3 level 1]]
- [[#Title 3 level 2|Title 3 level 2]]
`)
expect(md).toEqual(expectedMd)
})
test('Returns indented list with sanitized links from special chars', () => {
const options = parseOptionsFromSourceText('')
const md = getMarkdownFromHeadings(testHeadingsWithSpecialChars, options)
const expectedMd = sanitizeMd(`
- [[#Title 1 \`level 1\` {with special chars}, **bold**, _italic_, a-tag, ==highlighted== and ~~strikethrough~~ text|Title 1 level 1 {with special chars}, bold, italic, #a-tag, highlighted and strikethrough text]]
- [[#Title 1 level 2 <em style="color: black">with HTML</em>|Title 1 level 2 <em style="color: black">with HTML</em>]]
- [[#Title 1 level 2 wikilink1 wikilink2 wikitext2 [mdlink](https://mdurl)|Title 1 level 2 wikilink1 wikitext2 mdlink]]
- [[#Title 1 level 2 malformedlink a pi pe and [other chars]|Title 1 level 2 malformedlink a pi-pe - and [other chars]]]
`)
expect(md).toEqual(expectedMd)
})
test('Returns indented list without links from special chars', () => {
const options = parseOptionsFromSourceText('')
options.includeLinks = false
const md = getMarkdownFromHeadings(testHeadingsWithSpecialChars, options)
const expectedMd = sanitizeMd(`
- Title 1 \`level 1\` {with special chars}, **bold**, _italic_, #a-tag, ==highlighted== and ~~strikethrough~~ text
- Title 1 level 2 <em style="color: black">with HTML</em>
- Title 1 level 2 [[wikilink1]] [[wikilink2|wikitext2]] [mdlink](https://mdurl)
- Title 1 level 2 [[malformedlink a pi|pe | and [other chars]
`)
expect(md).toEqual(expectedMd)
})
test('Returns flat first-level list with links', () => {
const options = parseOptionsFromSourceText('')
options.style = 'inlineFirstLevel'
const md = getMarkdownFromHeadings(testStandardHeadings, options)
const expectedMd = sanitizeMd(`
[[#Title 1 level 1|Title 1 level 1]] | [[#Title 2 level 1|Title 2 level 1]] | [[#Title 3 level 1|Title 3 level 1]]
`)
expect(md).toEqual(expectedMd)
})
test('Returns flat first-level list without links', () => {
const options = parseOptionsFromSourceText('')
options.style = 'inlineFirstLevel'
options.includeLinks = false
const md = getMarkdownFromHeadings(testStandardHeadings, options)
const expectedMd = sanitizeMd(`
Title 1 level 1 | Title 2 level 1 | Title 3 level 1
`)
expect(md).toEqual(expectedMd)
})
test('Returns flat list with custom level', () => {
const options = parseOptionsFromSourceText('')
options.style = 'inlineFirstLevel'
options.includeLinks = false
options.minLevel = 3
const md = getMarkdownFromHeadings(testStandardHeadings, options)
const expectedMd = sanitizeMd(`
Title 1 level 3
`)
expect(md).toEqual(expectedMd)
})
test('Returns flat list with default level', () => {
const options = parseOptionsFromSourceText('')
options.style = 'inlineFirstLevel'
options.includeLinks = false
const md = getMarkdownFromHeadings(testHeadingsWithoutFirstLevel, options)
const expectedMd = sanitizeMd(`
Title 1 level 2 | Title 2 level 2 | Title 3 level 2
`)
expect(md).toEqual(expectedMd)
})
})
function sanitizeMd(md) {
return md.replaceAll(' ', '\t').replace(/^\n/, '').replace(/\n$/, '')
}

View File

@ -0,0 +1,8 @@
const path = require('path')
module.exports = async () => {
return {
testMatch: [path.join(__dirname, '**/*.test.js')],
testTimeout: 1000,
}
}

View File

@ -0,0 +1,102 @@
const { parseOptionsFromSourceText } = require('../main.js')
describe('Options', () => {
test('Returns default options if none are specified', () => {
const options = parseOptionsFromSourceText('')
expect(options).toEqual({
title: '',
style: 'nestedList',
includeLinks: true,
minLevel: 0,
maxLevel: 0,
hideWhenEmpty: false,
debugInConsole: false,
})
})
test('Returns custom options if specified', () => {
const optionsText = `
title: # Some title
style: inlineFirstLevel # Some comment
minLevel: 1
maxLevel: 2 # Some other comment
includeLinks: false
hideWhenEmpty: true
debugInConsole: true
`
const options = parseOptionsFromSourceText(optionsText)
expect(options).toEqual({
title: '# Some title',
style: 'inlineFirstLevel',
includeLinks: false,
minLevel: 1,
maxLevel: 2,
hideWhenEmpty: true,
debugInConsole: true,
})
})
test('Accepts comments in options', () => {
const options = parseOptionsFromSourceText('maxLevel: 5 # some comment')
expect(options.maxLevel).toEqual(5)
})
test('Ignores unknown options', () => {
const options = parseOptionsFromSourceText(`
maxLevel: 5
someOption: someValue
`)
expect(options.maxLevel).toEqual(5)
})
describe('Throw an error if the option value is invalid', () => {
test('On style', () => {
try {
const options = parseOptionsFromSourceText('style: someInvalidStyle')
expect(options.style).toEqual('Should have thrown')
} catch(error) {
expect(error.message).toContain('Invalid value')
}
})
test('On minLevel', () => {
try {
const options = parseOptionsFromSourceText('minLevel: -1')
expect(options.minLevel).toEqual('Should have thrown')
} catch(error) {
expect(error.message).toContain('Invalid value')
}
})
test('On maxLevel', () => {
try {
const options = parseOptionsFromSourceText('maxLevel: -1')
expect(options.maxLevel).toEqual('Should have thrown')
} catch(error) {
expect(error.message).toContain('Invalid value')
}
})
test('On includeLinks', () => {
try {
const options = parseOptionsFromSourceText('includeLinks: no')
expect(options.includeLinks).toEqual('Should have thrown')
} catch(error) {
expect(error.message).toContain('Invalid value')
}
})
test('On hideWhenEmpty', () => {
try {
const options = parseOptionsFromSourceText('hideWhenEmpty: maybe')
expect(options.hideWhenEmpty).toEqual('Should have thrown')
} catch(error) {
expect(error.message).toContain('Invalid value')
}
})
test('On debugInConsole', () => {
try {
const options = parseOptionsFromSourceText('debugInConsole: yes')
expect(options.debugInConsole).toEqual('Should have thrown')
} catch(error) {
expect(error.message).toContain('Invalid value')
}
})
})
})

0
.trash/未命名 3.md Normal file
View File

View File

@ -0,0 +1,15 @@
# 问题情况
使用Creo接口的 [借用](../相关操作/1.5-EDM-CAD/51.Creo接口使用.md#借用) 功能,出现了报错:无法获取当前活动文档的路径
![](assets/511be63d18592e49e114eadb6da2472%201.png)
# 解决办法
将模型保存到本地(无论是否保存到工作目录都可以),保存下来后,模型在电脑上就有了一个实际的存储位置
![](assets/Pasted%20image%2020250415092937.png)
# 问题原因
模型没有保存,对于电脑系统而言,这份文件没有存储的路径

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@ -0,0 +1,11 @@
# 问题情况
启动PLM服务端提示 `服务PLMMainService_Web没有正确安装`
![](assets/bf907add6114f32829dcbc11ed50491.png)
# 解决办法
**服务** 中选择 **卸载**,卸载后再进行 **安装** 即可
![](assets/Pasted%20image%2020250414161312.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

View File

@ -0,0 +1,139 @@
# 金蝶云星空·企业版字段映射配置
## 1、字段映射
### 1.1、基础信息
| 名称 | 说明 | 示例 |
| ---------- | ------------------------------ | ------------------------------------------------------------- |
| 同步顺序 | 按此设置的顺序进行同步,不能有重复的顺序 | |
| from语句 | pdmitem是物料表、cfobjkind是对象分类表 | pdmitem a left JOIN cfobjkind b on a.itemkindid = b.objkindid |
| where条件 | | viewid = 1 and a.stat = 4 and a.itemid > 0 |
| order by排序 | | itemcode |
| erp接口 | BD_MATERIAL物料接口ENG_BOMbom接口 | |
### 1.2、字段映射
#### 1.2.1、接口位置
[金蝶云星空企业版API文档](https://openapi.open.kingdee.com/ApiDoc)
![image-20250407154239474](assets/image-20250407154239474.png)
![image-20250407154447450](assets/image-20250407154447450.png)
#### 1.2.2、新增字段映射
![image-20250407155223180](assets/image-20250407155223180.png)
| 名称 | 说明 | 示例 |
| ----- | ------------------------------------------------------------------------------------- | -------------------- |
| 父字段 | 填写Model下的类型为Object且子字段不为FNumber的字段如果不是则不用填 | 物料接口中的FSubHeadEntity |
| erp字段 | 字段名称 | FNumber |
| 扩展字段 | ![image-20250407155738744](assets/image-20250407155738744.png)有些字段展开后有FNumber等字段的需要填写 | |
| 字段类型 | 对应接口文档中的类型,注意:如果有扩展字段则填扩展字段的字段类型 | |
| 处理方式 | | |
| 赋值 | 赋PLM字段值时建议加上别名以防字段重名 | a.itemcode |
| 仅新增 | 只在新增时添加字段数据,修改时不进行修改 | |
#### 1.2.3、发布
![image-20250407160315724](assets/image-20250407160315724.png)
在左侧输入框内查询条件,然后点测试发布,会在右侧中显示同步结果
## 2、日志
### 2.1、同步日志
用于查看同步日志,默认查看当天的日志
![image-20250407160541605](assets/image-20250407160541605.png)
### 2.2、操作日志
记录字段映射的操作日志
![image-20250407160643916](assets/image-20250407160643916.png)
## 3、注意事项
### 3.1、版本规则
将字母改成数字
![微信图片_20250407172907_32](assets/微信图片_20250407172907_32.png)
## 4、按钮脚本
#### 4.1、物料同步按钮
```
uses MyClass,Variables,BaseUtil,CommonFunc,DataConst,CFFrm,CFSimplePropFrm,Forms,StdCtrls,Variants,SysUtils,Classes,Controls,Dialogs,
CHostIntf,ProductClas,DocClas,LoginClas,VirtualTrees,CEntClas,PathClas,JsonHttpClient,JSON;
var
jsonObject: TJSONObject; //请求对象
request: string; //请求对象转成的json字符串
response : String;//返回json字符串
begin
if Application.MessageBox( '确定同步吗', '同步确认', 1) = 1 then begin
btncxtsCusPage_1.Enabled := False; //禁用按钮,防止重复点
try
jsonObject := TJSONObject.Create;
jsonObject.AddPair('sql', 'a.viewid = 1 and a.itemcode = ''' + fedtItemCode.Text + '''');
jsonObject.AddPair('syncId', '1904456130482036739');
request := jsonObject.ToString; //举例中转字符串后为:{“itemid”:”11111”}
response := TJsonHttpClient.post7TS1('http://192.168.254.20:8005/erp/sync/manual',request,'application/json',''); //发起请求读取返回值
finally
jsonObject.Free;
btncxtsCusPage_1.Enabled := True; //不管是否失败,释放资源,启用按钮
end;
//showmessage(inttostr(length(response)));
if(length(response) = 0) then exit;
//response := StringReplace(response,'\n',#10,[rfReplaceAll]);
//response := StringReplace(response,'\t',#9,[rfReplaceAll]);
showmessage(response);
end;
end.
```
#### 4.2、BOM同步按钮
```
uses MyClass,Variables,BaseUtil,CommonFunc,DataConst,CFFrm,CFSimplePropFrm,Forms,StdCtrls,Variants,SysUtils,Classes,Controls,Dialogs,
CHostIntf,ProductClas,DocClas,LoginClas,VirtualTrees,CEntClas,PathClas,JsonHttpClient,JSON;
var
jsonObject: TJSONObject; //请求对象
request: string; //请求对象转成的json字符串
response : String;//返回json字符串
begin
if Application.MessageBox( '确定同步吗', '同步确认', 1) = 1 then begin
btncxtsCusPage_2.Enabled := False; //禁用按钮,防止重复点
try
jsonObject := TJSONObject.Create;
jsonObject.AddPair('sql', 'a.viewid = 1 and a.itemcode = ''' + fedtItemCode.Text + '''');
jsonObject.AddPair('syncId', '1904456130482036737');
request := jsonObject.ToString; //举例中转字符串后为:{“itemid”:”11111”}
response := TJsonHttpClient.post7TS1('http://192.168.254.20:8005/erp/sync/manual',request,'application/json',''); //发起请求读取返回值
finally
jsonObject.Free;
btncxtsCusPage_2.Enabled := True; //不管是否失败,释放资源,启用按钮
end;
//showmessage(inttostr(length(response)));
if(length(response) = 0) then exit;
//response := StringReplace(response,'\n',#10,[rfReplaceAll]);
//response := StringReplace(response,'\t',#9,[rfReplaceAll]);
showmessage(response);
end;
end.
```
需修改脚本ip并开放端口

View File

@ -0,0 +1,10 @@
是否启用物料编码器,在数据库中受到 `cfobjkind` 表的 `usecoder` 属性设置为2代表启用
批量设置的代码如下(可 [从数据库中查询取得codeid值](从数据库中查询取得codeid值.md)
```SQL
update cfobjkind
set usecoder = 2
where codeid = 22
```

View File

@ -2,16 +2,17 @@
==**服务器要设置开机密码**==
找到控制模板
找到 **控制面板**
![](assets/Pasted%20image%2020250410125617.png)
控制面板\用户帐户\用户帐户\管理帐户\更改帐户 **修改密码**
在搜索栏中输入 `控制面板\用户帐户\用户帐户\管理帐户\更改帐户`,选择 **修改密码** 选项
![](assets/Pasted%20image%2020250410125734.png)
# 远程桌面设置
右键我的电脑,远程设置,远程,选中允许远程连接此计算机确定
右键我的电脑**远程设置****远程****选中允许远程连接此计算机**确定
==注:如果要设置其他账户可以选择用户添加==
@ -19,7 +20,7 @@
# 远程桌面访问
桌面上搜索**==远程==**,找到远程桌面
桌面上搜索 **==远程==**,找到远程桌面
![](assets/Pasted%20image%2020250410131004.png)
输入服务器IP地址服务器账号服务器密码点击确定

View File

@ -16,7 +16,7 @@
![发布窗体](../../../外发客户教程/assets/image-20240902201810278.png)
**添加用户窗体**可以选择整个部门将其添加进来也可以只选择某个人将其添加进来8.266版本存在发布给部门时部门内成员可跳过签收直接具备权限的bug
**添加用户窗体**可以选择整个部门将其添加进来也可以只选择某个人将其添加进来8.266及以后版本存在发布给部门时部门内成员可跳过签收直接具备权限的bug
![image-20240902202154617](../../../外发客户教程/assets/image-20240902202154617.png)
@ -34,6 +34,26 @@
![](assets/Pasted%20image%2020241120165847.png)
# 签收文件
所有文件经由线上发布之后接收人员需要打开PLM系统==在 **企业知识库****收发管理****我收到的发布** 中进行签收==
![image-20240829184008672](../../../外发客户教程/assets/image-20240829184008672.png)
双击未签收的记录,可以打开发布详情,在里面可以看到发布的文件,点击 **签收** 后才能选择文件,用 **打开** 按钮将其打开
![image-20240829185406549](../../../外发客户教程/assets/image-20240829185406549.png)
## 签收记录
所有的发布都有签收记录,可以双击已经签收的记录,将其打开查看该记录的详情内容
![image-20240829185826276](../../../外发客户教程/assets/image-20240829185826276.png)
## 签收说明
文件发布给部门或岗位后,需要该部门或岗位所有人签收后,部门或岗位的后添加新人才能在发布区查看到该文件(有发布记录,但是没有具体的签收记录,也不需要签收)
# 工作区文件发布
通常状态下,工作区的文件还处于 [拟制](5.文件生命周期.md#拟制态) 状态,是不允许被发布的,但是可以通过开启系统参数,实现 **文档工作区** 的 [拟制态](5.文件生命周期.md#拟制态) 文件 [发布](5.文件生命周期.md#发布) 操作

View File

@ -0,0 +1,20 @@
### 创建换章流程
文件为归档状态下直接进行变更,换章流程不会生效 所以归档的文件需要新的流程
如下图所示
1.开始
2.文件取消归档
3.换章流程
4.文件归档
5.结束
![](assets/Pasted%20image%2020250416101812.png)
流程设置好之后,选择对应的文件→创建工作流→完成审批即可

View File

@ -48,7 +48,7 @@
## 导入文件夹或文件
导入文件或文件夹,需要先选择导入的位置,选择好导入位置后,有两种方式可以实现导入操作
导入文件或文件夹,需要先选择导入的位置,选择好导入位置后,有两种方式可以实现导入操作
### 导入操作一

View File

@ -186,11 +186,11 @@
#### 前置要求
要求当前用户在当前文件夹下至少具备 [发布](20.文件权限管理.md#发布) 权限,处于 拟制态 的文件需要开启 [工作区文件发布](15.文件的收发管理.md#工作区文件发布) 的相关参数
要求当前用户在当前文件夹下至少具备 [发布](20.文件权限管理.md#发布) 权限,处于 **拟制态** 的文件需要开启 [工作区文件发布](15.文件的收发管理.md#工作区文件发布) 的相关参数
#### 发布操作
直接对 **未检出的文件** 右键 → **生命周期****发布**
直接对 **未检出的文件** 右键 → **生命周期**[发布](15.文件的收发管理.md#文档发布)
![](assets/Pasted%20image%2020241119135524.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View File

@ -10,7 +10,7 @@
## 登录
第一次打开 `AutoCAD`是需要通过PLM设计软件接口进行登录的
次打开 `AutoCAD`是需要通过PLM设计软件接口进行登录的
![](../1.0-EDM/assets/Pasted%20image%2020250301124159.png)

View File

@ -0,0 +1,46 @@
# 介绍
在Creo软件中的 **工具** 中可以选择 **PLM** 接口进行操作
![](assets/Pasted%20image%2020250323173346.png)
# 使用
## 登录
每次打开 `Creo`是需要通过PLM设计软件接口进行登录的
![](assets/Pasted%20image%2020250323173346.png)
说明如果当前已登录PLM软件那么在登录PLM设计软件接口时使用相同的账户那么软件和接口在系统中被判定为同一个并发点数而如果登录PLM设计软件接口时使用的是另外的账户那么系统会将软件和接口判定为两个并发点数
## 设置工作目录
工作目录是指本地的一个文件夹,在用户检出三维图时,可以选择将图纸检出到工作目录中进行修改
在通常情况下,工作目录就是 [Creo接口安装](50.Creo接口安装.md) 时设置的 **起始目录**
![](assets/Pasted%20image%2020250415091541.png)
## 填写参数模型
**填写参数模型** 功能可用于填写Creo三维模型的参数可填写范围包含 [Creo模型模板制作](55.Creo模型模板制作.md) 时定义好的参数
![](assets/Pasted%20image%2020250415092013.png)
## 借用
**借用** 可为装配体模型借用来系统内存储的Creo三维模型
找到要借用的三维模型图后,右键选择借用即可
![](assets/Pasted%20image%2020250415092349.png)
借用后模型被借用到当前模型中,但还需要点击 **确定** 来关闭弹窗
![](assets/Pasted%20image%2020250415092754.png)
### 可能出现的问题
[Creo借用无法获取当前活动文档的路径](../../报错处理/Creo借用无法获取当前活动文档的路径.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View File

@ -143,13 +143,16 @@
### 属性说明
| 名称 | 属性名 | 备注 |
| ----- | ----------- | ------------------------------------------------------------------------------------------------------ |
| --------- | ----------- | ------------------------------------------------------------------------------------------------------ |
| 层次码 | wbs | 层次码允许 **跳层** 不允许 **续接**,详情见 [层次码wbs说明](60.BOM搭建.md#层次码wbs说明) |
| 分类名称 | objkidname | |
| 分类码 | objkindcode | |
| 物料编码器 | codeid | 需要 [从数据库中查询取得codeid值](../../数据库/从数据库中查询取得codeid值.md) 然后再Excel表中写入 `codeid` 值,如果不需要设置物料编码器,那么输入 `0` 即可 |
| 是否启用物料编码器 | usecoder | 通过这个字段控制是否要启动物料编码器这个字段是无法从Excel表导入的只能在数据库中 [批量设置是否启用物料编码器](../../数据库/批量设置是否启用物料编码器.md) |
| 标签 | tag | 可用于区分叶子分类、抽象类,**不可通过Excel导入更新**,可通过数据库 [批量设置非叶子节点为抽象类](../../数据库/批量设置非叶子节点为抽象类.md) |
| 分类ID | objkindid | **不建议指定,以防冲突导致覆盖** |
| ERP分类编码 | erpid | |
# 扩展概念

View File

@ -96,7 +96,7 @@
### 导入操作
**标准物料库** 对文件夹右键选择 **导入**,选择 **物料** 即可
**标准物料库** 对文件夹右键选择 **导入**,选择 **物料** 即可(不能选择“标准物料库”右键导入,必须选择某个文件夹去右键选择导入)
![](assets/Pasted%20image%2020250105123522.png)

View File

@ -0,0 +1,11 @@
# 介绍
**装配属性** 会随着物料所处的BOM不同而有所不同使用 [从Excel导入BOM](60.BOM搭建.md#从Excel导入BOM) 功能是无法针对性的修改 **装配属性** 的,唯有 **系统设置****系统工具****产品相关****从Excel表更新BOM属性** 功能可以针对性的批量修改BOM内物料的 **装配属性**
![](assets/Pasted%20image%2020250415090005.png)
要使用此功能,需要使用专用的模板: [从excel表更新Bom属性_模板](Files/Documents/从excel表更新Bom属性_模板.xlsx)
以用量为例需要指定BOM结构的父物料编码以及要修改的子物料编码最后才是要修改的装配属性
![](assets/Pasted%20image%2020250415090447.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -26,5 +26,3 @@
![](assets/Pasted%20image%2020241203184059.png)
## 增补子任务

View File

@ -0,0 +1,74 @@
在项目中可能会遇到,项目启动了,但是任务只是临时排期的情况,这个时候我们就需要更改任务的计划时间,下面是流程操作
项目管理 → 项目跟踪 → 我未完成的任务 → 选择对应项目
![](assets/Pasted%20image%2020250412122154.png)
单击右键 → 点击项目任务树
![](assets/Pasted%20image%2020250412122359.png)
出现下面弹窗(方框中的是项目节点,计划时间不能更改)
![](assets/Pasted%20image%2020250412122519.png)
选中需要更改的任务 → 单击右键 → 变更任务日程
![](assets/Pasted%20image%2020250412122753.png)
点击后出现下图弹窗
![](assets/Pasted%20image%2020250412123109.png)
这里可以看到任务状态,不同的状态有不同的区别
完成:计划开始时间/计划结束时间都不能更改
执行中:计划开始时间不能更改,计划结束时间可以更改
待启动:计划开始时间/计划结束时间都可以更改
![](assets/Pasted%20image%2020250412123035.png)
更改时,选中需要更改的日期 → 点击如图所示小箭头
![](assets/Pasted%20image%2020250412123550.png)
在弹窗中选择日期,再点击下图所示图标
![](assets/Pasted%20image%2020250412123650.png)
最后点击左下角重新计算 → 执行变更 → 完成变更
![](assets/Pasted%20image%2020250412123839.png)
## 权限设置
用户需要具备 **项目管理** 权限才可使用项目管理模块
项目管理拥有以下模块
1.项目跟踪
2.项目模板
3.项目知识库
4.项目统计
5.问题管理
6.项目绩效
7.基础数据
![](assets/Pasted%20image%2020250415094337.png)
不同的模块可以设置不同的权限,用于区分不同的角色
具体设置可以查看
[三级角色](../0.5-SETOUT/43.建议角色设置.md#三级角色)
[项目管理员](../0.5-SETOUT/43.建议角色设置.md#项目管理员)

View File

@ -0,0 +1,45 @@
项目管理 → 项目跟踪 → 我未完成的任务 → 选择对应项目
![](assets/Pasted%20image%2020250412122154.png)
单击右键 → 点击项目任务树
![](assets/Pasted%20image%2020250412122359.png)
在弹窗中选择对应的任务 → 单击右键 → 增补子任务
![](assets/Pasted%20image%2020250412125755.png)
选择对应的任务 → 确定
![](assets/Pasted%20image%2020250412125912.png)
下一步
![](assets/Pasted%20image%2020250412125947.png)
下一步
![](assets/Pasted%20image%2020250412130013.png)
完成
![](assets/Pasted%20image%2020250412130046.png)
即可完成子任务的添加
![](assets/Pasted%20image%2020250412130103.png)
增加的子任务会受到上级任务的前置任务、后置任务的约束
不用状态下能否增补子任务有所不同
完成:不能增补子任务
执行中:可以增补子任务
## 注意
增补子任务时选择的任务模板必须是已经完成得
否则会出现一个任务走完流程,但有两个任务完成的情况

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@ -0,0 +1,109 @@
# 介绍
**对象属性** 指代 **对象分类****常规属性页面** 中可定义的属性
![image-20240606112908325](../2.0-PDM/assets/image-20240606112908325.png)
# 对象属性
想要定义 **对象属性**,需要在 **系统设置****参数配置****系统参数****数据定义相关****对象属性定义** 中定义
==文档或物料所能设置的属性,归由 **对象属性定义** 中的 **文档或物料** 管理==
![image-20240818152224182](assets/image-20240818152224182.png)
双击 **文档或物料** 可以对当前系统中文档或物料的所有相关属性进行设置,对这些对象的常规属性管理
![image-20240606115956707](../2.0-PDM/assets/image-20240606115956707.png)
## 新增自定义对象属性
如果 **对象属性定义** 中没有所需要的属性,可通过 **新建** 功能创建出新的对象属性
![img](../2.0-PDM/assets/clip_image002-17229359177821.jpg)
**对象属性定义界面** 需要填写 **名称****显示名**其中名称必须以“usr_”为前缀后面输入英文字母且不能与其他属性同名
![img](../2.0-PDM/assets/clip_image002-17229359843532.jpg)
显示名可以输入中文,比如仓库。另外字段长度表示用户在输入该属性时的最大长度,显示宽度表示界面中输入框的长度,值类型比较常用的有文本类型、整数类型、小数类型、枚举类型
![img](../2.0-PDM/assets/clip_image002-17229360105203.jpg)
### 值类型
#### 文本类型
字段长度默认为255在数据库中存储的是字符串类型为 `nvarchar(255)`
#### 整数类型
在数据库中存储的是整型,类型为 `int`
#### 小数类型
在数据库中存储的是高精度浮点数,类型为 `decimal(30, 15)`
#### 枚举类型
在数据库中存储的是字符串,类型为 `nvarchar(50)`
#### 时间类型
在数据库中存储的是字符串,类型为 `nvarchar(20)`
#### 布尔类型
在数据库中存储的是整型,类型为 `int`,其中 `1` 代表 `false``2` 代表 `true`
#### 图片类型
在数据库中存储的是整型,类型为 `int`
#### 日期范围
不明,定义失败
#### 多行文本
字段长度默认为255在数据库中存储的是字符串类型为 `nvarchar(255)`
### 多属性映射
若有多个 **对象属性** 要同步映射到同一个 **对象属性** 中,则可以在表达式中写上多个 **对象属性名称**,获取的内容会通过分隔符进行分隔
![image-20241209111918467](assets/image-20241209111918467.png)
> 如图,多属性映射情况下,填写两个属性,则按照顺序全部映射,且属性值之间使用分隔符进行分隔
>
> ![image-20241209112109008](assets/image-20241209112109008.png)
>
> 如图,多属性映射情况下,只填写了一个属性,则只映射该属性,且忽略分隔符
>
> ![image-20241209112213084](assets/image-20241209112213084.png)
### 属性数值计算
若某一 **对象属性** 需要进行计算,计算后的结果填入到另一 **对象属性** 中,则可以在 **属性数值计算** 中写好算式,系统会将计算后的结果写入到对应的 **对象属性**
前提要求是 ==参与计算的对象属性,与接收结果的对象属性 **值类型****整数类型****小数类型**==
![image-20241209113959715](assets/image-20241209113959715.png)
> 如图,属性数值计算情况下,填写重量后,可以将计算后的结果写入到备注中
>
> ![image-20241209113805159](assets/image-20241209113805159.png)
#### **说明**
若参与计算的对象属性值为空则接收结果的对象属性会接收到整个算式如属性B由属性A+10得出那么当属性A为空时属性B的值为+10
在设置 **属性数值计算**请务必检查不可出现多级计算的情况即属性B由属性A+10得出而属性C由属性B-2得出那么当属性A为空时属性B的值为+10属性C的值为+10-2又因为属性C要求存储整数或小数类型系统将会出现如下提示
![image-20250121151417460](assets/image-20250121151417460.png)
相较于 **属性数值计算** 更建议采用 [窗体表单脚本](200.Delphi脚本.md) 来实现属性值的计算
## 补充
系统内建的 **对象属性** 在多数情况下是不容许修改的,或者说修改无效,例如物料的 **单位**、**生产类型** 都要求存储数值类型,就算在 **对象属性** 中进行修改调整,也无法生效使用

View File

@ -1,12 +1,24 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 1.打开产品工作区
![image-20240815090942394](../assets/image-20240815090942394.png)
![image-20240815090942394](image-20240815090942394.png)
# 2.新建物料
找到要新建物料的位置,对文件夹右键选择 **新建** 选择要新建的物料
![image-20240815091247029](../assets/image-20240815091247029.png)
![image-20240815091247029](image-20240815091247029.png)
# 3.填写物料属性

View File

@ -1,3 +1,15 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 签收文件
所有文件经由线上发布之后接收人员需要打开PLM系统==在 **企业知识库****收发管理****我收到的发布** 中进行签收==
@ -13,4 +25,3 @@
所有的发布都有签收记录,可以双击已经签收的记录,将其打开查看该记录的详情内容
![image-20240829185826276](../assets/image-20240829185826276.png)

View File

@ -1,3 +1,15 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 签收文件存在哪?
所有文件经由线上发布后,接收人员需要先 **签收** 文件(签收操作见 “ 签收文件 ” 操作指南)

View File

@ -1,3 +1,15 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 基础介绍
**企业知识库** 中分有三个区域:**文档工作区**、**文档归档区**、**文档发布区**

View File

@ -1,3 +1,15 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 编辑文件
文件创建或导入后(见 “ 文档创建、导入 ” 操作指南)需要编辑文件,可以将文件 **检出** 到本地电脑上进行修改,修改完成后再 **检入** 回系统当中即可

View File

@ -1,3 +1,15 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 文档发布
文档发布需要在 **企业知识库****文档归档区** 中选择文件进行发布,对文件右键,在 **生命周期** 中选择 **发布**

View File

@ -1,3 +1,15 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 发起流程
对于编制好,需要在系统中走线上审签的文件,需要对其发起工作流

View File

@ -1,3 +1,15 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 流程审批
可在 **流程管理****流程审批****待我审批** 中找到当前执行到您处,需要由您审批的流程

View File

@ -1,3 +1,15 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 编辑明细表搭建 BOM
编辑明细表搭建指的是在产品工作区或者标准物料库通过 **编辑明细表** 的方式对物料搭建出相应的 BOM 结构

View File

@ -1,3 +1,15 @@
# 目录
```table-of-contents
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
```
# 全局查找
全局查找功能可用于在系统中查找 **文档****物料**

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Some files were not shown because too many files have changed in this diff Show More