优化文档相关
This commit is contained in:
Generated
+513
-10
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"@turbodocx/html-to-docx": "^1.22.2",
|
||||||
"@umoteam/editor": "^10.2.1",
|
"@umoteam/editor": "^10.2.1",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.13.1",
|
||||||
"chart": "^0.1.2",
|
"chart": "^0.1.2",
|
||||||
@@ -679,7 +680,6 @@
|
|||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -701,12 +701,21 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/source-map": {
|
||||||
|
"version": "0.3.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||||
|
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.25"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
@@ -717,7 +726,6 @@
|
|||||||
"version": "0.3.31",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -1024,6 +1032,102 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@oozcitak/dom": {
|
||||||
|
"version": "1.15.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@oozcitak/dom/-/dom-1.15.5.tgz",
|
||||||
|
"integrity": "sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/infra": "1.0.5",
|
||||||
|
"@oozcitak/url": "1.0.0",
|
||||||
|
"@oozcitak/util": "8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/dom/node_modules/@oozcitak/util": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@oozcitak/util/-/util-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/infra": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@oozcitak/infra/-/infra-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/util": "8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/infra/node_modules/@oozcitak/util": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@oozcitak/util/-/util-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/url": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@oozcitak/url/-/url-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/infra": "1.0.3",
|
||||||
|
"@oozcitak/util": "1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/url/node_modules/@oozcitak/infra": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@oozcitak/infra/-/infra-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/util": "1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/url/node_modules/@oozcitak/infra/node_modules/@oozcitak/util": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@oozcitak/util/-/util-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/url/node_modules/@oozcitak/util": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@oozcitak/util/-/util-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/util": {
|
||||||
|
"version": "8.3.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@oozcitak/util/-/util-8.3.3.tgz",
|
||||||
|
"integrity": "sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@popperjs/core": {
|
"node_modules/@popperjs/core": {
|
||||||
"name": "@sxzz/popperjs-es",
|
"name": "@sxzz/popperjs-es",
|
||||||
"version": "2.11.8",
|
"version": "2.11.8",
|
||||||
@@ -2009,6 +2113,30 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@turbodocx/html-to-docx": {
|
||||||
|
"version": "1.22.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@turbodocx/html-to-docx/-/html-to-docx-1.22.2.tgz",
|
||||||
|
"integrity": "sha512-OmwsD6TMVL8Z4EDLiashTcwfaibTs3ot3CdbdCV2Wt+BsQMN73XEaYfzuQZqt2iX8cnXqD6R+9/2uMwjiQBvvg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"color-name": "^1.1.4",
|
||||||
|
"html-entities": "^2.3.3",
|
||||||
|
"html-minifier-terser": "^7.2.0",
|
||||||
|
"htmlparser2": "^10.0.0",
|
||||||
|
"jszip": "^3.7.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lru-cache": "^10.4.3",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"nanoid": "^3.3.18",
|
||||||
|
"probe-image-size": "^7.2.3",
|
||||||
|
"xmlbuilder2": "2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/d3": {
|
"node_modules/@types/d3": {
|
||||||
"version": "7.4.3",
|
"version": "7.4.3",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz",
|
||||||
@@ -3160,7 +3288,6 @@
|
|||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz",
|
||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@@ -3304,6 +3431,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
@@ -3351,6 +3484,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camel-case": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/camel-case/-/camel-case-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pascal-case": "^3.1.2",
|
||||||
|
"tslib": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/canvg": {
|
"node_modules/canvg": {
|
||||||
"version": "3.0.11",
|
"version": "3.0.11",
|
||||||
"resolved": "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz",
|
"resolved": "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz",
|
||||||
@@ -3422,6 +3565,18 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clean-css": {
|
||||||
|
"version": "5.3.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/clean-css/-/clean-css-5.3.3.tgz",
|
||||||
|
"integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"source-map": "~0.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/codepage": {
|
"node_modules/codepage": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
|
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
|
||||||
@@ -3439,6 +3594,12 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -4216,12 +4377,65 @@
|
|||||||
"jszip": ">=3.0.0"
|
"jszip": ">=3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dom-serializer/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dom-to-image-more": {
|
"node_modules/dom-to-image-more": {
|
||||||
"version": "3.7.2",
|
"version": "3.7.2",
|
||||||
"resolved": "https://registry.npmmirror.com/dom-to-image-more/-/dom-to-image-more-3.7.2.tgz",
|
"resolved": "https://registry.npmmirror.com/dom-to-image-more/-/dom-to-image-more-3.7.2.tgz",
|
||||||
"integrity": "sha512-uQf+pHv6eQhgfI8t2bFuinV0KsPyT8TZgCLwcSU8uBVgN9v6leb0mMpvp6HQAlAcplP3NCcGjxbdqef6pTzvmw==",
|
"integrity": "sha512-uQf+pHv6eQhgfI8t2bFuinV0KsPyT8TZgCLwcSU8uBVgN9v6leb0mMpvp6HQAlAcplP3NCcGjxbdqef6pTzvmw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.4.12",
|
"version": "3.4.12",
|
||||||
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.12.tgz",
|
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.12.tgz",
|
||||||
@@ -4231,6 +4445,30 @@
|
|||||||
"@types/trusted-types": "^2.0.7"
|
"@types/trusted-types": "^2.0.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dot-case": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dot-case/-/dot-case-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"no-case": "^3.0.4",
|
||||||
|
"tslib": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -4972,6 +5210,64 @@
|
|||||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-entities": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/html-entities/-/html-entities-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/mdevils"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://patreon.com/mdevils"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/html-minifier-terser": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"camel-case": "^4.1.2",
|
||||||
|
"clean-css": "~5.3.2",
|
||||||
|
"commander": "^10.0.0",
|
||||||
|
"entities": "^4.4.0",
|
||||||
|
"param-case": "^3.0.4",
|
||||||
|
"relateurl": "^0.2.7",
|
||||||
|
"terser": "^5.15.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"html-minifier-terser": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-minifier-terser/node_modules/commander": {
|
||||||
|
"version": "10.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz",
|
||||||
|
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-minifier-terser/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html2canvas": {
|
"node_modules/html2canvas": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
@@ -4985,6 +5281,25 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"entities": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
@@ -5645,6 +5960,21 @@
|
|||||||
"lodash-es": "*"
|
"lodash-es": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.merge": {
|
||||||
|
"version": "4.6.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lower-case": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lower-case/-/lower-case-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lowlight": {
|
"node_modules/lowlight": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/lowlight/-/lowlight-3.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/lowlight/-/lowlight-3.3.0.tgz",
|
||||||
@@ -5660,6 +5990,12 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -5932,13 +6268,12 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.18",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -5970,6 +6305,16 @@
|
|||||||
"node": ">= 4.4.x"
|
"node": ">= 4.4.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/no-case": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/no-case/-/no-case-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lower-case": "^2.0.2",
|
||||||
|
"tslib": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
@@ -6093,6 +6438,16 @@
|
|||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
"license": "(MIT AND Zlib)"
|
"license": "(MIT AND Zlib)"
|
||||||
},
|
},
|
||||||
|
"node_modules/param-case": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/param-case/-/param-case-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dot-case": "^3.0.4",
|
||||||
|
"tslib": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parse-node-version": {
|
"node_modules/parse-node-version": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz",
|
||||||
@@ -6102,6 +6457,16 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pascal-case": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/pascal-case/-/pascal-case-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"no-case": "^3.0.4",
|
||||||
|
"tslib": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-data-parser": {
|
"node_modules/path-data-parser": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/path-data-parser/-/path-data-parser-0.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/path-data-parser/-/path-data-parser-0.1.0.tgz",
|
||||||
@@ -6280,6 +6645,65 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/probe-image-size": {
|
||||||
|
"version": "7.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/probe-image-size/-/probe-image-size-7.4.0.tgz",
|
||||||
|
"integrity": "sha512-cdEprVtZxV+awMde9X+4jILBFYh4CARxVrQaMl4wY4YcPWbul9jntXrIW95NInBDyJwcVUP3U0T6yukN8rMBaQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"needle": "^2.5.2",
|
||||||
|
"stream-parser": "~0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/probe-image-size/node_modules/debug": {
|
||||||
|
"version": "3.2.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
|
||||||
|
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/probe-image-size/node_modules/iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/probe-image-size/node_modules/needle": {
|
||||||
|
"version": "2.9.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/needle/-/needle-2.9.1.tgz",
|
||||||
|
"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^3.2.6",
|
||||||
|
"iconv-lite": "^0.4.4",
|
||||||
|
"sax": "^1.2.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"needle": "bin/needle"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
@@ -6651,6 +7075,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/relateurl": {
|
||||||
|
"version": "0.2.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/relateurl/-/relateurl-0.2.7.tgz",
|
||||||
|
"integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
|
||||||
@@ -6856,7 +7289,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.4.tgz",
|
"resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.4.tgz",
|
||||||
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=11.0.0"
|
"node": ">=11.0.0"
|
||||||
}
|
}
|
||||||
@@ -7013,7 +7445,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -7027,6 +7458,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/source-map-support": {
|
||||||
|
"version": "0.5.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"source-map": "^0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/speakingurl": {
|
"node_modules/speakingurl": {
|
||||||
"version": "14.0.1",
|
"version": "14.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||||
@@ -7071,6 +7512,30 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stream-parser": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/stream-parser/-/stream-parser-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stream-parser/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stream-parser/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
@@ -7204,6 +7669,30 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/terser": {
|
||||||
|
"version": "5.51.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/terser/-/terser-5.51.2.tgz",
|
||||||
|
"integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
|
"acorn": "^8.15.0",
|
||||||
|
"commander": "^2.20.0",
|
||||||
|
"source-map-support": "~0.5.20"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"terser": "bin/terser"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/terser/node_modules/commander": {
|
||||||
|
"version": "2.20.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz",
|
||||||
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tesseract.js": {
|
"node_modules/tesseract.js": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/tesseract.js/-/tesseract.js-7.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/tesseract.js/-/tesseract.js-7.0.0.tgz",
|
||||||
@@ -7970,6 +8459,20 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xmlbuilder2": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/xmlbuilder2/-/xmlbuilder2-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/dom": "1.15.5",
|
||||||
|
"@oozcitak/infra": "1.0.5",
|
||||||
|
"@oozcitak/util": "8.3.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y-prosemirror": {
|
"node_modules/y-prosemirror": {
|
||||||
"version": "1.2.15",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmmirror.com/y-prosemirror/-/y-prosemirror-1.2.15.tgz",
|
"resolved": "https://registry.npmmirror.com/y-prosemirror/-/y-prosemirror-1.2.15.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"@turbodocx/html-to-docx": "^1.22.2",
|
||||||
"@umoteam/editor": "^10.2.1",
|
"@umoteam/editor": "^10.2.1",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.13.1",
|
||||||
"chart": "^0.1.2",
|
"chart": "^0.1.2",
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import request from "@/utils/request";
|
|||||||
|
|
||||||
// ---------------- 分类 ----------------
|
// ---------------- 分类 ----------------
|
||||||
|
|
||||||
// 分类树(每个节点带 doc_count)
|
// 分类树(每个节点带 doc_count),全租户共用一套,不再区分个人/共享
|
||||||
export function getDocCategories() {
|
export function getDocCategories(params) {
|
||||||
return request({
|
return request({
|
||||||
url: "/backend/oa/document/categories",
|
url: "/backend/oa/document/categories",
|
||||||
method: "get"
|
method: "get",
|
||||||
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +47,12 @@ export function getDocList(params) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDocStats() {
|
// 概览统计:可见性维度(不再区分个人/共享)
|
||||||
|
export function getDocStats(params) {
|
||||||
return request({
|
return request({
|
||||||
url: "/backend/oa/document/stats",
|
url: "/backend/oa/document/stats",
|
||||||
method: "get"
|
method: "get",
|
||||||
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +64,14 @@ export function getDocDetail(id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在线文档编辑源(不累加查看次数),用于打开在线编辑器回填内容
|
||||||
|
export function getDocContent(id) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/content/${id}`,
|
||||||
|
method: "get"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createDoc(data) {
|
export function createDoc(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/backend/oa/document/create",
|
url: "/backend/oa/document/create",
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ import { useAuthStore } from './stores/auth'
|
|||||||
// 导入全局组件
|
// 导入全局组件
|
||||||
import UmoEditor from '@/views/components/UmoEditor.vue';
|
import UmoEditor from '@/views/components/UmoEditor.vue';
|
||||||
|
|
||||||
|
// 兼容某些依赖(如 HTML→docx 转换库)对 Node 全局变量 global 的引用
|
||||||
|
if (typeof window !== 'undefined' && !window.global) {
|
||||||
|
window.global = window;
|
||||||
|
}
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
// 全局注册 UmoEditor 组件
|
// 全局注册 UmoEditor 组件
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>文档图谱</h2>
|
<h2>文档图谱</h2>
|
||||||
<p>以知识图谱呈现文档之间的引用、相关与归属关系,点击节点查看文档详情</p>
|
<p>
|
||||||
|
默认展示顶级往下 3 级分类脉络,点击带 ▸ 的分类节点可继续延展 3 级;滚轮缩放,按住空格 + 左键拖拽平移
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<el-button :icon="RefreshLeft" @click="collapseAll">收起全部</el-button>
|
||||||
<el-button :icon="Refresh" @click="loadGraph">刷新</el-button>
|
<el-button :icon="Refresh" @click="loadGraph">刷新</el-button>
|
||||||
<el-button type="primary" :icon="Aim" @click="fitView">重新布局</el-button>
|
<el-button type="primary" :icon="Aim" @click="fitView">重新布局</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,12 +28,13 @@
|
|||||||
<el-tree-select
|
<el-tree-select
|
||||||
v-model="filters.category_id"
|
v-model="filters.category_id"
|
||||||
:data="categories"
|
:data="categories"
|
||||||
:props="{ label: 'name', children: 'children' }"
|
:props="{ label: 'name', children: 'children', disabled: 'disabled' }"
|
||||||
node-key="id"
|
node-key="id"
|
||||||
check-strictly
|
check-strictly
|
||||||
clearable
|
clearable
|
||||||
|
default-expand-all
|
||||||
placeholder="全部分类"
|
placeholder="全部分类"
|
||||||
style="width: 180px"
|
style="width: 200px"
|
||||||
/>
|
/>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="filters.relation"
|
v-model="filters.relation"
|
||||||
@@ -50,7 +54,12 @@
|
|||||||
<el-option label="最多 300 个" :value="300" />
|
<el-option label="最多 300 个" :value="300" />
|
||||||
<el-option label="最多 600 个" :value="600" />
|
<el-option label="最多 600 个" :value="600" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-select v-model="filters.layout" style="width: 130px">
|
||||||
|
<el-option label="力导向布局" value="force" />
|
||||||
|
<el-option label="环形布局" value="circular" />
|
||||||
|
</el-select>
|
||||||
<el-checkbox v-model="filters.includeCategory">显示分类中心</el-checkbox>
|
<el-checkbox v-model="filters.includeCategory">显示分类中心</el-checkbox>
|
||||||
|
<el-checkbox v-model="filters.showDocs">显示文档</el-checkbox>
|
||||||
<el-checkbox v-model="filters.onlyLinked">仅看有关系的文档</el-checkbox>
|
<el-checkbox v-model="filters.onlyLinked">仅看有关系的文档</el-checkbox>
|
||||||
<el-button type="primary" :icon="Search" @click="loadGraph">生成图谱</el-button>
|
<el-button type="primary" :icon="Search" @click="loadGraph">生成图谱</el-button>
|
||||||
<el-button :icon="RefreshLeft" @click="resetFilters">重置</el-button>
|
<el-button :icon="RefreshLeft" @click="resetFilters">重置</el-button>
|
||||||
@@ -59,8 +68,8 @@
|
|||||||
<div v-loading="loading" class="chart-wrap">
|
<div v-loading="loading" class="chart-wrap">
|
||||||
<div ref="chartRef" class="chart" />
|
<div ref="chartRef" class="chart" />
|
||||||
<el-empty
|
<el-empty
|
||||||
v-if="!loading && !nodes.length"
|
v-if="!loading && !displayNodes.length"
|
||||||
description="暂无可展示的文档,请先到文档库上传文档"
|
description="暂无可展示的分类或文档,请先到文档库新建分类并上传文档"
|
||||||
class="chart-empty"
|
class="chart-empty"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +79,13 @@
|
|||||||
<i class="dot" style="background: #409eff" />文档
|
<i class="dot" style="background: #409eff" />文档
|
||||||
</span>
|
</span>
|
||||||
<span class="legend-item">
|
<span class="legend-item">
|
||||||
<i class="dot" style="background: #e6a23c" />分类
|
<i class="dot" style="background: #4e7cf6" />1级分类
|
||||||
|
</span>
|
||||||
|
<span class="legend-item">
|
||||||
|
<i class="dot" style="background: #2bb673" />2级分类
|
||||||
|
</span>
|
||||||
|
<span class="legend-item">
|
||||||
|
<i class="dot" style="background: #e6a23c" />3级分类
|
||||||
</span>
|
</span>
|
||||||
<span class="legend-item">
|
<span class="legend-item">
|
||||||
<i class="line solid" />关联关系
|
<i class="line solid" />关联关系
|
||||||
@@ -78,7 +93,12 @@
|
|||||||
<span class="legend-item">
|
<span class="legend-item">
|
||||||
<i class="line dashed" />归属分类
|
<i class="line dashed" />归属分类
|
||||||
</span>
|
</span>
|
||||||
<span class="legend-tip">滚轮缩放 · 拖拽平移 · 点击节点查看详情</span>
|
<span class="legend-item">
|
||||||
|
<i class="line dotted" />分类层级
|
||||||
|
</span>
|
||||||
|
<span class="legend-tip"
|
||||||
|
>滚轮缩放 · 空格+左键拖拽平移 · 点击 ▸ 分类展开下级(更深层级循环使用这 3 色)</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -153,7 +173,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
import {
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
watch
|
||||||
|
} from "vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { Aim, Refresh, RefreshLeft, Search } from "@element-plus/icons-vue";
|
import { Aim, Refresh, RefreshLeft, Search } from "@element-plus/icons-vue";
|
||||||
import * as echarts from "echarts";
|
import * as echarts from "echarts";
|
||||||
@@ -167,9 +195,11 @@ let resizeObserver = null;
|
|||||||
let themeObserver = null;
|
let themeObserver = null;
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const categories = ref([]);
|
const categories = ref([]); // 分组后的分类(用于筛选下拉)
|
||||||
const nodes = ref([]);
|
const sharedTree = ref([]); // 共享分类树(用于分层展开)
|
||||||
const links = ref([]);
|
const personalTree = ref([]); // 私密分类树(用于分层展开)
|
||||||
|
const nodes = ref([]); // 原始节点
|
||||||
|
const links = ref([]); // 原始连线
|
||||||
const stats = ref({});
|
const stats = ref({});
|
||||||
const relationCounts = ref([]);
|
const relationCounts = ref([]);
|
||||||
const topNodes = ref([]);
|
const topNodes = ref([]);
|
||||||
@@ -177,15 +207,125 @@ const topNodes = ref([]);
|
|||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
const detailId = ref(0);
|
const detailId = ref(0);
|
||||||
|
|
||||||
|
// 分层展开:默认展示顶级往下 3 级,点击分支节点后在该分支继续延展 3 级
|
||||||
|
const INITIAL_DEPTH = 3;
|
||||||
|
const expandedIds = ref(new Set());
|
||||||
|
// 按住空格 + 鼠标左键拖拽平移
|
||||||
|
const spaceHeld = ref(false);
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
keyword: "",
|
keyword: "",
|
||||||
category_id: null,
|
category_id: null,
|
||||||
relation: "",
|
relation: "",
|
||||||
limit: 300,
|
limit: 300,
|
||||||
|
layout: "force",
|
||||||
includeCategory: true,
|
includeCategory: true,
|
||||||
onlyLinked: false
|
onlyLinked: false,
|
||||||
|
showDocs: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------- 分类层级索引 & 分层可见集合 ----------------
|
||||||
|
const catIndex = computed(() => {
|
||||||
|
const byId = new Map();
|
||||||
|
const childrenOf = new Map();
|
||||||
|
const parentOf = new Map();
|
||||||
|
const depthOf = new Map(); // 分类层级:顶级=1
|
||||||
|
const roots = [];
|
||||||
|
const walk = (list, parentId, depth) => {
|
||||||
|
(list || []).forEach(n => {
|
||||||
|
byId.set(n.id, n);
|
||||||
|
depthOf.set(n.id, depth);
|
||||||
|
if (parentId) parentOf.set(n.id, parentId);
|
||||||
|
else roots.push(n.id);
|
||||||
|
const kids = n.children || [];
|
||||||
|
if (kids.length) {
|
||||||
|
childrenOf.set(
|
||||||
|
n.id,
|
||||||
|
kids.map(c => c.id)
|
||||||
|
);
|
||||||
|
walk(kids, n.id, depth + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
walk(sharedTree.value, 0, 1);
|
||||||
|
walk(personalTree.value, 0, 1);
|
||||||
|
return { byId, childrenOf, parentOf, depthOf, roots };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分类层级配色:只保留 3 种颜色(对应同时展示的 3 级),更深层级按 1/2/3 级循环
|
||||||
|
const CATEGORY_LEVEL_COLORS = ["#4E7CF6", "#2BB673", "#E6A23C"];
|
||||||
|
function categoryColor(depth) {
|
||||||
|
const d = Math.max(1, Number(depth) || 1);
|
||||||
|
return CATEGORY_LEVEL_COLORS[(d - 1) % CATEGORY_LEVEL_COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleCatIds = computed(() => {
|
||||||
|
const { childrenOf, roots } = catIndex.value;
|
||||||
|
const expanded = expandedIds.value;
|
||||||
|
const visible = new Set();
|
||||||
|
const walk = (id, budget) => {
|
||||||
|
if (budget <= 0) return;
|
||||||
|
visible.add(id);
|
||||||
|
for (const kid of childrenOf.get(id) || []) {
|
||||||
|
// 已展开的分支:从该节点重新给 INITIAL_DEPTH 级预算;否则继续消耗剩余预算
|
||||||
|
walk(kid, expanded.has(id) ? INITIAL_DEPTH : budget - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
roots.forEach(r => walk(r, INITIAL_DEPTH));
|
||||||
|
return visible;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 每个可见分类隐藏的直接子级数量(用于 +N 提示可展开)
|
||||||
|
const hiddenChildCounts = computed(() => {
|
||||||
|
const { childrenOf } = catIndex.value;
|
||||||
|
const visible = visibleCatIds.value;
|
||||||
|
const map = new Map();
|
||||||
|
visible.forEach(id => {
|
||||||
|
const hidden = (childrenOf.get(id) || []).filter(k => !visible.has(k)).length;
|
||||||
|
if (hidden > 0) map.set(id, hidden);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 依据展开状态计算实际展示的节点与连线
|
||||||
|
const displayData = computed(() => {
|
||||||
|
const visible = visibleCatIds.value;
|
||||||
|
|
||||||
|
const docCat = new Map(); // 文档节点 → 所属分类 ID
|
||||||
|
links.value.forEach(l => {
|
||||||
|
if (
|
||||||
|
l.kind === "belong" &&
|
||||||
|
String(l.source).startsWith("d") &&
|
||||||
|
String(l.target).startsWith("c")
|
||||||
|
) {
|
||||||
|
docCat.set(l.source, Number(String(l.target).slice(1)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const catNodes = [];
|
||||||
|
const docNodes = [];
|
||||||
|
nodes.value.forEach(n => {
|
||||||
|
if (Number(n.category) >= 1) {
|
||||||
|
if (visible.has(Number(n.category_id))) catNodes.push(n);
|
||||||
|
} else if (filters.showDocs) {
|
||||||
|
const cid = docCat.get(n.id);
|
||||||
|
if (cid !== undefined && visible.has(cid)) docNodes.push(n);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const shown = new Set([...catNodes, ...docNodes].map(n => n.id));
|
||||||
|
const shownLinks = links.value.filter(
|
||||||
|
l => shown.has(l.source) && shown.has(l.target)
|
||||||
|
);
|
||||||
|
return { catNodes, docNodes, shownLinks };
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayNodes = computed(() => [
|
||||||
|
...displayData.value.catNodes,
|
||||||
|
...displayData.value.docNodes
|
||||||
|
]);
|
||||||
|
const displayLinks = computed(() => displayData.value.shownLinks);
|
||||||
|
|
||||||
function isDark() {
|
function isDark() {
|
||||||
return document.documentElement.classList.contains("dark");
|
return document.documentElement.classList.contains("dark");
|
||||||
}
|
}
|
||||||
@@ -196,20 +336,52 @@ function buildOption() {
|
|||||||
const subTextColor = dark ? "#a3a6ad" : "#909399";
|
const subTextColor = dark ? "#a3a6ad" : "#909399";
|
||||||
const axisColor = dark ? "#3a3a3a" : "#e0e6ed";
|
const axisColor = dark ? "#3a3a3a" : "#e0e6ed";
|
||||||
|
|
||||||
const data = nodes.value.map(n => {
|
const sourceNodes = displayNodes.value;
|
||||||
const isCategory = n.category === 1;
|
const sourceLinks = displayLinks.value;
|
||||||
const size = isCategory
|
const total = sourceNodes.length;
|
||||||
? 42
|
const dense = total > 160; // 节点较多时缩小节点,减少重叠
|
||||||
: Math.min(58, 22 + (n.degree || 0) * 5);
|
const childrenOf = catIndex.value.childrenOf;
|
||||||
|
const data = sourceNodes.map(n => {
|
||||||
|
const isCategory = Number(n.category) >= 1;
|
||||||
|
const isPrivateCat = Number(n.category) === 2;
|
||||||
|
const cid = Number(n.category_id);
|
||||||
|
const hasChildren =
|
||||||
|
isCategory && (childrenOf.get(cid) || []).length > 0;
|
||||||
|
const isExpanded = isCategory && expandedIds.value.has(cid);
|
||||||
|
const hidden = isCategory ? hiddenChildCounts.value.get(cid) || 0 : 0;
|
||||||
|
|
||||||
|
const base = dense ? 16 : 22;
|
||||||
|
const cap = dense ? 40 : 58;
|
||||||
|
let size = isCategory
|
||||||
|
? dense
|
||||||
|
? 34
|
||||||
|
: 42
|
||||||
|
: Math.min(cap, base + (n.degree || 0) * 5);
|
||||||
|
if (isCategory && hasChildren) size += 6; // 有子级的分类节点更大,便于点击展开
|
||||||
|
|
||||||
|
let displayName = n.name;
|
||||||
|
if (isCategory) {
|
||||||
|
const mark = hasChildren ? (isExpanded ? " ▾" : " ▸") : "";
|
||||||
|
const plus = hidden > 0 ? ` +${hidden}` : "";
|
||||||
|
const priv = isPrivateCat ? "私·" : "";
|
||||||
|
displayName = `${priv}${n.name}${mark}${plus}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const depth = isCategory ? catIndex.value.depthOf.get(cid) || 1 : 0;
|
||||||
|
const catColor = categoryColor(depth);
|
||||||
return {
|
return {
|
||||||
id: n.id,
|
id: n.id,
|
||||||
name: n.name,
|
name: n.name,
|
||||||
|
displayName,
|
||||||
category: n.category,
|
category: n.category,
|
||||||
symbolSize: size,
|
symbolSize: size,
|
||||||
value: n.value,
|
value: n.value,
|
||||||
// 供 tooltip / 点击回查
|
// 供 tooltip / 点击回查
|
||||||
docId: n.doc_id,
|
docId: n.doc_id,
|
||||||
categoryId: n.category_id,
|
categoryId: n.category_id,
|
||||||
|
hasChildren,
|
||||||
|
depth,
|
||||||
|
isPrivateCat,
|
||||||
docType: n.doc_type,
|
docType: n.doc_type,
|
||||||
status: n.status,
|
status: n.status,
|
||||||
ext: n.ext,
|
ext: n.ext,
|
||||||
@@ -218,15 +390,16 @@ function buildOption() {
|
|||||||
viewCount: n.view_count,
|
viewCount: n.view_count,
|
||||||
degree: n.degree,
|
degree: n.degree,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: isCategory ? "#e6a23c" : docTypeColor(n.doc_type),
|
// 分类按层级配色(1/2/3 级各不相同),文档按类型配色;纯色无边框
|
||||||
borderColor: dark ? "#1f1f1f" : "#fff",
|
color: isCategory ? catColor : docTypeColor(n.doc_type),
|
||||||
borderWidth: 2,
|
borderWidth: 0,
|
||||||
shadowBlur: 10,
|
shadowBlur: 8,
|
||||||
shadowColor: dark ? "rgba(0,0,0,0.5)" : "rgba(0,0,0,0.15)"
|
shadowColor: dark ? "rgba(0,0,0,0.4)" : "rgba(0,0,0,0.12)"
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: "right",
|
position: "right",
|
||||||
|
formatter: () => displayName,
|
||||||
fontSize: isCategory ? 13 : 12,
|
fontSize: isCategory ? 13 : 12,
|
||||||
fontWeight: isCategory ? 600 : 400,
|
fontWeight: isCategory ? 600 : 400,
|
||||||
color: textColor
|
color: textColor
|
||||||
@@ -234,14 +407,15 @@ function buildOption() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const edgeList = links.value.map(l => ({
|
const edgeList = sourceLinks.map(l => ({
|
||||||
source: l.source,
|
source: l.source,
|
||||||
target: l.target,
|
target: l.target,
|
||||||
value: l.label,
|
value: l.label,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: l.kind === "belong" ? 1 : 1.6,
|
width: l.kind === "doc" ? 1.6 : 1,
|
||||||
type: l.kind === "belong" ? "dashed" : "solid",
|
type:
|
||||||
opacity: l.kind === "belong" ? 0.45 : 0.8,
|
l.kind === "belong" ? "dashed" : l.kind === "cate" ? "dotted" : "solid",
|
||||||
|
opacity: l.kind === "doc" ? 0.8 : 0.45,
|
||||||
curveness: 0.12
|
curveness: 0.12
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -258,8 +432,14 @@ function buildOption() {
|
|||||||
return params.data?.value ? `关系:${params.data.value}` : "";
|
return params.data?.value ? `关系:${params.data.value}` : "";
|
||||||
}
|
}
|
||||||
const d = params.data;
|
const d = params.data;
|
||||||
if (d.category === 1) {
|
if (Number(d.category) >= 1) {
|
||||||
return `<b>${d.name}</b><br/>分类中心 · ${d.degree || 0} 个关联`;
|
const kind = Number(d.category) === 2 ? "私密分类" : "共享分类";
|
||||||
|
const tip = d.hasChildren
|
||||||
|
? `<br/>点击${d.isExpanded ? "收起" : "展开下级分类"}`
|
||||||
|
: "";
|
||||||
|
return `<b>${d.name}</b><br/>${d.depth || 1} 级分类 · ${kind} · ${
|
||||||
|
d.degree || 0
|
||||||
|
} 个关联${tip}`;
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
`<b>${d.name}</b>`,
|
`<b>${d.name}</b>`,
|
||||||
@@ -274,20 +454,32 @@ function buildOption() {
|
|||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
type: "graph",
|
type: "graph",
|
||||||
layout: "force",
|
layout: filters.layout,
|
||||||
|
// roam: 滚轮缩放 + 拖拽平移;按住空格时禁用节点拖拽,让左键拖拽整体平移
|
||||||
roam: true,
|
roam: true,
|
||||||
draggable: true,
|
scaleLimit: { min: 0.15, max: 6 },
|
||||||
|
draggable: !spaceHeld.value,
|
||||||
data,
|
data,
|
||||||
links: edgeList,
|
links: edgeList,
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "文档", itemStyle: { color: "#409eff" } },
|
{ name: "文档", itemStyle: { color: "#409eff" } },
|
||||||
{ name: "分类", itemStyle: { color: "#e6a23c" } }
|
{ name: "共享分类", itemStyle: { color: "#e6a23c" } },
|
||||||
|
{ name: "私密分类", itemStyle: { color: "#b37feb" } }
|
||||||
],
|
],
|
||||||
force: {
|
// 标签重叠时自动隐藏,避免文字糊在一起
|
||||||
repulsion: nodes.value.length > 200 ? 90 : 160,
|
labelLayout: { hideOverlap: true },
|
||||||
edgeLength: [70, 150],
|
force:
|
||||||
gravity: 0.08
|
filters.layout === "force"
|
||||||
},
|
? {
|
||||||
|
// 斥力随节点数放大,节点越多铺得越开,避免挤成一团
|
||||||
|
repulsion: Math.max(400, total * 14),
|
||||||
|
edgeLength: dense ? [140, 280] : [100, 200],
|
||||||
|
gravity: 0.06,
|
||||||
|
layoutAnimation: true
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
circular:
|
||||||
|
filters.layout === "circular" ? { rotateLabel: true } : undefined,
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: "adjacency",
|
focus: "adjacency",
|
||||||
scale: 1.15,
|
scale: 1.15,
|
||||||
@@ -302,32 +494,45 @@ function buildOption() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChart() {
|
function renderChart(reset = true) {
|
||||||
if (!chartRef.value) return;
|
if (!chartRef.value) return;
|
||||||
if (!chart) {
|
if (!chart) {
|
||||||
chart = echarts.init(chartRef.value);
|
chart = echarts.init(chartRef.value);
|
||||||
chart.on("click", handleChartClick);
|
chart.on("click", handleChartClick);
|
||||||
}
|
}
|
||||||
chart.setOption(buildOption(), true);
|
// 展开/收起时使用合并更新(reset=false),保留当前缩放与平移
|
||||||
|
chart.setOption(buildOption(), { notMerge: reset, lazyUpdate: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function fitView() {
|
function fitView() {
|
||||||
// 力导向图无内置"复位"动作,重新渲染即可回到初始布局与缩放
|
// 重新渲染回到初始布局与缩放
|
||||||
renderChart();
|
renderChart(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapseAll() {
|
||||||
|
expandedIds.value = new Set();
|
||||||
|
renderChart(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChartClick(params) {
|
function handleChartClick(params) {
|
||||||
if (params.dataType !== "node") return;
|
if (params.dataType !== "node") return;
|
||||||
openNode(params.data);
|
const d = params.data;
|
||||||
|
if (Number(d.category) >= 1) {
|
||||||
|
// 分类节点:有子级则展开 / 收起,展开后在该分支继续延展 3 级
|
||||||
|
const cid = Number(d.categoryId);
|
||||||
|
const kids = catIndex.value.childrenOf.get(cid) || [];
|
||||||
|
if (!kids.length) return;
|
||||||
|
const next = new Set(expandedIds.value);
|
||||||
|
if (next.has(cid)) next.delete(cid);
|
||||||
|
else next.add(cid);
|
||||||
|
expandedIds.value = next;
|
||||||
|
renderChart(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openNode(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNode(node) {
|
function openNode(node) {
|
||||||
if (Number(node.category) === 1) {
|
|
||||||
// 分类节点:按该分类过滤图谱
|
|
||||||
filters.category_id = node.categoryId ?? node.category_id ?? null;
|
|
||||||
loadGraph();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const docId = node.docId ?? node.doc_id;
|
const docId = node.docId ?? node.doc_id;
|
||||||
if (docId) {
|
if (docId) {
|
||||||
detailId.value = docId;
|
detailId.value = docId;
|
||||||
@@ -335,13 +540,71 @@ function openNode(node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCategories() {
|
// ---------------- 空格 + 鼠标左键拖拽平移 ----------------
|
||||||
const res = await getDocCategories();
|
function isTypingTarget(el) {
|
||||||
if (res?.code === 200) {
|
return (
|
||||||
categories.value = res.data || [];
|
!!el &&
|
||||||
|
(el.tagName === "INPUT" ||
|
||||||
|
el.tagName === "TEXTAREA" ||
|
||||||
|
el.isContentEditable === true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPanMode(on) {
|
||||||
|
if (chartRef.value) chartRef.value.style.cursor = on ? "grab" : "";
|
||||||
|
try {
|
||||||
|
// 空格按下时禁用节点拖拽,使左键拖拽变为整体平移
|
||||||
|
chart?.setOption({ series: [{ draggable: !on }] });
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSpaceDown(e) {
|
||||||
|
if (e.code !== "Space") return;
|
||||||
|
if (isTypingTarget(e.target)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (spaceHeld.value) return;
|
||||||
|
spaceHeld.value = true;
|
||||||
|
applyPanMode(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSpaceUp(e) {
|
||||||
|
if (e.code !== "Space") return;
|
||||||
|
spaceHeld.value = false;
|
||||||
|
applyPanMode(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCategories() {
|
||||||
|
// 图谱分类筛选需同时包含共享分类与当前用户的私密分类,按分组展示
|
||||||
|
const [sharedRes, personalRes] = await Promise.all([
|
||||||
|
getDocCategories({ scope: "shared" }),
|
||||||
|
getDocCategories({ scope: "personal" })
|
||||||
|
]);
|
||||||
|
const shared = sharedRes?.code === 200 ? sharedRes.data || [] : [];
|
||||||
|
const personal = personalRes?.code === 200 ? personalRes.data || [] : [];
|
||||||
|
sharedTree.value = shared;
|
||||||
|
personalTree.value = personal;
|
||||||
|
const groups = [];
|
||||||
|
if (shared.length) {
|
||||||
|
groups.push({
|
||||||
|
id: "grp-shared",
|
||||||
|
name: "共享分类",
|
||||||
|
disabled: true,
|
||||||
|
children: shared
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (personal.length) {
|
||||||
|
groups.push({
|
||||||
|
id: "grp-personal",
|
||||||
|
name: "私密分类",
|
||||||
|
disabled: true,
|
||||||
|
children: personal
|
||||||
|
});
|
||||||
|
}
|
||||||
|
categories.value = groups;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadGraph() {
|
async function loadGraph() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -375,8 +638,11 @@ function resetFilters() {
|
|||||||
filters.category_id = null;
|
filters.category_id = null;
|
||||||
filters.relation = "";
|
filters.relation = "";
|
||||||
filters.limit = 300;
|
filters.limit = 300;
|
||||||
|
filters.layout = "force";
|
||||||
filters.includeCategory = true;
|
filters.includeCategory = true;
|
||||||
filters.onlyLinked = false;
|
filters.onlyLinked = false;
|
||||||
|
filters.showDocs = true;
|
||||||
|
expandedIds.value = new Set();
|
||||||
loadGraph();
|
loadGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,22 +657,34 @@ function handleResize() {
|
|||||||
chart?.resize();
|
chart?.resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// "显示文档"开关即时生效(保留当前缩放/平移)
|
||||||
loadCategories();
|
watch(
|
||||||
loadGraph();
|
() => filters.showDocs,
|
||||||
|
() => renderChart(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 先加载分类树(分层展示依赖它),再加载图谱数据
|
||||||
|
await loadCategories();
|
||||||
|
await loadGraph();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (!chartRef.value) return;
|
if (!chartRef.value) return;
|
||||||
resizeObserver = new ResizeObserver(handleResize);
|
resizeObserver = new ResizeObserver(handleResize);
|
||||||
resizeObserver.observe(chartRef.value);
|
resizeObserver.observe(chartRef.value);
|
||||||
});
|
});
|
||||||
themeObserver = new MutationObserver(() => renderChart());
|
themeObserver = new MutationObserver(() => renderChart(false));
|
||||||
themeObserver.observe(document.documentElement, {
|
themeObserver.observe(document.documentElement, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ["class"]
|
attributeFilter: ["class"]
|
||||||
});
|
});
|
||||||
|
// 空格 + 左键拖拽平移
|
||||||
|
window.addEventListener("keydown", onSpaceDown);
|
||||||
|
window.addEventListener("keyup", onSpaceUp);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("keydown", onSpaceDown);
|
||||||
|
window.removeEventListener("keyup", onSpaceUp);
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
themeObserver?.disconnect();
|
themeObserver?.disconnect();
|
||||||
chart?.dispose();
|
chart?.dispose();
|
||||||
@@ -519,6 +797,10 @@ onBeforeUnmount(() => {
|
|||||||
&.dashed {
|
&.dashed {
|
||||||
border-top: 2px dashed #c0c4cc;
|
border-top: 2px dashed #c0c4cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.dotted {
|
||||||
|
border-top: 2px dotted #c0c4cc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-tip {
|
.legend-tip {
|
||||||
|
|||||||
@@ -391,8 +391,8 @@ async function loadDetail(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function categoryName(id) {
|
function categoryName(id) {
|
||||||
if (!id) return "未分类";
|
if (!id) return "-";
|
||||||
return findCategory(props.categories, id)?.name || "未分类";
|
return findCategory(props.categories, id)?.name || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function peer(row) {
|
function peer(row) {
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="isEdit ? '编辑文档' : '新建文档'"
|
||||||
|
class="doc-editor-dialog"
|
||||||
|
:class="{ 'is-fullscreen': fullscreen }"
|
||||||
|
width="78%"
|
||||||
|
top="5vh"
|
||||||
|
:fullscreen="fullscreen"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<template #header="{ titleId, titleClass }">
|
||||||
|
<div class="doc-editor-header">
|
||||||
|
<span :id="titleId" :class="titleClass">
|
||||||
|
{{ isEdit ? "编辑文档" : "新建文档" }}
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
:icon="FullScreen"
|
||||||
|
class="doc-editor-fs"
|
||||||
|
@click="fullscreen = !fullscreen"
|
||||||
|
>
|
||||||
|
{{ fullscreen ? "退出全屏" : "全屏" }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :model="form" label-width="72px" class="doc-editor-form">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="标题" required>
|
||||||
|
<el-input
|
||||||
|
v-model="form.title"
|
||||||
|
maxlength="120"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入文档标题"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="分类" required>
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.category_id"
|
||||||
|
:data="categories"
|
||||||
|
:props="{ label: 'name', children: 'children' }"
|
||||||
|
node-key="id"
|
||||||
|
check-strictly
|
||||||
|
clearable
|
||||||
|
placeholder="请选择分类"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="form.status" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="s in DOC_STATUS_OPTIONS"
|
||||||
|
:key="s.value"
|
||||||
|
:label="s.label"
|
||||||
|
:value="s.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="标签">
|
||||||
|
<el-select
|
||||||
|
v-model="form.tagList"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
placeholder="选择或输入标签"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option v-for="t in presetTags" :key="t" :label="t" :value="t" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="负责人">
|
||||||
|
<el-input v-model="form.owner_name" placeholder="负责人" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="可见性">
|
||||||
|
<el-radio-group v-model="form.visibility" :disabled="true">
|
||||||
|
<el-radio :value="0">公开</el-radio>
|
||||||
|
<el-radio :value="1">私密</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item label="摘要">
|
||||||
|
<el-input
|
||||||
|
v-model="form.summary"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="选填"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="正文">
|
||||||
|
<div
|
||||||
|
v-loading="contentLoading"
|
||||||
|
class="doc-editor-box"
|
||||||
|
:style="editorBoxStyle"
|
||||||
|
>
|
||||||
|
<UmoEditor v-if="ready" v-model="form.content" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="doc-editor-footer">
|
||||||
|
<span class="doc-editor-hint">
|
||||||
|
{{ props.scope === "personal" ? "保存为私密文档(绑定个人)" : "保存为共享文档(租户内可见)" }}
|
||||||
|
· 以 Word(docx)格式保存
|
||||||
|
</span>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||||
|
{{ isEdit ? "保存" : "创建" }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, watch } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { FullScreen } from "@element-plus/icons-vue";
|
||||||
|
import { uploadFile } from "@/api/file";
|
||||||
|
import { createDoc, getDocContent, updateDoc } from "@/api/oaDocument";
|
||||||
|
import { DOC_STATUS_OPTIONS, getFileUrl, parseTags } from "../../utils";
|
||||||
|
|
||||||
|
const DOCX_MIME =
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
scope: { type: String, default: "shared" },
|
||||||
|
categories: { type: Array, default: () => [] },
|
||||||
|
defaultCategoryId: { type: Number, default: 0 },
|
||||||
|
currentUserName: { type: String, default: "" },
|
||||||
|
// 传入则为编辑已有在线文档,否则为新建
|
||||||
|
doc: { type: Object, default: null }
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||||
|
|
||||||
|
const presetTags = ["制度", "流程", "模板", "合同", "培训", "报表", "归档"];
|
||||||
|
|
||||||
|
const visible = ref(props.modelValue);
|
||||||
|
const fullscreen = ref(false);
|
||||||
|
const ready = ref(false);
|
||||||
|
const contentLoading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
|
||||||
|
const isEdit = computed(() => !!props.doc?.id);
|
||||||
|
const lockedVisibility = computed(() => (props.scope === "personal" ? 1 : 0));
|
||||||
|
const editorBoxStyle = computed(() =>
|
||||||
|
fullscreen.value ? { height: "calc(100vh - 210px)" } : {}
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
title: "",
|
||||||
|
category_id: 0,
|
||||||
|
tagList: [],
|
||||||
|
status: 1,
|
||||||
|
visibility: 0,
|
||||||
|
owner_name: "",
|
||||||
|
summary: "",
|
||||||
|
content: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
val => {
|
||||||
|
visible.value = val;
|
||||||
|
if (val) open();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(visible, val => emit("update:modelValue", val));
|
||||||
|
|
||||||
|
async function open() {
|
||||||
|
fullscreen.value = false;
|
||||||
|
ready.value = false;
|
||||||
|
contentLoading.value = false;
|
||||||
|
Object.assign(form, {
|
||||||
|
title: props.doc?.title || "",
|
||||||
|
category_id: props.doc?.category_id || props.defaultCategoryId || 0,
|
||||||
|
tagList: parseTags(props.doc?.tags),
|
||||||
|
status: props.doc?.status ?? 1,
|
||||||
|
visibility: lockedVisibility.value,
|
||||||
|
owner_name: props.doc?.owner_name || props.currentUserName || "",
|
||||||
|
summary: props.doc?.summary || "",
|
||||||
|
content: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
contentLoading.value = true;
|
||||||
|
try {
|
||||||
|
let html = "";
|
||||||
|
const res = await getDocContent(props.doc.id);
|
||||||
|
if (res?.code === 200) html = res.data?.content || "";
|
||||||
|
// 兼容早期以 html 文件保存的在线文档
|
||||||
|
if (
|
||||||
|
!html &&
|
||||||
|
props.doc?.file_url &&
|
||||||
|
["html", "htm"].includes(String(props.doc.ext || "").toLowerCase())
|
||||||
|
) {
|
||||||
|
html = await fetchText(getFileUrl(props.doc.file_url));
|
||||||
|
}
|
||||||
|
form.content = absolutizeUrls(extractBody(html));
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error("文档内容加载失败,可重新编辑后覆盖保存");
|
||||||
|
} finally {
|
||||||
|
contentLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ready.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const title = String(form.title || "").trim();
|
||||||
|
if (!title) {
|
||||||
|
ElMessage.warning("请输入文档标题");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.category_id) {
|
||||||
|
ElMessage.warning("请选择所属分类");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasContent(form.content)) {
|
||||||
|
ElMessage.warning("请输入文档正文");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
// 图片地址转为绝对地址,便于导出 docx 时抓取
|
||||||
|
const html = absolutizeUrls(form.content);
|
||||||
|
|
||||||
|
let docxBlob;
|
||||||
|
try {
|
||||||
|
const { default: HTMLtoDOCX } = await import("@turbodocx/html-to-docx");
|
||||||
|
const out = await HTMLtoDOCX(html, null, {
|
||||||
|
font: "宋体",
|
||||||
|
fontSize: 24,
|
||||||
|
title,
|
||||||
|
creator: props.currentUserName || undefined,
|
||||||
|
footer: false,
|
||||||
|
pageNumber: false,
|
||||||
|
table: { row: { cantSplit: true } },
|
||||||
|
decodeUnicode: true
|
||||||
|
});
|
||||||
|
docxBlob = out instanceof Blob ? out : new Blob([out], { type: DOCX_MIME });
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error("生成 Word 文档失败:" + (e?.message || ""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = `${safeFileName(title)}.docx`;
|
||||||
|
const file = new File([docxBlob], fileName, { type: DOCX_MIME });
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", file);
|
||||||
|
fd.append("cate", "0");
|
||||||
|
const up = await uploadFile(fd);
|
||||||
|
const upData = up?.data || {};
|
||||||
|
if (up?.code !== 200 && up?.code !== 201) {
|
||||||
|
ElMessage.error(up?.msg || "文档保存失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
title,
|
||||||
|
category_id: form.category_id || 0,
|
||||||
|
tags: (form.tagList || []).join(","),
|
||||||
|
summary: form.summary || "",
|
||||||
|
status: form.status,
|
||||||
|
visibility: form.visibility,
|
||||||
|
owner_name: form.owner_name || "",
|
||||||
|
file_id: upData.id || 0,
|
||||||
|
file_url: upData.url || "",
|
||||||
|
file_name: fileName,
|
||||||
|
ext: "docx",
|
||||||
|
size: docxBlob.size,
|
||||||
|
content: html
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = isEdit.value
|
||||||
|
? await updateDoc(props.doc.id, payload)
|
||||||
|
: await createDoc(payload);
|
||||||
|
if (res?.code === 200) {
|
||||||
|
ElMessage.success(isEdit.value ? "已保存" : "创建成功");
|
||||||
|
visible.value = false;
|
||||||
|
emit("saved");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || "保存失败");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || "保存失败");
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchText(url) {
|
||||||
|
const resp = await fetch(url, { mode: "cors" });
|
||||||
|
if (!resp.ok) throw new Error(`请求失败(${resp.status})`);
|
||||||
|
return await resp.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断编辑器内容是否为空(去掉标签与空白后仍有字符) */
|
||||||
|
function hasContent(html) {
|
||||||
|
return (
|
||||||
|
String(html || "")
|
||||||
|
.replace(/<[^>]*>/g, "")
|
||||||
|
.replace(/ /gi, "")
|
||||||
|
.replace(/\s/g, "").length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从完整 HTML 文档中取出 body 内容,便于回填编辑器 */
|
||||||
|
function extractBody(html) {
|
||||||
|
const s = String(html || "");
|
||||||
|
if (!/<body[\s>]/i.test(s)) return s;
|
||||||
|
try {
|
||||||
|
return new DOMParser().parseFromString(s, "text/html").body.innerHTML;
|
||||||
|
} catch {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将相对图片地址补全为绝对地址(编辑器/导出 docx 都需要能直接访问) */
|
||||||
|
function absolutizeUrls(html) {
|
||||||
|
const s = String(html || "");
|
||||||
|
if (!s) return s;
|
||||||
|
const base = String(import.meta.env.VITE_API_BASE_URL || "").replace(/\/$/, "");
|
||||||
|
if (!base) return s;
|
||||||
|
try {
|
||||||
|
const doc = new DOMParser().parseFromString(s, "text/html");
|
||||||
|
doc.querySelectorAll("img").forEach(img => {
|
||||||
|
const src = img.getAttribute("src") || "";
|
||||||
|
if (src && !/^(https?:|data:|blob:)/i.test(src)) {
|
||||||
|
img.setAttribute("src", base + (src.startsWith("/") ? src : "/" + src));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return doc.body.innerHTML;
|
||||||
|
} catch {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeFileName(name) {
|
||||||
|
return String(name || "文档")
|
||||||
|
.replace(/[\\/:*?"<>|]/g, "_")
|
||||||
|
.slice(0, 80);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.doc-editor-dialog :deep(.el-dialog__body) {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-editor-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-editor-fs {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-editor-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 460px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-editor-box :deep(.umo-editor),
|
||||||
|
.doc-editor-box :deep(umo-editor) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-editor-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-editor-hint {
|
||||||
|
margin-right: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,543 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
class="doc-preview-dialog"
|
||||||
|
width="82%"
|
||||||
|
top="5vh"
|
||||||
|
:fullscreen="fullscreen"
|
||||||
|
destroy-on-close
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<template #header="{ titleId, titleClass }">
|
||||||
|
<div class="pv-header">
|
||||||
|
<span :id="titleId" :class="titleClass">{{ dialogTitle }}</span>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
:icon="fullscreen ? Close : FullScreen"
|
||||||
|
class="pv-fs"
|
||||||
|
@click="fullscreen = !fullscreen"
|
||||||
|
>
|
||||||
|
{{ fullscreen ? "退出全屏" : "全屏" }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="pv-toolbar">
|
||||||
|
<div class="pv-title">
|
||||||
|
<el-tag size="small" effect="plain">{{ kindLabel }}</el-tag>
|
||||||
|
<span class="pv-name" :title="fileName">{{ fileName }}</span>
|
||||||
|
<span v-if="fileSize" class="pv-size">{{ fileSize }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="pv-ops">
|
||||||
|
<el-button
|
||||||
|
v-if="kind === 'image'"
|
||||||
|
size="small"
|
||||||
|
:icon="ZoomIn"
|
||||||
|
@click="toggleImageFit"
|
||||||
|
>{{ imageFit === "original" ? "适应宽度" : "原始大小" }}</el-button
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:icon="RefreshRight"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="reload"
|
||||||
|
>重新加载</el-button
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:icon="Download"
|
||||||
|
@click="emit('download', doc)"
|
||||||
|
>下载</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="正在加载文档..."
|
||||||
|
class="pv-stage"
|
||||||
|
:style="stageStyle"
|
||||||
|
>
|
||||||
|
<template v-if="!loading">
|
||||||
|
<!-- 加载失败:引导下载 -->
|
||||||
|
<el-empty v-if="error" description="无法在线预览">
|
||||||
|
<p class="pv-tip">{{ error }}</p>
|
||||||
|
<el-button type="primary" :icon="Download" @click="emit('download', doc)"
|
||||||
|
>下载后查看</el-button
|
||||||
|
>
|
||||||
|
</el-empty>
|
||||||
|
|
||||||
|
<!-- 图片:默认适应宽度,可切原始大小;超出容器时滚动查看 -->
|
||||||
|
<img
|
||||||
|
v-else-if="kind === 'image'"
|
||||||
|
:src="fileUrl"
|
||||||
|
class="pv-image"
|
||||||
|
:style="imageStyle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- PDF:交给浏览器内置阅读器 -->
|
||||||
|
<iframe
|
||||||
|
v-else-if="kind === 'pdf'"
|
||||||
|
:src="fileUrl"
|
||||||
|
class="pv-frame"
|
||||||
|
frameborder="0"
|
||||||
|
:style="frameStyle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 视频 -->
|
||||||
|
<video
|
||||||
|
v-else-if="kind === 'video'"
|
||||||
|
:src="fileUrl"
|
||||||
|
controls
|
||||||
|
class="pv-media"
|
||||||
|
:style="mediaStyle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 音频 -->
|
||||||
|
<audio
|
||||||
|
v-else-if="kind === 'audio'"
|
||||||
|
:src="fileUrl"
|
||||||
|
controls
|
||||||
|
class="pv-audio"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Word(docx) -->
|
||||||
|
<div v-else-if="kind === 'docx'" ref="docxBox" class="pv-docx"></div>
|
||||||
|
|
||||||
|
<!-- Excel / CSV -->
|
||||||
|
<div v-else-if="kind === 'sheet'" class="pv-sheet">
|
||||||
|
<div v-for="s in sheets" :key="s.name" class="pv-sheet-item">
|
||||||
|
<div class="pv-sheet-name">{{ s.name }}</div>
|
||||||
|
<div class="pv-sheet-html" v-html="s.html"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 纯文本 -->
|
||||||
|
<pre v-else-if="kind === 'text'" class="pv-text">{{ textContent }}</pre>
|
||||||
|
|
||||||
|
<!-- Markdown / HTML -->
|
||||||
|
<div v-else-if="kind === 'html'" class="pv-html" v-html="safeHtml"></div>
|
||||||
|
|
||||||
|
<!-- 其他类型 -->
|
||||||
|
<el-empty v-else :description="`暂不支持在线预览 .${ext || ''} 文件`">
|
||||||
|
<p class="pv-tip">该格式无法在浏览器中安全渲染,请下载后使用本地软件打开。</p>
|
||||||
|
<el-button type="primary" :icon="Download" @click="emit('download', doc)"
|
||||||
|
>下载后查看</el-button
|
||||||
|
>
|
||||||
|
</el-empty>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, ref, watch } from "vue";
|
||||||
|
import {
|
||||||
|
Close,
|
||||||
|
Download,
|
||||||
|
FullScreen,
|
||||||
|
RefreshRight,
|
||||||
|
ZoomIn
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
import { formatFileSize, getFileUrl } from "../../utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
doc: { type: Object, default: null }
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["update:modelValue", "download"]);
|
||||||
|
|
||||||
|
const visible = ref(props.modelValue);
|
||||||
|
const fullscreen = ref(false);
|
||||||
|
const imageFit = ref("width"); // width=适应宽度;original=原始大小
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref("");
|
||||||
|
const textContent = ref("");
|
||||||
|
const safeHtml = ref("");
|
||||||
|
const sheets = ref([]);
|
||||||
|
const docxBox = ref(null);
|
||||||
|
|
||||||
|
// 全屏时让内容区自适应可视区域
|
||||||
|
const stageStyle = computed(() =>
|
||||||
|
fullscreen.value ? { maxHeight: "calc(100vh - 200px)" } : {}
|
||||||
|
);
|
||||||
|
const frameStyle = computed(() =>
|
||||||
|
fullscreen.value ? { height: "calc(100vh - 250px)" } : {}
|
||||||
|
);
|
||||||
|
const mediaStyle = computed(() =>
|
||||||
|
fullscreen.value ? { maxHeight: "calc(100vh - 250px)" } : {}
|
||||||
|
);
|
||||||
|
// 图片:适应宽度时按宽度等比缩放;原始大小时保持原尺寸,超出由容器滚动查看
|
||||||
|
const imageStyle = computed(() =>
|
||||||
|
imageFit.value === "original"
|
||||||
|
? { maxWidth: "none", width: "auto", height: "auto" }
|
||||||
|
: { maxWidth: "100%", width: "auto", height: "auto" }
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleImageFit() {
|
||||||
|
imageFit.value = imageFit.value === "original" ? "width" : "original";
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = computed(() => String(props.doc?.ext || "").toLowerCase());
|
||||||
|
const fileUrl = computed(() => getFileUrl(props.doc?.file_url || ""));
|
||||||
|
const fileName = computed(
|
||||||
|
() => props.doc?.file_name || props.doc?.title || "文档"
|
||||||
|
);
|
||||||
|
const dialogTitle = computed(() => props.doc?.title || fileName.value);
|
||||||
|
const fileSize = computed(() =>
|
||||||
|
props.doc?.size ? formatFileSize(props.doc.size) : ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const IMAGE_EXT = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg", "ico"];
|
||||||
|
const VIDEO_EXT = ["mp4", "webm", "ogv", "mov", "m4v", "avi", "mkv"];
|
||||||
|
const AUDIO_EXT = ["mp3", "wav", "ogg", "m4a", "aac", "flac"];
|
||||||
|
const SHEET_EXT = ["xls", "xlsx", "csv"];
|
||||||
|
const TEXT_EXT = [
|
||||||
|
"txt",
|
||||||
|
"log",
|
||||||
|
"json",
|
||||||
|
"xml",
|
||||||
|
"yml",
|
||||||
|
"yaml",
|
||||||
|
"ini",
|
||||||
|
"conf",
|
||||||
|
"js",
|
||||||
|
"ts",
|
||||||
|
"css",
|
||||||
|
"sql",
|
||||||
|
"sh"
|
||||||
|
];
|
||||||
|
const HTML_EXT = ["md", "markdown", "html", "htm"];
|
||||||
|
|
||||||
|
const kind = computed(() => {
|
||||||
|
const e = ext.value;
|
||||||
|
if (!e) return "unknown";
|
||||||
|
if (IMAGE_EXT.includes(e)) return "image";
|
||||||
|
if (e === "pdf") return "pdf";
|
||||||
|
if (VIDEO_EXT.includes(e)) return "video";
|
||||||
|
if (AUDIO_EXT.includes(e)) return "audio";
|
||||||
|
if (e === "docx") return "docx";
|
||||||
|
if (SHEET_EXT.includes(e)) return "sheet";
|
||||||
|
if (e === "md" || e === "markdown" || e === "html" || e === "htm")
|
||||||
|
return "html";
|
||||||
|
if (TEXT_EXT.includes(e)) return "text";
|
||||||
|
return "unknown";
|
||||||
|
});
|
||||||
|
|
||||||
|
const kindLabel = computed(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
image: "图片",
|
||||||
|
pdf: "PDF",
|
||||||
|
video: "视频",
|
||||||
|
audio: "音频",
|
||||||
|
docx: "Word",
|
||||||
|
sheet: "表格",
|
||||||
|
html: "文本",
|
||||||
|
text: "文本",
|
||||||
|
unknown: "文件"
|
||||||
|
})[kind.value] || "文件"
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
val => {
|
||||||
|
visible.value = val;
|
||||||
|
if (val) {
|
||||||
|
fullscreen.value = false;
|
||||||
|
imageFit.value = "width";
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(visible, val => emit("update:modelValue", val));
|
||||||
|
// 弹窗已打开时切换文档,重新加载预览内容
|
||||||
|
watch(
|
||||||
|
() => props.doc,
|
||||||
|
() => {
|
||||||
|
if (visible.value) start();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
error.value = "";
|
||||||
|
textContent.value = "";
|
||||||
|
safeHtml.value = "";
|
||||||
|
sheets.value = [];
|
||||||
|
if (docxBox.value) docxBox.value.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
reset();
|
||||||
|
if (!fileUrl.value) {
|
||||||
|
error.value = "该文档没有可预览的附件";
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 图片 / PDF / 音视频由浏览器直接渲染,无需预取
|
||||||
|
if (["image", "pdf", "video", "audio", "unknown"].includes(kind.value)) {
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (kind.value === "docx") {
|
||||||
|
const buf = await fetchBuffer(fileUrl.value);
|
||||||
|
loading.value = false;
|
||||||
|
await nextTick();
|
||||||
|
const { renderAsync } = await import("docx-preview");
|
||||||
|
await renderAsync(buf, docxBox.value, null, {
|
||||||
|
inWrapper: true,
|
||||||
|
ignoreWidth: false,
|
||||||
|
ignoreHeight: false,
|
||||||
|
breakPages: true,
|
||||||
|
useBase64URL: true
|
||||||
|
});
|
||||||
|
} else if (kind.value === "sheet") {
|
||||||
|
const buf = await fetchBuffer(fileUrl.value);
|
||||||
|
const XLSX = await import("xlsx");
|
||||||
|
const wb = XLSX.read(buf, { type: "array" });
|
||||||
|
sheets.value = (wb.SheetNames || []).map(name => ({
|
||||||
|
name,
|
||||||
|
html: tableOnly(XLSX.utils.sheet_to_html(wb.Sheets[name]))
|
||||||
|
}));
|
||||||
|
loading.value = false;
|
||||||
|
} else if (kind.value === "html") {
|
||||||
|
const txt = await fetchText(fileUrl.value);
|
||||||
|
safeHtml.value =
|
||||||
|
ext.value === "md" || ext.value === "markdown"
|
||||||
|
? sanitize((await import("marked")).marked.parse(txt))
|
||||||
|
: sanitize(txt);
|
||||||
|
loading.value = false;
|
||||||
|
} else if (kind.value === "text") {
|
||||||
|
textContent.value = await fetchText(fileUrl.value);
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e?.message ? `加载失败:${e.message}` : "加载失败";
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBuffer(url) {
|
||||||
|
const resp = await fetch(url, { mode: "cors" });
|
||||||
|
if (!resp.ok) throw new Error(`请求失败(${resp.status})`);
|
||||||
|
return await resp.arrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchText(url) {
|
||||||
|
const resp = await fetch(url, { mode: "cors" });
|
||||||
|
if (!resp.ok) throw new Error(`请求失败(${resp.status})`);
|
||||||
|
return await resp.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SheetJS 导出的 HTML 可能含 <html>/<body> 外壳,这里只保留 <table> */
|
||||||
|
function tableOnly(html) {
|
||||||
|
const m = /<table[\s\S]*<\/table>/i.exec(String(html || ""));
|
||||||
|
return m ? m[0] : html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 轻量 HTML 清洗:移除脚本/内联事件等危险内容,避免上传文档造成 XSS */
|
||||||
|
function sanitize(html) {
|
||||||
|
const doc = new DOMParser().parseFromString(String(html || ""), "text/html");
|
||||||
|
doc
|
||||||
|
.querySelectorAll("script,iframe,object,embed,link,style,meta,base")
|
||||||
|
.forEach(n => n.remove());
|
||||||
|
doc.querySelectorAll("*").forEach(el => {
|
||||||
|
[...el.attributes].forEach(attr => {
|
||||||
|
const name = attr.name.toLowerCase();
|
||||||
|
const val = String(attr.value || "")
|
||||||
|
.replace(/\s+/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
if (name.startsWith("on")) el.removeAttribute(attr.name);
|
||||||
|
if (
|
||||||
|
(name === "href" || name === "src" || name === "xlink:href") &&
|
||||||
|
val.startsWith("javascript:")
|
||||||
|
) {
|
||||||
|
el.removeAttribute(attr.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return doc.body.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.doc-preview-dialog :deep(.el-dialog__body) {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-fs {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 500;
|
||||||
|
max-width: 46vw;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-size {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-stage {
|
||||||
|
min-height: 260px;
|
||||||
|
max-height: 74vh;
|
||||||
|
overflow: auto;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-tip {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-image {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
/* 实际尺寸由内联样式控制(适应宽度 / 原始大小),超出时由 .pv-stage 滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 70vh;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-media {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-audio {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-docx {
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-docx :deep(.docx-wrapper) {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-sheet {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-sheet-item + .pv-sheet-item {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-sheet-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-sheet-html {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-sheet-html :deep(table) {
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-sheet-html :deep(td),
|
||||||
|
.pv-sheet-html :deep(th) {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
padding: 2px 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-text {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
font-family: Consolas, Monaco, "Courier New", monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-html {
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-html :deep(img) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-html :deep(table) {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-html :deep(td),
|
||||||
|
.pv-html :deep(th) {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
node-key="id"
|
node-key="id"
|
||||||
check-strictly
|
check-strictly
|
||||||
clearable
|
clearable
|
||||||
placeholder="未分类"
|
placeholder="请选择分类"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -36,12 +36,12 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="可见性">
|
<el-form-item label="可见性">
|
||||||
<el-radio-group v-model="form.visibility">
|
<el-radio-group v-model="form.visibility" :disabled="true">
|
||||||
<el-radio :value="0">租户公开</el-radio>
|
<el-radio :value="0">租户公开(共享文档)</el-radio>
|
||||||
<el-radio :value="1">私密</el-radio>
|
<el-radio :value="1">私密(私密文档)</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<div class="el-upload__tip">
|
<div class="el-upload__tip">
|
||||||
私密文档仅本人及被共享的用户/部门可见;设为私密后可在详情中设置共享范围。
|
可见性由当前所在空间决定(共享文档=公开,私密文档=仅自己及被共享者),如需改变空间请重新上传。
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="版本号">
|
<el-form-item label="版本号">
|
||||||
@@ -79,7 +79,9 @@ import { parseTags } from "../../utils";
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Boolean, default: false },
|
modelValue: { type: Boolean, default: false },
|
||||||
doc: { type: Object, default: null },
|
doc: { type: Object, default: null },
|
||||||
categories: { type: Array, default: () => [] }
|
categories: { type: Array, default: () => [] },
|
||||||
|
// 当前文档空间:shared=共享文档 personal=私密文档(决定可见性,且分类树已隔离)
|
||||||
|
scope: { type: String, default: "shared" }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue", "saved"]);
|
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||||
@@ -107,7 +109,7 @@ watch(
|
|||||||
category_id: props.doc.category_id || 0,
|
category_id: props.doc.category_id || 0,
|
||||||
tagList: parseTags(props.doc.tags),
|
tagList: parseTags(props.doc.tags),
|
||||||
status: props.doc.status ?? 0,
|
status: props.doc.status ?? 0,
|
||||||
visibility: props.doc.visibility ?? 0,
|
visibility: props.scope === "personal" ? 1 : 0,
|
||||||
version: props.doc.version || 1,
|
version: props.doc.version || 1,
|
||||||
owner_name: props.doc.owner_name || "",
|
owner_name: props.doc.owner_name || "",
|
||||||
summary: props.doc.summary || ""
|
summary: props.doc.summary || ""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="上传文档"
|
title="上传文档"
|
||||||
width="620px"
|
width="720px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@closed="handleClosed"
|
@closed="handleClosed"
|
||||||
>
|
>
|
||||||
@@ -11,41 +11,86 @@
|
|||||||
<el-upload
|
<el-upload
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
drag
|
drag
|
||||||
|
multiple
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:limit="1"
|
:show-file-list="false"
|
||||||
:file-list="fileList"
|
:on-change="handleChange"
|
||||||
:on-change="handleFileChange"
|
|
||||||
:on-remove="handleFileRemove"
|
|
||||||
:on-exceed="handleExceed"
|
|
||||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.md,.csv,.zip,.rar,.7z,.jpg,.jpeg,.png,.gif,.webp,.mp4,.mp3"
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.md,.csv,.zip,.rar,.7z,.jpg,.jpeg,.png,.gif,.webp,.mp4,.mp3"
|
||||||
>
|
>
|
||||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||||
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
<div class="el-upload__text">
|
||||||
|
将文件拖到此处,或<em>点击选择</em>(支持批量)
|
||||||
|
</div>
|
||||||
<template #tip>
|
<template #tip>
|
||||||
<div class="el-upload__tip">
|
<div class="el-upload__tip">
|
||||||
支持 PDF / Office / 图片 / 压缩包等,单个文件不超过 200MB
|
支持 PDF / Office / 图片 / 压缩包等,单个文件不超过 200MB;文档标题默认取文件名(不含扩展名),可在下方列表修改
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<el-progress
|
|
||||||
v-if="uploading"
|
|
||||||
:percentage="uploadPercent"
|
|
||||||
:stroke-width="14"
|
|
||||||
striped
|
|
||||||
style="margin-top: 10px"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="文档标题">
|
<el-form-item v-if="fileList.length" label="文件列表">
|
||||||
<el-input
|
<el-table
|
||||||
v-model="form.title"
|
:data="fileList"
|
||||||
maxlength="120"
|
size="small"
|
||||||
show-word-limit
|
border
|
||||||
placeholder="留空则使用文件名"
|
max-height="240"
|
||||||
/>
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column label="文档标题" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input
|
||||||
|
v-model="row.title"
|
||||||
|
size="small"
|
||||||
|
maxlength="120"
|
||||||
|
placeholder="默认使用文件名"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="文件名" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.name }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="大小" width="100">
|
||||||
|
<template #default="{ row }">{{
|
||||||
|
formatFileSize(row.size || row.raw?.size)
|
||||||
|
}}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="70">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
:disabled="uploading"
|
||||||
|
@click="removeFile($index)"
|
||||||
|
>移除</el-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="upload-tip">
|
||||||
|
共 {{ fileList.length }} 个文件
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:disabled="uploading"
|
||||||
|
@click="clearFiles"
|
||||||
|
>清空</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="所属分类">
|
<el-form-item v-if="uploading || uploadPercent > 0" label="进度">
|
||||||
|
<div style="width: 100%">
|
||||||
|
<el-progress
|
||||||
|
:percentage="uploadPercent"
|
||||||
|
:stroke-width="14"
|
||||||
|
striped
|
||||||
|
/>
|
||||||
|
<div class="upload-tip">{{ progressText }}</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="所属分类" required>
|
||||||
<el-tree-select
|
<el-tree-select
|
||||||
v-model="form.category_id"
|
v-model="form.category_id"
|
||||||
:data="categoryTree"
|
:data="categoryTree"
|
||||||
@@ -53,9 +98,12 @@
|
|||||||
node-key="id"
|
node-key="id"
|
||||||
check-strictly
|
check-strictly
|
||||||
clearable
|
clearable
|
||||||
placeholder="未分类"
|
placeholder="请选择分类(应用于全部文件)"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="!categoryTree.length" class="upload-tip">
|
||||||
|
暂无分类,请先在左侧「文档分类」中新建分类后再上传
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="标签">
|
<el-form-item label="标签">
|
||||||
@@ -65,7 +113,7 @@
|
|||||||
filterable
|
filterable
|
||||||
allow-create
|
allow-create
|
||||||
default-first-option
|
default-first-option
|
||||||
placeholder="输入后回车可新建标签"
|
placeholder="输入后回车可新建标签(应用于全部文件)"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option v-for="t in presetTags" :key="t" :label="t" :value="t" />
|
<el-option v-for="t in presetTags" :key="t" :label="t" :value="t" />
|
||||||
@@ -81,12 +129,18 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="可见性">
|
<el-form-item label="可见性">
|
||||||
<el-radio-group v-model="form.visibility">
|
<el-radio-group v-model="form.visibility" :disabled="true">
|
||||||
<el-radio :value="0">租户公开</el-radio>
|
<el-radio :value="0">团队公开(共享文档)</el-radio>
|
||||||
<el-radio :value="1">私密</el-radio>
|
<el-radio :value="1">私密(仅自己及被共享者可见)</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<div class="el-upload__tip">
|
<div class="el-upload__tip">
|
||||||
私密文档仅本人及被共享的用户/部门可见;设为私密后可在详情中设置共享范围。
|
当前所在空间已决定归属:
|
||||||
|
<template v-if="props.scope === 'personal'"
|
||||||
|
>私密文档绑定到你的个人空间,仅自己及被显式共享的用户/部门可见,可在详情中设置共享范围。</template
|
||||||
|
>
|
||||||
|
<template v-else
|
||||||
|
>共享文档对租户内所有人可见,归属到下方所选的共享分类。</template
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -102,22 +156,22 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="form.summary"
|
v-model="form.summary"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="3"
|
:rows="2"
|
||||||
maxlength="500"
|
maxlength="500"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
placeholder="选填,简要描述文档内容"
|
placeholder="选填,简要描述文档内容(应用于全部文件)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button :disabled="uploading" @click="visible = false">取消</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="submitting"
|
:loading="submitting"
|
||||||
:disabled="uploading"
|
:disabled="uploading || !fileList.length"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
>保存</el-button
|
>保存{{ fileList.length ? `(${fileList.length})` : "" }}</el-button
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -129,29 +183,36 @@ import { ElMessage } from "element-plus";
|
|||||||
import { UploadFilled } from "@element-plus/icons-vue";
|
import { UploadFilled } from "@element-plus/icons-vue";
|
||||||
import { uploadFile } from "@/api/file";
|
import { uploadFile } from "@/api/file";
|
||||||
import { createDoc } from "@/api/oaDocument";
|
import { createDoc } from "@/api/oaDocument";
|
||||||
|
import { formatFileSize } from "../../utils";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Boolean, default: false },
|
modelValue: { type: Boolean, default: false },
|
||||||
categories: { type: Array, default: () => [] },
|
categories: { type: Array, default: () => [] },
|
||||||
defaultCategoryId: { type: Number, default: 0 },
|
defaultCategoryId: { type: Number, default: 0 },
|
||||||
currentUserName: { type: String, default: "" }
|
currentUserName: { type: String, default: "" },
|
||||||
|
// 当前文档空间:shared=共享文档(固定公开)personal=私密文档(固定私密,绑定用户级别)
|
||||||
|
scope: { type: String, default: "shared" },
|
||||||
|
// 默认可见性:0-租户公开(共享文档)1-私密(个人文档)
|
||||||
|
defaultVisibility: { type: Number, default: 0 }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue", "saved"]);
|
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||||
|
|
||||||
|
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
|
||||||
|
const MAX_COUNT = 100;
|
||||||
|
|
||||||
const treeProps = { label: "name", children: "children" };
|
const treeProps = { label: "name", children: "children" };
|
||||||
const presetTags = ["制度", "流程", "模板", "合同", "培训", "报表", "归档"];
|
const presetTags = ["制度", "流程", "模板", "合同", "培训", "报表", "归档"];
|
||||||
|
|
||||||
const visible = ref(props.modelValue);
|
const visible = ref(props.modelValue);
|
||||||
const uploadRef = ref(null);
|
const uploadRef = ref(null);
|
||||||
const fileList = ref([]);
|
const fileList = ref([]);
|
||||||
const rawFile = ref(null);
|
|
||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
const uploadPercent = ref(0);
|
const uploadPercent = ref(0);
|
||||||
|
const progressText = ref("");
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
title: "",
|
|
||||||
category_id: 0,
|
category_id: 0,
|
||||||
tagList: [],
|
tagList: [],
|
||||||
status: 1,
|
status: 1,
|
||||||
@@ -161,119 +222,187 @@ const form = ref({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const categoryTree = computed(() => props.categories || []);
|
const categoryTree = computed(() => props.categories || []);
|
||||||
|
// 可见性随空间锁定:共享空间固定公开,私密空间固定私密(分类树已按空间隔离)
|
||||||
|
const lockedVisibility = computed(() =>
|
||||||
|
props.scope === "personal" ? 1 : 0
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
val => {
|
val => {
|
||||||
visible.value = val;
|
visible.value = val;
|
||||||
if (val) {
|
if (val) {
|
||||||
|
fileList.value = [];
|
||||||
|
uploadPercent.value = 0;
|
||||||
|
progressText.value = "";
|
||||||
form.value = {
|
form.value = {
|
||||||
title: "",
|
|
||||||
category_id: props.defaultCategoryId || 0,
|
category_id: props.defaultCategoryId || 0,
|
||||||
tagList: [],
|
tagList: [],
|
||||||
status: 1,
|
status: 1,
|
||||||
visibility: 0,
|
visibility: lockedVisibility.value,
|
||||||
owner_name: props.currentUserName || "",
|
owner_name: props.currentUserName || "",
|
||||||
summary: ""
|
summary: ""
|
||||||
};
|
};
|
||||||
uploadPercent.value = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(visible, val => emit("update:modelValue", val));
|
watch(visible, val => emit("update:modelValue", val));
|
||||||
|
|
||||||
function handleFileChange(file) {
|
// 选择文件后加入批量列表;标题默认取文件名(不含扩展名),可在此修改
|
||||||
rawFile.value = file.raw || file;
|
function handleChange(file) {
|
||||||
fileList.value = [file];
|
if (!file) return;
|
||||||
if (!form.value.title) {
|
if (fileList.value.some(f => f.uid === file.uid)) return;
|
||||||
form.value.title = String(file.name || "").replace(/\.[^.]+$/, "");
|
fileList.value.push({
|
||||||
}
|
uid: file.uid,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size || file.raw?.size || 0,
|
||||||
|
raw: file.raw || file,
|
||||||
|
title: String(file.name || "").replace(/\.[^.]+$/, "")
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileRemove() {
|
function removeFile(index) {
|
||||||
rawFile.value = null;
|
fileList.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFiles() {
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
|
uploadRef.value?.clearFiles?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExceed() {
|
function buildFormData(raw) {
|
||||||
ElMessage.warning("单次只能上传 1 个文件");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFormData() {
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("file", rawFile.value);
|
fd.append("file", raw);
|
||||||
fd.append("cate", "0");
|
fd.append("cate", "0");
|
||||||
fd.append("scope", "tenant");
|
|
||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!rawFile.value) {
|
const files = fileList.value.slice();
|
||||||
|
if (!files.length) {
|
||||||
ElMessage.warning("请选择要上传的文件");
|
ElMessage.warning("请选择要上传的文件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rawFile.value.size > 200 * 1024 * 1024) {
|
if (files.length > MAX_COUNT) {
|
||||||
ElMessage.warning("单个文件不能超过 200MB");
|
ElMessage.warning(`单次最多上传 ${MAX_COUNT} 个文件`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oversize = files.filter(
|
||||||
|
f => (f.size || f.raw?.size || 0) > MAX_SIZE
|
||||||
|
);
|
||||||
|
if (oversize.length) {
|
||||||
|
ElMessage.warning(
|
||||||
|
`以下文件超过 200MB:${oversize
|
||||||
|
.map(f => f.name)
|
||||||
|
.slice(0, 3)
|
||||||
|
.join("、")}${oversize.length > 3 ? " 等" : ""}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 文档必须归属分类:未选择分类(或还没有分类)时不允许上传
|
||||||
|
if (!categoryTree.value.length) {
|
||||||
|
ElMessage.warning("暂无文档分类,请先在左侧「文档分类」中新建分类");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.value.category_id) {
|
||||||
|
ElMessage.warning("请先选择所属分类");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
uploading.value = true;
|
uploading.value = true;
|
||||||
uploadPercent.value = 0;
|
uploadPercent.value = 0;
|
||||||
try {
|
|
||||||
const upRes = await uploadFile(buildFormData(), {
|
|
||||||
onUploadProgress: e => {
|
|
||||||
if (!e?.total) return;
|
|
||||||
uploadPercent.value = Math.min(
|
|
||||||
99,
|
|
||||||
Math.round((e.loaded * 100) / e.total)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 秒传命中时返回 code=201,同样携带 url/id/name
|
|
||||||
const upData = upRes?.data || {};
|
|
||||||
if (upRes?.code !== 200 && upRes?.code !== 201) {
|
|
||||||
ElMessage.error(upRes?.msg || "文件上传失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uploadPercent.value = 100;
|
|
||||||
|
|
||||||
const name = upData.name || rawFile.value.name || "";
|
const total = files.length;
|
||||||
const ext = name.includes(".") ? name.split(".").pop() : "";
|
const failures = [];
|
||||||
const res = await createDoc({
|
let okCount = 0;
|
||||||
title: form.value.title?.trim() || name.replace(/\.[^.]+$/, ""),
|
|
||||||
category_id: form.value.category_id || 0,
|
try {
|
||||||
file_id: upData.id || 0,
|
for (let i = 0; i < total; i++) {
|
||||||
file_url: upData.url || "",
|
const f = files[i];
|
||||||
file_name: name,
|
const raw = f.raw || f;
|
||||||
ext,
|
const base = (i / total) * 100;
|
||||||
size: rawFile.value.size || 0,
|
const span = 100 / total;
|
||||||
tags: form.value.tagList.join(","),
|
progressText.value = `正在上传 ${i + 1}/${total}:${f.name}`;
|
||||||
summary: form.value.summary || "",
|
|
||||||
status: form.value.status,
|
try {
|
||||||
visibility: form.value.visibility || 0,
|
const upRes = await uploadFile(buildFormData(raw), {
|
||||||
owner_name: form.value.owner_name || ""
|
onUploadProgress: e => {
|
||||||
});
|
if (!e?.total) return;
|
||||||
if (res?.code === 200) {
|
const p = Math.min(99, Math.round((e.loaded * 100) / e.total));
|
||||||
ElMessage.success("上传成功");
|
uploadPercent.value = Math.min(
|
||||||
|
100,
|
||||||
|
Math.round(base + (span * p) / 100)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 秒传命中时返回 code=201,同样携带 url/id/name
|
||||||
|
const upData = upRes?.data || {};
|
||||||
|
if (upRes?.code !== 200 && upRes?.code !== 201) {
|
||||||
|
failures.push(`${f.name}:${upRes?.msg || "上传失败"}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = upData.name || raw.name || "";
|
||||||
|
const ext = name.includes(".") ? name.split(".").pop() : "";
|
||||||
|
const res = await createDoc({
|
||||||
|
title:
|
||||||
|
String(f.title || "").trim() || name.replace(/\.[^.]+$/, ""),
|
||||||
|
category_id: form.value.category_id || 0,
|
||||||
|
file_id: upData.id || 0,
|
||||||
|
file_url: upData.url || "",
|
||||||
|
file_name: name,
|
||||||
|
ext,
|
||||||
|
size: raw.size || 0,
|
||||||
|
tags: form.value.tagList.join(","),
|
||||||
|
summary: form.value.summary || "",
|
||||||
|
status: form.value.status,
|
||||||
|
visibility: form.value.visibility || 0,
|
||||||
|
owner_name: form.value.owner_name || ""
|
||||||
|
});
|
||||||
|
if (res?.code === 200) {
|
||||||
|
okCount++;
|
||||||
|
} else {
|
||||||
|
failures.push(`${f.name}:${res?.msg || "保存失败"}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
failures.push(`${f.name}:${err?.message || "上传失败"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadPercent.value = Math.round(((i + 1) / total) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (okCount > 0) {
|
||||||
|
if (failures.length) {
|
||||||
|
ElMessage.warning(
|
||||||
|
`成功 ${okCount} 个,失败 ${failures.length} 个:${failures
|
||||||
|
.slice(0, 3)
|
||||||
|
.join(";")}${failures.length > 3 ? " 等" : ""}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ElMessage.success(`成功上传 ${okCount} 个文档`);
|
||||||
|
}
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
emit("saved");
|
emit("saved");
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res?.msg || "保存失败");
|
ElMessage.error(
|
||||||
|
`上传失败:${failures.slice(0, 3).join(";")}${
|
||||||
|
failures.length > 3 ? " 等" : ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
ElMessage.error(err?.message || "上传失败");
|
|
||||||
} finally {
|
} finally {
|
||||||
uploading.value = false;
|
uploading.value = false;
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
|
progressText.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClosed() {
|
function handleClosed() {
|
||||||
rawFile.value = null;
|
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
uploadPercent.value = 0;
|
uploadPercent.value = 0;
|
||||||
|
progressText.value = "";
|
||||||
uploadRef.value?.clearFiles?.();
|
uploadRef.value?.clearFiles?.();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -282,4 +411,14 @@ function handleClosed() {
|
|||||||
:deep(.el-upload-dragger) {
|
:deep(.el-upload-dragger) {
|
||||||
padding: 18px 12px;
|
padding: 18px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,16 +3,44 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>文档库</h2>
|
<h2>文档库</h2>
|
||||||
<p>按分类集中管理租户文档,支持上传、检索、预览、关联与归档</p>
|
<p>
|
||||||
|
按「共享文档 / 私密文档」两个空间分别管理:共享文档租户内可见,私密文档绑定到个人,支持在线制作、上传、检索、预览、关联与归档
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<el-button :icon="Plus" @click="openCreateCategory()">新建分类</el-button>
|
<el-button :icon="Plus" @click="openCreateCategory()">新建分类</el-button>
|
||||||
|
<el-button :icon="EditPen" @click="openDocEditor()">新建文档</el-button>
|
||||||
<el-button type="primary" :icon="Upload" @click="uploadVisible = true"
|
<el-button type="primary" :icon="Upload" @click="uploadVisible = true"
|
||||||
>上传文档</el-button
|
>上传文档</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 空间切换:共享文档 / 私密文档(各自独立的分类树与文档) -->
|
||||||
|
<div class="scope-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="scope-tab"
|
||||||
|
:class="{ active: activeScope === 'shared' }"
|
||||||
|
@click="switchScope('shared')"
|
||||||
|
>
|
||||||
|
<el-icon><FolderOpened /></el-icon>
|
||||||
|
<span>共享文档</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="scope-tab"
|
||||||
|
:class="{ active: activeScope === 'personal' }"
|
||||||
|
@click="switchScope('personal')"
|
||||||
|
>
|
||||||
|
<el-icon><Lock /></el-icon>
|
||||||
|
<span>私密文档</span>
|
||||||
|
</button>
|
||||||
|
<span class="scope-hint" v-if="activeScope === 'personal'"
|
||||||
|
>仅自己及被共享者可见</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 概览 -->
|
<!-- 概览 -->
|
||||||
<div class="stats-row">
|
<div class="stats-row">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
@@ -40,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
<!-- 左侧:分类树 -->
|
<!-- 左侧:统一分类树 -->
|
||||||
<div class="category-pane">
|
<div class="category-pane">
|
||||||
<div class="pane-header">
|
<div class="pane-header">
|
||||||
<span>文档分类</span>
|
<span>文档分类</span>
|
||||||
@@ -57,22 +85,17 @@
|
|||||||
<div class="quick-list">
|
<div class="quick-list">
|
||||||
<div
|
<div
|
||||||
class="quick-item"
|
class="quick-item"
|
||||||
:class="{ active: activeCategoryId === 0 && !uncategorized }"
|
:class="{ active: activeCategoryId === 0 }"
|
||||||
@click="selectQuick(0)"
|
@click="selectQuick(0)"
|
||||||
>
|
>
|
||||||
<el-icon><Files /></el-icon>
|
<el-icon><Files /></el-icon>
|
||||||
<span>全部文档</span>
|
<span>全部文档</span>
|
||||||
<span class="count">{{ stats.total || 0 }}</span>
|
<span class="count">{{ stats.total || 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
class="quick-item"
|
|
||||||
:class="{ active: uncategorized }"
|
<div v-if="!categoryTree.length" class="tree-empty">
|
||||||
@click="selectQuick(-1)"
|
暂无分类,点击右上角 + 新建
|
||||||
>
|
|
||||||
<el-icon><FolderOpened /></el-icon>
|
|
||||||
<span>未分类</span>
|
|
||||||
<span class="count">{{ uncategorizedCount }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-scrollbar class="tree-scroll">
|
<el-scrollbar class="tree-scroll">
|
||||||
@@ -90,6 +113,14 @@
|
|||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<div class="tree-node">
|
<div class="tree-node">
|
||||||
<span class="node-name" :title="data.name">{{ data.name }}</span>
|
<span class="node-name" :title="data.name">{{ data.name }}</span>
|
||||||
|
<el-tag
|
||||||
|
v-if="isSystemCategory(data)"
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
effect="plain"
|
||||||
|
class="builtin-tag"
|
||||||
|
>内置</el-tag
|
||||||
|
>
|
||||||
<span class="node-count">{{ data.doc_count || 0 }}</span>
|
<span class="node-count">{{ data.doc_count || 0 }}</span>
|
||||||
<span class="node-actions" @click.stop>
|
<span class="node-actions" @click.stop>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -107,6 +138,7 @@
|
|||||||
@click="openRenameCategory(data)"
|
@click="openRenameCategory(data)"
|
||||||
/>
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
|
v-if="!isSystemCategory(data)"
|
||||||
link
|
link
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -167,6 +199,9 @@
|
|||||||
<el-checkbox v-model="filters.star" :true-value="1" :false-value="0"
|
<el-checkbox v-model="filters.star" :true-value="1" :false-value="0"
|
||||||
>仅看收藏</el-checkbox
|
>仅看收藏</el-checkbox
|
||||||
>
|
>
|
||||||
|
<el-checkbox v-model="mineFilter" :true-value="true" :false-value="false"
|
||||||
|
>仅看我创建的</el-checkbox
|
||||||
|
>
|
||||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,12 +260,21 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="可见性" width="80">
|
<el-table-column label="可见性" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.visibility === 1" type="danger" size="small" effect="plain">
|
<el-tag
|
||||||
私密
|
:type="row.visibility === 1 ? 'danger' : 'success'"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{{ activeScope === 'personal' ? '私密' : (row.visibility === 1 ? '私密' : '公开') }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag v-else type="success" size="small" effect="plain">公开</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="creator_name"
|
||||||
|
label="创建人"
|
||||||
|
width="100"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
<el-table-column label="状态" width="90">
|
<el-table-column label="状态" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="docStatusTagType(row.status)" size="small">{{
|
<el-tag :type="docStatusTagType(row.status)" size="small">{{
|
||||||
@@ -266,10 +310,17 @@
|
|||||||
formatDateTime(row.update_time)
|
formatDateTime(row.update_time)
|
||||||
}}</template>
|
}}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="230" fixed="right">
|
<el-table-column label="操作" width="300" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
||||||
<el-button link type="primary" @click="handlePreview(row)">预览</el-button>
|
<el-button link type="primary" @click="handlePreview(row)">预览</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="isOnlineDoc(row)"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openDocEditor(row)"
|
||||||
|
>编辑</el-button
|
||||||
|
>
|
||||||
<el-button link type="primary" @click="handleDownload(row)">下载</el-button>
|
<el-button link type="primary" @click="handleDownload(row)">下载</el-button>
|
||||||
<el-button link type="primary" @click="handleStar(row)">{{
|
<el-button link type="primary" @click="handleStar(row)">{{
|
||||||
row.is_star === 1 ? "取消收藏" : "收藏"
|
row.is_star === 1 ? "取消收藏" : "收藏"
|
||||||
@@ -277,7 +328,9 @@
|
|||||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<template #empty><el-empty description="暂无文档" /></template>
|
<template #empty>
|
||||||
|
<el-empty description="暂无文档" />
|
||||||
|
</template>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
@@ -296,8 +349,10 @@
|
|||||||
<!-- 上传 -->
|
<!-- 上传 -->
|
||||||
<UploadDialog
|
<UploadDialog
|
||||||
v-model="uploadVisible"
|
v-model="uploadVisible"
|
||||||
|
:scope="activeScope"
|
||||||
:categories="categoryTree"
|
:categories="categoryTree"
|
||||||
:default-category-id="activeCategoryId > 0 ? activeCategoryId : 0"
|
:default-category-id="activeCategoryId > 0 ? activeCategoryId : 0"
|
||||||
|
:default-visibility="activeScope === 'personal' ? 1 : 0"
|
||||||
:current-user-name="currentUserName"
|
:current-user-name="currentUserName"
|
||||||
@saved="onDataChanged"
|
@saved="onDataChanged"
|
||||||
/>
|
/>
|
||||||
@@ -307,6 +362,18 @@
|
|||||||
v-model="editVisible"
|
v-model="editVisible"
|
||||||
:doc="editingDoc"
|
:doc="editingDoc"
|
||||||
:categories="categoryTree"
|
:categories="categoryTree"
|
||||||
|
:scope="activeScope"
|
||||||
|
@saved="onDataChanged"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 在线制作 / 编辑文档 -->
|
||||||
|
<DocEditorDialog
|
||||||
|
v-model="editorVisible"
|
||||||
|
:doc="editorDoc"
|
||||||
|
:scope="activeScope"
|
||||||
|
:categories="categoryTree"
|
||||||
|
:default-category-id="activeCategoryId > 0 ? activeCategoryId : 0"
|
||||||
|
:current-user-name="currentUserName"
|
||||||
@saved="onDataChanged"
|
@saved="onDataChanged"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -332,7 +399,7 @@
|
|||||||
node-key="id"
|
node-key="id"
|
||||||
check-strictly
|
check-strictly
|
||||||
clearable
|
clearable
|
||||||
placeholder="未分类"
|
placeholder="请选择目标分类"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -341,29 +408,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 图片预览 -->
|
<!-- 文档在线预览 -->
|
||||||
<el-dialog v-model="previewVisible" :title="previewRow?.title" width="70%">
|
<DocPreviewDialog
|
||||||
<div class="preview-body">
|
v-model="previewVisible"
|
||||||
<el-image
|
:doc="previewRow"
|
||||||
v-if="previewRow && isImageDoc(previewRow)"
|
@download="handleDownload"
|
||||||
:src="getFileUrl(previewRow.file_url)"
|
/>
|
||||||
fit="contain"
|
|
||||||
:preview-src-list="[getFileUrl(previewRow.file_url)]"
|
|
||||||
class="preview-image"
|
|
||||||
/>
|
|
||||||
<iframe
|
|
||||||
v-else-if="previewRow"
|
|
||||||
:src="getFileUrl(previewRow.file_url)"
|
|
||||||
class="preview-frame"
|
|
||||||
frameborder="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import {
|
import {
|
||||||
Delete,
|
Delete,
|
||||||
@@ -372,6 +427,7 @@ import {
|
|||||||
Film,
|
Film,
|
||||||
Files,
|
Files,
|
||||||
FolderOpened,
|
FolderOpened,
|
||||||
|
Lock,
|
||||||
Microphone,
|
Microphone,
|
||||||
Picture,
|
Picture,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -386,12 +442,15 @@ import { useAuthStore } from "@/stores/auth";
|
|||||||
import UploadDialog from "./components/uploadDialog.vue";
|
import UploadDialog from "./components/uploadDialog.vue";
|
||||||
import EditDialog from "./components/editDialog.vue";
|
import EditDialog from "./components/editDialog.vue";
|
||||||
import DocDetailDrawer from "./components/docDetailDrawer.vue";
|
import DocDetailDrawer from "./components/docDetailDrawer.vue";
|
||||||
|
import DocPreviewDialog from "./components/docPreviewDialog.vue";
|
||||||
|
import DocEditorDialog from "./components/docEditorDialog.vue";
|
||||||
import {
|
import {
|
||||||
batchDeleteDocs,
|
batchDeleteDocs,
|
||||||
createDocCategory,
|
createDocCategory,
|
||||||
deleteDoc,
|
deleteDoc,
|
||||||
deleteDocCategory,
|
deleteDocCategory,
|
||||||
getDocCategories,
|
getDocCategories,
|
||||||
|
getDocDetail,
|
||||||
getDocList,
|
getDocList,
|
||||||
getDocStats,
|
getDocStats,
|
||||||
moveDocs,
|
moveDocs,
|
||||||
@@ -410,7 +469,6 @@ import {
|
|||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatFileSize,
|
formatFileSize,
|
||||||
getFileUrl,
|
getFileUrl,
|
||||||
isImageDoc,
|
|
||||||
parseTags
|
parseTags
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
@@ -422,8 +480,13 @@ const categoryKeyword = ref("");
|
|||||||
const treeRef = ref(null);
|
const treeRef = ref(null);
|
||||||
const expandedKeys = ref([]);
|
const expandedKeys = ref([]);
|
||||||
|
|
||||||
|
// 当前所在文档空间:shared=共享文档,personal=私密文档(各自独立的分类树)
|
||||||
|
const activeScope = ref("shared");
|
||||||
|
|
||||||
const activeCategoryId = ref(0);
|
const activeCategoryId = ref(0);
|
||||||
const uncategorized = ref(false);
|
|
||||||
|
// 我创建的(在当前空间内进一步筛选)
|
||||||
|
const mineFilter = ref(false);
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
keyword: "",
|
keyword: "",
|
||||||
@@ -442,6 +505,8 @@ const stats = ref({});
|
|||||||
const uploadVisible = ref(false);
|
const uploadVisible = ref(false);
|
||||||
const editVisible = ref(false);
|
const editVisible = ref(false);
|
||||||
const editingDoc = ref(null);
|
const editingDoc = ref(null);
|
||||||
|
const editorVisible = ref(false);
|
||||||
|
const editorDoc = ref(null);
|
||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
const detailId = ref(0);
|
const detailId = ref(0);
|
||||||
const moveVisible = ref(false);
|
const moveVisible = ref(false);
|
||||||
@@ -450,16 +515,19 @@ const moving = ref(false);
|
|||||||
const previewVisible = ref(false);
|
const previewVisible = ref(false);
|
||||||
const previewRow = ref(null);
|
const previewRow = ref(null);
|
||||||
|
|
||||||
const uncategorizedCount = computed(() => stats.value?.uncategorized || 0);
|
|
||||||
const currentCategoryName = computed(() => {
|
const currentCategoryName = computed(() => {
|
||||||
if (uncategorized.value) return "未分类";
|
|
||||||
if (!activeCategoryId.value) return "全部文档";
|
if (!activeCategoryId.value) return "全部文档";
|
||||||
return findCategory(categoryTree.value, activeCategoryId.value)?.name || "全部文档";
|
return findCategory(categoryTree.value, activeCategoryId.value)?.name || "全部文档";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 系统内置分类:由后端按租户自动补建(is_system=1),租户隔离且不允许删除。
|
||||||
|
function isSystemCategory(data) {
|
||||||
|
return Number(data?.is_system) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
function categoryName(id) {
|
function categoryName(id) {
|
||||||
if (!id) return "未分类";
|
if (!id) return "-";
|
||||||
return findCategory(categoryTree.value, id)?.name || "未分类";
|
return findCategory(categoryTree.value, id)?.name || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function typeIcon(docType) {
|
function typeIcon(docType) {
|
||||||
@@ -479,7 +547,7 @@ function typeIcon(docType) {
|
|||||||
|
|
||||||
// ---------------- 数据加载 ----------------
|
// ---------------- 数据加载 ----------------
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
const res = await getDocCategories();
|
const res = await getDocCategories({ scope: activeScope.value });
|
||||||
if (res?.code === 200) {
|
if (res?.code === 200) {
|
||||||
categoryTree.value = res.data || [];
|
categoryTree.value = res.data || [];
|
||||||
expandedKeys.value = (res.data || []).map(c => c.id);
|
expandedKeys.value = (res.data || []).map(c => c.id);
|
||||||
@@ -487,7 +555,7 @@ async function loadCategories() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
const res = await getDocStats();
|
const res = await getDocStats({ scope: activeScope.value });
|
||||||
if (res?.code === 200) {
|
if (res?.code === 200) {
|
||||||
stats.value = res.data || {};
|
stats.value = res.data || {};
|
||||||
}
|
}
|
||||||
@@ -497,6 +565,7 @@ async function loadList() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getDocList({
|
const res = await getDocList({
|
||||||
|
scope: activeScope.value,
|
||||||
page: pagination.page,
|
page: pagination.page,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
keyword: filters.keyword || "",
|
keyword: filters.keyword || "",
|
||||||
@@ -504,8 +573,8 @@ async function loadList() {
|
|||||||
doc_type: filters.doc_type === "" ? -1 : filters.doc_type,
|
doc_type: filters.doc_type === "" ? -1 : filters.doc_type,
|
||||||
tag: filters.tag || "",
|
tag: filters.tag || "",
|
||||||
star: filters.star ? 1 : -1,
|
star: filters.star ? 1 : -1,
|
||||||
category_id: uncategorized.value ? 0 : activeCategoryId.value,
|
category_id: activeCategoryId.value,
|
||||||
uncategorized: uncategorized.value ? 1 : 0
|
mine: mineFilter.value ? 1 : 0
|
||||||
});
|
});
|
||||||
if (res?.code === 200) {
|
if (res?.code === 200) {
|
||||||
list.value = res.data?.list || [];
|
list.value = res.data?.list || [];
|
||||||
@@ -524,6 +593,16 @@ function loadData() {
|
|||||||
loadCategories();
|
loadCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换空间(共享 / 私密):重置选中并重新加载该空间的数据
|
||||||
|
function switchScope(scope) {
|
||||||
|
if (activeScope.value === scope) return;
|
||||||
|
activeScope.value = scope;
|
||||||
|
activeCategoryId.value = 0;
|
||||||
|
mineFilter.value = false;
|
||||||
|
pagination.page = 1;
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
function onDataChanged() {
|
function onDataChanged() {
|
||||||
loadList();
|
loadList();
|
||||||
loadStats();
|
loadStats();
|
||||||
@@ -538,16 +617,21 @@ function filterCategory(value, data) {
|
|||||||
|
|
||||||
watch(categoryKeyword, val => treeRef.value?.filter(val));
|
watch(categoryKeyword, val => treeRef.value?.filter(val));
|
||||||
|
|
||||||
|
// 快捷入口与分类树共用同一选中态,切换时同步树的高亮
|
||||||
|
watch([activeCategoryId, categoryTree], () => {
|
||||||
|
nextTick(() => {
|
||||||
|
treeRef.value?.setCurrentKey(activeCategoryId.value > 0 ? activeCategoryId.value : null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function handleNodeClick(data) {
|
function handleNodeClick(data) {
|
||||||
uncategorized.value = false;
|
|
||||||
activeCategoryId.value = data.id;
|
activeCategoryId.value = data.id;
|
||||||
pagination.page = 1;
|
pagination.page = 1;
|
||||||
loadList();
|
loadList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectQuick(id) {
|
function selectQuick(id) {
|
||||||
uncategorized.value = id === -1;
|
activeCategoryId.value = id > 0 ? id : 0;
|
||||||
activeCategoryId.value = id === -1 ? 0 : 0;
|
|
||||||
pagination.page = 1;
|
pagination.page = 1;
|
||||||
loadList();
|
loadList();
|
||||||
}
|
}
|
||||||
@@ -561,6 +645,7 @@ async function openCreateCategory(parentId = 0) {
|
|||||||
inputErrorMessage: "分类名称不能为空"
|
inputErrorMessage: "分类名称不能为空"
|
||||||
});
|
});
|
||||||
const res = await createDocCategory({
|
const res = await createDocCategory({
|
||||||
|
scope: activeScope.value,
|
||||||
parent_id: parentId || 0,
|
parent_id: parentId || 0,
|
||||||
name: String(value || "").trim(),
|
name: String(value || "").trim(),
|
||||||
sort: 0
|
sort: 0
|
||||||
@@ -601,6 +686,10 @@ async function openRenameCategory(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteCategory(data) {
|
async function handleDeleteCategory(data) {
|
||||||
|
if (isSystemCategory(data)) {
|
||||||
|
ElMessage.warning("系统内置分类不可删除");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`确定删除分类「${data.name}」吗?`,
|
`确定删除分类「${data.name}」吗?`,
|
||||||
@@ -674,13 +763,33 @@ async function handleStar(row) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 在线制作的文档(可再次编辑):后端标记 is_online,兼容早期以 html 文件保存的文档 */
|
||||||
|
function isOnlineDoc(row) {
|
||||||
|
if (Number(row?.is_online) === 1) return true;
|
||||||
|
return ["html", "htm"].includes(String(row?.ext || "").toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开在线文档编辑器:传 row 为编辑已有文档,不传为新建 */
|
||||||
|
function openDocEditor(row) {
|
||||||
|
editorDoc.value = row || null;
|
||||||
|
editorVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
function handlePreview(row) {
|
function handlePreview(row) {
|
||||||
|
if (!row?.file_url) {
|
||||||
|
ElMessage.warning("该文档没有附件,无法预览");
|
||||||
|
return;
|
||||||
|
}
|
||||||
previewRow.value = row;
|
previewRow.value = row;
|
||||||
previewVisible.value = true;
|
previewVisible.value = true;
|
||||||
if (!isImageDoc(row) && !String(row.ext).toLowerCase().includes("pdf")) {
|
// 预览即视为"查看",用详情接口累加查看次数(失败不影响预览)
|
||||||
window.open(getFileUrl(row.file_url), "_blank");
|
getDocDetail(row.id)
|
||||||
previewVisible.value = false;
|
.then(res => {
|
||||||
}
|
if (res?.code === 200 && res.data) {
|
||||||
|
row.view_count = res.data.view_count;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDownload(row) {
|
async function handleDownload(row) {
|
||||||
@@ -810,6 +919,49 @@ onMounted(() => {
|
|||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scope-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #c6e2ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #ecf5ff;
|
||||||
|
border-color: #409eff;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-hint {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #ebeef5;
|
||||||
@@ -896,11 +1048,29 @@ onMounted(() => {
|
|||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统内置分类标记(项目文档)
|
||||||
|
.builtin-tag {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-scroll {
|
.tree-scroll {
|
||||||
max-height: 560px;
|
max-height: 560px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-empty {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 16px 8px;
|
||||||
|
border: 1px dashed var(--el-border-color, #dcdfe6);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-node {
|
.tree-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -938,6 +1108,58 @@ onMounted(() => {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visibility-chips {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip:hover {
|
||||||
|
border-color: #c6e2ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.active {
|
||||||
|
background: #ecf5ff;
|
||||||
|
border-color: #409eff;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-count {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.active .chip-count {
|
||||||
|
background: rgba(64, 158, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
.filter-bar {
|
.filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -954,6 +1176,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-title {
|
.toolbar-title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@@ -1011,24 +1236,6 @@ onMounted(() => {
|
|||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-body {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 420px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-image {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 60vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-frame {
|
|
||||||
width: 100%;
|
|
||||||
height: 60vh;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 暗色主题适配 =====
|
// ===== 暗色主题适配 =====
|
||||||
html.dark & {
|
html.dark & {
|
||||||
.page-header p {
|
.page-header p {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { UmoEditor } from '@umoteam/editor'
|
import { UmoEditor } from '@umoteam/editor'
|
||||||
|
import '@umoteam/editor/style'
|
||||||
import { uploadFile } from '@/api/file'
|
import { uploadFile } from '@/api/file'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ func (c *BackendOaDocumentController) oaDocFail(err error) {
|
|||||||
status, code, msg = 404, 404, err.Error()
|
status, code, msg = 404, 404, err.Error()
|
||||||
case services.ErrDocLinkDuplicate:
|
case services.ErrDocLinkDuplicate:
|
||||||
status, code, msg = 409, 409, err.Error()
|
status, code, msg = 409, 409, err.Error()
|
||||||
|
case services.ErrDocCategorySystem, services.ErrDocCategorySystemLocked,
|
||||||
|
services.ErrDocCategoryNameKept:
|
||||||
|
status, code, msg = 403, 403, err.Error()
|
||||||
default:
|
default:
|
||||||
msg = err.Error()
|
msg = err.Error()
|
||||||
}
|
}
|
||||||
@@ -95,14 +98,15 @@ func (c *BackendOaDocumentController) oaDocActor(claims *jwtutil.Claims) service
|
|||||||
|
|
||||||
// ---------------- 分类 ----------------
|
// ---------------- 分类 ----------------
|
||||||
|
|
||||||
// Categories GET /backend/oa/document/categories 分类树(含文档数量)
|
// Categories GET /backend/oa/document/categories?scope=shared|personal 文档分类树(含各分类文档数量)
|
||||||
func (c *BackendOaDocumentController) Categories() {
|
func (c *BackendOaDocumentController) Categories() {
|
||||||
claims, err := c.oaDocClaims()
|
claims, err := c.oaDocClaims()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.oaDocFail(err)
|
c.oaDocFail(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tree, err := services.OaDocCategoryTree(c.oaDocActor(claims))
|
scope := strings.TrimSpace(c.GetString("scope"))
|
||||||
|
tree, err := services.OaDocCategoryTree(c.oaDocActor(claims), scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.oaDocFail(err)
|
c.oaDocFail(err)
|
||||||
return
|
return
|
||||||
@@ -118,6 +122,7 @@ func (c *BackendOaDocumentController) CreateCategory() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
ParentID uint64 `json:"parent_id"`
|
ParentID uint64 `json:"parent_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
@@ -126,7 +131,8 @@ func (c *BackendOaDocumentController) CreateCategory() {
|
|||||||
if !c.oaDocParseBody(&body) {
|
if !c.oaDocParseBody(&body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item, err := services.OaDocCategoryCreate(claims.TenantId, body.ParentID, body.Name, body.Remark, body.Sort)
|
item, err := services.OaDocCategoryCreate(
|
||||||
|
c.oaDocActor(claims), body.Scope, body.ParentID, body.Name, body.Remark, body.Sort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.oaDocFail(err)
|
c.oaDocFail(err)
|
||||||
return
|
return
|
||||||
@@ -169,7 +175,7 @@ func (c *BackendOaDocumentController) UpdateCategory() {
|
|||||||
if body.Sort != nil {
|
if body.Sort != nil {
|
||||||
sort = *body.Sort
|
sort = *body.Sort
|
||||||
}
|
}
|
||||||
item, err := services.OaDocCategoryUpdate(claims.TenantId, id, name, remark, sort, parentID)
|
item, err := services.OaDocCategoryUpdate(c.oaDocActor(claims), id, name, remark, sort, parentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.oaDocFail(err)
|
c.oaDocFail(err)
|
||||||
return
|
return
|
||||||
@@ -188,7 +194,7 @@ func (c *BackendOaDocumentController) DeleteCategory() {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := services.OaDocCategoryDelete(claims.TenantId, id); err != nil {
|
if err := services.OaDocCategoryDelete(c.oaDocActor(claims), id); err != nil {
|
||||||
c.oaDocFail(err)
|
c.oaDocFail(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -211,13 +217,19 @@ func (c *BackendOaDocumentController) List() {
|
|||||||
docType, _ := c.GetInt("doc_type", -1)
|
docType, _ := c.GetInt("doc_type", -1)
|
||||||
star, _ := c.GetInt("star", -1)
|
star, _ := c.GetInt("star", -1)
|
||||||
categoryID, _ := c.GetUint64("category_id")
|
categoryID, _ := c.GetUint64("category_id")
|
||||||
|
visibility, _ := c.GetInt("visibility", -1)
|
||||||
|
mine := strings.TrimSpace(c.GetString("mine")) == "1"
|
||||||
|
scope := strings.TrimSpace(c.GetString("scope"))
|
||||||
|
|
||||||
actor := c.oaDocActor(claims)
|
actor := c.oaDocActor(claims)
|
||||||
list, total, err := services.OaDocList(actor, services.OaDocListParams{
|
list, total, err := services.OaDocList(actor, services.OaDocListParams{
|
||||||
Actor: actor,
|
Actor: actor,
|
||||||
|
Scope: scope,
|
||||||
Keyword: strings.TrimSpace(c.GetString("keyword")),
|
Keyword: strings.TrimSpace(c.GetString("keyword")),
|
||||||
CategoryID: categoryID,
|
CategoryID: categoryID,
|
||||||
Uncategorized: strings.TrimSpace(c.GetString("uncategorized")) == "1",
|
Uncategorized: strings.TrimSpace(c.GetString("uncategorized")) == "1",
|
||||||
|
Visibility: visibility,
|
||||||
|
Mine: mine,
|
||||||
Status: status,
|
Status: status,
|
||||||
DocType: docType,
|
DocType: docType,
|
||||||
Tag: strings.TrimSpace(c.GetString("tag")),
|
Tag: strings.TrimSpace(c.GetString("tag")),
|
||||||
@@ -232,14 +244,15 @@ func (c *BackendOaDocumentController) List() {
|
|||||||
c.oaDocOk(map[string]interface{}{"list": list, "total": total})
|
c.oaDocOk(map[string]interface{}{"list": list, "total": total})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats GET /backend/oa/document/stats 文档库概览
|
// Stats GET /backend/oa/document/stats?scope=shared|personal|all 文档库概览
|
||||||
func (c *BackendOaDocumentController) Stats() {
|
func (c *BackendOaDocumentController) Stats() {
|
||||||
claims, err := c.oaDocClaims()
|
claims, err := c.oaDocClaims()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.oaDocFail(err)
|
c.oaDocFail(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stats, err := services.OaDocStats(c.oaDocActor(claims))
|
scope := strings.TrimSpace(c.GetString("scope"))
|
||||||
|
stats, err := services.OaDocStats(c.oaDocActor(claims), scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.oaDocFail(err)
|
c.oaDocFail(err)
|
||||||
return
|
return
|
||||||
@@ -266,6 +279,25 @@ func (c *BackendOaDocumentController) Detail() {
|
|||||||
c.oaDocOk(item)
|
c.oaDocOk(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Content GET /backend/oa/document/content/:id 读取在线文档的编辑源(不累加查看次数)
|
||||||
|
func (c *BackendOaDocumentController) Content() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := services.OaDocGet(c.oaDocActor(claims), id)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(map[string]interface{}{"content": item.Content})
|
||||||
|
}
|
||||||
|
|
||||||
// Create POST /backend/oa/document/create
|
// Create POST /backend/oa/document/create
|
||||||
func (c *BackendOaDocumentController) Create() {
|
func (c *BackendOaDocumentController) Create() {
|
||||||
claims, err := c.oaDocClaims()
|
claims, err := c.oaDocClaims()
|
||||||
@@ -406,10 +438,10 @@ func (c *BackendOaDocumentController) Download() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.oaDocOk(map[string]interface{}{
|
c.oaDocOk(map[string]interface{}{
|
||||||
"id": item.ID,
|
"id": item.ID,
|
||||||
"title": item.Title,
|
"title": item.Title,
|
||||||
"file_url": item.FileURL,
|
"file_url": item.FileURL,
|
||||||
"file_name": item.FileName,
|
"file_name": item.FileName,
|
||||||
"download_count": item.DownCount,
|
"download_count": item.DownCount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -605,38 +637,44 @@ func (c *BackendOaDocumentController) oaDocParseSaveBody() (services.OaDocSavePa
|
|||||||
var body struct {
|
var body struct {
|
||||||
CategoryID *uint64 `json:"category_id"`
|
CategoryID *uint64 `json:"category_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
FileID uint64 `json:"file_id"`
|
FileID uint64 `json:"file_id"`
|
||||||
FileURL string `json:"file_url"`
|
FileURL string `json:"file_url"`
|
||||||
FileName string `json:"file_name"`
|
FileName string `json:"file_name"`
|
||||||
Ext string `json:"ext"`
|
Ext string `json:"ext"`
|
||||||
Size uint64 `json:"size"`
|
Size uint64 `json:"size"`
|
||||||
Tags string `json:"tags"`
|
Tags string `json:"tags"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Status *int8 `json:"status"`
|
Content *string `json:"content"`
|
||||||
Version int `json:"version"`
|
Status *int8 `json:"status"`
|
||||||
OwnerID uint64 `json:"owner_id"`
|
Version int `json:"version"`
|
||||||
OwnerName string `json:"owner_name"`
|
OwnerID uint64 `json:"owner_id"`
|
||||||
IsStar *int8 `json:"is_star"`
|
OwnerName string `json:"owner_name"`
|
||||||
Visibility *int8 `json:"visibility"`
|
IsStar *int8 `json:"is_star"`
|
||||||
|
Visibility *int8 `json:"visibility"`
|
||||||
}
|
}
|
||||||
if !c.oaDocParseBody(&body) {
|
if !c.oaDocParseBody(&body) {
|
||||||
return services.OaDocSaveParams{}, false
|
return services.OaDocSaveParams{}, false
|
||||||
}
|
}
|
||||||
params := services.OaDocSaveParams{
|
params := services.OaDocSaveParams{
|
||||||
Title: body.Title,
|
Title: body.Title,
|
||||||
FileID: body.FileID,
|
FileID: body.FileID,
|
||||||
FileURL: body.FileURL,
|
FileURL: body.FileURL,
|
||||||
FileName: body.FileName,
|
FileName: body.FileName,
|
||||||
Ext: body.Ext,
|
Ext: body.Ext,
|
||||||
Size: body.Size,
|
Size: body.Size,
|
||||||
Tags: body.Tags,
|
Tags: body.Tags,
|
||||||
Summary: body.Summary,
|
Summary: body.Summary,
|
||||||
Version: body.Version,
|
Version: body.Version,
|
||||||
OwnerID: body.OwnerID,
|
OwnerID: body.OwnerID,
|
||||||
OwnerName: body.OwnerName,
|
OwnerName: body.OwnerName,
|
||||||
|
}
|
||||||
|
if body.Content != nil {
|
||||||
|
params.SetContent = true
|
||||||
|
params.Content = *body.Content
|
||||||
}
|
}
|
||||||
if body.Visibility != nil {
|
if body.Visibility != nil {
|
||||||
params.Visibility = *body.Visibility
|
params.Visibility = *body.Visibility
|
||||||
|
params.SetVisibility = true
|
||||||
}
|
}
|
||||||
if body.CategoryID != nil {
|
if body.CategoryID != nil {
|
||||||
params.SetCategory = true
|
params.SetCategory = true
|
||||||
|
|||||||
@@ -18,18 +18,40 @@
|
|||||||
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_category (
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_category (
|
||||||
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '分类归属用户:共享空间共享=0(租户级);私密空间=创建者的 yz_system_tenant_user.uid',
|
||||||
parent_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级ID,0为一级分类',
|
parent_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级ID,0为一级分类',
|
||||||
name varchar(128) NOT NULL DEFAULT '' COMMENT '分类名称',
|
name varchar(128) NOT NULL DEFAULT '' COMMENT '分类名称',
|
||||||
sort int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前',
|
sort int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前',
|
||||||
remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
|
is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)',
|
||||||
|
scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属空间 shared-共享文档空间 personal-私密文档空间(绑定用户级别)',
|
||||||
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
|
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
|
||||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
delete_time datetime DEFAULT NULL COMMENT '删除时间',
|
delete_time datetime DEFAULT NULL COMMENT '删除时间',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
KEY idx_tid_parent (tid, parent_id)
|
KEY idx_tid_parent (tid, parent_id),
|
||||||
|
KEY idx_tid_scope_user (tid, scope, user_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表';
|
||||||
|
|
||||||
|
-- 1.1 历史库补齐列(特性上线前已建表时执行,已存在则报错可忽略)
|
||||||
|
-- ALTER TABLE yz_backend_oa_doc_category
|
||||||
|
-- ADD COLUMN is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)';
|
||||||
|
-- ALTER TABLE yz_backend_oa_doc_category
|
||||||
|
-- ADD COLUMN scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属空间 shared-共享 personal-私密';
|
||||||
|
-- ALTER TABLE yz_backend_oa_doc_category
|
||||||
|
-- ADD COLUMN user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '分类归属用户(私密空间=租户用户 uid)';
|
||||||
|
|
||||||
|
-- 1.2 分类归属规则(双空间模型,2026-09 起):
|
||||||
|
-- 文档库按「共享文档 / 私密文档」两个空间组织,每个空间各自一棵分类树:
|
||||||
|
-- - 共享空间:scope=shared、user_id=0(租户级,全租户共用一套,任何人可建/删,内置分类除外);
|
||||||
|
-- - 私密空间:scope=personal、user_id=当前用户的 yz_system_tenant_user.uid(绑定到用户级别,各用户独立一套)。
|
||||||
|
-- 文档可见性(OaDoc.visibility)与所在空间一一对应:共享空间→0(租户公开),私密空间→1(私密,仅创建者与被共享者可见)。
|
||||||
|
-- 系统内置分类「项目文档」(scope=shared, user_id=0, is_system=1) 属于共享空间,按租户自动补建
|
||||||
|
-- (models.EnsureOaDocDefaultCategories),一般无需手工插入。
|
||||||
|
-- 历史脏数据(scope 不在 shared/personal)在 EnsureOaDocumentTables 中自动归为共享空间(user_id=0, scope=shared)。
|
||||||
|
-- 说明:「未分类」是虚拟分类,文档 category_id = 0 即表示未分类,两个空间各自独立计算,不落库、不可删除。
|
||||||
|
|
||||||
-- 2. 文档表
|
-- 2. 文档表
|
||||||
-- doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
|
-- doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
|
||||||
-- status : 0-草稿 1-已发布 2-已归档
|
-- status : 0-草稿 1-已发布 2-已归档
|
||||||
|
|||||||
+204
-44
@@ -4,6 +4,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/beego/beego/v2/client/orm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OA 文档管理(文档库 / 文档图谱)数据模型。
|
// OA 文档管理(文档库 / 文档图谱)数据模型。
|
||||||
@@ -14,14 +16,28 @@ import (
|
|||||||
// - yz_backend_oa_doc_link 文档关联关系(引用/相关/版本/从属),文档图谱的连线来源
|
// - yz_backend_oa_doc_link 文档关联关系(引用/相关/版本/从属),文档图谱的连线来源
|
||||||
|
|
||||||
// OaDocCategory OA文档分类: yz_backend_oa_doc_category
|
// OaDocCategory OA文档分类: yz_backend_oa_doc_category
|
||||||
|
//
|
||||||
|
// 双空间模型:文档库按「共享文档 / 私密文档」两个空间组织,每个空间各自有一棵分类树。
|
||||||
|
// - 共享文档空间(Scope="shared"、UserID=0):租户级,租户内所有成员共用同一套文件夹结构;
|
||||||
|
// - 私密文档空间(Scope="personal"、UserID=登录用户的 yz_system_tenant_user.uid):
|
||||||
|
// 绑定到租户的用户级别,每位用户拥有自己独立的一套文件夹,互不干扰;
|
||||||
|
// - 文档的可见性(OaDoc.Visibility)与所在空间一一对应:共享空间→租户公开(0),私密空间→私密(1);
|
||||||
|
// - 系统内置的「项目文档」属于共享空间(Scope="shared"、UserID=0);
|
||||||
|
// - 「未分类」是虚拟分类(文档 category_id=0),不落库、不可删除,两个空间各自独立计算。
|
||||||
type OaDocCategory struct {
|
type OaDocCategory struct {
|
||||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
Tid int `orm:"column(tid)" json:"tid"`
|
Tid int `orm:"column(tid)" json:"tid"`
|
||||||
ParentID uint64 `orm:"column(parent_id);default(0)" json:"parent_id"`
|
// UserID 分类归属用户:共享空间恒为 0(租户级);私密空间为创建者的 yz_system_tenant_user.uid。
|
||||||
Name string `orm:"column(name);size(128)" json:"name"`
|
UserID uint64 `orm:"column(user_id);default(0)" json:"user_id"`
|
||||||
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
ParentID uint64 `orm:"column(parent_id);default(0)" json:"parent_id"`
|
||||||
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
|
Name string `orm:"column(name);size(128)" json:"name"`
|
||||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
||||||
|
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
|
||||||
|
IsSystem int8 `orm:"column(is_system);default(0)" json:"is_system"`
|
||||||
|
// Scope 所属空间:shared=共享文档空间,personal=私密文档空间(绑定到用户级别)。
|
||||||
|
Scope string `orm:"column(scope);size(16);default(shared)" json:"scope"`
|
||||||
|
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||||
|
|
||||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
||||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||||
@@ -35,33 +51,38 @@ func (m *OaDocCategory) TableName() string {
|
|||||||
// status: 0-草稿 1-已发布 2-已归档
|
// status: 0-草稿 1-已发布 2-已归档
|
||||||
// doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
|
// doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
|
||||||
type OaDoc struct {
|
type OaDoc struct {
|
||||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
Tid int `orm:"column(tid)" json:"tid"`
|
Tid int `orm:"column(tid)" json:"tid"`
|
||||||
CategoryID uint64 `orm:"column(category_id);default(0)" json:"category_id"`
|
CategoryID uint64 `orm:"column(category_id);default(0)" json:"category_id"`
|
||||||
Title string `orm:"column(title);size(255)" json:"title"`
|
Title string `orm:"column(title);size(255)" json:"title"`
|
||||||
FileID uint64 `orm:"column(file_id);default(0)" json:"file_id"`
|
FileID uint64 `orm:"column(file_id);default(0)" json:"file_id"`
|
||||||
FileURL string `orm:"column(file_url);size(512);default()" json:"file_url"`
|
FileURL string `orm:"column(file_url);size(512);default()" json:"file_url"`
|
||||||
FileName string `orm:"column(file_name);size(255);default()" json:"file_name"`
|
FileName string `orm:"column(file_name);size(255);default()" json:"file_name"`
|
||||||
Ext string `orm:"column(ext);size(16);default()" json:"ext"`
|
Ext string `orm:"column(ext);size(16);default()" json:"ext"`
|
||||||
Size uint64 `orm:"column(size);default(0)" json:"size"`
|
Size uint64 `orm:"column(size);default(0)" json:"size"`
|
||||||
DocType int8 `orm:"column(doc_type);default(0)" json:"doc_type"`
|
DocType int8 `orm:"column(doc_type);default(0)" json:"doc_type"`
|
||||||
Tags string `orm:"column(tags);size(255);default()" json:"tags"`
|
Tags string `orm:"column(tags);size(255);default()" json:"tags"`
|
||||||
Summary string `orm:"column(summary);size(1000);default()" json:"summary"`
|
Summary string `orm:"column(summary);size(1000);default()" json:"summary"`
|
||||||
Status int8 `orm:"column(status);default(0)" json:"status"`
|
// Content 在线制作文档的编辑源(富文本 HTML),仅用于再次编辑;上传文件或普通文档为空。
|
||||||
Version int `orm:"column(version);default(1)" json:"version"`
|
// 注意:文档对外的写入/下载格式为 docx(由该源导出),此字段只是编辑器的中间态。
|
||||||
IsStar int8 `orm:"column(is_star);default(0)" json:"is_star"`
|
Content string `orm:"column(content);type(longtext);null" json:"content,omitempty"`
|
||||||
OwnerID uint64 `orm:"column(owner_id);default(0)" json:"owner_id"`
|
// IsOnline 是否在线制作(非持久化字段,列表接口按 Content 是否为空计算)
|
||||||
OwnerName string `orm:"column(owner_name);size(100);default()" json:"owner_name"`
|
IsOnline int8 `orm:"-" json:"is_online"`
|
||||||
ViewCount int `orm:"column(view_count);default(0)" json:"view_count"`
|
Status int8 `orm:"column(status);default(0)" json:"status"`
|
||||||
DownCount int `orm:"column(download_count);default(0)" json:"download_count"`
|
Version int `orm:"column(version);default(1)" json:"version"`
|
||||||
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
|
IsStar int8 `orm:"column(is_star);default(0)" json:"is_star"`
|
||||||
CreatorName string `orm:"column(creator_name);size(100);default()" json:"creator_name"`
|
OwnerID uint64 `orm:"column(owner_id);default(0)" json:"owner_id"`
|
||||||
|
OwnerName string `orm:"column(owner_name);size(100);default()" json:"owner_name"`
|
||||||
|
ViewCount int `orm:"column(view_count);default(0)" json:"view_count"`
|
||||||
|
DownCount int `orm:"column(download_count);default(0)" json:"download_count"`
|
||||||
|
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
|
||||||
|
CreatorName string `orm:"column(creator_name);size(100);default()" json:"creator_name"`
|
||||||
// Visibility 可见性:0-租户公开(默认,租户内所有后台用户可见)1-私密(仅创建者与被共享者可见)
|
// Visibility 可见性:0-租户公开(默认,租户内所有后台用户可见)1-私密(仅创建者与被共享者可见)
|
||||||
Visibility int8 `orm:"column(visibility);default(0)" json:"visibility"`
|
Visibility int8 `orm:"column(visibility);default(0)" json:"visibility"`
|
||||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
||||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *OaDoc) TableName() string {
|
func (m *OaDoc) TableName() string {
|
||||||
@@ -88,7 +109,8 @@ func (m *OaDocLink) TableName() string {
|
|||||||
// OaDocShare OA文档共享: yz_backend_oa_doc_share
|
// OaDocShare OA文档共享: yz_backend_oa_doc_share
|
||||||
// 私密文档(visibility=1)在"创建者可见"之外,额外授权给指定用户或部门。
|
// 私密文档(visibility=1)在"创建者可见"之外,额外授权给指定用户或部门。
|
||||||
// share_type: 0-用户(target_id 为 yz_system_tenant_user.uid)
|
// share_type: 0-用户(target_id 为 yz_system_tenant_user.uid)
|
||||||
// 1-部门(target_id 为 yz_backend_organization.id,含其全部子部门)
|
//
|
||||||
|
// 1-部门(target_id 为 yz_backend_organization.id,含其全部子部门)
|
||||||
type OaDocShare struct {
|
type OaDocShare struct {
|
||||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
Tid int `orm:"column(tid)" json:"tid"`
|
Tid int `orm:"column(tid)" json:"tid"`
|
||||||
@@ -103,17 +125,39 @@ func (m *OaDocShare) TableName() string {
|
|||||||
return "yz_backend_oa_doc_share"
|
return "yz_backend_oa_doc_share"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 文档分类内置标记常量(is_system)
|
||||||
|
const (
|
||||||
|
DocCategoryNormal int8 = 0 // 普通分类,租户可自行删除
|
||||||
|
DocCategorySystem int8 = 1 // 系统内置分类,任何租户都不可删除
|
||||||
|
)
|
||||||
|
|
||||||
|
// 文档范围(scope):文档库按「共享文档 / 私密文档」两个空间组织,分类同样按此隔离。
|
||||||
|
// 共享空间(shared):租户级,所有人共用;私密空间(personal):绑定到用户级别,每人独立一套。
|
||||||
|
// 文档侧复用 visibility 字段:personal 对应私密(1),shared 对应租户公开(0)。
|
||||||
|
const (
|
||||||
|
DocScopeAll = "all"
|
||||||
|
DocScopePersonal = "personal"
|
||||||
|
DocScopeShared = "shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DocCategoryNameProject 系统内置分类名称:项目文档,只属于共享文档范围。
|
||||||
|
// 每个租户首次访问文档接口时自动补建自己那一份(按 tid 隔离),租户之间互不可见。
|
||||||
|
const DocCategoryNameProject = "项目文档"
|
||||||
|
|
||||||
|
// docCategorySystemRemark 内置分类的默认备注
|
||||||
|
const docCategorySystemRemark = "系统内置分类,不可删除"
|
||||||
|
|
||||||
// 文档类型常量(doc_type)
|
// 文档类型常量(doc_type)
|
||||||
const (
|
const (
|
||||||
DocTypeOther int8 = 0
|
DocTypeOther int8 = 0
|
||||||
DocTypeDoc int8 = 1 // doc / docx / txt / md / rtf
|
DocTypeDoc int8 = 1 // doc / docx / txt / md / rtf
|
||||||
DocTypeSheet int8 = 2 // xls / xlsx / csv
|
DocTypeSheet int8 = 2 // xls / xlsx / csv
|
||||||
DocTypeSlide int8 = 3 // ppt / pptx
|
DocTypeSlide int8 = 3 // ppt / pptx
|
||||||
DocTypePDF int8 = 4 // pdf
|
DocTypePDF int8 = 4 // pdf
|
||||||
DocTypeImage int8 = 5 // jpg / png / gif / bmp / webp
|
DocTypeImage int8 = 5 // jpg / png / gif / bmp / webp
|
||||||
DocTypeZip int8 = 6 // zip / rar / 7z / tar / gz
|
DocTypeZip int8 = 6 // zip / rar / 7z / tar / gz
|
||||||
DocTypeVideo int8 = 7
|
DocTypeVideo int8 = 7
|
||||||
DocTypeAudio int8 = 8
|
DocTypeAudio int8 = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
// 文档状态常量(status)
|
// 文档状态常量(status)
|
||||||
@@ -179,16 +223,20 @@ func EnsureOaDocumentTables() error {
|
|||||||
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_category (
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_category (
|
||||||
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '归属用户ID 0-租户级(共享分类) >0-该用户的个人分类',
|
||||||
parent_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级ID,0为一级分类',
|
parent_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级ID,0为一级分类',
|
||||||
name varchar(128) NOT NULL DEFAULT '' COMMENT '分类名称',
|
name varchar(128) NOT NULL DEFAULT '' COMMENT '分类名称',
|
||||||
sort int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前',
|
sort int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前',
|
||||||
remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
|
is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)',
|
||||||
|
scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属范围 personal-个人文档 shared-共享文档',
|
||||||
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
|
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
|
||||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
delete_time datetime DEFAULT NULL COMMENT '删除时间',
|
delete_time datetime DEFAULT NULL COMMENT '删除时间',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
KEY idx_tid_parent (tid, parent_id)
|
KEY idx_tid_parent (tid, parent_id),
|
||||||
|
KEY idx_tid_scope_user (tid, scope, user_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表'`).Exec()
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表'`).Exec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -208,6 +256,7 @@ CREATE TABLE IF NOT EXISTS yz_backend_oa_doc (
|
|||||||
doc_type tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频',
|
doc_type tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频',
|
||||||
tags varchar(255) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔',
|
tags varchar(255) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔',
|
||||||
summary varchar(1000) NOT NULL DEFAULT '' COMMENT '摘要/描述',
|
summary varchar(1000) NOT NULL DEFAULT '' COMMENT '摘要/描述',
|
||||||
|
content longtext NULL COMMENT '在线制作文档的编辑源(富文本HTML),仅用于再次编辑',
|
||||||
status tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已归档',
|
status tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已归档',
|
||||||
version int NOT NULL DEFAULT 1 COMMENT '版本号',
|
version int NOT NULL DEFAULT 1 COMMENT '版本号',
|
||||||
is_star tinyint NOT NULL DEFAULT 0 COMMENT '是否收藏 0-否 1-是',
|
is_star tinyint NOT NULL DEFAULT 0 COMMENT '是否收藏 0-否 1-是',
|
||||||
@@ -256,6 +305,30 @@ CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_link (
|
|||||||
ADD COLUMN visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)'
|
ADD COLUMN visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)'
|
||||||
`).Exec()
|
`).Exec()
|
||||||
|
|
||||||
|
// 历史库补齐在线文档编辑源列 content(在线制作功能上线前已建表)
|
||||||
|
_, _ = Orm.Raw(`
|
||||||
|
ALTER TABLE yz_backend_oa_doc
|
||||||
|
ADD COLUMN content longtext NULL COMMENT '在线制作文档的编辑源(富文本HTML),仅用于再次编辑'
|
||||||
|
`).Exec()
|
||||||
|
|
||||||
|
// 历史库补齐分类表的 is_system 列(内置分类特性上线前已建表)
|
||||||
|
_, _ = Orm.Raw(`
|
||||||
|
ALTER TABLE yz_backend_oa_doc_category
|
||||||
|
ADD COLUMN is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)'
|
||||||
|
`).Exec()
|
||||||
|
|
||||||
|
// 历史库补齐分类表的 scope 列:既有分类默认归入共享文档,保证升级后仍可见
|
||||||
|
_, _ = Orm.Raw(`
|
||||||
|
ALTER TABLE yz_backend_oa_doc_category
|
||||||
|
ADD COLUMN scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属范围 personal-个人文档 shared-共享文档'
|
||||||
|
`).Exec()
|
||||||
|
|
||||||
|
// 历史库补齐分类表的 user_id 列:既有分类默认归租户级(共享分类)
|
||||||
|
_, _ = Orm.Raw(`
|
||||||
|
ALTER TABLE yz_backend_oa_doc_category
|
||||||
|
ADD COLUMN user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '归属用户ID 0-租户级(共享分类) >0-该用户的个人分类'
|
||||||
|
`).Exec()
|
||||||
|
|
||||||
_, err = Orm.Raw(`
|
_, err = Orm.Raw(`
|
||||||
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share (
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share (
|
||||||
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
@@ -270,6 +343,93 @@ CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share (
|
|||||||
KEY idx_tid_doc (tid, doc_id),
|
KEY idx_tid_doc (tid, doc_id),
|
||||||
KEY idx_target (tid, share_type, target_id)
|
KEY idx_target (tid, share_type, target_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档共享表'`).Exec()
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档共享表'`).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 双空间模型:兼容存量数据,将不在 (shared, personal) 范围内的分类统一归为共享空间
|
||||||
|
// (user_id 置 0、scope 置 shared),避免历史脏数据导致分类不可见。
|
||||||
|
// 注意:不再把 personal 并入 shared,私密空间需保留各自的 user_id 隔离。
|
||||||
|
_, _ = Orm.Raw(`
|
||||||
|
UPDATE yz_backend_oa_doc_category
|
||||||
|
SET user_id = 0, scope = 'shared'
|
||||||
|
WHERE scope NOT IN ('shared', 'personal') AND is_deleted = 0
|
||||||
|
`).Exec()
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocDefaultCategoryOnce 记录已完成内置分类补建的租户,避免每次请求重复查库。
|
||||||
|
// key 为 tid(int),value 为 struct{}{};补建失败时不写缓存,下次请求继续重试。
|
||||||
|
var oaDocDefaultCategoryOnce sync.Map
|
||||||
|
|
||||||
|
// EnsureOaDocDefaultCategories 保证指定租户的系统内置分类齐全(当前为「项目文档」)。
|
||||||
|
//
|
||||||
|
// 租户隔离:内置分类是真实数据行,每个租户各自持有一份(tid 不同),
|
||||||
|
// 首次访问文档接口时自动补建,租户之间互不可见、互不影响。
|
||||||
|
//
|
||||||
|
// 范围隔离:「项目文档」属于共享文档空间(scope=shared、user_id=0),租户内所有人共用。
|
||||||
|
// 私密文档空间不预置内置分类,由用户自行建立自己的文件夹。
|
||||||
|
//
|
||||||
|
// 「未分类」不落库:它是虚拟分类,文档 category_id = 0 即表示未分类,因此天然不可删除;
|
||||||
|
// 同时在 services 层把「未分类」「项目文档」列为保留名,禁止新建同名分类与之混淆。
|
||||||
|
//
|
||||||
|
// 任何失败均静默忽略,不阻断主流程(与 EnsureDefaultTenantRoles 保持一致)。
|
||||||
|
func EnsureOaDocDefaultCategories(tid int) {
|
||||||
|
if Orm == nil || tid <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, done := oaDocDefaultCategoryOnce.Load(tid); done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ensureOaDocProjectCategory(tid); err == nil {
|
||||||
|
oaDocDefaultCategoryOnce.Store(tid, struct{}{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureOaDocProjectCategory 补建租户的「项目文档」内置分类:
|
||||||
|
// 1. 已有任意内置分类(is_system=1):说明租户已初始化过(可能被改名),直接返回;
|
||||||
|
// 2. 已有同名普通分类:升级为内置分类,避免出现两个「项目文档」;
|
||||||
|
// 3. 都没有:插入一条一级内置分类。
|
||||||
|
func ensureOaDocProjectCategory(tid int) error {
|
||||||
|
// 「项目文档」为统一租户分类树下的内置分类(scope=shared)
|
||||||
|
qs := Orm.QueryTable(new(OaDocCategory)).
|
||||||
|
Filter("tid", tid).
|
||||||
|
Filter("scope", DocScopeShared)
|
||||||
|
|
||||||
|
sysCount, err := qs.Filter("is_system", DocCategorySystem).Count()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sysCount > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var existed []OaDocCategory
|
||||||
|
if _, err := qs.Filter("name", DocCategoryNameProject).
|
||||||
|
Filter("is_deleted", 0).
|
||||||
|
OrderBy("id").Limit(1).All(&existed); err != nil && err != orm.ErrNoRows {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(existed) > 0 {
|
||||||
|
item := existed[0]
|
||||||
|
item.IsSystem = DocCategorySystem
|
||||||
|
if item.Remark == "" {
|
||||||
|
item.Remark = docCategorySystemRemark
|
||||||
|
}
|
||||||
|
_, err := Orm.Update(&item, "IsSystem", "Remark")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Orm.Insert(&OaDocCategory{
|
||||||
|
Tid: tid,
|
||||||
|
ParentID: 0,
|
||||||
|
Name: DocCategoryNameProject,
|
||||||
|
Sort: 0,
|
||||||
|
Remark: docCategorySystemRemark,
|
||||||
|
IsSystem: DocCategorySystem,
|
||||||
|
Scope: DocScopeShared,
|
||||||
|
IsDeleted: 0,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ func RegisterAuthRoutes() {
|
|||||||
beego.Router("/backend/oa/document/links", &controllers.BackendOaDocumentController{}, "get:LinkList;post:LinkCreate")
|
beego.Router("/backend/oa/document/links", &controllers.BackendOaDocumentController{}, "get:LinkList;post:LinkCreate")
|
||||||
beego.Router("/backend/oa/document/links/:id", &controllers.BackendOaDocumentController{}, "delete:LinkDelete")
|
beego.Router("/backend/oa/document/links/:id", &controllers.BackendOaDocumentController{}, "delete:LinkDelete")
|
||||||
beego.Router("/backend/oa/document/detail/:id", &controllers.BackendOaDocumentController{}, "get:Detail")
|
beego.Router("/backend/oa/document/detail/:id", &controllers.BackendOaDocumentController{}, "get:Detail")
|
||||||
|
beego.Router("/backend/oa/document/content/:id", &controllers.BackendOaDocumentController{}, "get:Content")
|
||||||
beego.Router("/backend/oa/document/create", &controllers.BackendOaDocumentController{}, "post:Create")
|
beego.Router("/backend/oa/document/create", &controllers.BackendOaDocumentController{}, "post:Create")
|
||||||
beego.Router("/backend/oa/document/update/:id", &controllers.BackendOaDocumentController{}, "post:Update")
|
beego.Router("/backend/oa/document/update/:id", &controllers.BackendOaDocumentController{}, "post:Update")
|
||||||
beego.Router("/backend/oa/document/delete/:id", &controllers.BackendOaDocumentController{}, "delete:Delete")
|
beego.Router("/backend/oa/document/delete/:id", &controllers.BackendOaDocumentController{}, "delete:Delete")
|
||||||
|
|||||||
+476
-129
@@ -18,20 +18,23 @@ import (
|
|||||||
|
|
||||||
// 业务错误
|
// 业务错误
|
||||||
var (
|
var (
|
||||||
ErrDocCategoryNotFound = errors.New("分类不存在")
|
ErrDocCategoryNotFound = errors.New("分类不存在")
|
||||||
ErrDocCategoryHasChild = errors.New("存在子分类,请先删除子分类")
|
ErrDocCategoryHasChild = errors.New("存在子分类,请先删除子分类")
|
||||||
ErrDocCategoryHasDoc = errors.New("分类下仍有文档,请先移动或删除")
|
ErrDocCategoryHasDoc = errors.New("分类下仍有文档,请先移动或删除")
|
||||||
ErrDocCategoryNameEmpty = errors.New("请输入分类名称")
|
ErrDocCategoryNameEmpty = errors.New("请输入分类名称")
|
||||||
ErrDocCategoryCircular = errors.New("不能将分类移动到自己或其子分类下")
|
ErrDocCategoryCircular = errors.New("不能将分类移动到自己或其子分类下")
|
||||||
ErrDocNotFound = errors.New("文档不存在")
|
ErrDocCategorySystem = errors.New("系统内置分类不可删除")
|
||||||
ErrDocTitleEmpty = errors.New("请输入文档标题")
|
ErrDocCategoryNameKept = errors.New("该分类名称为系统保留,不可使用")
|
||||||
ErrDocCategoryInvalid = errors.New("所属分类不存在")
|
ErrDocCategorySystemLocked = errors.New("系统内置分类固定为一级分类,不可移动")
|
||||||
ErrDocLinkSelf = errors.New("不能关联文档自身")
|
ErrDocNotFound = errors.New("文档不存在")
|
||||||
ErrDocLinkDuplicate = errors.New("该关联关系已存在")
|
ErrDocTitleEmpty = errors.New("请输入文档标题")
|
||||||
ErrDocLinkTargetNotFound = errors.New("被关联的文档不存在")
|
ErrDocCategoryInvalid = errors.New("所属分类不存在")
|
||||||
ErrDocLinkNotFound = errors.New("关联关系不存在")
|
ErrDocLinkSelf = errors.New("不能关联文档自身")
|
||||||
ErrDocIDEmpty = errors.New("请选择要操作的文档")
|
ErrDocLinkDuplicate = errors.New("该关联关系已存在")
|
||||||
ErrDocNoPermission = errors.New("无权操作该私密文档")
|
ErrDocLinkTargetNotFound = errors.New("被关联的文档不存在")
|
||||||
|
ErrDocLinkNotFound = errors.New("关联关系不存在")
|
||||||
|
ErrDocIDEmpty = errors.New("请选择要操作的文档")
|
||||||
|
ErrDocNoPermission = errors.New("无权操作该私密文档")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------- 访问者上下文与可见性 ----------------
|
// ---------------- 访问者上下文与可见性 ----------------
|
||||||
@@ -54,6 +57,8 @@ type OaDocActor struct {
|
|||||||
func NewOaDocActor(tid int, uid uint64) OaDocActor {
|
func NewOaDocActor(tid int, uid uint64) OaDocActor {
|
||||||
models.EnsureOaDocumentTables()
|
models.EnsureOaDocumentTables()
|
||||||
models.EnsureTenantUserOrgColumn()
|
models.EnsureTenantUserOrgColumn()
|
||||||
|
// 补建租户自己的系统内置分类(项目文档),按 tid 隔离
|
||||||
|
models.EnsureOaDocDefaultCategories(tid)
|
||||||
|
|
||||||
actor := OaDocActor{Tid: tid, UID: uid, OrgIDs: []uint64{}}
|
actor := OaDocActor{Tid: tid, UID: uid, OrgIDs: []uint64{}}
|
||||||
|
|
||||||
@@ -164,6 +169,48 @@ func oaDocApplyVisible(qs orm.QuerySeter, a OaDocActor) orm.QuerySeter {
|
|||||||
return qs.SetCond(qs.GetCond().AndCond(cond))
|
return qs.SetCond(qs.GetCond().AndCond(cond))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- 可见性(共享 / 私密) ----------------
|
||||||
|
//
|
||||||
|
// 文档库按「共享文档 / 私密文档」两个空间组织,每个空间各自一棵分类树;
|
||||||
|
// 文档可见性由 visibility 决定,并与所在空间一一对应:0=租户公开(共享空间),1=私密(私密空间,仅创建者及被共享者可见)。
|
||||||
|
// "我的文档"由 creator_id 决定,不与空间字段强耦合。
|
||||||
|
|
||||||
|
// oaDocVisibilityRestrictSQL 生成 Raw SQL 场景复用的「用户级可见性」条件片段
|
||||||
|
// (非管理员才附加:公开 OR 本人创建 OR 已共享给我)。prefix 为表别名前缀。
|
||||||
|
func oaDocVisibilityRestrictSQL(a OaDocActor, prefix string) (string, []interface{}) {
|
||||||
|
if a.IsAdmin {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
shared := oaDocSharedDocIDs(a)
|
||||||
|
if len(shared) == 0 {
|
||||||
|
shared = []uint64{0} // 避免 SQL 出现 IN ()
|
||||||
|
}
|
||||||
|
placeholders := make([]string, len(shared))
|
||||||
|
for i := range shared {
|
||||||
|
placeholders[i] = "?"
|
||||||
|
}
|
||||||
|
args := []interface{}{a.UID}
|
||||||
|
for _, id := range shared {
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
" AND (%svisibility = 0 OR %screator_id = ? OR %sid IN (%s))",
|
||||||
|
prefix, prefix, prefix, strings.Join(placeholders, ",")), args
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocApplyVisibility 给查询集叠加绝对可见性过滤(按文档自身的 visibility 字段)。
|
||||||
|
// v<0 不过滤;0=公开;1=私密。
|
||||||
|
func oaDocApplyVisibility(qs orm.QuerySeter, v int) orm.QuerySeter {
|
||||||
|
switch v {
|
||||||
|
case 0:
|
||||||
|
return qs.Filter("visibility", models.DocVisibilityPublic)
|
||||||
|
case 1:
|
||||||
|
return qs.Filter("visibility", models.DocVisibilityPrivate)
|
||||||
|
default:
|
||||||
|
return qs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// oaDocVisibleIDSet 从给定文档 ID 中筛出当前访问者可见的部分。
|
// oaDocVisibleIDSet 从给定文档 ID 中筛出当前访问者可见的部分。
|
||||||
func oaDocVisibleIDSet(a OaDocActor, ids []uint64) map[uint64]bool {
|
func oaDocVisibleIDSet(a OaDocActor, ids []uint64) map[uint64]bool {
|
||||||
out := make(map[uint64]bool, len(ids))
|
out := make(map[uint64]bool, len(ids))
|
||||||
@@ -217,24 +264,114 @@ type OaDocCategoryItem struct {
|
|||||||
Children []*OaDocCategoryItem `json:"children"`
|
Children []*OaDocCategoryItem `json:"children"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// oaDocCategoryBase 租户内未删除分类的基础查询集
|
// ---------------- 范围(共享 / 私密) ----------------
|
||||||
func oaDocCategoryBase(tid int) orm.QuerySeter {
|
|
||||||
|
// oaDocScopeOf 归一化范围参数:空或未知一律视为共享文档空间。
|
||||||
|
func oaDocScopeOf(scope string) string {
|
||||||
|
if scope == models.DocScopePersonal {
|
||||||
|
return models.DocScopePersonal
|
||||||
|
}
|
||||||
|
if scope == models.DocScopeAll {
|
||||||
|
return models.DocScopeAll
|
||||||
|
}
|
||||||
|
return models.DocScopeShared
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocScopeVisibility 返回某范围对应的文档可见性:私密→私密(1),其余→租户公开(0)。
|
||||||
|
func oaDocScopeVisibility(scope string) int8 {
|
||||||
|
if scope == models.DocScopePersonal {
|
||||||
|
return models.DocVisibilityPrivate
|
||||||
|
}
|
||||||
|
return models.DocVisibilityPublic
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocScopeOfVisibility 由文档可见性反推范围:私密(1)→私密文档空间,其余→共享文档空间。
|
||||||
|
func oaDocScopeOfVisibility(v int8) string {
|
||||||
|
if v == models.DocVisibilityPrivate {
|
||||||
|
return models.DocScopePersonal
|
||||||
|
}
|
||||||
|
return models.DocScopeShared
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocCategoryUserID 返回某范围下分类的归属用户:共享→0(租户级),私密→当前用户(绑定到用户级别)。
|
||||||
|
func oaDocCategoryUserID(a OaDocActor, scope string) uint64 {
|
||||||
|
if scope == models.DocScopePersonal {
|
||||||
|
return a.UID
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocCategoryBaseAny 租户内未删除分类的基础查询集(不限范围,用于按 ID 检索 / 权限校验)
|
||||||
|
func oaDocCategoryBaseAny(tid int) orm.QuerySeter {
|
||||||
return models.Orm.QueryTable(new(models.OaDocCategory)).
|
return models.Orm.QueryTable(new(models.OaDocCategory)).
|
||||||
Filter("tid", tid).
|
Filter("tid", tid).
|
||||||
Filter("is_deleted", 0)
|
Filter("is_deleted", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocCategoryTree 返回分类树,每个节点附带该分类下的文档数量(仅统计直属文档)。
|
// oaDocCategoryBase 返回指定范围内的未删除分类基础查询集。
|
||||||
// 数量统计同样受可见性约束:无权限的私密文档不计入。
|
// 共享空间:scope=shared 且 user_id=0(租户级,所有人共用);
|
||||||
func OaDocCategoryTree(a OaDocActor) ([]*OaDocCategoryItem, error) {
|
// 私密空间:scope=personal 且 user_id=当前用户(绑定到用户级别,互不干扰);
|
||||||
|
// all:跨两个空间(仅图谱等全局视图使用)。
|
||||||
|
func oaDocCategoryBase(a OaDocActor, scope string) orm.QuerySeter {
|
||||||
|
s := oaDocScopeOf(scope)
|
||||||
|
if s == models.DocScopeAll {
|
||||||
|
// 跨空间:不加 scope / user_id 过滤
|
||||||
|
return oaDocCategoryBaseAny(a.Tid)
|
||||||
|
}
|
||||||
|
qs := oaDocCategoryBaseAny(a.Tid).Filter("scope", s)
|
||||||
|
if s == models.DocScopePersonal {
|
||||||
|
qs = qs.Filter("user_id", a.UID)
|
||||||
|
} else if s == models.DocScopeShared {
|
||||||
|
qs = qs.Filter("user_id", 0)
|
||||||
|
}
|
||||||
|
return qs
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocCategoryIDs 返回指定范围内全部未删除分类的 ID。
|
||||||
|
func oaDocCategoryIDs(a OaDocActor, scope string) ([]uint64, error) {
|
||||||
|
var list []models.OaDocCategory
|
||||||
|
qs := oaDocCategoryBaseAny(a.Tid)
|
||||||
|
if scope != models.DocScopeAll {
|
||||||
|
s := oaDocScopeOf(scope)
|
||||||
|
qs = qs.Filter("scope", s)
|
||||||
|
if s == models.DocScopePersonal {
|
||||||
|
qs = qs.Filter("user_id", a.UID)
|
||||||
|
} else if s == models.DocScopeShared {
|
||||||
|
qs = qs.Filter("user_id", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := qs.All(&list); err != nil && err != orm.ErrNoRows {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ids := make([]uint64, 0, len(list))
|
||||||
|
for _, c := range list {
|
||||||
|
ids = append(ids, c.ID)
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocApplyUncategorized 筛选"未分类"文档:category_id=0 或不属于当前范围任何分类。
|
||||||
|
func oaDocApplyUncategorized(qs orm.QuerySeter, a OaDocActor, scope string) orm.QuerySeter {
|
||||||
|
ids, err := oaDocCategoryIDs(a, scope)
|
||||||
|
if err != nil || len(ids) == 0 {
|
||||||
|
return qs // 范围内没有任何分类,全部视为未分类
|
||||||
|
}
|
||||||
|
return qs.Exclude("category_id__in", ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OaDocCategoryTree 返回指定范围内的文档分类树,每个节点附带该分类下的文档数量。
|
||||||
|
// scope="shared" 返回共享空间树(租户级),scope="personal" 返回当前用户的私密空间树。
|
||||||
|
// 系统内置分类(项目文档)置顶;数量统计受可见性约束:无权限的私密文档不计入。
|
||||||
|
func OaDocCategoryTree(a OaDocActor, scope string) ([]*OaDocCategoryItem, error) {
|
||||||
models.EnsureOaDocumentTables()
|
models.EnsureOaDocumentTables()
|
||||||
|
|
||||||
|
scope = oaDocScopeOf(scope)
|
||||||
var list []models.OaDocCategory
|
var list []models.OaDocCategory
|
||||||
if _, err := oaDocCategoryBase(a.Tid).OrderBy("sort", "id").All(&list); err != nil && err != orm.ErrNoRows {
|
if _, err := oaDocCategoryBase(a, scope).OrderBy("-is_system", "sort", "id").All(&list); err != nil && err != orm.ErrNoRows {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
counts, err := OaDocCountByCategory(a)
|
counts, err := OaDocCountByCategory(a, scope, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -249,6 +386,7 @@ func OaDocCategoryTree(a OaDocActor) ([]*OaDocCategoryItem, error) {
|
|||||||
}
|
}
|
||||||
nodes[item.ID] = item
|
nodes[item.ID] = item
|
||||||
}
|
}
|
||||||
|
// 父节点必须也在本范围内(私密空间的父节点不会是共享空间的分类),否则归入根
|
||||||
for i := range list {
|
for i := range list {
|
||||||
item := nodes[list[i].ID]
|
item := nodes[list[i].ID]
|
||||||
if parent, ok := nodes[list[i].ParentID]; ok && list[i].ParentID != 0 {
|
if parent, ok := nodes[list[i].ParentID]; ok && list[i].ParentID != 0 {
|
||||||
@@ -257,33 +395,44 @@ func OaDocCategoryTree(a OaDocActor) ([]*OaDocCategoryItem, error) {
|
|||||||
roots = append(roots, item)
|
roots = append(roots, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 汇总子孙分类的文档数到父级:父级展示的是"包含所有子分类"的文档总数,
|
||||||
|
// 避免出现父级有子分类却显示 0 的情况。
|
||||||
|
var rollup func(item *OaDocCategoryItem) int64
|
||||||
|
rollup = func(item *OaDocCategoryItem) int64 {
|
||||||
|
total := item.DocCount
|
||||||
|
for _, child := range item.Children {
|
||||||
|
total += rollup(child)
|
||||||
|
}
|
||||||
|
item.DocCount = total
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
for _, root := range roots {
|
||||||
|
rollup(root)
|
||||||
|
}
|
||||||
return roots, nil
|
return roots, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocCountByCategory 统计各分类下未删除文档的数量(仅统计当前访问者可见的文档)。
|
// OaDocCountByCategory 统计指定范围内各分类下未删除文档的数量。
|
||||||
func OaDocCountByCategory(a OaDocActor) (map[uint64]int64, error) {
|
// visibility<0 时按范围自动取对应可见性(共享→公开,私密→私密);可见性约束同样生效:无权限的私密文档不计入。
|
||||||
|
// scope="all" 时跨两个空间统计(不限定 visibility)。
|
||||||
|
func OaDocCountByCategory(a OaDocActor, scope string, visibility int) (map[uint64]int64, error) {
|
||||||
models.EnsureOaDocumentTables()
|
models.EnsureOaDocumentTables()
|
||||||
|
|
||||||
|
scope = oaDocScopeOf(scope)
|
||||||
sql := "SELECT category_id, COUNT(*) AS cnt FROM yz_backend_oa_doc WHERE tid = ? AND is_deleted = 0"
|
sql := "SELECT category_id, COUNT(*) AS cnt FROM yz_backend_oa_doc WHERE tid = ? AND is_deleted = 0"
|
||||||
args := []interface{}{a.Tid}
|
args := []interface{}{a.Tid}
|
||||||
if !a.IsAdmin {
|
if scope != models.DocScopeAll {
|
||||||
shared := oaDocSharedDocIDs(a)
|
vis := int8(0)
|
||||||
if len(shared) == 0 {
|
if visibility >= 0 {
|
||||||
shared = []uint64{0}
|
vis = int8(visibility)
|
||||||
|
} else {
|
||||||
|
vis = oaDocScopeVisibility(scope)
|
||||||
}
|
}
|
||||||
placeholders := make([]string, len(shared))
|
sql += fmt.Sprintf(" AND visibility = %d", vis)
|
||||||
for i := range shared {
|
|
||||||
placeholders[i] = "?"
|
|
||||||
}
|
|
||||||
// 占位符顺序需与 args 一致:先 creator_id,再 IN (...)
|
|
||||||
args = append(args, a.UID)
|
|
||||||
for _, id := range shared {
|
|
||||||
args = append(args, id)
|
|
||||||
}
|
|
||||||
sql += fmt.Sprintf(
|
|
||||||
" AND (visibility = 0 OR creator_id = ? OR id IN (%s))",
|
|
||||||
strings.Join(placeholders, ","))
|
|
||||||
}
|
}
|
||||||
|
extra, extraArgs := oaDocVisibilityRestrictSQL(a, "")
|
||||||
|
sql += extra
|
||||||
|
args = append(args, extraArgs...)
|
||||||
sql += " GROUP BY category_id"
|
sql += " GROUP BY category_id"
|
||||||
|
|
||||||
var rows []struct {
|
var rows []struct {
|
||||||
@@ -300,16 +449,30 @@ func OaDocCountByCategory(a OaDocActor) (map[uint64]int64, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocCategoryCreate 新建分类。
|
// oaDocIsReservedName 判断分类名是否为系统保留名。
|
||||||
func OaDocCategoryCreate(tid int, parentID uint64, name, remark string, sort int) (*models.OaDocCategory, error) {
|
// 「未分类」是虚拟分类(文档 category_id=0),不允许新建同名真实分类;
|
||||||
|
// 「项目文档」为系统内置分类名(共享空间),禁止重复创建。
|
||||||
|
func oaDocIsReservedName(name string) bool {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
return name == "未分类" || name == models.DocCategoryNameProject
|
||||||
|
}
|
||||||
|
|
||||||
|
// OaDocCategoryCreate 在指定空间下新建分类。
|
||||||
|
// scope="shared" 创建到共享空间(user_id=0,租户级);scope="personal" 创建到当前用户的私密空间(user_id=uid)。
|
||||||
|
// 父级分类必须与目标空间一致,否则视为不存在。
|
||||||
|
func OaDocCategoryCreate(a OaDocActor, scope string, parentID uint64, name, remark string, sort int) (*models.OaDocCategory, error) {
|
||||||
models.EnsureOaDocumentTables()
|
models.EnsureOaDocumentTables()
|
||||||
|
|
||||||
|
scope = oaDocScopeOf(scope)
|
||||||
name = strings.TrimSpace(name)
|
name = strings.TrimSpace(name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return nil, ErrDocCategoryNameEmpty
|
return nil, ErrDocCategoryNameEmpty
|
||||||
}
|
}
|
||||||
|
if oaDocIsReservedName(name) {
|
||||||
|
return nil, ErrDocCategoryNameKept
|
||||||
|
}
|
||||||
if parentID != 0 {
|
if parentID != 0 {
|
||||||
ok, err := oaDocCategoryExists(tid, parentID)
|
ok, err := oaDocCategoryExists(a, scope, parentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -319,11 +482,13 @@ func OaDocCategoryCreate(tid int, parentID uint64, name, remark string, sort int
|
|||||||
}
|
}
|
||||||
|
|
||||||
item := &models.OaDocCategory{
|
item := &models.OaDocCategory{
|
||||||
Tid: tid,
|
Tid: a.Tid,
|
||||||
|
UserID: oaDocCategoryUserID(a, scope),
|
||||||
ParentID: parentID,
|
ParentID: parentID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Remark: strings.TrimSpace(remark),
|
Remark: strings.TrimSpace(remark),
|
||||||
Sort: sort,
|
Sort: sort,
|
||||||
|
Scope: scope,
|
||||||
IsDeleted: 0,
|
IsDeleted: 0,
|
||||||
}
|
}
|
||||||
if _, err := models.Orm.Insert(item); err != nil {
|
if _, err := models.Orm.Insert(item); err != nil {
|
||||||
@@ -333,16 +498,22 @@ func OaDocCategoryCreate(tid int, parentID uint64, name, remark string, sort int
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OaDocCategoryUpdate 修改分类名称/备注/排序,parentID < 0 表示不调整父级。
|
// OaDocCategoryUpdate 修改分类名称/备注/排序,parentID < 0 表示不调整父级。
|
||||||
func OaDocCategoryUpdate(tid int, id uint64, name, remark string, sort int, parentID int64) (*models.OaDocCategory, error) {
|
func OaDocCategoryUpdate(a OaDocActor, id uint64, name, remark string, sort int, parentID int64) (*models.OaDocCategory, error) {
|
||||||
models.EnsureOaDocumentTables()
|
models.EnsureOaDocumentTables()
|
||||||
|
|
||||||
var item models.OaDocCategory
|
var item models.OaDocCategory
|
||||||
if err := oaDocCategoryBase(tid).Filter("id", id).One(&item); err != nil {
|
if err := oaDocCategoryBaseAny(a.Tid).Filter("id", id).One(&item); err != nil {
|
||||||
return nil, ErrDocCategoryNotFound
|
return nil, ErrDocCategoryNotFound
|
||||||
}
|
}
|
||||||
|
if err := oaDocCheckCategoryOwner(a, &item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
fields := []string{"UpdateTime"}
|
fields := []string{"UpdateTime"}
|
||||||
if name = strings.TrimSpace(name); name != "" && name != item.Name {
|
if name = strings.TrimSpace(name); name != "" && name != item.Name {
|
||||||
|
if item.IsSystem != models.DocCategorySystem && oaDocIsReservedName(name) {
|
||||||
|
return nil, ErrDocCategoryNameKept
|
||||||
|
}
|
||||||
item.Name = name
|
item.Name = name
|
||||||
fields = append(fields, "Name")
|
fields = append(fields, "Name")
|
||||||
}
|
}
|
||||||
@@ -355,20 +526,22 @@ func OaDocCategoryUpdate(tid int, id uint64, name, remark string, sort int, pare
|
|||||||
fields = append(fields, "Sort")
|
fields = append(fields, "Sort")
|
||||||
}
|
}
|
||||||
if parentID >= 0 && uint64(parentID) != item.ParentID {
|
if parentID >= 0 && uint64(parentID) != item.ParentID {
|
||||||
|
if item.IsSystem == models.DocCategorySystem {
|
||||||
|
return nil, ErrDocCategorySystemLocked
|
||||||
|
}
|
||||||
newParent := uint64(parentID)
|
newParent := uint64(parentID)
|
||||||
if newParent == id {
|
if newParent == id {
|
||||||
return nil, ErrDocCategoryCircular
|
return nil, ErrDocCategoryCircular
|
||||||
}
|
}
|
||||||
if newParent != 0 {
|
if newParent != 0 {
|
||||||
ok, err := oaDocCategoryExists(tid, newParent)
|
ok, err := oaDocCategoryExists(a, item.Scope, newParent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrDocCategoryNotFound
|
return nil, ErrDocCategoryNotFound
|
||||||
}
|
}
|
||||||
// 禁止挂到自己的子孙节点下,避免形成环
|
childIDs, err := OaDocCategoryDescendants(a, newParent)
|
||||||
childIDs, err := OaDocCategoryDescendants(tid, id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -394,9 +567,10 @@ func OaDocCategoryUpdate(tid int, id uint64, name, remark string, sort int, pare
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OaDocCategoryDescendants 收集某分类的全部子孙 ID(不含自身)。
|
// OaDocCategoryDescendants 收集某分类的全部子孙 ID(不含自身)。
|
||||||
func OaDocCategoryDescendants(tid int, rootID uint64) ([]uint64, error) {
|
// 遍历租户内全部未删除分类(跨空间),用于移动时的环路检测。
|
||||||
|
func OaDocCategoryDescendants(a OaDocActor, rootID uint64) ([]uint64, error) {
|
||||||
var list []models.OaDocCategory
|
var list []models.OaDocCategory
|
||||||
if _, err := oaDocCategoryBase(tid).All(&list); err != nil && err != orm.ErrNoRows {
|
if _, err := oaDocCategoryBaseAny(a.Tid).All(&list); err != nil && err != orm.ErrNoRows {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
children := make(map[uint64][]uint64)
|
children := make(map[uint64][]uint64)
|
||||||
@@ -420,27 +594,35 @@ func OaDocCategoryDescendants(tid int, rootID uint64) ([]uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// oaDocCategoryIDsWithChildren 返回指定分类及其所有子孙 ID;rootID=0 时返回 nil 表示不限制。
|
// oaDocCategoryIDsWithChildren 返回指定分类及其所有子孙 ID;rootID=0 时返回 nil 表示不限制。
|
||||||
func oaDocCategoryIDsWithChildren(tid int, rootID uint64) ([]uint64, error) {
|
func oaDocCategoryIDsWithChildren(a OaDocActor, scope string, rootID uint64) ([]uint64, error) {
|
||||||
if rootID == 0 {
|
if rootID == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
children, err := OaDocCategoryDescendants(tid, rootID)
|
children, err := OaDocCategoryDescendants(a, rootID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return append([]uint64{rootID}, children...), nil
|
return append([]uint64{rootID}, children...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocCategoryDelete 删除分类:存在子分类或文档时拒绝删除。
|
// OaDocCategoryDelete 删除分类:系统内置分类、存在子分类或文档时拒绝删除。
|
||||||
func OaDocCategoryDelete(tid int, id uint64) error {
|
// 共享空间分类任何租户成员均可删除(内置分类除外);私密空间分类仅本人可删除。
|
||||||
|
func OaDocCategoryDelete(a OaDocActor, id uint64) error {
|
||||||
models.EnsureOaDocumentTables()
|
models.EnsureOaDocumentTables()
|
||||||
|
|
||||||
var item models.OaDocCategory
|
var item models.OaDocCategory
|
||||||
if err := oaDocCategoryBase(tid).Filter("id", id).One(&item); err != nil {
|
if err := oaDocCategoryBaseAny(a.Tid).Filter("id", id).One(&item); err != nil {
|
||||||
return ErrDocCategoryNotFound
|
return ErrDocCategoryNotFound
|
||||||
}
|
}
|
||||||
|
// 内置分类(如「项目文档」)为默认分类,任何租户都不可删除
|
||||||
|
if item.IsSystem == models.DocCategorySystem {
|
||||||
|
return ErrDocCategorySystem
|
||||||
|
}
|
||||||
|
if err := oaDocCheckCategoryOwner(a, &item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
childCount, err := oaDocCategoryBase(tid).Filter("parent_id", id).Count()
|
childCount, err := oaDocCategoryBase(a, item.Scope).Filter("parent_id", id).Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -448,7 +630,7 @@ func OaDocCategoryDelete(tid int, id uint64) error {
|
|||||||
return ErrDocCategoryHasChild
|
return ErrDocCategoryHasChild
|
||||||
}
|
}
|
||||||
|
|
||||||
docCount, err := oaDocBase(tid).Filter("category_id", id).Count()
|
docCount, err := oaDocBase(a.Tid).Filter("category_id", id).Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -457,7 +639,7 @@ func OaDocCategoryDelete(tid int, id uint64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
_, err = oaDocCategoryBase(tid).Filter("id", id).Update(orm.Params{
|
_, err = oaDocCategoryBaseAny(a.Tid).Filter("id", id).Update(orm.Params{
|
||||||
"IsDeleted": 1,
|
"IsDeleted": 1,
|
||||||
"DeleteTime": now,
|
"DeleteTime": now,
|
||||||
"UpdateTime": now,
|
"UpdateTime": now,
|
||||||
@@ -465,14 +647,37 @@ func OaDocCategoryDelete(tid int, id uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func oaDocCategoryExists(tid int, id uint64) (bool, error) {
|
// oaDocCategoryExists 判断指定范围内是否存在该分类(私密空间按 user_id 隔离)。
|
||||||
n, err := oaDocCategoryBase(tid).Filter("id", id).Count()
|
func oaDocCategoryExists(a OaDocActor, scope string, id uint64) (bool, error) {
|
||||||
|
n, err := oaDocCategoryBase(a, scope).Filter("id", id).Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return n > 0, nil
|
return n > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// oaDocCheckCategoryOwner 校验分类是否可被该访问者操作。
|
||||||
|
// 共享分类(user_id=0)租户内任何成员均可操作;私密分类(user_id>0)仅归属用户本人可操作。
|
||||||
|
func oaDocCheckCategoryOwner(a OaDocActor, item *models.OaDocCategory) error {
|
||||||
|
if item.UserID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if item.UserID == a.UID && a.UID != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ErrDocCategoryNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocCategoryUsable 读取指定范围内的分类并校验当前访问者是否可以使用它(用于文档归类)。
|
||||||
|
// 不存在或不属于当前范围/用户时返回 ok=false。
|
||||||
|
func oaDocCategoryUsable(a OaDocActor, scope string, id uint64) (models.OaDocCategory, bool, error) {
|
||||||
|
var item models.OaDocCategory
|
||||||
|
if err := oaDocCategoryBase(a, scope).Filter("id", id).One(&item); err != nil {
|
||||||
|
return item, false, nil
|
||||||
|
}
|
||||||
|
return item, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------- 文档 ----------------
|
// ---------------- 文档 ----------------
|
||||||
|
|
||||||
// oaDocBase 租户内未删除文档的基础查询集
|
// oaDocBase 租户内未删除文档的基础查询集
|
||||||
@@ -483,13 +688,17 @@ func oaDocBase(tid int) orm.QuerySeter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OaDocListParams 文档列表查询条件。
|
// OaDocListParams 文档列表查询条件。
|
||||||
// Status/DocType/Star 使用 -1 表示"全部";CategoryID > 0 时包含其全部子孙分类。
|
// Scope 限定空间:"shared"=共享文档(租户公开),"personal"=私密文档(绑定用户级别),"all"=跨两个空间。
|
||||||
// Actor 为访问者上下文,用于租户内用户级可见性过滤(私密文档只对创建者/被共享者可见)。
|
// Status/DocType/Star 使用 -1 表示"全部";CategoryID > 0 时包含其全部子孙分类(须与 Scope 同空间)。
|
||||||
|
// Mine 为 true 时只看本人创建的文档。Actor 用于租户内用户级可见性过滤。
|
||||||
type OaDocListParams struct {
|
type OaDocListParams struct {
|
||||||
Actor OaDocActor
|
Actor OaDocActor
|
||||||
|
Scope string
|
||||||
Keyword string
|
Keyword string
|
||||||
CategoryID uint64
|
CategoryID uint64
|
||||||
Uncategorized bool // true 时只看未分类(category_id = 0)的文档
|
Uncategorized bool // true 时只看未分类(category_id = 0)的文档
|
||||||
|
Visibility int // -1 不限(由 Scope 推导);0 公开;1 私密
|
||||||
|
Mine bool // true 时只看本人创建的文档
|
||||||
Status int
|
Status int
|
||||||
DocType int
|
DocType int
|
||||||
Tag string
|
Tag string
|
||||||
@@ -498,11 +707,18 @@ type OaDocListParams struct {
|
|||||||
PageSize int
|
PageSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocList 分页查询文档(自动应用可见性过滤)。
|
// OaDocList 分页查询文档(自动应用可见性与范围过滤)。
|
||||||
func OaDocList(a OaDocActor, p OaDocListParams) ([]models.OaDoc, int64, error) {
|
func OaDocList(a OaDocActor, p OaDocListParams) ([]models.OaDoc, int64, error) {
|
||||||
models.EnsureOaDocumentTables()
|
models.EnsureOaDocumentTables()
|
||||||
|
|
||||||
|
scope := oaDocScopeOf(p.Scope)
|
||||||
qs := oaDocApplyVisible(oaDocBase(a.Tid), a)
|
qs := oaDocApplyVisible(oaDocBase(a.Tid), a)
|
||||||
|
if scope != models.DocScopeAll {
|
||||||
|
qs = qs.Filter("visibility", oaDocScopeVisibility(scope))
|
||||||
|
}
|
||||||
|
if p.Mine {
|
||||||
|
qs = qs.Filter("creator_id", a.UID)
|
||||||
|
}
|
||||||
if kw := strings.TrimSpace(p.Keyword); kw != "" {
|
if kw := strings.TrimSpace(p.Keyword); kw != "" {
|
||||||
cond := orm.NewCondition()
|
cond := orm.NewCondition()
|
||||||
cond = cond.Or("title__icontains", kw).
|
cond = cond.Or("title__icontains", kw).
|
||||||
@@ -513,14 +729,17 @@ func OaDocList(a OaDocActor, p OaDocListParams) ([]models.OaDoc, int64, error) {
|
|||||||
qs = qs.SetCond(qs.GetCond().AndCond(cond))
|
qs = qs.SetCond(qs.GetCond().AndCond(cond))
|
||||||
}
|
}
|
||||||
if p.Uncategorized {
|
if p.Uncategorized {
|
||||||
qs = qs.Filter("category_id", 0)
|
qs = oaDocApplyUncategorized(qs, a, scope)
|
||||||
} else if p.CategoryID > 0 {
|
} else if p.CategoryID > 0 {
|
||||||
ids, err := oaDocCategoryIDsWithChildren(a.Tid, p.CategoryID)
|
ids, err := oaDocCategoryIDsWithChildren(a, scope, p.CategoryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
if len(ids) > 0 {
|
if len(ids) > 0 {
|
||||||
qs = qs.Filter("category_id__in", ids)
|
qs = qs.Filter("category_id__in", ids)
|
||||||
|
} else {
|
||||||
|
// 所选分类不在当前空间,直接返回空结果
|
||||||
|
qs = qs.Filter("category_id__in", []uint64{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p.Status >= 0 {
|
if p.Status >= 0 {
|
||||||
@@ -559,6 +778,13 @@ func OaDocList(a OaDocActor, p OaDocListParams) ([]models.OaDoc, int64, error) {
|
|||||||
if list == nil {
|
if list == nil {
|
||||||
list = []models.OaDoc{}
|
list = []models.OaDoc{}
|
||||||
}
|
}
|
||||||
|
// 列表不返回体积较大的编辑源,仅标记是否在线制作
|
||||||
|
for i := range list {
|
||||||
|
if strings.TrimSpace(list[i].Content) != "" {
|
||||||
|
list[i].IsOnline = 1
|
||||||
|
}
|
||||||
|
list[i].Content = ""
|
||||||
|
}
|
||||||
return list, total, nil
|
return list, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,20 +829,26 @@ func OaDocDownload(a OaDocActor, id uint64) (*models.OaDoc, error) {
|
|||||||
type OaDocSaveParams struct {
|
type OaDocSaveParams struct {
|
||||||
SetCategory bool
|
SetCategory bool
|
||||||
CategoryID uint64
|
CategoryID uint64
|
||||||
Title string
|
// SetVisibility 为 true 时才以 Visibility 覆盖原值,
|
||||||
FileID uint64
|
// 避免"未传该字段"被当成"改为租户公开"(int8 零值无法区分未传与显式 0)
|
||||||
FileURL string
|
SetVisibility bool
|
||||||
FileName string
|
Title string
|
||||||
Ext string
|
FileID uint64
|
||||||
Size uint64
|
FileURL string
|
||||||
Tags string
|
FileName string
|
||||||
Summary string
|
Ext string
|
||||||
Status int8
|
Size uint64
|
||||||
Version int
|
Tags string
|
||||||
OwnerID uint64
|
Summary string
|
||||||
OwnerName string
|
// Content 在线制作文档的编辑源(富文本 HTML),SetContent 为 true 时才覆盖
|
||||||
IsStar int8
|
SetContent bool
|
||||||
Visibility int8 // 0-租户公开 1-私密
|
Content string
|
||||||
|
Status int8
|
||||||
|
Version int
|
||||||
|
OwnerID uint64
|
||||||
|
OwnerName string
|
||||||
|
IsStar int8
|
||||||
|
Visibility int8 // 0-租户公开 1-私密
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocCreate 新增文档。
|
// OaDocCreate 新增文档。
|
||||||
@@ -632,12 +864,12 @@ func OaDocCreate(a OaDocActor, operatorName string, p OaDocSaveParams) (*models.
|
|||||||
if title == "" {
|
if title == "" {
|
||||||
return nil, ErrDocTitleEmpty
|
return nil, ErrDocTitleEmpty
|
||||||
}
|
}
|
||||||
|
// 分类须与文档可见性同空间:共享(visibility=0)→共享空间;私密(visibility=1)→私密空间(绑定用户级别)。
|
||||||
if p.CategoryID > 0 {
|
if p.CategoryID > 0 {
|
||||||
ok, err := oaDocCategoryExists(tid, p.CategoryID)
|
scope := oaDocScopeOfVisibility(p.Visibility)
|
||||||
if err != nil {
|
if _, ok, err := oaDocCategoryUsable(a, scope, p.CategoryID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
} else if !ok {
|
||||||
if !ok {
|
|
||||||
return nil, ErrDocCategoryInvalid
|
return nil, ErrDocCategoryInvalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -666,6 +898,7 @@ func OaDocCreate(a OaDocActor, operatorName string, p OaDocSaveParams) (*models.
|
|||||||
DocType: models.DocTypeByExt(ext),
|
DocType: models.DocTypeByExt(ext),
|
||||||
Tags: strings.TrimSpace(p.Tags),
|
Tags: strings.TrimSpace(p.Tags),
|
||||||
Summary: strings.TrimSpace(p.Summary),
|
Summary: strings.TrimSpace(p.Summary),
|
||||||
|
Content: p.Content,
|
||||||
Status: p.Status,
|
Status: p.Status,
|
||||||
Version: version,
|
Version: version,
|
||||||
IsStar: p.IsStar,
|
IsStar: p.IsStar,
|
||||||
@@ -703,23 +936,29 @@ func OaDocUpdate(a OaDocActor, id uint64, p OaDocSaveParams) (*models.OaDoc, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
fields := []string{"UpdateTime"}
|
fields := []string{"UpdateTime"}
|
||||||
if v := visibilityOf(p.Visibility); v != item.Visibility {
|
|
||||||
item.Visibility = v
|
|
||||||
fields = append(fields, "Visibility")
|
|
||||||
}
|
|
||||||
if p.SetCategory && p.CategoryID != item.CategoryID {
|
if p.SetCategory && p.CategoryID != item.CategoryID {
|
||||||
|
// 分类须与文档可见性同空间:以"更新后"的可见性推导范围
|
||||||
if p.CategoryID > 0 {
|
if p.CategoryID > 0 {
|
||||||
ok, err := oaDocCategoryExists(a.Tid, p.CategoryID)
|
v := item.Visibility
|
||||||
if err != nil {
|
if p.SetVisibility {
|
||||||
return nil, err
|
v = p.Visibility
|
||||||
}
|
}
|
||||||
if !ok {
|
scope := oaDocScopeOfVisibility(v)
|
||||||
|
if _, ok, err := oaDocCategoryUsable(a, scope, p.CategoryID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !ok {
|
||||||
return nil, ErrDocCategoryInvalid
|
return nil, ErrDocCategoryInvalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item.CategoryID = p.CategoryID
|
item.CategoryID = p.CategoryID
|
||||||
fields = append(fields, "CategoryID")
|
fields = append(fields, "CategoryID")
|
||||||
}
|
}
|
||||||
|
if p.SetVisibility {
|
||||||
|
if v := visibilityOf(p.Visibility); v != item.Visibility {
|
||||||
|
item.Visibility = v
|
||||||
|
fields = append(fields, "Visibility")
|
||||||
|
}
|
||||||
|
}
|
||||||
if title := strings.TrimSpace(p.Title); title != "" && title != item.Title {
|
if title := strings.TrimSpace(p.Title); title != "" && title != item.Title {
|
||||||
item.Title = title
|
item.Title = title
|
||||||
fields = append(fields, "Title")
|
fields = append(fields, "Title")
|
||||||
@@ -775,6 +1014,11 @@ func OaDocUpdate(a OaDocActor, id uint64, p OaDocSaveParams) (*models.OaDoc, err
|
|||||||
item.OwnerName = owner
|
item.OwnerName = owner
|
||||||
fields = append(fields, "OwnerName")
|
fields = append(fields, "OwnerName")
|
||||||
}
|
}
|
||||||
|
// 在线文档编辑源:仅在显式传入时覆盖
|
||||||
|
if p.SetContent && p.Content != item.Content {
|
||||||
|
item.Content = p.Content
|
||||||
|
fields = append(fields, "Content")
|
||||||
|
}
|
||||||
|
|
||||||
if len(fields) == 1 {
|
if len(fields) == 1 {
|
||||||
return &item, nil
|
return &item, nil
|
||||||
@@ -840,13 +1084,16 @@ func OaDocMove(a OaDocActor, ids []uint64, categoryID uint64) (int64, error) {
|
|||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return 0, ErrDocIDEmpty
|
return 0, ErrDocIDEmpty
|
||||||
}
|
}
|
||||||
|
// 移动仅改变文档归属分类;分类须与每篇文档的可见性同空间(共享/私密)
|
||||||
if categoryID > 0 {
|
if categoryID > 0 {
|
||||||
ok, err := oaDocCategoryExists(a.Tid, categoryID)
|
var sample models.OaDoc
|
||||||
if err != nil {
|
if _, err := oaDocApplyVisible(oaDocBase(a.Tid).Filter("id__in", ids), a).OrderBy("id").Limit(1).All(&sample); err == nil {
|
||||||
return 0, err
|
scope := oaDocScopeOfVisibility(sample.Visibility)
|
||||||
}
|
if _, ok, err := oaDocCategoryUsable(a, scope, categoryID); err != nil {
|
||||||
if !ok {
|
return 0, err
|
||||||
return 0, ErrDocCategoryInvalid
|
} else if !ok {
|
||||||
|
return 0, ErrDocCategoryInvalid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,10 +1112,11 @@ func OaDocMove(a OaDocActor, ids []uint64, categoryID uint64) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return oaDocBase(a.Tid).Filter("id__in", allowed).Update(orm.Params{
|
params := orm.Params{
|
||||||
"CategoryID": categoryID,
|
"CategoryID": categoryID,
|
||||||
"UpdateTime": now,
|
"UpdateTime": now,
|
||||||
})
|
}
|
||||||
|
return oaDocBase(a.Tid).Filter("id__in", allowed).Update(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocToggleStar 收藏 / 取消收藏切换,返回切换后的状态。
|
// OaDocToggleStar 收藏 / 取消收藏切换,返回切换后的状态。
|
||||||
@@ -892,16 +1140,20 @@ func OaDocToggleStar(a OaDocActor, id uint64) (int8, error) {
|
|||||||
|
|
||||||
// OaDocStatsResult 文档库概览统计
|
// OaDocStatsResult 文档库概览统计
|
||||||
type OaDocStatsResult struct {
|
type OaDocStatsResult struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Published int64 `json:"published"`
|
Published int64 `json:"published"`
|
||||||
Draft int64 `json:"draft"`
|
Draft int64 `json:"draft"`
|
||||||
Archived int64 `json:"archived"`
|
Archived int64 `json:"archived"`
|
||||||
Starred int64 `json:"starred"`
|
Starred int64 `json:"starred"`
|
||||||
Uncategorized int64 `json:"uncategorized"`
|
Uncategorized int64 `json:"uncategorized"`
|
||||||
LinkCount int64 `json:"link_count"`
|
LinkCount int64 `json:"link_count"`
|
||||||
TypeCounts []OaDocTypeCount `json:"type_counts"`
|
// 可见性维度概览(不受当前 visibility 筛选影响,始终反映全库口径):
|
||||||
RecentDocs []models.OaDoc `json:"recent_docs"`
|
PublicCount int64 `json:"public_count"` // 团队公开文档数
|
||||||
HotDocs []models.OaDoc `json:"hot_docs"`
|
PrivateCount int64 `json:"private_count"` // 私密文档数
|
||||||
|
MineCount int64 `json:"mine_count"` // 我创建的文档数
|
||||||
|
TypeCounts []OaDocTypeCount `json:"type_counts"`
|
||||||
|
RecentDocs []models.OaDoc `json:"recent_docs"`
|
||||||
|
HotDocs []models.OaDoc `json:"hot_docs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocTypeCount 按文档类型统计
|
// OaDocTypeCount 按文档类型统计
|
||||||
@@ -910,26 +1162,71 @@ type OaDocTypeCount struct {
|
|||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocStats 文档库概览:总数、状态分布、类型分布、最近更新与热门文档(均受可见性约束)。
|
// OaDocStats 文档库概览:总数、状态分布、类型分布、最近更新与热门文档。
|
||||||
func OaDocStats(a OaDocActor) (*OaDocStatsResult, error) {
|
// scope 限定统计口径("shared" 共享文档空间 / "personal" 私密文档空间 / "all" 跨空间);可见性约束同样生效。
|
||||||
|
func OaDocStats(a OaDocActor, scope string) (*OaDocStatsResult, error) {
|
||||||
models.EnsureOaDocumentTables()
|
models.EnsureOaDocumentTables()
|
||||||
|
|
||||||
base := oaDocApplyVisible(oaDocBase(a.Tid), a)
|
scope = oaDocScopeOf(scope)
|
||||||
|
vis := int8(-1)
|
||||||
|
if scope != models.DocScopeAll {
|
||||||
|
vis = oaDocScopeVisibility(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前范围内的全部可见文档(私密仅创建者/被共享者可见)
|
||||||
|
allVisible := oaDocApplyVisible(oaDocBase(a.Tid), a)
|
||||||
|
if vis >= 0 {
|
||||||
|
allVisible = allVisible.Filter("visibility", vis)
|
||||||
|
}
|
||||||
|
mineCount, _ := allVisible.Filter("creator_id", a.UID).Count()
|
||||||
|
|
||||||
|
base := allVisible
|
||||||
total, _ := base.Count()
|
total, _ := base.Count()
|
||||||
published, _ := base.Filter("status", models.DocStatusPublish).Count()
|
published, _ := base.Filter("status", models.DocStatusPublish).Count()
|
||||||
draft, _ := base.Filter("status", models.DocStatusDraft).Count()
|
draft, _ := base.Filter("status", models.DocStatusDraft).Count()
|
||||||
archived, _ := base.Filter("status", models.DocStatusArchive).Count()
|
archived, _ := base.Filter("status", models.DocStatusArchive).Count()
|
||||||
starred, _ := base.Filter("is_star", 1).Count()
|
starred, _ := base.Filter("is_star", 1).Count()
|
||||||
uncategorized, _ := base.Filter("category_id", 0).Count()
|
uncategorized, _ := oaDocApplyUncategorized(base, a, scope).Count()
|
||||||
linkCount, _ := models.Orm.QueryTable(new(models.OaDocLink)).Filter("tid", a.Tid).Count()
|
|
||||||
|
// 关联数只统计"当前空间口径下文档发起"的连线
|
||||||
|
linkCount := int64(0)
|
||||||
|
linkExtra, linkExtraArgs := oaDocVisibilityRestrictSQL(a, "d.")
|
||||||
|
linkArgs := []interface{}{a.Tid}
|
||||||
|
if vis >= 0 {
|
||||||
|
linkArgs = append(linkArgs, vis)
|
||||||
|
}
|
||||||
|
linkArgs = append(linkArgs, linkExtraArgs...)
|
||||||
|
var linkRows []struct {
|
||||||
|
Cnt int64 `orm:"column(cnt)"`
|
||||||
|
}
|
||||||
|
visClause := ""
|
||||||
|
if vis >= 0 {
|
||||||
|
visClause = " AND d.visibility = ?"
|
||||||
|
}
|
||||||
|
_, _ = models.Orm.Raw(
|
||||||
|
`SELECT COUNT(*) AS cnt FROM yz_backend_oa_doc_link l
|
||||||
|
WHERE l.tid = ?`+visClause+` AND EXISTS (
|
||||||
|
SELECT 1 FROM yz_backend_oa_doc d
|
||||||
|
WHERE d.id = l.source_id AND d.is_deleted = 0`+linkExtra+")",
|
||||||
|
linkArgs...).QueryRows(&linkRows)
|
||||||
|
if len(linkRows) > 0 {
|
||||||
|
linkCount = linkRows[0].Cnt
|
||||||
|
}
|
||||||
|
|
||||||
var rows []struct {
|
var rows []struct {
|
||||||
DocType int8 `orm:"column(doc_type)"`
|
DocType int8 `orm:"column(doc_type)"`
|
||||||
Cnt int64 `orm:"column(cnt)"`
|
Cnt int64 `orm:"column(cnt)"`
|
||||||
}
|
}
|
||||||
|
typeExtra, typeExtraArgs := oaDocVisibilityRestrictSQL(a, "")
|
||||||
|
typeArgs := []interface{}{a.Tid}
|
||||||
|
if vis >= 0 {
|
||||||
|
typeArgs = append(typeArgs, vis)
|
||||||
|
}
|
||||||
|
typeArgs = append(typeArgs, typeExtraArgs...)
|
||||||
_, _ = models.Orm.Raw(
|
_, _ = models.Orm.Raw(
|
||||||
"SELECT doc_type, COUNT(*) AS cnt FROM yz_backend_oa_doc WHERE tid = ? AND is_deleted = 0 GROUP BY doc_type ORDER BY cnt DESC",
|
"SELECT doc_type, COUNT(*) AS cnt FROM yz_backend_oa_doc WHERE tid = ? AND is_deleted = 0"+
|
||||||
a.Tid).QueryRows(&rows)
|
visClause+typeExtra+" GROUP BY doc_type ORDER BY cnt DESC",
|
||||||
|
typeArgs...).QueryRows(&rows)
|
||||||
typeCounts := make([]OaDocTypeCount, 0, len(rows))
|
typeCounts := make([]OaDocTypeCount, 0, len(rows))
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
typeCounts = append(typeCounts, OaDocTypeCount{DocType: r.DocType, Count: r.Cnt})
|
typeCounts = append(typeCounts, OaDocTypeCount{DocType: r.DocType, Count: r.Cnt})
|
||||||
@@ -946,6 +1243,17 @@ func OaDocStats(a OaDocActor) (*OaDocStatsResult, error) {
|
|||||||
hot = []models.OaDoc{}
|
hot = []models.OaDoc{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publicCount, privateCount := int64(0), int64(0)
|
||||||
|
switch scope {
|
||||||
|
case models.DocScopeShared:
|
||||||
|
publicCount = total
|
||||||
|
case models.DocScopePersonal:
|
||||||
|
privateCount = total
|
||||||
|
case models.DocScopeAll:
|
||||||
|
publicCount, _ = oaDocApplyVisible(oaDocBase(a.Tid), a).Filter("visibility", models.DocVisibilityPublic).Count()
|
||||||
|
privateCount, _ = oaDocApplyVisible(oaDocBase(a.Tid), a).Filter("visibility", models.DocVisibilityPrivate).Count()
|
||||||
|
}
|
||||||
|
|
||||||
return &OaDocStatsResult{
|
return &OaDocStatsResult{
|
||||||
Total: total,
|
Total: total,
|
||||||
Published: published,
|
Published: published,
|
||||||
@@ -954,6 +1262,9 @@ func OaDocStats(a OaDocActor) (*OaDocStatsResult, error) {
|
|||||||
Starred: starred,
|
Starred: starred,
|
||||||
Uncategorized: uncategorized,
|
Uncategorized: uncategorized,
|
||||||
LinkCount: linkCount,
|
LinkCount: linkCount,
|
||||||
|
PublicCount: publicCount,
|
||||||
|
PrivateCount: privateCount,
|
||||||
|
MineCount: mineCount,
|
||||||
TypeCounts: typeCounts,
|
TypeCounts: typeCounts,
|
||||||
RecentDocs: recent,
|
RecentDocs: recent,
|
||||||
HotDocs: hot,
|
HotDocs: hot,
|
||||||
@@ -1371,13 +1682,13 @@ type OaDocRelationCount struct {
|
|||||||
|
|
||||||
// OaDocGraphStats 图谱概览统计
|
// OaDocGraphStats 图谱概览统计
|
||||||
type OaDocGraphStats struct {
|
type OaDocGraphStats struct {
|
||||||
DocCount int64 `json:"doc_count"`
|
DocCount int64 `json:"doc_count"`
|
||||||
LinkCount int64 `json:"link_count"`
|
LinkCount int64 `json:"link_count"`
|
||||||
CategoryCount int64 `json:"category_count"`
|
CategoryCount int64 `json:"category_count"`
|
||||||
IsolatedCount int64 `json:"isolated_count"`
|
IsolatedCount int64 `json:"isolated_count"`
|
||||||
AvgDegree float64 `json:"avg_degree"`
|
AvgDegree float64 `json:"avg_degree"`
|
||||||
RelationCounts []OaDocRelationCount `json:"relation_counts"`
|
RelationCounts []OaDocRelationCount `json:"relation_counts"`
|
||||||
TopNodes []OaDocGraphNode `json:"top_nodes"`
|
TopNodes []OaDocGraphNode `json:"top_nodes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OaDocGraphResult 图谱数据
|
// OaDocGraphResult 图谱数据
|
||||||
@@ -1402,7 +1713,8 @@ func OaDocGraph(a OaDocActor, p OaDocGraphParams) (*OaDocGraphResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
docs, _, err := OaDocList(a, OaDocListParams{
|
docs, _, err := OaDocList(a, OaDocListParams{
|
||||||
Actor: a,
|
Actor: a,
|
||||||
|
Scope: models.DocScopeAll,
|
||||||
Keyword: p.Keyword,
|
Keyword: p.Keyword,
|
||||||
CategoryID: p.CategoryID,
|
CategoryID: p.CategoryID,
|
||||||
Status: -1,
|
Status: -1,
|
||||||
@@ -1476,18 +1788,31 @@ func OaDocGraph(a OaDocActor, p OaDocGraphParams) (*OaDocGraphResult, error) {
|
|||||||
// 归属分类连线 + 分类中心节点
|
// 归属分类连线 + 分类中心节点
|
||||||
categoryCount := int64(0)
|
categoryCount := int64(0)
|
||||||
if p.IncludeCategory {
|
if p.IncludeCategory {
|
||||||
|
// 图谱包含两类分类节点:共享分类(user_id=0,Category=1)与当前用户自己的私密分类(Category=2)。
|
||||||
|
// 只取用户可见范围(共享 + 本人私密),避免把其他用户的私密分类暴露出来。
|
||||||
|
catCond := orm.NewCondition().
|
||||||
|
And("tid", a.Tid).
|
||||||
|
And("is_deleted", 0).
|
||||||
|
AndCond(orm.NewCondition().Or("user_id", 0).Or("user_id", a.UID))
|
||||||
var cats []models.OaDocCategory
|
var cats []models.OaDocCategory
|
||||||
if _, err := oaDocCategoryBase(a.Tid).OrderBy("sort", "id").All(&cats); err != nil && err != orm.ErrNoRows {
|
if _, err := models.Orm.QueryTable(new(models.OaDocCategory)).SetCond(catCond).OrderBy("sort", "id").All(&cats); err != nil && err != orm.ErrNoRows {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
categoryCount = int64(len(cats))
|
categoryCount = int64(len(cats))
|
||||||
|
|
||||||
used := make(map[uint64]bool, len(cats))
|
catByID := make(map[uint64]models.OaDocCategory, len(cats))
|
||||||
|
for _, c := range cats {
|
||||||
|
catByID[c.ID] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文档 → 分类 归属连线(仅当分类在当前可见范围内,避免出现指向不存在节点的悬空连线)
|
||||||
for _, d := range docs {
|
for _, d := range docs {
|
||||||
if d.CategoryID == 0 {
|
if d.CategoryID == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
used[d.CategoryID] = true
|
if _, ok := catByID[d.CategoryID]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
links = append(links, OaDocGraphLink{
|
links = append(links, OaDocGraphLink{
|
||||||
Source: fmt.Sprintf("d%d", d.ID),
|
Source: fmt.Sprintf("d%d", d.ID),
|
||||||
Target: fmt.Sprintf("c%d", d.CategoryID),
|
Target: fmt.Sprintf("c%d", d.CategoryID),
|
||||||
@@ -1498,14 +1823,36 @@ func OaDocGraph(a OaDocActor, p OaDocGraphParams) (*OaDocGraphResult, error) {
|
|||||||
degree[fmt.Sprintf("d%d", d.ID)]++
|
degree[fmt.Sprintf("d%d", d.ID)]++
|
||||||
degree[fmt.Sprintf("c%d", d.CategoryID)]++
|
degree[fmt.Sprintf("c%d", d.CategoryID)]++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分类 → 父分类 层级连线(补全分类树的上下级结构,避免顶级/上级分类缺失)
|
||||||
for _, c := range cats {
|
for _, c := range cats {
|
||||||
if !used[c.ID] {
|
if c.ParentID == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if _, ok := catByID[c.ParentID]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
links = append(links, OaDocGraphLink{
|
||||||
|
Source: fmt.Sprintf("c%d", c.ID),
|
||||||
|
Target: fmt.Sprintf("c%d", c.ParentID),
|
||||||
|
Relation: models.DocRelationBelong,
|
||||||
|
Label: "子分类",
|
||||||
|
Kind: "cate",
|
||||||
|
})
|
||||||
|
degree[fmt.Sprintf("c%d", c.ID)]++
|
||||||
|
degree[fmt.Sprintf("c%d", c.ParentID)]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前可见范围内的全部分类都作为中心节点,保证分类树完整
|
||||||
|
for _, c := range cats {
|
||||||
|
catKind := 1
|
||||||
|
if c.UserID != 0 {
|
||||||
|
catKind = 2 // 私密分类
|
||||||
|
}
|
||||||
nodes = append(nodes, OaDocGraphNode{
|
nodes = append(nodes, OaDocGraphNode{
|
||||||
ID: fmt.Sprintf("c%d", c.ID),
|
ID: fmt.Sprintf("c%d", c.ID),
|
||||||
Name: c.Name,
|
Name: c.Name,
|
||||||
Category: 1,
|
Category: catKind,
|
||||||
CategoryID: c.ID,
|
CategoryID: c.ID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user