feat: add rich text help article editor
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<!-- gitnexus:start -->
|
<!-- gitnexus:start -->
|
||||||
# GitNexus — Code Intelligence
|
# GitNexus — Code Intelligence
|
||||||
|
|
||||||
This project is indexed by GitNexus as **anxinyan** (5079 symbols, 12462 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
This project is indexed by GitNexus as **anxinyan** (5135 symbols, 12579 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||||
|
|
||||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- gitnexus:start -->
|
<!-- gitnexus:start -->
|
||||||
# GitNexus — Code Intelligence
|
# GitNexus — Code Intelligence
|
||||||
|
|
||||||
This project is indexed by GitNexus as **anxinyan** (5079 symbols, 12462 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
This project is indexed by GitNexus as **anxinyan** (5135 symbols, 12579 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||||
|
|
||||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||||
|
|
||||||
|
|||||||
537
admin-web/package-lock.json
generated
537
admin-web/package-lock.json
generated
@@ -10,6 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.15.0",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.13.7",
|
"element-plus": "^2.13.7",
|
||||||
@@ -76,6 +78,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
|
||||||
@@ -515,6 +526,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@transloadit/prettier-bytes": {
|
||||||
|
"version": "0.0.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
|
||||||
|
"integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
@@ -526,6 +543,12 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/event-emitter": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.17.24",
|
"version": "4.17.24",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz",
|
||||||
@@ -565,6 +588,61 @@
|
|||||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@uppy/companion-client": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@uppy/utils": "^4.1.2",
|
||||||
|
"namespace-emitter": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@uppy/core": {
|
||||||
|
"version": "2.3.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
|
||||||
|
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@transloadit/prettier-bytes": "0.0.7",
|
||||||
|
"@uppy/store-default": "^2.1.1",
|
||||||
|
"@uppy/utils": "^4.1.3",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"mime-match": "^1.0.2",
|
||||||
|
"namespace-emitter": "^2.0.1",
|
||||||
|
"nanoid": "^3.1.25",
|
||||||
|
"preact": "^10.5.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@uppy/store-default": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@uppy/utils": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.throttle": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@uppy/xhr-upload": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@uppy/companion-client": "^2.2.2",
|
||||||
|
"@uppy/utils": "^4.1.2",
|
||||||
|
"nanoid": "^3.1.25"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@uppy/core": "^2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "6.0.6",
|
"version": "6.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz",
|
||||||
@@ -842,6 +920,165 @@
|
|||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@wangeditor/basic-modules": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-url": "^1.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/code-highlight": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prismjs": "^1.23.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/core": {
|
||||||
|
"version": "1.1.19",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
|
||||||
|
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/event-emitter": "^0.3.3",
|
||||||
|
"event-emitter": "^0.3.5",
|
||||||
|
"html-void-elements": "^2.0.0",
|
||||||
|
"i18next": "^20.4.0",
|
||||||
|
"scroll-into-view-if-needed": "^2.2.28",
|
||||||
|
"slate-history": "^0.66.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@uppy/core": "^2.1.1",
|
||||||
|
"@uppy/xhr-upload": "^2.0.3",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"is-hotkey": "^0.2.0",
|
||||||
|
"lodash.camelcase": "^4.3.0",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.foreach": "^4.5.0",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"lodash.toarray": "^4.4.0",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/editor": {
|
||||||
|
"version": "5.1.23",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
|
||||||
|
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@uppy/core": "^2.1.1",
|
||||||
|
"@uppy/xhr-upload": "^2.0.3",
|
||||||
|
"@wangeditor/basic-modules": "^1.1.7",
|
||||||
|
"@wangeditor/code-highlight": "^1.0.3",
|
||||||
|
"@wangeditor/core": "^1.1.19",
|
||||||
|
"@wangeditor/list-module": "^1.0.5",
|
||||||
|
"@wangeditor/table-module": "^1.1.4",
|
||||||
|
"@wangeditor/upload-image-module": "^1.0.2",
|
||||||
|
"@wangeditor/video-module": "^1.1.4",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"is-hotkey": "^0.2.0",
|
||||||
|
"lodash.camelcase": "^4.3.0",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.foreach": "^4.5.0",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"lodash.toarray": "^4.4.0",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/editor-for-vue": {
|
||||||
|
"version": "5.1.12",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
|
||||||
|
"integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/editor": ">=5.1.0",
|
||||||
|
"vue": "^3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/list-module": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/table-module": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/upload-image-module": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@uppy/core": "^2.0.3",
|
||||||
|
"@uppy/xhr-upload": "^2.0.3",
|
||||||
|
"@wangeditor/basic-modules": "1.x",
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"lodash.foreach": "^4.5.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/video-module": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@uppy/core": "^2.1.4",
|
||||||
|
"@uppy/xhr-upload": "^2.0.7",
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"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",
|
||||||
@@ -1027,6 +1264,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compute-scroll-into-view": {
|
||||||
|
"version": "1.0.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
|
||||||
|
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/confbox": {
|
"node_modules/confbox": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz",
|
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz",
|
||||||
@@ -1054,6 +1297,19 @@
|
|||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d/-/d-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"es5-ext": "^0.10.64",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.20",
|
"version": "1.11.20",
|
||||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz",
|
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz",
|
||||||
@@ -1094,6 +1350,15 @@
|
|||||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dom7": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ssr-window": "^3.0.0-alpha.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
@@ -1213,18 +1478,92 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es5-ext": {
|
||||||
|
"version": "0.10.64",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz",
|
||||||
|
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"es6-iterator": "^2.0.3",
|
||||||
|
"es6-symbol": "^3.1.3",
|
||||||
|
"esniff": "^2.0.1",
|
||||||
|
"next-tick": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-iterator": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "^0.10.35",
|
||||||
|
"es6-symbol": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-symbol": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.2",
|
||||||
|
"ext": "^1.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esniff": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/esniff/-/esniff-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.1",
|
||||||
|
"es5-ext": "^0.10.62",
|
||||||
|
"event-emitter": "^0.3.5",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/event-emitter": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "~0.10.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ext": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"type": "^2.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -1418,6 +1757,35 @@
|
|||||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/html-void-elements": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/i18next": {
|
||||||
|
"version": "20.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz",
|
||||||
|
"integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "9.0.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
|
||||||
|
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
@@ -1427,6 +1795,27 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-hotkey": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/is-plain-object": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-url": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-what": {
|
"node_modules/is-what": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
|
||||||
@@ -1776,6 +2165,49 @@
|
|||||||
"lodash-es": "*"
|
"lodash-es": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.camelcase": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.clonedeep": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.debounce": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.foreach": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.throttle": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.toarray": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"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",
|
||||||
@@ -1824,6 +2256,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wildcard": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime-types": {
|
"node_modules/mime-types": {
|
||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
@@ -1877,6 +2318,12 @@
|
|||||||
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/namespace-emitter": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -1895,6 +2342,12 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-tick": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/normalize-wheel-es": {
|
"node_modules/normalize-wheel-es": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||||
@@ -2052,6 +2505,25 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.29.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.29.2.tgz",
|
||||||
|
"integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prismjs": {
|
||||||
|
"version": "1.30.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz",
|
||||||
|
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
@@ -2169,6 +2641,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/scroll-into-view-if-needed": {
|
||||||
|
"version": "2.2.31",
|
||||||
|
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
|
||||||
|
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"compute-scroll-into-view": "^1.0.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scule": {
|
"node_modules/scule": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
|
||||||
@@ -2181,6 +2662,38 @@
|
|||||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/slate": {
|
||||||
|
"version": "0.72.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
|
||||||
|
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immer": "^9.0.6",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"tiny-warning": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/slate-history": {
|
||||||
|
"version": "0.66.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz",
|
||||||
|
"integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-plain-object": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"slate": ">=0.65.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/snabbdom": {
|
||||||
|
"version": "3.6.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.3.tgz",
|
||||||
|
"integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -2199,6 +2712,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssr-window": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||||
@@ -2237,6 +2756,12 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-warning": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.16",
|
||||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||||
@@ -2261,6 +2786,12 @@
|
|||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/type": {
|
||||||
|
"version": "2.7.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz",
|
||||||
|
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz",
|
||||||
@@ -2536,6 +3067,12 @@
|
|||||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/wildcard": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.15.0",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.13.7",
|
"element-plus": "^2.13.7",
|
||||||
|
|||||||
@@ -1509,6 +1509,7 @@ export interface AdminHelpArticleItem {
|
|||||||
title: string;
|
title: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
|
content_html: string;
|
||||||
content_blocks: string[];
|
content_blocks: string[];
|
||||||
is_recommended: boolean;
|
is_recommended: boolean;
|
||||||
is_enabled: boolean;
|
is_enabled: boolean;
|
||||||
@@ -1522,6 +1523,7 @@ export interface AdminHelpArticlePayload {
|
|||||||
title: string;
|
title: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
|
content_html: string;
|
||||||
content_blocks: string[];
|
content_blocks: string[];
|
||||||
is_recommended: boolean;
|
is_recommended: boolean;
|
||||||
is_enabled: boolean;
|
is_enabled: boolean;
|
||||||
|
|||||||
220
admin-web/src/components/RichTextEditor.vue
Normal file
220
admin-web/src/components/RichTextEditor.vue
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import "@wangeditor/editor/dist/css/style.css";
|
||||||
|
import { computed, onBeforeUnmount, ref, shallowRef, watch } from "vue";
|
||||||
|
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
||||||
|
import type { IDomEditor, IEditorConfig, IToolbarConfig } from "@wangeditor/editor";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
modelValue: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
minHeight?: number;
|
||||||
|
uploadImage?: (file: File) => Promise<string>;
|
||||||
|
}>(), {
|
||||||
|
minHeight: 560,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:modelValue", value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const mode = "default";
|
||||||
|
const editorRef = shallowRef<IDomEditor>();
|
||||||
|
const valueHtml = ref(props.modelValue || "");
|
||||||
|
|
||||||
|
const editorHeight = computed(() => `${props.minHeight}px`);
|
||||||
|
|
||||||
|
const toolbarConfig: Partial<IToolbarConfig> = {
|
||||||
|
toolbarKeys: [
|
||||||
|
"header2",
|
||||||
|
"header3",
|
||||||
|
"header4",
|
||||||
|
"|",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"underline",
|
||||||
|
"through",
|
||||||
|
"blockquote",
|
||||||
|
"|",
|
||||||
|
"bulletedList",
|
||||||
|
"numberedList",
|
||||||
|
"|",
|
||||||
|
"insertLink",
|
||||||
|
{
|
||||||
|
key: "group-image",
|
||||||
|
title: "图片",
|
||||||
|
menuKeys: ["insertImage", "uploadImage"],
|
||||||
|
},
|
||||||
|
"|",
|
||||||
|
"clearStyle",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const editorConfig: Partial<IEditorConfig> = {
|
||||||
|
placeholder: "请输入文章正文,可使用标题、列表、引用、链接和图片。",
|
||||||
|
scroll: true,
|
||||||
|
MENU_CONF: {
|
||||||
|
uploadImage: {
|
||||||
|
maxFileSize: 5 * 1024 * 1024,
|
||||||
|
allowedFileTypes: ["image/*"],
|
||||||
|
async customUpload(file: File, insertFn: (url: string, alt?: string, href?: string) => void) {
|
||||||
|
if (!file.type.startsWith("image/")) {
|
||||||
|
ElMessage.error("仅支持上传图片文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!props.uploadImage) {
|
||||||
|
ElMessage.error("图片上传接口未配置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = await props.uploadImage(file);
|
||||||
|
insertFn(url, file.name);
|
||||||
|
ElMessage.success("图片已插入");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
ElMessage.error("图片上传失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
insertLink: {
|
||||||
|
checkLink: (_text: string, url: string) => {
|
||||||
|
if (!url.trim()) {
|
||||||
|
return "链接地址不能为空";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(value) => {
|
||||||
|
if (value === valueHtml.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
valueHtml.value = value || "";
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(valueHtml, (value) => {
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.disabled,
|
||||||
|
(disabled) => {
|
||||||
|
const editor = editorRef.value;
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (disabled) {
|
||||||
|
editor.disable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.enable();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleCreated(editor: IDomEditor) {
|
||||||
|
editorRef.value = editor;
|
||||||
|
if (props.disabled) {
|
||||||
|
editor.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
editorRef.value?.destroy();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rich-text-editor" :class="{ 'rich-text-editor--disabled': disabled }">
|
||||||
|
<Toolbar class="rich-text-editor__toolbar" :editor="editorRef" :default-config="toolbarConfig" :mode="mode" />
|
||||||
|
<Editor
|
||||||
|
v-model="valueHtml"
|
||||||
|
class="rich-text-editor__body"
|
||||||
|
:style="{ height: editorHeight }"
|
||||||
|
:default-config="editorConfig"
|
||||||
|
:mode="mode"
|
||||||
|
@on-created="handleCreated"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rich-text-editor {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 14px 36px rgba(34, 28, 18, 0.06);
|
||||||
|
--w-e-toolbar-bg-color: #fff;
|
||||||
|
--w-e-toolbar-color: var(--admin-text-main);
|
||||||
|
--w-e-toolbar-active-bg-color: rgba(200, 164, 93, 0.14);
|
||||||
|
--w-e-toolbar-active-color: var(--admin-text-main);
|
||||||
|
--w-e-toolbar-border-color: var(--admin-border);
|
||||||
|
--w-e-textarea-bg-color: #fff;
|
||||||
|
--w-e-textarea-color: var(--admin-text-main);
|
||||||
|
--w-e-textarea-selected-border-color: rgba(200, 164, 93, 0.55);
|
||||||
|
--w-e-textarea-slight-bg-color: #fbf7ef;
|
||||||
|
--w-e-textarea-slight-color: #9aa0aa;
|
||||||
|
--w-e-modal-button-bg-color: #fff;
|
||||||
|
--w-e-modal-button-border-color: var(--admin-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-editor--disabled {
|
||||||
|
opacity: 0.74;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-editor__toolbar {
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
background: linear-gradient(180deg, #fff 0%, #fbfbfc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-toolbar) {
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-bar-item button) {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-bar-item button:hover) {
|
||||||
|
background: rgba(200, 164, 93, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-text-container) {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-text-placeholder) {
|
||||||
|
color: #9aa0aa;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-scroll) {
|
||||||
|
padding: 24px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-text-container [data-slate-editor]) {
|
||||||
|
color: var(--admin-text-main);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-text-container [data-slate-editor] h2),
|
||||||
|
:deep(.w-e-text-container [data-slate-editor] h3),
|
||||||
|
:deep(.w-e-text-container [data-slate-editor] h4) {
|
||||||
|
color: var(--admin-text);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-text-container [data-slate-editor] img) {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
admin-web/src/pages/content/article-edit.vue
Normal file
202
admin-web/src/pages/content/article-edit.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { adminApi, type AdminHelpArticleItem } from "../../api/admin";
|
||||||
|
import RichTextEditor from "../../components/RichTextEditor.vue";
|
||||||
|
import { articleCategoryOptions, contentBlocksFromHtml, parseLines, resetArticleForm, type ArticleFormState } from "./shared";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const articleForm = reactive<ArticleFormState>({
|
||||||
|
category: "service",
|
||||||
|
title: "",
|
||||||
|
summary: "",
|
||||||
|
keywordsText: "",
|
||||||
|
contentHtml: "",
|
||||||
|
is_recommended: false,
|
||||||
|
is_enabled: true,
|
||||||
|
sort_order: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const articleId = computed(() => Number(route.params.id || 0));
|
||||||
|
const isEditMode = computed(() => articleId.value > 0);
|
||||||
|
const pageTitle = computed(() => (isEditMode.value ? "编辑帮助文章" : "新增帮助文章"));
|
||||||
|
|
||||||
|
function backToList() {
|
||||||
|
router.push({ name: "content-articles" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchArticle() {
|
||||||
|
if (!isEditMode.value) {
|
||||||
|
resetArticleForm(articleForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await adminApi.getHelpArticles();
|
||||||
|
const article = response.data.list.find((item: AdminHelpArticleItem) => item.id === articleId.value);
|
||||||
|
if (!article) {
|
||||||
|
ElMessage.error("帮助文章不存在");
|
||||||
|
backToList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetArticleForm(articleForm, article);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
ElMessage.error("帮助文章加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadArticleImage(file: File) {
|
||||||
|
const response = await adminApi.uploadContentImage(file);
|
||||||
|
return response.data.file_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitArticle() {
|
||||||
|
const contentBlocks = contentBlocksFromHtml(articleForm.contentHtml);
|
||||||
|
if (!articleForm.title.trim()) {
|
||||||
|
ElMessage.error("请填写文章标题");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!articleForm.summary.trim()) {
|
||||||
|
ElMessage.error("请填写文章摘要");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!contentBlocks.length) {
|
||||||
|
ElMessage.error("请填写文章正文");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await adminApi.saveHelpArticle({
|
||||||
|
id: articleForm.id,
|
||||||
|
category: articleForm.category,
|
||||||
|
title: articleForm.title.trim(),
|
||||||
|
summary: articleForm.summary.trim(),
|
||||||
|
keywords: parseLines(articleForm.keywordsText),
|
||||||
|
content_html: articleForm.contentHtml,
|
||||||
|
content_blocks: contentBlocks,
|
||||||
|
is_recommended: articleForm.is_recommended,
|
||||||
|
is_enabled: articleForm.is_enabled,
|
||||||
|
sort_order: articleForm.sort_order,
|
||||||
|
});
|
||||||
|
ElMessage.success(articleForm.id ? "帮助文章已更新" : "帮助文章已创建");
|
||||||
|
backToList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
ElMessage.error("帮助文章保存失败");
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchArticle);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="article-edit-page" v-loading="loading">
|
||||||
|
<el-card class="panel-card article-edit-header" shadow="never">
|
||||||
|
<div>
|
||||||
|
<div class="article-edit-header__title">{{ pageTitle }}</div>
|
||||||
|
<div class="article-edit-header__desc">独立编辑页提供更大的正文区域,适合维护长文章和图文说明。</div>
|
||||||
|
</div>
|
||||||
|
<div class="article-edit-header__actions">
|
||||||
|
<el-button @click="backToList">返回列表</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitArticle">保存文章</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="panel-card" shadow="never">
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-row :gutter="18">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="分类">
|
||||||
|
<el-select v-model="articleForm.category" style="width: 100%">
|
||||||
|
<el-option v-for="item in articleCategoryOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number v-model="articleForm.sort_order" :min="0" :step="10" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-form-item label="推荐">
|
||||||
|
<el-switch v-model="articleForm.is_recommended" inline-prompt active-text="是" inactive-text="否" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-form-item label="启用">
|
||||||
|
<el-switch v-model="articleForm.is_enabled" inline-prompt active-text="启用" inactive-text="停用" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input v-model="articleForm.title" maxlength="120" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="摘要">
|
||||||
|
<el-input v-model="articleForm.summary" type="textarea" :rows="3" maxlength="300" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="关键词">
|
||||||
|
<el-input v-model="articleForm.keywordsText" type="textarea" :rows="3" placeholder="每行一个关键词" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="正文内容">
|
||||||
|
<RichTextEditor v-model="articleForm.contentHtml" :disabled="saving" :upload-image="uploadArticleImage" :min-height="640" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.article-edit-page {
|
||||||
|
min-height: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-edit-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-edit-header :deep(.el-card__body) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-edit-header__title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-edit-header__desc {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--admin-text-subtle);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-edit-header__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,24 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import { adminApi, type AdminHelpArticleItem } from "../../api/admin";
|
import { adminApi, type AdminHelpArticleItem } from "../../api/admin";
|
||||||
import { articleCategoryOptions, parseLines, resetArticleForm, type ArticleFormState } from "./shared";
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const articleSaving = ref(false);
|
|
||||||
const articleDialogVisible = ref(false);
|
|
||||||
const articles = ref<AdminHelpArticleItem[]>([]);
|
const articles = ref<AdminHelpArticleItem[]>([]);
|
||||||
|
const router = useRouter();
|
||||||
const articleForm = reactive<ArticleFormState>({
|
|
||||||
category: "service",
|
|
||||||
title: "",
|
|
||||||
summary: "",
|
|
||||||
keywordsText: "",
|
|
||||||
contentBlocksText: "",
|
|
||||||
is_recommended: false,
|
|
||||||
is_enabled: true,
|
|
||||||
sort_order: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const articleStats = computed(() => ({
|
const articleStats = computed(() => ({
|
||||||
total: articles.value.length,
|
total: articles.value.length,
|
||||||
@@ -40,38 +28,11 @@ async function fetchArticles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openCreateArticle() {
|
function openCreateArticle() {
|
||||||
resetArticleForm(articleForm);
|
router.push({ name: "content-article-create" });
|
||||||
articleDialogVisible.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditArticle(row: AdminHelpArticleItem) {
|
function openEditArticle(row: AdminHelpArticleItem) {
|
||||||
resetArticleForm(articleForm, row);
|
router.push({ name: "content-article-edit", params: { id: row.id } });
|
||||||
articleDialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitArticle() {
|
|
||||||
articleSaving.value = true;
|
|
||||||
try {
|
|
||||||
await adminApi.saveHelpArticle({
|
|
||||||
id: articleForm.id,
|
|
||||||
category: articleForm.category,
|
|
||||||
title: articleForm.title.trim(),
|
|
||||||
summary: articleForm.summary.trim(),
|
|
||||||
keywords: parseLines(articleForm.keywordsText),
|
|
||||||
content_blocks: parseLines(articleForm.contentBlocksText),
|
|
||||||
is_recommended: articleForm.is_recommended,
|
|
||||||
is_enabled: articleForm.is_enabled,
|
|
||||||
sort_order: articleForm.sort_order,
|
|
||||||
});
|
|
||||||
ElMessage.success(articleForm.id ? "帮助文章已更新" : "帮助文章已创建");
|
|
||||||
articleDialogVisible.value = false;
|
|
||||||
await fetchArticles();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
ElMessage.error("帮助文章保存失败");
|
|
||||||
} finally {
|
|
||||||
articleSaving.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteArticle(row: AdminHelpArticleItem) {
|
async function deleteArticle(row: AdminHelpArticleItem) {
|
||||||
@@ -133,58 +94,4 @@ onMounted(fetchArticles);
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog v-model="articleDialogVisible" :title="articleForm.id ? '编辑帮助文章' : '新增帮助文章'" width="760px">
|
|
||||||
<el-form label-position="top">
|
|
||||||
<el-row :gutter="16">
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="分类">
|
|
||||||
<el-select v-model="articleForm.category" style="width: 100%">
|
|
||||||
<el-option v-for="item in articleCategoryOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="排序">
|
|
||||||
<el-input-number v-model="articleForm.sort_order" :min="0" :step="10" style="width: 100%" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="4">
|
|
||||||
<el-form-item label="推荐">
|
|
||||||
<el-switch v-model="articleForm.is_recommended" inline-prompt active-text="是" inactive-text="否" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="4">
|
|
||||||
<el-form-item label="启用">
|
|
||||||
<el-switch v-model="articleForm.is_enabled" inline-prompt active-text="启用" inactive-text="停用" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-form-item label="标题">
|
|
||||||
<el-input v-model="articleForm.title" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-form-item label="摘要">
|
|
||||||
<el-input v-model="articleForm.summary" type="textarea" :rows="2" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-form-item label="关键词">
|
|
||||||
<el-input v-model="articleForm.keywordsText" type="textarea" :rows="3" placeholder="每行一个关键词" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-form-item label="正文内容">
|
|
||||||
<el-input v-model="articleForm.contentBlocksText" type="textarea" :rows="8" placeholder="每行一段正文内容" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="articleDialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="articleSaving" @click="submitArticle">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type ArticleFormState = {
|
|||||||
title: string;
|
title: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
keywordsText: string;
|
keywordsText: string;
|
||||||
contentBlocksText: string;
|
contentHtml: string;
|
||||||
is_recommended: boolean;
|
is_recommended: boolean;
|
||||||
is_enabled: boolean;
|
is_enabled: boolean;
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
@@ -143,7 +143,7 @@ export function resetArticleForm(target: ArticleFormState, row?: AdminHelpArticl
|
|||||||
target.title = row?.title || "";
|
target.title = row?.title || "";
|
||||||
target.summary = row?.summary || "";
|
target.summary = row?.summary || "";
|
||||||
target.keywordsText = row?.keywords?.join("\n") || "";
|
target.keywordsText = row?.keywords?.join("\n") || "";
|
||||||
target.contentBlocksText = row?.content_blocks?.join("\n") || "";
|
target.contentHtml = row ? articleContentHtml(row) : "";
|
||||||
target.is_recommended = row?.is_recommended || false;
|
target.is_recommended = row?.is_recommended || false;
|
||||||
target.is_enabled = row ? row.is_enabled : true;
|
target.is_enabled = row ? row.is_enabled : true;
|
||||||
target.sort_order = row?.sort_order || 0;
|
target.sort_order = row?.sort_order || 0;
|
||||||
@@ -155,3 +155,42 @@ export function parseLines(value: string) {
|
|||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function contentBlocksFromHtml(value: string) {
|
||||||
|
if (!value.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(value, "text/html");
|
||||||
|
const blocks = Array.from(doc.body.querySelectorAll("h2, h3, h4, p, li, blockquote"))
|
||||||
|
.map((item) => (item.textContent || "").replace(/\s+/g, " ").trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (blocks.length) {
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyText = (doc.body.textContent || "").replace(/\s+/g, " ").trim();
|
||||||
|
return bodyText ? [bodyText] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function articleContentHtml(row: AdminHelpArticleItem) {
|
||||||
|
if (row.content_html?.trim()) {
|
||||||
|
return row.content_html;
|
||||||
|
}
|
||||||
|
return contentBlocksToHtml(row.content_blocks || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentBlocksToHtml(blocks: string[]) {
|
||||||
|
return blocks.map((item) => `<p>${escapeHtml(item)}</p>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value: string) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|||||||
@@ -202,6 +202,30 @@ const adminChildren = [
|
|||||||
contentTab: "articles",
|
contentTab: "articles",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "articles/create",
|
||||||
|
name: "content-article-create",
|
||||||
|
component: () => import("../pages/content/article-edit.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "内容中心",
|
||||||
|
desc: "新增帮助文章:编辑分类、摘要、关键词和富文本正文。",
|
||||||
|
permission: "system.manage",
|
||||||
|
menuIndex: "content",
|
||||||
|
contentTab: "articles",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "articles/:id/edit",
|
||||||
|
name: "content-article-edit",
|
||||||
|
component: () => import("../pages/content/article-edit.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "内容中心",
|
||||||
|
desc: "编辑帮助文章:维护富文本正文、推荐状态和排序。",
|
||||||
|
permission: "system.manage",
|
||||||
|
menuIndex: "content",
|
||||||
|
contentTab: "articles",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
6
admin-web/src/types/wangeditor.d.ts
vendored
Normal file
6
admin-web/src/types/wangeditor.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
declare module "@wangeditor/editor-for-vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
|
||||||
|
export const Editor: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
|
||||||
|
export const Toolbar: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
|
||||||
|
}
|
||||||
@@ -118,6 +118,7 @@ class ContentsController
|
|||||||
'title' => trim((string)$request->input('title', '')),
|
'title' => trim((string)$request->input('title', '')),
|
||||||
'summary' => trim((string)$request->input('summary', '')),
|
'summary' => trim((string)$request->input('summary', '')),
|
||||||
'keywords' => (array)$request->input('keywords', []),
|
'keywords' => (array)$request->input('keywords', []),
|
||||||
|
'content_html' => (string)$request->input('content_html', ''),
|
||||||
'content_blocks' => (array)$request->input('content_blocks', []),
|
'content_blocks' => (array)$request->input('content_blocks', []),
|
||||||
'is_recommended' => (bool)$request->input('is_recommended', false),
|
'is_recommended' => (bool)$request->input('is_recommended', false),
|
||||||
'is_enabled' => (bool)$request->input('is_enabled', true),
|
'is_enabled' => (bool)$request->input('is_enabled', true),
|
||||||
|
|||||||
@@ -160,6 +160,17 @@ class ContentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
return array_map(function (array $item) {
|
return array_map(function (array $item) {
|
||||||
|
$contentBlocks = $this->decodeJsonConfig($item['content_blocks_json'] ?? '', []);
|
||||||
|
$contentHtml = trim((string)($item['content_html'] ?? ''));
|
||||||
|
if ($contentHtml === '') {
|
||||||
|
$contentHtml = $this->contentBlocksToHtml($contentBlocks);
|
||||||
|
} else {
|
||||||
|
$contentHtml = $this->sanitizeHelpArticleHtml($contentHtml);
|
||||||
|
}
|
||||||
|
if (!$contentBlocks && $contentHtml !== '') {
|
||||||
|
$contentBlocks = $this->contentBlocksFromHtml($contentHtml);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int)$item['id'],
|
'id' => (int)$item['id'],
|
||||||
'category' => (string)$item['category'],
|
'category' => (string)$item['category'],
|
||||||
@@ -171,7 +182,8 @@ class ContentService
|
|||||||
'is_recommended' => (bool)$item['is_recommended'],
|
'is_recommended' => (bool)$item['is_recommended'],
|
||||||
'is_enabled' => (bool)$item['is_enabled'],
|
'is_enabled' => (bool)$item['is_enabled'],
|
||||||
'sort_order' => (int)$item['sort_order'],
|
'sort_order' => (int)$item['sort_order'],
|
||||||
'content_blocks' => $this->decodeJsonConfig($item['content_blocks_json'] ?? '', []),
|
'content_html' => $contentHtml,
|
||||||
|
'content_blocks' => $contentBlocks,
|
||||||
];
|
];
|
||||||
}, $query->select()->toArray());
|
}, $query->select()->toArray());
|
||||||
}
|
}
|
||||||
@@ -227,11 +239,19 @@ class ContentService
|
|||||||
$title = trim((string)($payload['title'] ?? ''));
|
$title = trim((string)($payload['title'] ?? ''));
|
||||||
$summary = trim((string)($payload['summary'] ?? ''));
|
$summary = trim((string)($payload['summary'] ?? ''));
|
||||||
$keywords = $this->normalizeStringList($payload['keywords'] ?? [], []);
|
$keywords = $this->normalizeStringList($payload['keywords'] ?? [], []);
|
||||||
|
$contentHtml = $this->sanitizeHelpArticleHtml((string)($payload['content_html'] ?? ''));
|
||||||
$contentBlocks = $this->normalizeStringList($payload['content_blocks'] ?? [], []);
|
$contentBlocks = $this->normalizeStringList($payload['content_blocks'] ?? [], []);
|
||||||
$isRecommended = !empty($payload['is_recommended']) ? 1 : 0;
|
$isRecommended = !empty($payload['is_recommended']) ? 1 : 0;
|
||||||
$isEnabled = array_key_exists('is_enabled', $payload) ? (!empty($payload['is_enabled']) ? 1 : 0) : 1;
|
$isEnabled = array_key_exists('is_enabled', $payload) ? (!empty($payload['is_enabled']) ? 1 : 0) : 1;
|
||||||
$sortOrder = (int)($payload['sort_order'] ?? 0);
|
$sortOrder = (int)($payload['sort_order'] ?? 0);
|
||||||
|
|
||||||
|
if ($contentHtml !== '') {
|
||||||
|
$contentBlocks = $this->contentBlocksFromHtml($contentHtml);
|
||||||
|
}
|
||||||
|
if ($contentHtml === '' && $contentBlocks) {
|
||||||
|
$contentHtml = $this->contentBlocksToHtml($contentBlocks);
|
||||||
|
}
|
||||||
|
|
||||||
if ($title === '' || $summary === '') {
|
if ($title === '' || $summary === '') {
|
||||||
throw new \RuntimeException('文章标题和摘要不能为空');
|
throw new \RuntimeException('文章标题和摘要不能为空');
|
||||||
}
|
}
|
||||||
@@ -248,6 +268,7 @@ class ContentService
|
|||||||
'title' => $title,
|
'title' => $title,
|
||||||
'summary' => $summary,
|
'summary' => $summary,
|
||||||
'keywords_json' => json_encode($keywords, JSON_UNESCAPED_UNICODE),
|
'keywords_json' => json_encode($keywords, JSON_UNESCAPED_UNICODE),
|
||||||
|
'content_html' => $contentHtml,
|
||||||
'content_blocks_json' => json_encode($contentBlocks, JSON_UNESCAPED_UNICODE),
|
'content_blocks_json' => json_encode($contentBlocks, JSON_UNESCAPED_UNICODE),
|
||||||
'is_recommended' => $isRecommended,
|
'is_recommended' => $isRecommended,
|
||||||
'is_enabled' => $isEnabled,
|
'is_enabled' => $isEnabled,
|
||||||
@@ -402,6 +423,7 @@ class ContentService
|
|||||||
{
|
{
|
||||||
$exists = Db::query(sprintf("SHOW TABLES LIKE '%s'", self::HELP_TABLE));
|
$exists = Db::query(sprintf("SHOW TABLES LIKE '%s'", self::HELP_TABLE));
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
|
$this->ensureHelpArticlesContentHtmlColumn();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +434,7 @@ class ContentService
|
|||||||
title VARCHAR(255) NOT NULL DEFAULT '',
|
title VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
summary VARCHAR(500) NOT NULL DEFAULT '',
|
summary VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
keywords_json LONGTEXT NULL,
|
keywords_json LONGTEXT NULL,
|
||||||
|
content_html LONGTEXT NULL,
|
||||||
content_blocks_json LONGTEXT NULL,
|
content_blocks_json LONGTEXT NULL,
|
||||||
is_recommended TINYINT(1) NOT NULL DEFAULT 0,
|
is_recommended TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
is_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
is_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
@@ -497,12 +520,14 @@ class ContentService
|
|||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
foreach ($this->defaultHelpArticles() as $index => $item) {
|
foreach ($this->defaultHelpArticles() as $index => $item) {
|
||||||
|
$contentBlocks = $item['content_blocks'];
|
||||||
Db::name(self::HELP_TABLE)->insert([
|
Db::name(self::HELP_TABLE)->insert([
|
||||||
'category' => $item['category'],
|
'category' => $item['category'],
|
||||||
'title' => $item['title'],
|
'title' => $item['title'],
|
||||||
'summary' => $item['summary'],
|
'summary' => $item['summary'],
|
||||||
'keywords_json' => json_encode($item['keywords'], JSON_UNESCAPED_UNICODE),
|
'keywords_json' => json_encode($item['keywords'], JSON_UNESCAPED_UNICODE),
|
||||||
'content_blocks_json' => json_encode($item['content_blocks'], JSON_UNESCAPED_UNICODE),
|
'content_html' => $this->contentBlocksToHtml($contentBlocks),
|
||||||
|
'content_blocks_json' => json_encode($contentBlocks, JSON_UNESCAPED_UNICODE),
|
||||||
'is_recommended' => !empty($item['is_recommended']) ? 1 : 0,
|
'is_recommended' => !empty($item['is_recommended']) ? 1 : 0,
|
||||||
'is_enabled' => 1,
|
'is_enabled' => 1,
|
||||||
'sort_order' => $item['sort_order'] ?? ($index + 1) * 10,
|
'sort_order' => $item['sort_order'] ?? ($index + 1) * 10,
|
||||||
@@ -728,6 +753,166 @@ class ContentService
|
|||||||
return $normalized ?: $default;
|
return $normalized ?: $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensureHelpArticlesContentHtmlColumn(): void
|
||||||
|
{
|
||||||
|
$exists = Db::query(sprintf("SHOW COLUMNS FROM %s LIKE 'content_html'", self::HELP_TABLE));
|
||||||
|
if ($exists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::execute(sprintf(
|
||||||
|
"ALTER TABLE %s ADD COLUMN content_html LONGTEXT NULL AFTER keywords_json",
|
||||||
|
self::HELP_TABLE
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeHelpArticleHtml(string $html): string
|
||||||
|
{
|
||||||
|
$html = trim($html);
|
||||||
|
if ($html === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists(\DOMDocument::class)) {
|
||||||
|
return $this->sanitizeHelpArticleHtmlFallback($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = new \DOMDocument('1.0', 'UTF-8');
|
||||||
|
$previous = libxml_use_internal_errors(true);
|
||||||
|
$loaded = $document->loadHTML('<?xml encoding="UTF-8"><div>' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||||
|
libxml_clear_errors();
|
||||||
|
libxml_use_internal_errors($previous);
|
||||||
|
|
||||||
|
if (!$loaded || !$document->documentElement) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sanitizeHelpArticleNode($document->documentElement);
|
||||||
|
|
||||||
|
$output = '';
|
||||||
|
foreach ($document->documentElement->childNodes as $childNode) {
|
||||||
|
$output .= $document->saveHTML($childNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($output);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeHelpArticleNode(\DOMNode $node): void
|
||||||
|
{
|
||||||
|
$allowedTags = ['p', 'br', 'strong', 'em', 'u', 's', 'h2', 'h3', 'h4', 'ul', 'ol', 'li', 'blockquote', 'a', 'img'];
|
||||||
|
|
||||||
|
for ($index = $node->childNodes->length - 1; $index >= 0; $index--) {
|
||||||
|
$child = $node->childNodes->item($index);
|
||||||
|
if (!$child) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($child instanceof \DOMElement) {
|
||||||
|
$tagName = strtolower($child->tagName);
|
||||||
|
if (!in_array($tagName, $allowedTags, true)) {
|
||||||
|
if (in_array($tagName, ['script', 'style', 'iframe', 'object', 'embed'], true)) {
|
||||||
|
$node->removeChild($child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->sanitizeHelpArticleNode($child);
|
||||||
|
while ($child->firstChild) {
|
||||||
|
$node->insertBefore($child->firstChild, $child);
|
||||||
|
}
|
||||||
|
$node->removeChild($child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tagName === 'img' && !$this->isSafeHelpArticleUrl(trim((string)$child->getAttribute('src')))) {
|
||||||
|
$node->removeChild($child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sanitizeHelpArticleAttributes($child, $tagName);
|
||||||
|
$this->sanitizeHelpArticleNode($child);
|
||||||
|
} elseif ($child->nodeType !== XML_TEXT_NODE) {
|
||||||
|
$node->removeChild($child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeHelpArticleAttributes(\DOMElement $node, string $tagName): void
|
||||||
|
{
|
||||||
|
$href = trim((string)$node->getAttribute('href'));
|
||||||
|
$src = trim((string)$node->getAttribute('src'));
|
||||||
|
$alt = trim((string)$node->getAttribute('alt'));
|
||||||
|
$title = trim((string)$node->getAttribute('title'));
|
||||||
|
|
||||||
|
while ($node->attributes->length > 0) {
|
||||||
|
$node->removeAttribute($node->attributes->item(0)->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tagName === 'a') {
|
||||||
|
if ($this->isSafeHelpArticleUrl($href)) {
|
||||||
|
$node->setAttribute('href', $href);
|
||||||
|
$node->setAttribute('target', '_blank');
|
||||||
|
$node->setAttribute('rel', 'noopener noreferrer');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tagName !== 'img') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isSafeHelpArticleUrl($src)) {
|
||||||
|
$node->setAttribute('src', $src);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($alt !== '') {
|
||||||
|
$node->setAttribute('alt', $this->limitHelpArticleAttribute($alt));
|
||||||
|
}
|
||||||
|
if ($title !== '') {
|
||||||
|
$node->setAttribute('title', $this->limitHelpArticleAttribute($title));
|
||||||
|
}
|
||||||
|
$node->setAttribute('loading', 'lazy');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function limitHelpArticleAttribute(string $value): string
|
||||||
|
{
|
||||||
|
return function_exists('mb_substr') ? mb_substr($value, 0, 120) : substr($value, 0, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeHelpArticleHtmlFallback(string $html): string
|
||||||
|
{
|
||||||
|
$html = strip_tags($html, '<p><br><strong><em><u><s><h2><h3><h4><ul><ol><li><blockquote><a><img>');
|
||||||
|
$html = preg_replace('/\s+on[a-z]+\s*=\s*(["\']).*?\1/i', '', $html) ?? '';
|
||||||
|
$html = preg_replace('/\s+(href|src)\s*=\s*(["\'])\s*(javascript|data):.*?\2/i', '', $html) ?? '';
|
||||||
|
return trim($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSafeHelpArticleUrl(string $url): bool
|
||||||
|
{
|
||||||
|
if ($url === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool)preg_match('/^(https?:\/\/|\/(?!\/)|#)/i', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function contentBlocksFromHtml(string $html): array
|
||||||
|
{
|
||||||
|
$html = preg_replace('/<\/(h2|h3|h4|p|li|blockquote)>/i', "\n", $html) ?? $html;
|
||||||
|
$text = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(
|
||||||
|
fn ($item) => trim((string)preg_replace('/\s+/u', ' ', $item)),
|
||||||
|
preg_split('/\R/u', $text) ?: []
|
||||||
|
), fn ($item) => $item !== ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function contentBlocksToHtml(array $blocks): string
|
||||||
|
{
|
||||||
|
return implode('', array_map(
|
||||||
|
fn ($item) => '<p>' . htmlspecialchars((string)$item, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</p>',
|
||||||
|
$this->normalizeStringList($blocks, [])
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
private function defaultHomeConfig(): array
|
private function defaultHomeConfig(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -1093,12 +1278,14 @@ class ContentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$contentBlocks = $definition['content_blocks'];
|
||||||
return (int)Db::name(self::HELP_TABLE)->insertGetId([
|
return (int)Db::name(self::HELP_TABLE)->insertGetId([
|
||||||
'category' => $definition['category'],
|
'category' => $definition['category'],
|
||||||
'title' => $definition['title'],
|
'title' => $definition['title'],
|
||||||
'summary' => $definition['summary'],
|
'summary' => $definition['summary'],
|
||||||
'keywords_json' => json_encode($definition['keywords'], JSON_UNESCAPED_UNICODE),
|
'keywords_json' => json_encode($definition['keywords'], JSON_UNESCAPED_UNICODE),
|
||||||
'content_blocks_json' => json_encode($definition['content_blocks'], JSON_UNESCAPED_UNICODE),
|
'content_html' => $this->contentBlocksToHtml($contentBlocks),
|
||||||
|
'content_blocks_json' => json_encode($contentBlocks, JSON_UNESCAPED_UNICODE),
|
||||||
'is_recommended' => !empty($definition['is_recommended']) ? 1 : 0,
|
'is_recommended' => !empty($definition['is_recommended']) ? 1 : 0,
|
||||||
'is_enabled' => 1,
|
'is_enabled' => 1,
|
||||||
'sort_order' => (int)$definition['sort_order'],
|
'sort_order' => (int)$definition['sort_order'],
|
||||||
|
|||||||
@@ -1391,6 +1391,7 @@ CREATE TABLE help_articles (
|
|||||||
title VARCHAR(255) NOT NULL DEFAULT '',
|
title VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
summary VARCHAR(500) NOT NULL DEFAULT '',
|
summary VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
keywords_json LONGTEXT NULL,
|
keywords_json LONGTEXT NULL,
|
||||||
|
content_html LONGTEXT NULL,
|
||||||
content_blocks_json LONGTEXT NULL,
|
content_blocks_json LONGTEXT NULL,
|
||||||
is_recommended TINYINT(1) NOT NULL DEFAULT 0,
|
is_recommended TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
is_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
is_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export interface MessagePageCopy {
|
|||||||
|
|
||||||
export interface HelpArticleDetailData {
|
export interface HelpArticleDetailData {
|
||||||
article: HelpArticleSummary & {
|
article: HelpArticleSummary & {
|
||||||
|
content_html: string;
|
||||||
content_blocks: string[];
|
content_blocks: string[];
|
||||||
};
|
};
|
||||||
related_articles: HelpArticleSummary[];
|
related_articles: HelpArticleSummary[];
|
||||||
|
|||||||
@@ -558,6 +558,8 @@ export const helpArticleDetailFallback: HelpArticleDetailData = {
|
|||||||
keywords: ["实物鉴定", "中检鉴定", "服务区别"],
|
keywords: ["实物鉴定", "中检鉴定", "服务区别"],
|
||||||
updated_at: "2026-04-21 09:00:00",
|
updated_at: "2026-04-21 09:00:00",
|
||||||
is_recommended: true,
|
is_recommended: true,
|
||||||
|
content_html:
|
||||||
|
"<p>实物鉴定和中检鉴定都会经过下单、填写信息、上传资料、寄送商品、鉴定和查看报告这几个核心步骤。</p><p>两者最大的区别在于出具机构不同。实物鉴定由安心验提供标准实物鉴定服务;中检鉴定由更高规格合作机构提供服务,适合对机构资质有更高要求的场景。</p><p>中检鉴定通常价格更高、时效也会略长一些。下单前建议先根据您的使用场景、预算和时效要求选择合适服务。</p>",
|
||||||
content_blocks: [
|
content_blocks: [
|
||||||
"实物鉴定和中检鉴定都会经过下单、填写信息、上传资料、寄送商品、鉴定和查看报告这几个核心步骤。",
|
"实物鉴定和中检鉴定都会经过下单、填写信息、上传资料、寄送商品、鉴定和查看报告这几个核心步骤。",
|
||||||
"两者最大的区别在于出具机构不同。实物鉴定由安心验提供标准实物鉴定服务;中检鉴定由更高规格合作机构提供服务,适合对机构资质有更高要求的场景。",
|
"两者最大的区别在于出具机构不同。实物鉴定由安心验提供标准实物鉴定服务;中检鉴定由更高规格合作机构提供服务,适合对机构资质有更高要求的场景。",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
import { appApi, type HelpArticleDetailData } from "../../api/app";
|
import { appApi, type HelpArticleDetailData } from "../../api/app";
|
||||||
import { helpArticleDetailFallback } from "../../mocks/app";
|
import { helpArticleDetailFallback } from "../../mocks/app";
|
||||||
@@ -10,6 +10,7 @@ const articleId = ref(0);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const pageReady = ref(false);
|
const pageReady = ref(false);
|
||||||
const loadError = ref("");
|
const loadError = ref("");
|
||||||
|
const articleContentHtml = computed(() => detail.value.article.content_html?.trim() || "");
|
||||||
|
|
||||||
function openArticle(id: number) {
|
function openArticle(id: number) {
|
||||||
uni.redirectTo({ url: `/pages/help/detail?id=${id}` });
|
uni.redirectTo({ url: `/pages/help/detail?id=${id}` });
|
||||||
@@ -71,6 +72,8 @@ onLoad(async (options) => {
|
|||||||
|
|
||||||
<view class="section section-card section-card--soft">
|
<view class="section section-card section-card--soft">
|
||||||
<view class="section__title">详细说明</view>
|
<view class="section__title">详细说明</view>
|
||||||
|
<rich-text v-if="articleContentHtml" class="article-rich-text" :nodes="articleContentHtml" />
|
||||||
|
<template v-else>
|
||||||
<view
|
<view
|
||||||
v-for="(item, index) in detail.article.content_blocks"
|
v-for="(item, index) in detail.article.content_blocks"
|
||||||
:key="`${detail.article.id}-${index}`"
|
:key="`${detail.article.id}-${index}`"
|
||||||
@@ -79,6 +82,7 @@ onLoad(async (options) => {
|
|||||||
<text class="report-meta__label">{{ index + 1 }}.</text>
|
<text class="report-meta__label">{{ index + 1 }}.</text>
|
||||||
<text class="report-meta__value">{{ item }}</text>
|
<text class="report-meta__value">{{ item }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="detail.related_articles.length" class="section">
|
<view v-if="detail.related_articles.length" class="section">
|
||||||
@@ -118,3 +122,65 @@ onLoad(async (options) => {
|
|||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.article-rich-text {
|
||||||
|
display: block;
|
||||||
|
color: var(--color-body);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-rich-text :deep(p),
|
||||||
|
.article-rich-text :deep(h2),
|
||||||
|
.article-rich-text :deep(h3),
|
||||||
|
.article-rich-text :deep(h4),
|
||||||
|
.article-rich-text :deep(blockquote),
|
||||||
|
.article-rich-text :deep(ul),
|
||||||
|
.article-rich-text :deep(ol) {
|
||||||
|
margin: 0 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-rich-text :deep(h2) {
|
||||||
|
color: var(--color-heading);
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-rich-text :deep(h3),
|
||||||
|
.article-rich-text :deep(h4) {
|
||||||
|
color: var(--color-heading);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-rich-text :deep(ul),
|
||||||
|
.article-rich-text :deep(ol) {
|
||||||
|
padding-left: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-rich-text :deep(li) {
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-rich-text :deep(blockquote) {
|
||||||
|
padding: 18rpx 20rpx;
|
||||||
|
border-left: 6rpx solid var(--color-accent);
|
||||||
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||||
|
background: var(--color-brand-ivory);
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-rich-text :deep(a) {
|
||||||
|
color: var(--color-brand-gold-deep);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-rich-text :deep(img) {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 20rpx 0;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user