feat: 重做出入库逻辑
This commit is contained in:
parent
210e0d7dd5
commit
8ffd117dcf
|
@ -41,20 +41,27 @@
|
||||||
"@ant-design/icons-vue": "~7.0.1",
|
"@ant-design/icons-vue": "~7.0.1",
|
||||||
"@iconify/vue": "^4.1.1",
|
"@iconify/vue": "^4.1.1",
|
||||||
"@tinymce/tinymce-vue": "^5.1.1",
|
"@tinymce/tinymce-vue": "^5.1.1",
|
||||||
|
"@types/dat.gui": "^0.7.12",
|
||||||
|
"@types/three": "^0.162.0",
|
||||||
"@vueuse/core": "~10.8.0",
|
"@vueuse/core": "~10.8.0",
|
||||||
"ant-design-vue": "~4.1.2",
|
"ant-design-vue": "~4.1.2",
|
||||||
"axios": "~1.6.7",
|
"axios": "~1.6.7",
|
||||||
|
"dat.gui": "^0.7.9",
|
||||||
"dayjs": "~1.11.10",
|
"dayjs": "~1.11.10",
|
||||||
|
"dhtmlx-gantt": "^8.0.6",
|
||||||
"echarts": "^5.5.0",
|
"echarts": "^5.5.0",
|
||||||
"file-saver": "~2.0.5",
|
"file-saver": "~2.0.5",
|
||||||
|
"gsap": "^3.12.5",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"lodash-es": "~4.17.21",
|
"lodash-es": "~4.17.21",
|
||||||
|
"mathjs": "^12.4.1",
|
||||||
"mitt": "~3.0.1",
|
"mitt": "~3.0.1",
|
||||||
"nprogress": "~1.0.0-1",
|
"nprogress": "~1.0.0-1",
|
||||||
"pinia": "~2.1.7",
|
"pinia": "~2.1.7",
|
||||||
"qiniu-js": "^3.4.2",
|
"qiniu-js": "^3.4.2",
|
||||||
"qs": "~6.11.2",
|
"qs": "~6.11.2",
|
||||||
"sortablejs": "~1.15.2",
|
"sortablejs": "~1.15.2",
|
||||||
|
"three": "^0.162.0",
|
||||||
"tinymce": "^6.8.3",
|
"tinymce": "^6.8.3",
|
||||||
"vue": "~3.4.19",
|
"vue": "~3.4.19",
|
||||||
"vue-echarts": "^6.6.9",
|
"vue-echarts": "^6.6.9",
|
||||||
|
|
127
pnpm-lock.yaml
127
pnpm-lock.yaml
|
@ -14,6 +14,12 @@ dependencies:
|
||||||
'@tinymce/tinymce-vue':
|
'@tinymce/tinymce-vue':
|
||||||
specifier: ^5.1.1
|
specifier: ^5.1.1
|
||||||
version: 5.1.1(vue@3.4.19)
|
version: 5.1.1(vue@3.4.19)
|
||||||
|
'@types/dat.gui':
|
||||||
|
specifier: ^0.7.12
|
||||||
|
version: 0.7.12
|
||||||
|
'@types/three':
|
||||||
|
specifier: ^0.162.0
|
||||||
|
version: 0.162.0
|
||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ~10.8.0
|
specifier: ~10.8.0
|
||||||
version: 10.8.0(vue@3.4.19)
|
version: 10.8.0(vue@3.4.19)
|
||||||
|
@ -23,21 +29,33 @@ dependencies:
|
||||||
axios:
|
axios:
|
||||||
specifier: ~1.6.7
|
specifier: ~1.6.7
|
||||||
version: 1.6.7(debug@4.3.4)
|
version: 1.6.7(debug@4.3.4)
|
||||||
|
dat.gui:
|
||||||
|
specifier: ^0.7.9
|
||||||
|
version: 0.7.9
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ~1.11.10
|
specifier: ~1.11.10
|
||||||
version: 1.11.10
|
version: 1.11.10
|
||||||
|
dhtmlx-gantt:
|
||||||
|
specifier: ^8.0.6
|
||||||
|
version: 8.0.6
|
||||||
echarts:
|
echarts:
|
||||||
specifier: ^5.5.0
|
specifier: ^5.5.0
|
||||||
version: 5.5.0
|
version: 5.5.0
|
||||||
file-saver:
|
file-saver:
|
||||||
specifier: ~2.0.5
|
specifier: ~2.0.5
|
||||||
version: 2.0.5
|
version: 2.0.5
|
||||||
|
gsap:
|
||||||
|
specifier: ^3.12.5
|
||||||
|
version: 3.12.5
|
||||||
js-file-download:
|
js-file-download:
|
||||||
specifier: ^0.4.12
|
specifier: ^0.4.12
|
||||||
version: 0.4.12
|
version: 0.4.12
|
||||||
lodash-es:
|
lodash-es:
|
||||||
specifier: ~4.17.21
|
specifier: ~4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
mathjs:
|
||||||
|
specifier: ^12.4.1
|
||||||
|
version: 12.4.1
|
||||||
mitt:
|
mitt:
|
||||||
specifier: ~3.0.1
|
specifier: ~3.0.1
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
|
@ -56,6 +74,9 @@ dependencies:
|
||||||
sortablejs:
|
sortablejs:
|
||||||
specifier: ~1.15.2
|
specifier: ~1.15.2
|
||||||
version: 1.15.2
|
version: 1.15.2
|
||||||
|
three:
|
||||||
|
specifier: ^0.162.0
|
||||||
|
version: 0.162.0
|
||||||
tinymce:
|
tinymce:
|
||||||
specifier: ^6.8.3
|
specifier: ^6.8.3
|
||||||
version: 6.8.3
|
version: 6.8.3
|
||||||
|
@ -1753,6 +1774,13 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
|
|
||||||
|
/@babel/runtime@7.24.1:
|
||||||
|
resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime: 0.14.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@babel/template@7.22.15:
|
/@babel/template@7.22.15:
|
||||||
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
@ -2903,10 +2931,18 @@ packages:
|
||||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@tweenjs/tween.js@23.1.1:
|
||||||
|
resolution: {integrity: sha512-ZpboH7pCPPeyBWKf8c7TJswtCEQObFo3bOBYalm99NzZarATALYCo5OhbCa/n4RQyJyHfhkdx+hNrdL5ByFYDw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/cookie@0.6.0:
|
/@types/cookie@0.6.0:
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/dat.gui@0.7.12:
|
||||||
|
resolution: {integrity: sha512-el5dYeQZu2r6YW6Ft4rGtjr/dLe/uzXESMoie5UM6/weVShB1V8IRpXtTKrczd4qe7044fTKZS2l8d6EBFOkoA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/eslint@7.29.0:
|
/@types/eslint@7.29.0:
|
||||||
resolution: {integrity: sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==}
|
resolution: {integrity: sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2981,6 +3017,10 @@ packages:
|
||||||
resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==}
|
resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/stats.js@0.17.3:
|
||||||
|
resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/statuses@2.0.4:
|
/@types/statuses@2.0.4:
|
||||||
resolution: {integrity: sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==}
|
resolution: {integrity: sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -2991,6 +3031,16 @@ packages:
|
||||||
'@types/node': 20.11.16
|
'@types/node': 20.11.16
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/three@0.162.0:
|
||||||
|
resolution: {integrity: sha512-0j5yZcVukVIhrhSIC7+LmBPkkMoMuEJ1AfYBZfgNytdYqYREMuiyXWhYOMeZLBElTEAlJIZn7r2W3vqTIgjWlg==}
|
||||||
|
dependencies:
|
||||||
|
'@tweenjs/tween.js': 23.1.1
|
||||||
|
'@types/stats.js': 0.17.3
|
||||||
|
'@types/webxr': 0.5.14
|
||||||
|
fflate: 0.6.10
|
||||||
|
meshoptimizer: 0.18.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/unist@2.0.10:
|
/@types/unist@2.0.10:
|
||||||
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -2999,6 +3049,10 @@ packages:
|
||||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/webxr@0.5.14:
|
||||||
|
resolution: {integrity: sha512-UEMMm/Xn3DtEa+gpzUrOcDj+SJS1tk5YodjwOxcqStNhCfPcwgyC5Srg2ToVKyg2Fhq16Ffpb0UWUQHqoT9AMA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/wrap-ansi@3.0.0:
|
/@types/wrap-ansi@3.0.0:
|
||||||
resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
|
resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4698,6 +4752,10 @@ packages:
|
||||||
dot-prop: 5.3.0
|
dot-prop: 5.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/complex.js@2.1.1:
|
||||||
|
resolution: {integrity: sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/component-emitter@1.3.1:
|
/component-emitter@1.3.1:
|
||||||
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
|
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -5263,6 +5321,10 @@ packages:
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/dat.gui@0.7.9:
|
||||||
|
resolution: {integrity: sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dateformat@3.0.3:
|
/dateformat@3.0.3:
|
||||||
resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
|
resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -5320,6 +5382,10 @@ packages:
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/decimal.js@10.4.3:
|
||||||
|
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/decode-uri-component@0.2.2:
|
/decode-uri-component@0.2.2:
|
||||||
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
|
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
|
@ -5415,6 +5481,10 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/dhtmlx-gantt@8.0.6:
|
||||||
|
resolution: {integrity: sha512-GrEQ40/vgV1wDWkv/IvjJEM27Z4lDv76XvE5nlvMtFQTqUuo5VnL1XNDv/uFSJVMRnaN9StYaPxP1ebGamDLFg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/diff@4.0.2:
|
/diff@4.0.2:
|
||||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
|
@ -5790,6 +5860,10 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/escape-latex@1.2.0:
|
||||||
|
resolution: {integrity: sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/escape-string-regexp@1.0.5:
|
/escape-string-regexp@1.0.5:
|
||||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||||
engines: {node: '>=0.8.0'}
|
engines: {node: '>=0.8.0'}
|
||||||
|
@ -6459,6 +6533,10 @@ packages:
|
||||||
reusify: 1.0.4
|
reusify: 1.0.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/fflate@0.6.10:
|
||||||
|
resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/figures@2.0.0:
|
/figures@2.0.0:
|
||||||
resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==}
|
resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -6639,6 +6717,10 @@ packages:
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/fraction.js@4.3.4:
|
||||||
|
resolution: {integrity: sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fragment-cache@0.2.1:
|
/fragment-cache@0.2.1:
|
||||||
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
|
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -6989,6 +7071,10 @@ packages:
|
||||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/gsap@3.12.5:
|
||||||
|
resolution: {integrity: sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/gzip-size@6.0.0:
|
/gzip-size@6.0.0:
|
||||||
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
|
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -7721,6 +7807,10 @@ packages:
|
||||||
'@pkgjs/parseargs': 0.11.0
|
'@pkgjs/parseargs': 0.11.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/javascript-natural-sort@0.7.1:
|
||||||
|
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/jiti@1.21.0:
|
/jiti@1.21.0:
|
||||||
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
|
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -8319,6 +8409,22 @@ packages:
|
||||||
object-visit: 1.0.1
|
object-visit: 1.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/mathjs@12.4.1:
|
||||||
|
resolution: {integrity: sha512-welnW3khgwYjPYvECFHO+xkCxAx9IKIIPDDWPi8B5rKAvmgoEHnQX9slEmHKZTNaJiE+OS4qrJJcB4sfDn/4sw==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.1
|
||||||
|
complex.js: 2.1.1
|
||||||
|
decimal.js: 10.4.3
|
||||||
|
escape-latex: 1.2.0
|
||||||
|
fraction.js: 4.3.4
|
||||||
|
javascript-natural-sort: 0.7.1
|
||||||
|
seedrandom: 3.0.5
|
||||||
|
tiny-emitter: 2.1.0
|
||||||
|
typed-function: 4.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mathml-tag-names@2.1.3:
|
/mathml-tag-names@2.1.3:
|
||||||
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
|
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -8445,6 +8551,10 @@ packages:
|
||||||
resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
|
resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/meshoptimizer@0.18.1:
|
||||||
|
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/micromark@2.11.4:
|
/micromark@2.11.4:
|
||||||
resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==}
|
resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -10192,6 +10302,10 @@ packages:
|
||||||
compute-scroll-into-view: 1.0.20
|
compute-scroll-into-view: 1.0.20
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/seedrandom@3.0.5:
|
||||||
|
resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/semver@5.7.2:
|
/semver@5.7.2:
|
||||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -11132,6 +11246,10 @@ packages:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/three@0.162.0:
|
||||||
|
resolution: {integrity: sha512-xfCYj4RnlozReCmUd+XQzj6/5OjDNHBy5nT6rVwrOKGENAvpXe2z1jL+DZYaMu4/9pNsjH/4Os/VvS9IrH7IOQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/throttle-debounce@5.0.0:
|
/throttle-debounce@5.0.0:
|
||||||
resolution: {integrity: sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==}
|
resolution: {integrity: sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==}
|
||||||
engines: {node: '>=12.22'}
|
engines: {node: '>=12.22'}
|
||||||
|
@ -11161,6 +11279,10 @@ packages:
|
||||||
next-tick: 1.1.0
|
next-tick: 1.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tiny-emitter@2.1.0:
|
||||||
|
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tiny-invariant@1.3.3:
|
/tiny-invariant@1.3.3:
|
||||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -11398,6 +11520,11 @@ packages:
|
||||||
is-typed-array: 1.1.12
|
is-typed-array: 1.1.12
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/typed-function@4.1.1:
|
||||||
|
resolution: {integrity: sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/typedarray-to-buffer@3.1.5:
|
/typedarray-to-buffer@3.1.5:
|
||||||
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
|
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -63,7 +63,7 @@ export async function materialsInventoryInfo(
|
||||||
options?: RequestOptions,
|
options?: RequestOptions,
|
||||||
) {
|
) {
|
||||||
const { id: param0, ...queryParams } = params;
|
const { id: param0, ...queryParams } = params;
|
||||||
return request<API.MaterialsInventoryEntity>(`/api/contract/${param0}`, {
|
return request<API.MaterialsInventoryEntity>(`/api/materials-inventory/${param0}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: { ...queryParams },
|
params: { ...queryParams },
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
|
|
|
@ -1381,14 +1381,16 @@ declare namespace API {
|
||||||
type MaterialsInventoryListParams = {
|
type MaterialsInventoryListParams = {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
keyword?: string;
|
||||||
field?: string;
|
field?: string;
|
||||||
|
isCreateInout?: boolean;
|
||||||
order?: 'ASC' | 'DESC';
|
order?: 'ASC' | 'DESC';
|
||||||
_t?: number;
|
_t?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MaterialsInventoryEntity = {
|
type MaterialsInventoryEntity = {
|
||||||
/** 项目 */
|
/** 项目 */
|
||||||
project?: ProjectEntity;
|
project: ProjectEntity;
|
||||||
/** 产品 */
|
/** 产品 */
|
||||||
product: ProductEntity;
|
product: ProductEntity;
|
||||||
/** 单价 */
|
/** 单价 */
|
||||||
|
@ -1580,6 +1582,10 @@ declare namespace API {
|
||||||
};
|
};
|
||||||
// Product
|
// Product
|
||||||
type ProductEntity = {
|
type ProductEntity = {
|
||||||
|
/** 产品编号 */
|
||||||
|
productNumber: string;
|
||||||
|
/** 产品规格 */
|
||||||
|
productSpecification: string;
|
||||||
/** 产品名称 */
|
/** 产品名称 */
|
||||||
name: string;
|
name: string;
|
||||||
/** 所属公司 */
|
/** 所属公司 */
|
||||||
|
@ -1597,6 +1603,8 @@ declare namespace API {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
type ProductDto = {
|
type ProductDto = {
|
||||||
|
/** 产品规格 */
|
||||||
|
productSpecification: string;
|
||||||
/** 产品名称 */
|
/** 产品名称 */
|
||||||
name: string;
|
name: string;
|
||||||
/** 所属公司 */
|
/** 所属公司 */
|
||||||
|
@ -1613,6 +1621,7 @@ declare namespace API {
|
||||||
type ProductParams = {
|
type ProductParams = {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
keyword?:string;
|
||||||
company?: string;
|
company?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
field?: string;
|
field?: string;
|
||||||
|
@ -1625,6 +1634,8 @@ declare namespace API {
|
||||||
type ProductUpdateDto = {
|
type ProductUpdateDto = {
|
||||||
/** 产品名称 */
|
/** 产品名称 */
|
||||||
name?: string;
|
name?: string;
|
||||||
|
/** 产品规格 */
|
||||||
|
productSpecification: string;
|
||||||
/** 所属公司 */
|
/** 所属公司 */
|
||||||
companyId?: number;
|
companyId?: number;
|
||||||
/** 单位 */
|
/** 单位 */
|
||||||
|
@ -1774,6 +1785,8 @@ declare namespace API {
|
||||||
};
|
};
|
||||||
|
|
||||||
type MaterialsInOutEntity = {
|
type MaterialsInOutEntity = {
|
||||||
|
/** 入库后的库存Id */
|
||||||
|
inventoryId: number;
|
||||||
/** 库存编号 */
|
/** 库存编号 */
|
||||||
inventoryNumber: string;
|
inventoryNumber: string;
|
||||||
/** 公司信息 */
|
/** 公司信息 */
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
export const LOGIN_NAME = 'Login';
|
export const LOGIN_NAME = 'Login';
|
||||||
|
|
||||||
|
export const THREE = 'Three';
|
||||||
|
|
||||||
export const REDIRECT_NAME = 'Redirect';
|
export const REDIRECT_NAME = 'Redirect';
|
||||||
|
|
||||||
export const PARENT_LAYOUT_NAME = 'ParentLayout';
|
export const PARENT_LAYOUT_NAME = 'ParentLayout';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
import { LOGIN_NAME } from '@/router/constant';
|
import { LOGIN_NAME, THREE } from '@/router/constant';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* layout布局之外的路由
|
* layout布局之外的路由
|
||||||
|
@ -12,5 +12,12 @@ export const LoginRoute: RouteRecordRaw = {
|
||||||
title: '登录',
|
title: '登录',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
export const ThreeRoute: RouteRecordRaw = {
|
||||||
export default [LoginRoute];
|
path: '/three',
|
||||||
|
name: THREE,
|
||||||
|
component: () => import('@/views/three/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'web3d',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default [LoginRoute,ThreeRoute];
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
@import './transition.less';
|
@import './transition.less';
|
||||||
@import './common.less';
|
@import './common.less';
|
||||||
@import './antdv.override.less';
|
@import './antdv.override.less';
|
||||||
|
@import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
|
||||||
|
|
||||||
#global-loading {
|
#global-loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type { DataNode } from 'ant-design-vue/es/vc-tree-select/interface';
|
import type { DataNode } from 'ant-design-vue/es/vc-tree-select/interface';
|
||||||
|
import { add, subtract, multiply, divide, bignumber, type BigNumber, format } from 'mathjs';
|
||||||
/**
|
/**
|
||||||
* @description 处理首字母大写 abc => Abc
|
* @description 处理首字母大写 abc => Abc
|
||||||
* @param str
|
* @param str
|
||||||
|
@ -205,3 +205,31 @@ export const str2tree = (str: string, treeData: DataNode[] = [], separator = ':'
|
||||||
}
|
}
|
||||||
}, treeData);
|
}, treeData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CalclateOption = 'add' | 'subtract' | 'multiply' | 'divide';
|
||||||
|
export function calcNumber(
|
||||||
|
firstNumber: number,
|
||||||
|
secondNumber: number,
|
||||||
|
option: CalclateOption,
|
||||||
|
precision: number = 14,
|
||||||
|
): number {
|
||||||
|
let result: BigNumber;
|
||||||
|
switch (option) {
|
||||||
|
case 'add':
|
||||||
|
// 添加精度保留2位小数 add(bignumber(firstNumber), bignumber(secondNumber)).toNumber();
|
||||||
|
result = add(bignumber(firstNumber), bignumber(secondNumber));
|
||||||
|
break;
|
||||||
|
case 'subtract':
|
||||||
|
result = subtract(bignumber(firstNumber), bignumber(secondNumber));
|
||||||
|
break;
|
||||||
|
case 'multiply':
|
||||||
|
result = multiply(bignumber(firstNumber), bignumber(secondNumber)) as BigNumber;
|
||||||
|
break;
|
||||||
|
case 'divide':
|
||||||
|
result = divide(bignumber(firstNumber), bignumber(secondNumber)) as BigNumber;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return parseFloat(result.toFixed(precision));
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div class="login-logo">
|
<div class="login-logo">
|
||||||
<!-- <svg-icon name="logo" :size="45" /> -->
|
<!-- <svg-icon name="logo" :size="45" /> -->
|
||||||
<img src="~@/assets/images/logowithtext.png" width="100%" height="70px" />
|
<img :src="logo" width="100%" height="70px" />
|
||||||
<!-- <h1 class="mb-0 ml-2 text-3xl font-bold">Antd Admin</h1> -->
|
<!-- <h1 class="mb-0 ml-2 text-3xl font-bold">Antd Admin</h1> -->
|
||||||
</div>
|
</div>
|
||||||
<a-form layout="horizontal" :model="state.formInline" @submit.prevent="handleSubmit">
|
<a-form layout="horizontal" :model="state.formInline" @submit.prevent="handleSubmit">
|
||||||
|
@ -49,14 +49,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
import { UserOutlined, LockOutlined, SafetyOutlined } from '@ant-design/icons-vue';
|
import { UserOutlined, LockOutlined, SafetyOutlined } from '@ant-design/icons-vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import { message, Modal } from 'ant-design-vue';
|
||||||
import { useUserStore } from '@/store/modules/user';
|
import { useUserStore } from '@/store/modules/user';
|
||||||
import Api from '@/api/';
|
import Api from '@/api/';
|
||||||
import { to } from '@/utils/awaitTo';
|
import { to } from '@/utils/awaitTo';
|
||||||
|
import logo1 from '@/assets/images/logowithtext.png';
|
||||||
|
const logo = ref(logo1);
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
loading: false,
|
loading: false,
|
||||||
captcha: '',
|
captcha: '',
|
||||||
|
|
|
@ -10,7 +10,7 @@ export type TableQueryItem = API.MaterialsInOutListParams;
|
||||||
export type TableColumnItem = TableColumn<TableListItem>;
|
export type TableColumnItem = TableColumn<TableListItem>;
|
||||||
export const baseColumns: TableColumnItem[] = [
|
export const baseColumns: TableColumnItem[] = [
|
||||||
{
|
{
|
||||||
title: '原材料库存编号',
|
title: '出入库单号',
|
||||||
width: 100,
|
width: 100,
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
dataIndex: 'inventoryNumber',
|
dataIndex: 'inventoryNumber',
|
||||||
|
@ -29,7 +29,7 @@ export const baseColumns: TableColumnItem[] = [
|
||||||
return record?.project?.name || '';
|
return record?.project?.name || '';
|
||||||
},
|
},
|
||||||
formItemProps: {
|
formItemProps: {
|
||||||
component:'Select',
|
component: 'Select',
|
||||||
componentProps: ({ formInstance, schema, formModel }) => ({
|
componentProps: ({ formInstance, schema, formModel }) => ({
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
filterOption: false,
|
filterOption: false,
|
||||||
|
|
|
@ -3,6 +3,9 @@ import type { FormSchema } from '@/components/core/schema-form/';
|
||||||
import Api from '@/api';
|
import Api from '@/api';
|
||||||
import { debounce, isEmpty } from 'lodash-es';
|
import { debounce, isEmpty } from 'lodash-es';
|
||||||
import { MaterialsInOutEnum } from '@/enums/materialsInventoryEnum';
|
import { MaterialsInOutEnum } from '@/enums/materialsInventoryEnum';
|
||||||
|
import { toRaw } from 'vue';
|
||||||
|
import { calcNumber } from '@/utils/common';
|
||||||
|
import { formatToDate } from '@/utils/dateUtil';
|
||||||
export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEntity>[] => [
|
export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEntity>[] => [
|
||||||
{
|
{
|
||||||
field: 'inOrOut',
|
field: 'inOrOut',
|
||||||
|
@ -23,14 +26,16 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'inventoryNumber',
|
field: 'inventoryId',
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
label: '库存编号',
|
label: '产品', //出口用
|
||||||
vIf: ({ formModel }) => formModel.inOrOut === MaterialsInOutEnum.Out,
|
rules: [{ required: true, type: 'number' }],
|
||||||
|
vIf: ({ formModel }) => formModel.inOrOut === MaterialsInOutEnum.Out && !isEdit,
|
||||||
colProps: {
|
colProps: {
|
||||||
span: 24,
|
span: 24,
|
||||||
},
|
},
|
||||||
helpMessage: '出库必须选择入库时的编号',
|
helpMessage:
|
||||||
|
'出库必须选择某个价格的某个产品。因为库中的相同产品的价格,可能会不同(不同批次的产品)。另外在库存管理中会显示不同价格的相同产品。',
|
||||||
componentProps: ({ formInstance, schema, formModel }) => ({
|
componentProps: ({ formInstance, schema, formModel }) => ({
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
filterOption: false,
|
filterOption: false,
|
||||||
|
@ -49,7 +54,7 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
options: [] as LabelValueOptions,
|
options: [] as LabelValueOptions,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const options = await getInventoryNumberOptions().finally(() => (schema.loading = false));
|
const options = await getInventories().finally(() => (schema.loading = false));
|
||||||
newSchema.componentProps.options = options;
|
newSchema.componentProps.options = options;
|
||||||
formInstance?.updateSchema([newSchema]);
|
formInstance?.updateSchema([newSchema]);
|
||||||
},
|
},
|
||||||
|
@ -60,20 +65,27 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
},
|
},
|
||||||
callback: async ({ formModel }) => {
|
callback: async ({ formModel }) => {
|
||||||
if (formModel.inOrOut === MaterialsInOutEnum.Out) {
|
if (formModel.inOrOut === MaterialsInOutEnum.Out) {
|
||||||
return getInventoryNumberOptions();
|
return getInventories();
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve([] as LabelValueOptions);
|
return Promise.resolve([] as LabelValueOptions);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onSelect: async (inventoryNumber: string) => {
|
onSelect: async (id: number) => {
|
||||||
const { items = [] } = await Api.materialsInOut.materialsInOutList({
|
const data = await Api.materialsInventory.materialsInventoryInfo({
|
||||||
inventoryNumber,
|
id,
|
||||||
isCreateOut: true,
|
|
||||||
});
|
});
|
||||||
if (!isEmpty(items)) {
|
if (!isEmpty(data)) {
|
||||||
const { inOrOut, id, ...ext } = items[0] as API.MaterialsInOutEntity;
|
const { project, unitPrice, quantity, product } = data as API.MaterialsInventoryEntity;
|
||||||
formInstance?.setFieldsValue(ext);
|
const amount = calcNumber(unitPrice, quantity, 'multiply');
|
||||||
|
formInstance?.setFieldsValue({
|
||||||
|
projectId: project.id,
|
||||||
|
productId: product.id,
|
||||||
|
unitPrice,
|
||||||
|
quantity,
|
||||||
|
amount,
|
||||||
|
});
|
||||||
|
console.log(data, formModel);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSearch: debounce(async (keyword) => {
|
onSearch: debounce(async (keyword) => {
|
||||||
|
@ -85,9 +97,7 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
formInstance?.updateSchema([newSchema]);
|
formInstance?.updateSchema([newSchema]);
|
||||||
const options = await getInventoryNumberOptions(keyword).finally(
|
const options = await getInventories(keyword).finally(() => (schema.loading = false));
|
||||||
() => (schema.loading = false),
|
|
||||||
);
|
|
||||||
newSchema.componentProps.options = options;
|
newSchema.componentProps.options = options;
|
||||||
formInstance?.updateSchema([newSchema]);
|
formInstance?.updateSchema([newSchema]);
|
||||||
}, 500),
|
}, 500),
|
||||||
|
@ -97,7 +107,7 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
{
|
{
|
||||||
field: 'productId',
|
field: 'productId',
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
vIf: ({ formModel }) => formModel.inOrOut === MaterialsInOutEnum.In || isEdit,
|
vShow: ({ formModel }) => formModel.inOrOut === MaterialsInOutEnum.In || isEdit,
|
||||||
label: '产品',
|
label: '产品',
|
||||||
colProps: {
|
colProps: {
|
||||||
span: 16,
|
span: 16,
|
||||||
|
@ -152,11 +162,11 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
span: 12,
|
span: 12,
|
||||||
},
|
},
|
||||||
vIf: ({ formModel }) =>
|
vIf: ({ formModel }) =>
|
||||||
formModel.inOrOut === MaterialsInOutEnum.In || isEdit || formModel.inventoryNumber,
|
formModel.inOrOut === MaterialsInOutEnum.In || isEdit || formModel.inventoryId,
|
||||||
rules: [{ required: true, type: 'number' }],
|
rules: [{ required: true, type: 'number' }],
|
||||||
helpMessage: '如未找到对应项目,请先去项目管理添加项目。',
|
helpMessage: '如未找到对应项目,请先去项目管理添加项目。出库时选择的项目可以入库时不同',
|
||||||
componentProps: ({ formInstance, schema, formModel }) => ({
|
componentProps: ({ formInstance, schema, formModel }) => ({
|
||||||
disabled: Object.is(formModel.inOrOut, MaterialsInOutEnum.Out) || isEdit,
|
// disabled: Object.is(formModel.inOrOut, MaterialsInOutEnum.Out) || isEdit,
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
filterOption: false,
|
filterOption: false,
|
||||||
fieldNames: {
|
fieldNames: {
|
||||||
|
@ -179,17 +189,6 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
request: () => {
|
request: () => {
|
||||||
return getProjectOptions();
|
return getProjectOptions();
|
||||||
},
|
},
|
||||||
// request: {
|
|
||||||
// watchFields: ['framework'],
|
|
||||||
// options: {
|
|
||||||
// immediate: true,
|
|
||||||
// },
|
|
||||||
// callback: async ({ formModel }) => {
|
|
||||||
// console.log('values', formModel);
|
|
||||||
// formModel['lib'] = undefined;
|
|
||||||
// return fetchLibData(formModel['framework']);
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
onSearch: debounce(async (keyword) => {
|
onSearch: debounce(async (keyword) => {
|
||||||
schema.loading = true;
|
schema.loading = true;
|
||||||
const newSchema = {
|
const newSchema = {
|
||||||
|
@ -209,17 +208,50 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
label: '时间',
|
label: '时间',
|
||||||
field: 'time',
|
field: 'time',
|
||||||
component: 'DatePicker',
|
component: 'DatePicker',
|
||||||
|
defaultValue: formatToDate(new Date()),
|
||||||
colProps: {
|
colProps: {
|
||||||
span: 12,
|
span: 12,
|
||||||
},
|
},
|
||||||
|
componentProps: {
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '数量',
|
label: '数量',
|
||||||
|
helpMessage: ({ formModel }) => {
|
||||||
|
return '出库时,选择产品后显示的数量为实时的库存剩余数量。';
|
||||||
|
},
|
||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
colProps: {
|
colProps: {
|
||||||
span: 12,
|
span: 12,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
rules: [{ required: true, type: 'number', min: 1 }],
|
||||||
|
componentProps: ({ formInstance, formModel }) => ({
|
||||||
|
onBlur(e) {
|
||||||
|
const { quantity, unitPrice, amount, inOrOut } = toRaw(formModel);
|
||||||
|
// 出库只关心数量,单价已经有了,直接算出出库金额
|
||||||
|
if (MaterialsInOutEnum.Out === inOrOut) {
|
||||||
|
if (unitPrice) {
|
||||||
|
formInstance?.setFieldsValue({
|
||||||
|
amount: calcNumber(unitPrice, quantity, 'multiply'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 入库一般是先输入总价和数量,然后计算单价
|
||||||
|
if (amount) {
|
||||||
|
formInstance?.setFieldsValue({
|
||||||
|
unitPrice: calcNumber(amount, quantity, 'divide'),
|
||||||
|
});
|
||||||
|
} else if (unitPrice) {
|
||||||
|
formInstance?.setFieldsValue({
|
||||||
|
amount: calcNumber(unitPrice, quantity, 'multiply'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '单价',
|
label: '单价',
|
||||||
|
@ -228,6 +260,22 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
colProps: {
|
colProps: {
|
||||||
span: 12,
|
span: 12,
|
||||||
},
|
},
|
||||||
|
helpMessage: ({ formModel }) => {
|
||||||
|
return '出库时,单价不可更改,如需修改单价,请找到入库记录进行修改。';
|
||||||
|
},
|
||||||
|
dynamicDisabled: ({ formModel }) => {
|
||||||
|
return formModel.inOrOut === MaterialsInOutEnum.Out;
|
||||||
|
},
|
||||||
|
componentProps: ({ formInstance, formModel }) => ({
|
||||||
|
onBlur(e) {
|
||||||
|
const { quantity, unitPrice } = toRaw(formModel);
|
||||||
|
if (quantity) {
|
||||||
|
formInstance?.setFieldsValue({
|
||||||
|
amount: calcNumber(unitPrice, quantity, 'multiply'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '金额',
|
label: '金额',
|
||||||
|
@ -236,6 +284,22 @@ export const formSchemas = (isEdit?: boolean): FormSchema<API.MaterialsInOutEnti
|
||||||
colProps: {
|
colProps: {
|
||||||
span: 12,
|
span: 12,
|
||||||
},
|
},
|
||||||
|
helpMessage: ({ formModel }) => {
|
||||||
|
return '出库时,金额不可更改,输入数量会自动计算。';
|
||||||
|
},
|
||||||
|
dynamicDisabled: ({ formModel }) => {
|
||||||
|
return formModel.inOrOut === MaterialsInOutEnum.Out;
|
||||||
|
},
|
||||||
|
componentProps: ({ formInstance, formModel }) => ({
|
||||||
|
onBlur(e) {
|
||||||
|
const { quantity, amount } = toRaw(formModel);
|
||||||
|
if (quantity) {
|
||||||
|
formInstance?.setFieldsValue({
|
||||||
|
unitPrice: calcNumber(amount, quantity, 'divide'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '经办人',
|
label: '经办人',
|
||||||
|
@ -273,10 +337,10 @@ export const getProjectOptions = async (keyword?: string): Promise<LabelValueOpt
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProductOptions = async (keyword?: string): Promise<LabelValueOptions> => {
|
const getProductOptions = async (keyword?: string): Promise<LabelValueOptions> => {
|
||||||
const { items: result } = await Api.product.productList({ pageSize: 100, name: keyword });
|
const { items: result } = await Api.product.productList({ pageSize: 100, keyword: keyword });
|
||||||
return (
|
return (
|
||||||
result?.map((item) => ({
|
result?.map((item) => ({
|
||||||
label: `${item.name}` + (item.company?.name ? `(${item.company?.name})` : ''),
|
label: ` ${item.name} ${item.productNumber ? `(${item.productNumber})` : ''} ${item.company?.name ? `(${item.company?.name})` : ''} `,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
|
@ -295,3 +359,16 @@ const getInventoryNumberOptions = async (inventoryNumber?: string): Promise<Labe
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const getInventories = async (keyword?: string): Promise<LabelValueOptions> => {
|
||||||
|
const { items: result } = await Api.materialsInventory.materialsInventoryList({
|
||||||
|
keyword,
|
||||||
|
isCreateInout: true,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
result?.map((item) => ({
|
||||||
|
label: `${item.product?.name} (¥${parseFloat(item.unitPrice)}) (${item.product?.company?.name || '-'}) (${item.project?.name || '-'}) `,
|
||||||
|
value: item.id,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,53 +1,51 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="columns?.length">
|
<div v-if="columns?.length">
|
||||||
<DynamicTable
|
<DynamicTable row-key="id"
|
||||||
row-key="id"
|
|
||||||
header-title="原材料出入库记录"
|
header-title="原材料出入库记录"
|
||||||
title-tooltip="要创建入库记录,必须创建入库产品,产品所属公司。"
|
title-tooltip="要创建入库记录,必须创建入库产品,产品所属公司。"
|
||||||
:data-request="Api.materialsInOut.materialsInOutList"
|
:data-request="Api.materialsInOut.materialsInOutList"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
bordered
|
bordered
|
||||||
:scroll="{ x: 1920 }"
|
:scroll="{ x: 1920 }"
|
||||||
size="small"
|
size="small">
|
||||||
>
|
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<a-button
|
<a-button type="primary"
|
||||||
type="primary"
|
|
||||||
:disabled="!$auth('materials_inventory:history_in_out:create')"
|
:disabled="!$auth('materials_inventory:history_in_out:create')"
|
||||||
@click="openEditModal({})"
|
@click="openEditModal({})">
|
||||||
>
|
|
||||||
新增
|
新增
|
||||||
</a-button>
|
</a-button> <a-button type="primary"
|
||||||
|
@click="openExportModal">导出</a-button>
|
||||||
</template>
|
</template>
|
||||||
</DynamicTable>
|
</DynamicTable>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup
|
||||||
|
lang="tsx">
|
||||||
import { useTable } from '@/components/core/dynamic-table';
|
import { useTable } from '@/components/core/dynamic-table';
|
||||||
import {
|
import {
|
||||||
baseColumns,
|
baseColumns,
|
||||||
type TableColumnItem,
|
type TableColumnItem,
|
||||||
type TableListItem,
|
type TableListItem,
|
||||||
type TableQueryItem,
|
|
||||||
} from './columns';
|
} from './columns';
|
||||||
import Api from '@/api/';
|
import Api from '@/api/';
|
||||||
import { onMounted, ref, type FunctionalComponent, unref } from 'vue';
|
import { onMounted, ref, type FunctionalComponent } from 'vue';
|
||||||
import { useFormModal, useModal } from '@/hooks/useModal';
|
import { useFormModal, useModal } from '@/hooks/useModal';
|
||||||
import { Button } from 'ant-design-vue';
|
import { Button } from 'ant-design-vue';
|
||||||
import { formSchemas } from './formSchemas';
|
import { formSchemas } from './formSchemas';
|
||||||
|
|
||||||
import AttachmentManage from '@/components/business/attachment-manage/index.vue';
|
import AttachmentManage from '@/components/business/attachment-manage/index.vue';
|
||||||
import AttachmentUpload from '@/components/business/attachment-upload/index.vue';
|
import AttachmentUpload from '@/components/business/attachment-upload/index.vue';
|
||||||
import fileDownload from 'js-file-download';
|
import { useExportExcelModal, jsonToSheetXlsx } from '@/components/basic/excel';
|
||||||
import dayjs from 'dayjs';
|
import { MaterialsInOutEnum } from '@/enums/materialsInventoryEnum';
|
||||||
|
import { formatToDate } from '@/utils/dateUtil';
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MaterialsInOut',
|
name: 'MaterialsInOut',
|
||||||
});
|
});
|
||||||
const [DynamicTable, dynamicTableInstance] = useTable({ formProps: { autoSubmitOnEnter: true } });
|
const [DynamicTable, dynamicTableInstance] = useTable({ formProps: { autoSubmitOnEnter: true } });
|
||||||
const [showModal] = useFormModal();
|
const [showModal] = useFormModal();
|
||||||
|
const exportExcelModal = useExportExcelModal();
|
||||||
const [fnModal] = useModal();
|
const [fnModal] = useModal();
|
||||||
const isUploadPopupVisiable = ref(false);
|
const isUploadPopupVisiable = ref(false);
|
||||||
|
|
||||||
|
@ -101,7 +99,62 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const openExportModal = () => {
|
||||||
|
exportExcelModal.openModal({
|
||||||
|
onOk: ({ filename, bookType }) => {
|
||||||
|
const tableData: TableListItem[] = dynamicTableInstance.tableData;
|
||||||
|
let exportData: any[] = []
|
||||||
|
for (let item of tableData) {
|
||||||
|
exportData.push({
|
||||||
|
projectName:item.project?.name,
|
||||||
|
inOrOut: item.inOrOut === MaterialsInOutEnum.In ? '入库' : '出库',
|
||||||
|
inventoryNumber:item.inventoryNumber,
|
||||||
|
time:formatToDate(item.time),
|
||||||
|
company:item.product?.company?.name,
|
||||||
|
productName:item.product?.name,
|
||||||
|
productSpecification:item.product?.productSpecification,
|
||||||
|
unit:item.product?.unit?.label,
|
||||||
|
quantity:item.quantity,
|
||||||
|
unitPrice:parseFloat(item.unitPrice),
|
||||||
|
amount:parseFloat(item.amount),
|
||||||
|
agent:item.agent,
|
||||||
|
issuanceNumber:item.issuanceNumber,
|
||||||
|
remark:item.remark
|
||||||
|
})
|
||||||
|
}
|
||||||
|
jsonToSheetXlsx({
|
||||||
|
data: exportData,
|
||||||
|
header: {
|
||||||
|
inOrOut: '出/入库',
|
||||||
|
inventoryNumber: '出入库单号',
|
||||||
|
time: '时间',
|
||||||
|
projectName:'项目',
|
||||||
|
company: '公司',
|
||||||
|
productName: '产品名',
|
||||||
|
productSpecification: "产品规格",
|
||||||
|
unit: '单位',
|
||||||
|
quantity: '数量',
|
||||||
|
unitPrice: '单价',
|
||||||
|
amount: '金额',
|
||||||
|
agent: '经办人',
|
||||||
|
issuanceNumber: '领料单号',
|
||||||
|
remark: '备注'
|
||||||
|
},
|
||||||
|
filename,
|
||||||
|
write2excelOpts: {
|
||||||
|
bookType,
|
||||||
|
},
|
||||||
|
json2sheetOpts: {
|
||||||
|
// 指定顺序
|
||||||
|
header: [
|
||||||
|
'inOrOut', 'inventoryNumber', 'time','projectName', 'company', 'productName', 'productSpecification', 'unit', 'quantity',
|
||||||
|
'unitPrice', 'amount', 'agent', 'issuanceNumber', 'remark'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
const openAttachmentUploadModal = async (record: TableListItem) => {
|
const openAttachmentUploadModal = async (record: TableListItem) => {
|
||||||
isUploadPopupVisiable.value = true;
|
isUploadPopupVisiable.value = true;
|
||||||
fnModal.show({
|
fnModal.show({
|
||||||
|
@ -219,4 +272,5 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less"
|
||||||
|
scoped></style>
|
||||||
|
|
|
@ -33,6 +33,22 @@ export const baseColumns: TableColumnItem[] = [
|
||||||
return record?.product?.name || '';
|
return record?.product?.name || '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '产品规格',
|
||||||
|
width: 180,
|
||||||
|
dataIndex: 'productSpecification',
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
return record?.product?.productSpecification || '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '产品编号',
|
||||||
|
width: 180,
|
||||||
|
dataIndex: 'productNumber',
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
return record?.product?.productNumber || '';
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '单位',
|
title: '单位',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
@ -57,7 +73,7 @@ export const baseColumns: TableColumnItem[] = [
|
||||||
dataIndex: 'quantity',
|
dataIndex: 'quantity',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '库存单价',
|
title: '入库库存单价',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
width: 80,
|
width: 80,
|
||||||
dataIndex: 'unitPrice',
|
dataIndex: 'unitPrice',
|
||||||
|
|
|
@ -4,10 +4,19 @@ import type { TableColumn } from '@/components/core/dynamic-table';
|
||||||
export type TableListItem = API.ProductEntity;
|
export type TableListItem = API.ProductEntity;
|
||||||
export type TableColumnItem = TableColumn<TableListItem>;
|
export type TableColumnItem = TableColumn<TableListItem>;
|
||||||
export const baseColumns: TableColumnItem[] = [
|
export const baseColumns: TableColumnItem[] = [
|
||||||
|
{
|
||||||
|
title: '产品编号',
|
||||||
|
dataIndex: 'productNumber',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '产品规格',
|
||||||
|
dataIndex: 'productSpecification',
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: '所属公司',
|
title: '所属公司',
|
||||||
dataIndex: 'company',
|
dataIndex: 'company',
|
||||||
|
@ -18,7 +27,7 @@ export const baseColumns: TableColumnItem[] = [
|
||||||
{
|
{
|
||||||
title: '单位',
|
title: '单位',
|
||||||
dataIndex: 'unit',
|
dataIndex: 'unit',
|
||||||
width:80,
|
width: 80,
|
||||||
customRender: ({ record }) => {
|
customRender: ({ record }) => {
|
||||||
return record?.unit?.label || '';
|
return record?.unit?.label || '';
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,6 +14,14 @@ export const formSchemas: FormSchema<API.ProductDto>[] = [
|
||||||
span: 12,
|
span: 12,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'productSpecification',
|
||||||
|
component: 'Input',
|
||||||
|
label: '产品规格',
|
||||||
|
colProps: {
|
||||||
|
span: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '单位',
|
label: '单位',
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<template>
|
||||||
|
<div ref="ganttContainer"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { gantt } from 'dhtmlx-gantt';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
const ganttContainer = ref();
|
||||||
|
const props = defineProps({
|
||||||
|
tasks: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
gantt.config.date_format = '%Y-%m-%d';
|
||||||
|
gantt.init(ganttContainer.value);
|
||||||
|
gantt.config.columns = [
|
||||||
|
{ name: 'text', label: 'Task name', tree: true, width: '*' },
|
||||||
|
{ name: 'start_date', label: 'Start time', align: 'center' },
|
||||||
|
{ name: 'end_date', label: 'End time', align: 'center' },
|
||||||
|
{ name: 'duration', label: 'Duration', align: 'center' },
|
||||||
|
{ name: 'add', label: '' },
|
||||||
|
];
|
||||||
|
gantt.parse(props.tasks);
|
||||||
|
// gantt.parse(this.$props.tasks);
|
||||||
|
});
|
||||||
|
defineOptions({
|
||||||
|
name: 'HuaxinGantt',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -0,0 +1,121 @@
|
||||||
|
export default {
|
||||||
|
data: [
|
||||||
|
{ id: 11, text: "Project #1", type: "project", progress: 0.6, open: true },
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
text: "Task #1",
|
||||||
|
start_date: "03-04-2018",
|
||||||
|
duration: "5",
|
||||||
|
parent: "11",
|
||||||
|
progress: 1,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
text: "Task #2",
|
||||||
|
start_date: "03-04-2018",
|
||||||
|
type: "project",
|
||||||
|
parent: "11",
|
||||||
|
progress: 0.5,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
text: "Task #3",
|
||||||
|
start_date: "02-04-2018",
|
||||||
|
duration: "6",
|
||||||
|
parent: "11",
|
||||||
|
progress: 0.8,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
{ id: 15, text: "Task #4", type: "project", parent: "11", progress: 0.2, open: true },
|
||||||
|
{
|
||||||
|
id: 16,
|
||||||
|
text: "Final milestone",
|
||||||
|
start_date: "15-04-2018",
|
||||||
|
type: "milestone",
|
||||||
|
parent: "11",
|
||||||
|
progress: 0,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 17,
|
||||||
|
text: "Task #2.1",
|
||||||
|
start_date: "03-04-2018",
|
||||||
|
duration: "2",
|
||||||
|
parent: "13",
|
||||||
|
progress: 1,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 18,
|
||||||
|
text: "Task #2.2",
|
||||||
|
start_date: "06-04-2018",
|
||||||
|
duration: "3",
|
||||||
|
parent: "13",
|
||||||
|
progress: 0.8,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 19,
|
||||||
|
text: "Task #2.3",
|
||||||
|
start_date: "10-04-2018",
|
||||||
|
duration: "4",
|
||||||
|
parent: "13",
|
||||||
|
progress: 0.2,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20,
|
||||||
|
text: "Task #2.4",
|
||||||
|
start_date: "10-04-2018",
|
||||||
|
duration: "4",
|
||||||
|
parent: "13",
|
||||||
|
progress: 0,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
text: "Task #4.1",
|
||||||
|
start_date: "03-04-2018",
|
||||||
|
duration: "4",
|
||||||
|
parent: "15",
|
||||||
|
progress: 0.5,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 22,
|
||||||
|
text: "Task #4.2",
|
||||||
|
start_date: "03-04-2018",
|
||||||
|
duration: "4",
|
||||||
|
parent: "15",
|
||||||
|
progress: 0.1,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 23,
|
||||||
|
text: "Mediate milestone",
|
||||||
|
start_date: "14-04-2018",
|
||||||
|
type: "milestone",
|
||||||
|
parent: "15",
|
||||||
|
progress: 0,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{ id: "10", source: "11", target: "12", type: "1" },
|
||||||
|
{ id: "11", source: "11", target: "13", type: "1" },
|
||||||
|
{ id: "12", source: "11", target: "14", type: "1" },
|
||||||
|
{ id: "13", source: "11", target: "15", type: "1" },
|
||||||
|
{ id: "14", source: "23", target: "16", type: "0" },
|
||||||
|
{ id: "15", source: "13", target: "17", type: "1" },
|
||||||
|
{ id: "16", source: "17", target: "18", type: "0" },
|
||||||
|
{ id: "17", source: "18", target: "19", type: "0" },
|
||||||
|
{ id: "18", source: "19", target: "20", type: "0" },
|
||||||
|
{ id: "19", source: "15", target: "21", type: "2" },
|
||||||
|
{ id: "20", source: "15", target: "22", type: "2" },
|
||||||
|
{ id: "21", source: "15", target: "23", type: "0" },
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="container"><HuaxinGantt class="left-container" :tasks="tasks"></HuaxinGantt></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import HuaxinGantt from './gantt.vue';
|
||||||
|
import GanttData from './ganttData'
|
||||||
|
const tasks = ref(GanttData);
|
||||||
|
defineOptions({
|
||||||
|
name: 'Task',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.left-container {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,120 @@
|
||||||
|
<template>
|
||||||
|
<div class="slider-block">
|
||||||
|
<div class="slider-item">
|
||||||
|
<span class="demonstration">鼠标视角控制器</span>
|
||||||
|
<a-switch v-model:checked="mouseValue" @change="switchChange" />
|
||||||
|
</div>
|
||||||
|
<div class="slider-item">
|
||||||
|
<span class="demonstration">关节一(绕Y轴旋转)</span>
|
||||||
|
<a-slider
|
||||||
|
v-model:value="value1"
|
||||||
|
show-input
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="0.01"
|
||||||
|
@change="sliderInput($event, 'D1', 'y')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="slider-item">
|
||||||
|
<span class="demonstration">关节二(绕Z轴旋转)</span>
|
||||||
|
<a-slider
|
||||||
|
v-model:value="value2"
|
||||||
|
show-input
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="0.01"
|
||||||
|
@change="sliderInput($event, 'D2', 'z')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="slider-item">
|
||||||
|
<span class="demonstration">关节三(绕Z轴旋转)</span>
|
||||||
|
<a-slider
|
||||||
|
v-model:value="value3"
|
||||||
|
show-input
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="0.01"
|
||||||
|
@change="sliderInput($event, 'D3', 'z')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="slider-item">
|
||||||
|
<span class="demonstration">关节四(绕Z轴旋转)</span>
|
||||||
|
<a-slider
|
||||||
|
v-model:value="value4"
|
||||||
|
show-input
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="0.01"
|
||||||
|
@change="sliderInput($event, 'D4', 'z')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="slider-item">
|
||||||
|
<p class="demonstration">关节五</p>
|
||||||
|
<span class="demonstration">绕x轴旋转</span>
|
||||||
|
<a-slider
|
||||||
|
v-model:value="value5_1"
|
||||||
|
show-input
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="0.01"
|
||||||
|
@change="sliderInput($event, 'D5', 'x')"
|
||||||
|
/>
|
||||||
|
<span class="demonstration">绕y轴旋转</span>
|
||||||
|
<a-slider
|
||||||
|
v-model:value="value5_2"
|
||||||
|
show-input
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="0.01"
|
||||||
|
@change="sliderInput($event, 'D5', 'y')"
|
||||||
|
/>
|
||||||
|
<span class="demonstration">绕Z轴旋转</span>
|
||||||
|
<a-slider
|
||||||
|
v-model:value="value5_3"
|
||||||
|
show-input
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="0.01"
|
||||||
|
@change="sliderInput($event, 'D5', 'z')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineEmits } from 'vue';
|
||||||
|
const mouseValue = ref(true);
|
||||||
|
const value1 = ref(0);
|
||||||
|
const value2 = ref(0);
|
||||||
|
const value3 = ref(0);
|
||||||
|
const value4 = ref(0);
|
||||||
|
|
||||||
|
const value5_1 = ref(0);
|
||||||
|
const value5_2 = ref(0);
|
||||||
|
const value5_3 = ref(0);
|
||||||
|
|
||||||
|
const min = ref(Number(-Math.PI.toFixed(2)));
|
||||||
|
const max = ref(Number(Math.PI.toFixed(2)));
|
||||||
|
|
||||||
|
const emit = defineEmits(['sliderInput', 'switchChange']);
|
||||||
|
|
||||||
|
const sliderInput = (e, name, direction) => {
|
||||||
|
emit('sliderInput', e, name, direction);
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchChange = (e) => {
|
||||||
|
emit('switchChange', e);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scope>
|
||||||
|
.slider-block {
|
||||||
|
padding: 20px 10px;
|
||||||
|
}
|
||||||
|
.slider-item {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.demonstration {
|
||||||
|
margin: 0 10px 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<canvas class="webgl" ref="webgl"></canvas>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import BaseManager from './manager/BaseManager.js';
|
||||||
|
|
||||||
|
let base = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
base = new BaseManager(document.querySelector('canvas.webgl'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const setRobotRotation = (e, name, direction) => {
|
||||||
|
base.setRobotRotation(e, name, direction);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setControlsEnabled = (enabled) => {
|
||||||
|
base.setControlsEnabled(enabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ setRobotRotation, setControlsEnabled });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scscope>
|
||||||
|
.webgl {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,293 @@
|
||||||
|
import * as dat from "dat.gui";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||||
|
|
||||||
|
export default class baseManager {
|
||||||
|
constructor(canvas) {
|
||||||
|
//Gui
|
||||||
|
this.gui = new dat.GUI();
|
||||||
|
this.gui.hide();
|
||||||
|
//Canvas
|
||||||
|
this.canvas = canvas
|
||||||
|
//Sizes
|
||||||
|
this.sizes = {}
|
||||||
|
//Camera
|
||||||
|
this.camera = null
|
||||||
|
//Renderer
|
||||||
|
this.renderer = null
|
||||||
|
// Scene
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
|
||||||
|
//AnimateTick
|
||||||
|
this.clock = new THREE.Clock();
|
||||||
|
this.previousTime = 0;
|
||||||
|
|
||||||
|
this.initWindowSizes()
|
||||||
|
this.initcamera()
|
||||||
|
this.inLights()
|
||||||
|
this.initHelper()
|
||||||
|
this.initControls()
|
||||||
|
this.initRobot()
|
||||||
|
this.initRenderer()
|
||||||
|
this.initAnimateTick()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sizes
|
||||||
|
*/
|
||||||
|
initWindowSizes() {
|
||||||
|
/**
|
||||||
|
* Sizes
|
||||||
|
*/
|
||||||
|
const sizes = {
|
||||||
|
width: this.canvas.parentNode.clientWidth,
|
||||||
|
height: this.canvas.parentNode.clientHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
// Update sizes
|
||||||
|
sizes.width = this.canvas.parentNode.clientWidth;
|
||||||
|
sizes.height = this.canvas.parentNode.clientHeight;
|
||||||
|
// Update camera
|
||||||
|
this.camera.aspect = sizes.width / sizes.height;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
// Update renderer
|
||||||
|
this.renderer.setSize(sizes.width, sizes.height);
|
||||||
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sizes = sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Camera
|
||||||
|
*/
|
||||||
|
initcamera() {
|
||||||
|
// Base camera
|
||||||
|
const camera = new THREE.PerspectiveCamera(
|
||||||
|
75,
|
||||||
|
this.sizes.width / this.sizes.height,
|
||||||
|
0.1,
|
||||||
|
100
|
||||||
|
);
|
||||||
|
camera.position.set(0, 4, 10);
|
||||||
|
this.scene.add(camera);
|
||||||
|
this.camera = camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* inLights
|
||||||
|
*/
|
||||||
|
inLights() {
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
|
||||||
|
this.scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||||
|
directionalLight.castShadow = true;
|
||||||
|
directionalLight.shadow.mapSize.set(1024, 1024);
|
||||||
|
directionalLight.shadow.camera.far = 25;
|
||||||
|
directionalLight.shadow.camera.left = -7;
|
||||||
|
directionalLight.shadow.camera.top = 7;
|
||||||
|
directionalLight.shadow.camera.right = 7;
|
||||||
|
directionalLight.shadow.camera.bottom = -7;
|
||||||
|
directionalLight.position.set(5, 5, 5);
|
||||||
|
this.scene.add(directionalLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper
|
||||||
|
*/
|
||||||
|
initHelper() {
|
||||||
|
const axes = new THREE.AxesHelper(20);
|
||||||
|
this.scene.add(axes);
|
||||||
|
|
||||||
|
const gridHelper = new THREE.GridHelper(100, 100);
|
||||||
|
this.scene.add(gridHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls
|
||||||
|
*/
|
||||||
|
initControls() {
|
||||||
|
const controls = new OrbitControls(this.camera, this.canvas);
|
||||||
|
controls.target.set(0, 0.75, 0);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
this.controls = controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
setControlsEnabled(enabled) {
|
||||||
|
this.controls.enabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Robot
|
||||||
|
*/
|
||||||
|
initRobot() {
|
||||||
|
const D1 = new THREE.Mesh(
|
||||||
|
new THREE.CylinderGeometry(1, 1, 0.5, 32),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: "#E45826",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.gui.add(D1.rotation, "y").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
const D2 = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(0.5, 32, 16),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: "#1B1A17",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
D2.position.set(0, 0.75, 0);
|
||||||
|
// D2.rotation.z = Math.PI / 4;
|
||||||
|
D1.add(D2);
|
||||||
|
this.gui.add(D2.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
const B1 = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(0.5, 3, 0.5),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: "#E45826",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
B1.position.set(-1.5 * Math.sin(Math.PI / 4), 1, 0);
|
||||||
|
B1.rotation.z = Math.PI / 4;
|
||||||
|
D2.add(B1);
|
||||||
|
|
||||||
|
const D3 = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(0.5, 32, 16),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: "#1B1A17",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
D3.position.set(0, 1.5, 0);
|
||||||
|
// D3.rotation.z = -Math.PI / 2;
|
||||||
|
B1.add(D3);
|
||||||
|
this.gui.add(D3.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
const B2 = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(0.5, 3, 0.5),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: "#E45826",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
B2.position.set(1.5, 0, 0);
|
||||||
|
B2.rotation.z = -Math.PI / 2;
|
||||||
|
D3.add(B2);
|
||||||
|
|
||||||
|
const D4 = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(0.5, 32, 16),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: "#1B1A17",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
D4.position.set(0, 1.5, 0);
|
||||||
|
// D4.rotation.z = -Math.PI / 4;
|
||||||
|
B2.add(D4);
|
||||||
|
this.gui.add(D4.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
const B3 = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(0.5, 3, 0.5),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: "#E45826",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// B3.position.set(0, 1.5, 0);
|
||||||
|
B3.position.set(1.5 * Math.cos(Math.PI / 4), 1.5 * Math.sin(Math.PI / 4), 0);
|
||||||
|
B3.rotation.z = -Math.PI / 4;
|
||||||
|
D4.add(B3);
|
||||||
|
|
||||||
|
const D5 = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(0.5, 32, 16),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: "#1B1A17",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
D5.position.set(0, 1.5, 0);
|
||||||
|
// D5.rotation.z = -Math.PI / 2;
|
||||||
|
B3.add(D5);
|
||||||
|
this.gui.add(D5.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
const B4 = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(0.5, 1, 0.5),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: "#E45826",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
B4.position.set(0.5, 0, 0);
|
||||||
|
B4.rotation.z = -Math.PI / 2;
|
||||||
|
D5.add(B4);
|
||||||
|
|
||||||
|
D1.castShadow = true;
|
||||||
|
D2.castShadow = true;
|
||||||
|
B1.castShadow = true;
|
||||||
|
D3.castShadow = true;
|
||||||
|
B2.castShadow = true;
|
||||||
|
D4.castShadow = true;
|
||||||
|
B3.castShadow = true;
|
||||||
|
D5.castShadow = true;
|
||||||
|
B4.castShadow = true;
|
||||||
|
|
||||||
|
|
||||||
|
this.scene.add(D1);
|
||||||
|
this.D1 = D1
|
||||||
|
this.D2 = D2
|
||||||
|
this.D3 = D3
|
||||||
|
this.D4 = D4
|
||||||
|
this.D5 = D5
|
||||||
|
}
|
||||||
|
setRobotRotation(rotation, name, direction) {
|
||||||
|
this[name].rotation[direction] = rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderer
|
||||||
|
*/
|
||||||
|
initRenderer() {
|
||||||
|
this.renderer = new THREE.WebGLRenderer({
|
||||||
|
canvas: this.canvas,
|
||||||
|
});
|
||||||
|
this.renderer.shadowMap.enabled = true;
|
||||||
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
this.renderer.setSize(this.sizes.width, this.sizes.height);
|
||||||
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
// this.renderer.setClearColor("#fff");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AnimateTick
|
||||||
|
*/
|
||||||
|
initAnimateTick() {
|
||||||
|
const elapsedTime = this.clock.getElapsedTime();
|
||||||
|
const deltaTime = elapsedTime - this.previousTime;
|
||||||
|
this.previousTime = elapsedTime;
|
||||||
|
|
||||||
|
//Update controls
|
||||||
|
this.controls.update();
|
||||||
|
|
||||||
|
// Render
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
|
||||||
|
// Call tick again on the next frame
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
this.initAnimateTick()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
// import * as THREE from "three";
|
||||||
|
// import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||||
|
// import { TransformControls } from "three/examples/jsm/controls/TransformControls.js";
|
||||||
|
// import * as dat from "dat.gui";
|
||||||
|
// import { onMounted, nextTick } from "@vue/runtime-core";
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Base
|
||||||
|
// */
|
||||||
|
// // Debug
|
||||||
|
// const gui = new dat.GUI();
|
||||||
|
// gui.close();
|
||||||
|
|
||||||
|
// // Canvas
|
||||||
|
// const canvas = document.querySelector("canvas.webgl");
|
||||||
|
|
||||||
|
// // Scene
|
||||||
|
// const scene = new THREE.Scene();
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Floor
|
||||||
|
// */
|
||||||
|
// const floor = new THREE.Mesh(
|
||||||
|
// new THREE.PlaneBufferGeometry(100, 100),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#444444",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// floor.receiveShadow = true;
|
||||||
|
// floor.rotation.x = -Math.PI * 0.5;
|
||||||
|
// floor.receiveShadow = true;
|
||||||
|
// // scene.add(floor);
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Robot
|
||||||
|
// */
|
||||||
|
// const D1 = new THREE.Mesh(
|
||||||
|
// new THREE.CylinderGeometry(1, 1, 0.5, 32),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#E45826",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// gui.add(D1.rotation, "y").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
// const D2 = new THREE.Mesh(
|
||||||
|
// new THREE.SphereGeometry(0.5, 32, 16),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#1B1A17",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// D2.position.set(0, 0.75, 0);
|
||||||
|
// // D2.rotation.z = Math.PI / 4;
|
||||||
|
// D1.add(D2);
|
||||||
|
// gui.add(D2.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
// const B1 = new THREE.Mesh(
|
||||||
|
// new THREE.BoxGeometry(0.5, 3, 0.5),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#E45826",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// B1.position.set(-1.5 * Math.sin(Math.PI / 4), 1, 0);
|
||||||
|
// B1.rotation.z = Math.PI / 4;
|
||||||
|
// D2.add(B1);
|
||||||
|
|
||||||
|
// const D3 = new THREE.Mesh(
|
||||||
|
// new THREE.SphereGeometry(0.5, 32, 16),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#1B1A17",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// D3.position.set(0, 1.5, 0);
|
||||||
|
// // D3.rotation.z = -Math.PI / 2;
|
||||||
|
// B1.add(D3);
|
||||||
|
// gui.add(D3.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
// const B2 = new THREE.Mesh(
|
||||||
|
// new THREE.BoxGeometry(0.5, 3, 0.5),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#E45826",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// B2.position.set(1.5, 0, 0);
|
||||||
|
// B2.rotation.z = -Math.PI / 2;
|
||||||
|
// D3.add(B2);
|
||||||
|
|
||||||
|
// const D4 = new THREE.Mesh(
|
||||||
|
// new THREE.SphereGeometry(0.5, 32, 16),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#1B1A17",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// D4.position.set(0, 1.5, 0);
|
||||||
|
// // D4.rotation.z = -Math.PI / 4;
|
||||||
|
// B2.add(D4);
|
||||||
|
// gui.add(D4.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
// const B3 = new THREE.Mesh(
|
||||||
|
// new THREE.BoxGeometry(0.5, 3, 0.5),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#E45826",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// // B3.position.set(0, 1.5, 0);
|
||||||
|
// B3.position.set(1.5 * Math.cos(Math.PI / 4), 1.5 * Math.sin(Math.PI / 4), 0);
|
||||||
|
// B3.rotation.z = -Math.PI / 4;
|
||||||
|
// D4.add(B3);
|
||||||
|
// scene.add(D1);
|
||||||
|
|
||||||
|
// const D5 = new THREE.Mesh(
|
||||||
|
// new THREE.SphereGeometry(0.5, 32, 16),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#1B1A17",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// D5.position.set(0, 1.5, 0);
|
||||||
|
// // D5.rotation.z = -Math.PI / 2;
|
||||||
|
// B3.add(D5);
|
||||||
|
// gui.add(D5.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
|
||||||
|
|
||||||
|
// const B4 = new THREE.Mesh(
|
||||||
|
// new THREE.BoxGeometry(0.5, 1, 0.5),
|
||||||
|
// new THREE.MeshStandardMaterial({
|
||||||
|
// color: "#E45826",
|
||||||
|
// metalness: 0,
|
||||||
|
// roughness: 0.5,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// B4.position.set(0.5, 0, 0);
|
||||||
|
// B4.rotation.z = -Math.PI / 2;
|
||||||
|
// D5.add(B4);
|
||||||
|
|
||||||
|
// D1.castShadow = true;
|
||||||
|
// D2.castShadow = true;
|
||||||
|
// B1.castShadow = true;
|
||||||
|
// D3.castShadow = true;
|
||||||
|
// B2.castShadow = true;
|
||||||
|
// D4.castShadow = true;
|
||||||
|
// B3.castShadow = true;
|
||||||
|
// D5.castShadow = true;
|
||||||
|
|
||||||
|
// scene.add(D1);
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * helper
|
||||||
|
// */
|
||||||
|
// const axes = new THREE.AxesHelper(20);
|
||||||
|
// scene.add(axes);
|
||||||
|
|
||||||
|
// const gridHelper = new THREE.GridHelper(100, 100);
|
||||||
|
// scene.add(gridHelper);
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Lights
|
||||||
|
// */
|
||||||
|
// const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
|
||||||
|
// scene.add(ambientLight);
|
||||||
|
|
||||||
|
// const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||||
|
// directionalLight.castShadow = true;
|
||||||
|
// directionalLight.shadow.mapSize.set(1024, 1024);
|
||||||
|
// directionalLight.shadow.camera.far = 25;
|
||||||
|
// directionalLight.shadow.camera.left = -7;
|
||||||
|
// directionalLight.shadow.camera.top = 7;
|
||||||
|
// directionalLight.shadow.camera.right = 7;
|
||||||
|
// directionalLight.shadow.camera.bottom = -7;
|
||||||
|
// directionalLight.position.set(5, 5, 5);
|
||||||
|
// scene.add(directionalLight);
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Sizes
|
||||||
|
// */
|
||||||
|
// const sizes = {
|
||||||
|
// width: canvas.parentNode.clientWidth,
|
||||||
|
// height: canvas.parentNode.clientHeight,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// window.addEventListener("resize", () => {
|
||||||
|
// // Update sizes
|
||||||
|
// sizes.width = canvas.parentNode.clientWidth;
|
||||||
|
// sizes.height = canvas.parentNode.clientHeight;
|
||||||
|
// // Update camera
|
||||||
|
// camera.aspect = sizes.width / sizes.height;
|
||||||
|
// camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
// // Update renderer
|
||||||
|
// renderer.setSize(sizes.width, sizes.height);
|
||||||
|
// renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Camera
|
||||||
|
// */
|
||||||
|
// // Base camera
|
||||||
|
// const camera = new THREE.PerspectiveCamera(
|
||||||
|
// 75,
|
||||||
|
// sizes.width / sizes.height,
|
||||||
|
// 0.1,
|
||||||
|
// 100
|
||||||
|
// );
|
||||||
|
// camera.position.set(0, 4, 10);
|
||||||
|
// scene.add(camera);
|
||||||
|
|
||||||
|
// // Controls
|
||||||
|
// const controls = new OrbitControls(camera, canvas);
|
||||||
|
// controls.target.set(0, 0.75, 0);
|
||||||
|
// controls.enableDamping = true;
|
||||||
|
// // controls.enabled = false;
|
||||||
|
|
||||||
|
// // const transformControls = new TransformControls(camera, canvas);
|
||||||
|
// // transformControls.setMode("rotate");
|
||||||
|
// // transformControls.attach(D1);
|
||||||
|
// // transformControls.showX = false;
|
||||||
|
// // // transformControls.showY = false;
|
||||||
|
// // transformControls.showZ = false;
|
||||||
|
// // scene.add(transformControls);
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Renderer
|
||||||
|
// */
|
||||||
|
// const renderer = new THREE.WebGLRenderer({
|
||||||
|
// canvas: canvas,
|
||||||
|
// });
|
||||||
|
// renderer.shadowMap.enabled = true;
|
||||||
|
// renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
// renderer.setSize(sizes.width, sizes.height);
|
||||||
|
// renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
// renderer.setClearColor("#fff");
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Animate
|
||||||
|
// */
|
||||||
|
// const clock = new THREE.Clock();
|
||||||
|
// let previousTime = 0;
|
||||||
|
|
||||||
|
// const tick = () => {
|
||||||
|
// const elapsedTime = clock.getElapsedTime();
|
||||||
|
// const deltaTime = elapsedTime - previousTime;
|
||||||
|
// previousTime = elapsedTime;
|
||||||
|
|
||||||
|
// // Update controls
|
||||||
|
// controls.update();
|
||||||
|
|
||||||
|
// // Render
|
||||||
|
// renderer.render(scene, camera);
|
||||||
|
|
||||||
|
// // Call tick again on the next frame
|
||||||
|
// window.requestAnimationFrame(tick);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// tick();
|
|
@ -0,0 +1,657 @@
|
||||||
|
<template>
|
||||||
|
<a-spin v-if="isLoading" class="loading" :tip="progress"> </a-spin>
|
||||||
|
<div ref="container" class="main-container"></div>
|
||||||
|
<div class="fps-container">{{ fpsDisplay }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 引入threejs
|
||||||
|
// import * as THREE from 'three';
|
||||||
|
import gsap from 'gsap';
|
||||||
|
import {
|
||||||
|
refDebounced,
|
||||||
|
refThrottled,
|
||||||
|
useDebounceFn,
|
||||||
|
useElementSize,
|
||||||
|
useResizeObserver,
|
||||||
|
} from '@vueuse/core';
|
||||||
|
import {
|
||||||
|
AxesHelper,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
Scene,
|
||||||
|
BoxGeometry,
|
||||||
|
Mesh,
|
||||||
|
Box3,
|
||||||
|
Vector3,
|
||||||
|
Vector2,
|
||||||
|
PerspectiveCamera,
|
||||||
|
WebGLRenderer,
|
||||||
|
DoubleSide,
|
||||||
|
PlaneGeometry,
|
||||||
|
MeshPhongMaterial,
|
||||||
|
AmbientLight,
|
||||||
|
SpotLight,
|
||||||
|
MeshLambertMaterial,
|
||||||
|
SpotLightHelper,
|
||||||
|
Clock,
|
||||||
|
Raycaster,
|
||||||
|
Group,
|
||||||
|
CameraHelper,
|
||||||
|
Object3D,
|
||||||
|
} from 'three';
|
||||||
|
// 引入扩展库OrbitControls.js
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
// 引入GUI
|
||||||
|
import { GUI } from 'dat.gui';
|
||||||
|
// 引入性能监视器stats.js
|
||||||
|
// import Stats from 'three/addons/libs/stats.module.js';
|
||||||
|
// 引入扩展库GLTFLoader.js
|
||||||
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||||
|
// 引入扩展库Loader.js
|
||||||
|
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
|
||||||
|
import { computed, onMounted } from 'vue';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const container = ref();
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const progress = ref('0%');
|
||||||
|
let scene: Scene;
|
||||||
|
let camera: PerspectiveCamera;
|
||||||
|
let renderer: WebGLRenderer;
|
||||||
|
let axesHelper: AxesHelper;
|
||||||
|
let mesh: Object3D;
|
||||||
|
let spotLight: SpotLight;
|
||||||
|
let lightHelper: SpotLightHelper;
|
||||||
|
let camerahelper: CameraHelper;
|
||||||
|
let controls: OrbitControls;
|
||||||
|
const fps = ref(0);
|
||||||
|
const fpsValue = refThrottled(fps, 500); // 显示节流
|
||||||
|
const fpsDisplay = computed(() => {
|
||||||
|
return `${fpsValue.value} FPS`;
|
||||||
|
});
|
||||||
|
const clock = new Clock();
|
||||||
|
defineOptions({
|
||||||
|
name: 'ThreeBase',
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
initScece();
|
||||||
|
initSTL().then((res) => {
|
||||||
|
initAmbientLight();
|
||||||
|
initSpotLight();
|
||||||
|
initPlane();
|
||||||
|
// initMesh();
|
||||||
|
initCamera();
|
||||||
|
initGUI();
|
||||||
|
initRenderer();
|
||||||
|
initControls();
|
||||||
|
initAxesHelper();
|
||||||
|
initCameraHelper();
|
||||||
|
initLightHelper(spotLight); // 放在最后
|
||||||
|
|
||||||
|
render();
|
||||||
|
initListener();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建3D场景对象Scene
|
||||||
|
function initScece() {
|
||||||
|
scene = new Scene();
|
||||||
|
}
|
||||||
|
const initSTL = (): Promise<void> => {
|
||||||
|
isLoading.value = true;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const loader: GLTFLoader = new GLTFLoader();
|
||||||
|
loader.load(
|
||||||
|
'/model/3.gltf',
|
||||||
|
(geometry) => {
|
||||||
|
// const material = new MeshLambertMaterial({ color: 0x00ff00, side: DoubleSide });
|
||||||
|
console.log(geometry);
|
||||||
|
// 缩放物体到当前场景大小
|
||||||
|
const model = geometry.scene;
|
||||||
|
// var box = new Box3().setFromObject(model);
|
||||||
|
// var size = box.getSize(new Vector3()).length();
|
||||||
|
// var scale = 1 / size;
|
||||||
|
model.scale.set(0.01, 0.01, 0.01);
|
||||||
|
model.name = 'zhijia';
|
||||||
|
mesh = model;
|
||||||
|
mesh.position.set(20, 0, 0);
|
||||||
|
scene.add(mesh);
|
||||||
|
initRayCaster();
|
||||||
|
isLoading.value = false;
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
(event: ProgressEvent) => {
|
||||||
|
const { loaded, total } = event;
|
||||||
|
progress.value = `${Math.floor((loaded / total) * 100)}%`;
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
isLoading.value = false;
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 创建一个坐标轴辅助观察
|
||||||
|
function initAxesHelper() {
|
||||||
|
axesHelper = new AxesHelper(150);
|
||||||
|
scene.add(axesHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 光源辅助观察
|
||||||
|
function initLightHelper(light) {
|
||||||
|
if (light) {
|
||||||
|
lightHelper = new SpotLightHelper(light);
|
||||||
|
lightHelper.visible = false;
|
||||||
|
scene.add(lightHelper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 相机辅助观察
|
||||||
|
function initCameraHelper() {
|
||||||
|
camerahelper = new CameraHelper(camera);
|
||||||
|
camerahelper.visible = false;
|
||||||
|
scene.add(camerahelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个聚光灯
|
||||||
|
function initSpotLight() {
|
||||||
|
spotLight = new SpotLight(0xffffff, 8000);
|
||||||
|
spotLight.angle = Math.PI / 6;
|
||||||
|
spotLight.position.set(0, 30, 40);
|
||||||
|
spotLight.lookAt(0, 0, 0);
|
||||||
|
|
||||||
|
scene.add(spotLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个环境光
|
||||||
|
function initAmbientLight() {
|
||||||
|
const light = new AmbientLight('#ffffff', 1);
|
||||||
|
scene.add(light);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个平面
|
||||||
|
function initPlane() {
|
||||||
|
// 创建一个平面几何对象Geometry
|
||||||
|
const geometry = new PlaneGeometry(100, 100);
|
||||||
|
// 创建一个材质对象Material
|
||||||
|
const material = new MeshLambertMaterial({
|
||||||
|
color: 0xcccccc, // 0x00ff00绿色
|
||||||
|
// side: DoubleSide, // 双面渲染
|
||||||
|
});
|
||||||
|
const plane = new Mesh(geometry, material); // 网格模型对象Mesh
|
||||||
|
plane.rotateX(-Math.PI / 2); // 面向上方
|
||||||
|
scene.add(plane); // 网格模型添加到场景中
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个网格模型对象
|
||||||
|
function initMesh() {
|
||||||
|
// 创建一个长方体几何对象Geometry
|
||||||
|
const geometry = new BoxGeometry(10, 10, 10);
|
||||||
|
// 创建一个材质对象Material
|
||||||
|
const material = new MeshPhongMaterial({
|
||||||
|
color: 0xff0000, // 0xff0000设置材质颜色为红色
|
||||||
|
// transparent: true, // 开启透明
|
||||||
|
shininess: 100, // 高光部分的亮度,默认30
|
||||||
|
specular: 0x444444, // 高光部分的颜色
|
||||||
|
// opacity: 0.8, // 设置透明度
|
||||||
|
});
|
||||||
|
mesh = new Mesh(geometry, material); // 网格模型对象Mesh
|
||||||
|
mesh.position.setY(5);
|
||||||
|
scene.add(mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个透视相机
|
||||||
|
function initCamera() {
|
||||||
|
const width = container.value.clientWidth;
|
||||||
|
const height = container.value.clientHeight;
|
||||||
|
const aspect = width / height; // 窗口宽高比
|
||||||
|
camera = new PerspectiveCamera(45, aspect, 0.1, 1000);
|
||||||
|
camera.position.set(-10, 10, 100); // 设置相机位置
|
||||||
|
camera.lookAt(0, 0, 0); // 设置相机方向(指向的场景对象)
|
||||||
|
scene.add(camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建渲染器对象
|
||||||
|
function initRenderer() {
|
||||||
|
renderer = new WebGLRenderer({
|
||||||
|
antialias: true, // 抗锯齿开启
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
|
||||||
|
renderer.render(scene, camera); // 执行渲染操作
|
||||||
|
container.value.appendChild(renderer.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加相机控制器
|
||||||
|
function initControls() {
|
||||||
|
controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
// 添加阻尼
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.target.set(10, 10, 10);
|
||||||
|
// 改变相机观察目标点
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
controls.addEventListener('change', () => {
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 渲染循环
|
||||||
|
let angle = 0; // 用于圆周运动计算的角度值
|
||||||
|
const R = 100; // 相机圆周运动的半径
|
||||||
|
const animates = {
|
||||||
|
meshRotate: false,
|
||||||
|
cameraMove: false,
|
||||||
|
meshMoveX: false,
|
||||||
|
gsap: false,
|
||||||
|
};
|
||||||
|
let raycaster;
|
||||||
|
let mouse;
|
||||||
|
let selectableGroups: any[] = [];
|
||||||
|
// 鼠标射线移动监听
|
||||||
|
function initRayCaster() {
|
||||||
|
// let zhijiaObj;
|
||||||
|
// scene.traverse((object) => {
|
||||||
|
// if (object.name === 'zhijia') {
|
||||||
|
// zhijiaObj = object;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
mesh.traverse((object) => {
|
||||||
|
if (object.id < 50) selectableGroups.push(object);
|
||||||
|
});
|
||||||
|
|
||||||
|
raycaster = new Raycaster();
|
||||||
|
mouse = new Vector2();
|
||||||
|
window.addEventListener(
|
||||||
|
'mousemove',
|
||||||
|
(event) => {
|
||||||
|
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||||
|
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 动画函数
|
||||||
|
function initAnimate() {
|
||||||
|
if (animates.meshMoveX) {
|
||||||
|
mesh.position.x += 0.1;
|
||||||
|
if (mesh.position.x > 50) {
|
||||||
|
mesh.position.x = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (animates.meshRotate) {
|
||||||
|
// mesh.rotation.set(Math.PI / 4, 0, 0, 'XZY'); // 每次绕y轴旋转0.01弧度
|
||||||
|
mesh.rotation.x += 0.01;
|
||||||
|
mesh.rotation.y += 0.01;
|
||||||
|
mesh.rotation.z += 0.01;
|
||||||
|
}
|
||||||
|
if (animates.cameraMove) {
|
||||||
|
angle += 0.01;
|
||||||
|
// 相机y坐标不变,在XOZ平面上做圆周运动
|
||||||
|
camera.position.x = R * Math.cos(angle);
|
||||||
|
camera.position.z = R * Math.sin(angle);
|
||||||
|
// .position改变,重新执行lookAt(0,0,0)计算相机视线方向
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let INTERSECTED;
|
||||||
|
let selectedGroup;
|
||||||
|
|
||||||
|
function chooseGroup(intersects) {
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
const object = intersects[0].object;
|
||||||
|
|
||||||
|
// 找到 object 所在的组
|
||||||
|
let group = object;
|
||||||
|
while (group.parent) {
|
||||||
|
if (group.parent?.id === 7) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
group = group.parent;
|
||||||
|
}
|
||||||
|
if (group && group !== selectedGroup) {
|
||||||
|
// 如果选中了一个新的组,将旧的组的颜色恢复
|
||||||
|
if (selectedGroup) {
|
||||||
|
selectedGroup.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.material.color.set(child.currentHex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存新的组,并将其颜色设置为红色
|
||||||
|
selectedGroup = group;
|
||||||
|
console.log(group);
|
||||||
|
group.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.currentHex = child.material.color.getHex();
|
||||||
|
child.material = child.material.clone();
|
||||||
|
child.material.color.set(0xff0000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有选中任何组,将旧的组的颜色恢复
|
||||||
|
if (selectedGroup) {
|
||||||
|
selectedGroup.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.material.color.set(child.currentHex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
selectedGroup = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let gsapAnimate: gsap.core.Tween;
|
||||||
|
function initAnimateByGsap() {
|
||||||
|
// gsap.to(mesh.position, {
|
||||||
|
// x: 50,
|
||||||
|
// duration: 2,
|
||||||
|
// repeat: -1,
|
||||||
|
// yoyo: true,
|
||||||
|
// ease: 'power1.inOut',
|
||||||
|
// });
|
||||||
|
// gsap.to(mesh.rotation, {
|
||||||
|
// x: Math.PI / 2,
|
||||||
|
// duration: 2,
|
||||||
|
// repeat: -1,
|
||||||
|
// yoyo: true,
|
||||||
|
// ease: 'power1.inOut',
|
||||||
|
// });
|
||||||
|
gsapAnimate = gsap.to(camera.position, {
|
||||||
|
x: 100,
|
||||||
|
duration: 2,
|
||||||
|
repeat: -1,
|
||||||
|
yoyo: true,
|
||||||
|
ease: 'power1.inOut',
|
||||||
|
});
|
||||||
|
// camera.lookAt(0, 0, 0);
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
// 创建窗口resize监听
|
||||||
|
function initListener() {
|
||||||
|
// onresize 事件会在窗口被调整大小时发生
|
||||||
|
// 第一种方案原生直接监听windows
|
||||||
|
const debouncedFn = useDebounceFn(() => {
|
||||||
|
// 重置渲染器输出画布canvas尺寸
|
||||||
|
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
|
||||||
|
// 设置渲染器的像素比
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
// 全屏情况下:设置观察范围长宽比aspect为窗口宽高比
|
||||||
|
camera.aspect = container.value.clientWidth / container.value.clientHeight;
|
||||||
|
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
|
||||||
|
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
|
||||||
|
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
}, 50);
|
||||||
|
window.addEventListener('resize', debouncedFn);
|
||||||
|
window.addEventListener('dblclick', () => {
|
||||||
|
const fullScreenElement = document.fullscreenElement;
|
||||||
|
if (!fullScreenElement) {
|
||||||
|
// 双击控制屏幕进入全屏,退出全屏
|
||||||
|
// 让画布对象全屏
|
||||||
|
renderer.domElement.requestFullscreen();
|
||||||
|
} else {
|
||||||
|
// 退出全屏,使用document对象
|
||||||
|
document.exitFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 第二种方案:vueuse监听 白屏闪烁
|
||||||
|
// useResizeObserver(container, entries => {
|
||||||
|
// const entry = entries[0];
|
||||||
|
// const { width, height } = entry.contentRect;
|
||||||
|
// renderer.setSize(width, height);
|
||||||
|
// camera.aspect = width / height;
|
||||||
|
// camera.updateProjectionMatrix();
|
||||||
|
// });
|
||||||
|
// 第三种方案:vueuse指令 白屏闪烁
|
||||||
|
// function onResize({ width, height }: { width: number; height: number }) {
|
||||||
|
// // v-element-size="onResize"
|
||||||
|
// renderer.setSize(width, height);
|
||||||
|
// camera.aspect = width / height;
|
||||||
|
// camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
// }
|
||||||
|
// 第四种方案 使用watch 白屏闪烁
|
||||||
|
// const { width, height } = useElementSize(container);
|
||||||
|
// watch(
|
||||||
|
// [width, height],
|
||||||
|
// (oldValue, newValue) => {
|
||||||
|
// console.log(newValue);
|
||||||
|
// const [width, height] = newValue;
|
||||||
|
// if (!width || !height) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// renderer.setSize(width, height);
|
||||||
|
// camera.aspect = width / height;
|
||||||
|
// camera.updateProjectionMatrix();
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// immediate: false,
|
||||||
|
// flush: 'sync',
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
// initAnimateByGsap();
|
||||||
|
// 初始化GUI
|
||||||
|
function initGUI() {
|
||||||
|
// 创建一个GUI对象
|
||||||
|
const gui = new GUI();
|
||||||
|
// initAnimateByGsap();
|
||||||
|
gui
|
||||||
|
.add(animates, 'gsap')
|
||||||
|
.name('GSAP动画')
|
||||||
|
.onChange((flg) => {
|
||||||
|
if (gsapAnimate.isActive()) {
|
||||||
|
gsapAnimate.pause();
|
||||||
|
} else {
|
||||||
|
gsapAnimate.resume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const zhijiaGroup = gui.addFolder('支架');
|
||||||
|
const zhijia1 = scene.getObjectById(8);
|
||||||
|
const zhijia2 = scene.getObjectById(9);
|
||||||
|
const zhijia3 = scene.getObjectById(16);
|
||||||
|
const zhuzi1 = scene.getObjectById(53);
|
||||||
|
const zhuzi2 = scene.getObjectById(67);
|
||||||
|
const zhijia1Clone = zhijia1?.clone();
|
||||||
|
const zhijia2Clone = zhijia2?.clone();
|
||||||
|
const zhijia3Clone = zhijia3?.clone();
|
||||||
|
const zhuzi1Clone = zhuzi1?.clone();
|
||||||
|
const zhuzi2Clone = zhuzi2?.clone();
|
||||||
|
// if (zhijia1) {
|
||||||
|
// moveDownGroup.add(zhijia1);
|
||||||
|
// }
|
||||||
|
// if (zhijia2) {
|
||||||
|
// moveDownGroup.add(zhijia2);
|
||||||
|
// }
|
||||||
|
// if (zhijia3) {
|
||||||
|
// moveDownGroup.add(zhijia3);
|
||||||
|
// }
|
||||||
|
// scene.add(moveDownGroup)
|
||||||
|
// 使zhijia8绕z轴旋转,旋转角度为0.01弧度
|
||||||
|
const zhijiaProps = {
|
||||||
|
zhijiatouRotate: 0,
|
||||||
|
zhijiabiRotate: 0,
|
||||||
|
};
|
||||||
|
zhijiaGroup
|
||||||
|
.add(zhijiaProps, 'zhijiatouRotate', Math.PI / 2, Math.PI / 1)
|
||||||
|
.name('支架头部旋转')
|
||||||
|
.onChange((e) => {
|
||||||
|
if (zhijia1) {
|
||||||
|
zhijia1.rotation.z = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
zhijiaGroup
|
||||||
|
.add(zhijiaProps, 'zhijiabiRotate', 0, 500)
|
||||||
|
.name('支架整体下移')
|
||||||
|
.onChange((e) => {
|
||||||
|
// moveDownGroup.position.y = moveDownGroup.position.y - e;
|
||||||
|
if (zhijia3 && zhijia3Clone) {
|
||||||
|
zhijia3.position.y = zhijia3Clone.position.y - e;
|
||||||
|
if (zhijia2 && zhijia2Clone) {
|
||||||
|
zhijia2.position.y = zhijia2Clone.position.y - e;
|
||||||
|
}
|
||||||
|
if (zhijia1 && zhijia1Clone) {
|
||||||
|
zhijia1.position.y = zhijia1Clone.position.y - e;
|
||||||
|
}
|
||||||
|
if (zhuzi1 && zhuzi1Clone) {
|
||||||
|
zhuzi1.rotation.z = zhuzi1Clone.rotation.z - e / 500;
|
||||||
|
}
|
||||||
|
if (zhuzi2 && zhuzi2Clone) {
|
||||||
|
zhuzi2.rotation.z = zhuzi2Clone.rotation.z - e / 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const spotLightGroup = gui.addFolder('聚光灯属性');
|
||||||
|
const spotProps = {
|
||||||
|
intensity: 10000,
|
||||||
|
spotLightHelpVisiable: false,
|
||||||
|
};
|
||||||
|
spotLightGroup
|
||||||
|
.add(spotProps, 'spotLightHelpVisiable')
|
||||||
|
.name('显示聚光灯辅助')
|
||||||
|
.onChange((e) => {
|
||||||
|
lightHelper.visible = e;
|
||||||
|
});
|
||||||
|
if (spotLight) {
|
||||||
|
spotLightGroup.add(spotLight.position, 'x', -50, 50).onChange((e) => {
|
||||||
|
lightHelper.update();
|
||||||
|
});
|
||||||
|
spotLightGroup.add(spotLight.position, 'y', -50, 50).onChange((e) => {
|
||||||
|
lightHelper.update();
|
||||||
|
});
|
||||||
|
spotLightGroup.add(spotLight.position, 'z', -50, 50).onChange((e) => {
|
||||||
|
lightHelper.update();
|
||||||
|
});
|
||||||
|
spotLightGroup.add(spotLight, 'angle', Math.PI / 10, Math.PI / 2).onChange((e) => {
|
||||||
|
spotLight.angle = e;
|
||||||
|
lightHelper.update();
|
||||||
|
});
|
||||||
|
spotLightGroup.add(spotProps, 'intensity', 0, 20000).onChange((e) => {
|
||||||
|
spotLight.intensity = e;
|
||||||
|
lightHelper.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const meshPostionGroup = gui.addFolder('位置');
|
||||||
|
const meshPostionProps = {
|
||||||
|
x: 30,
|
||||||
|
y: 60,
|
||||||
|
z: 300,
|
||||||
|
};
|
||||||
|
meshPostionGroup.add(meshPostionProps, 'x', -50, 50).onChange((e) => {
|
||||||
|
mesh.position.x = e;
|
||||||
|
});
|
||||||
|
meshPostionGroup.add(meshPostionProps, 'y', -50, 50).onChange((e) => {
|
||||||
|
mesh.position.y = e;
|
||||||
|
});
|
||||||
|
meshPostionGroup.add(meshPostionProps, 'z', -50, 50).onChange((e) => {
|
||||||
|
mesh.position.z = e;
|
||||||
|
});
|
||||||
|
meshPostionGroup.add(animates, 'meshMoveX').name('物体x轴运动');
|
||||||
|
meshPostionGroup.add(animates, 'meshRotate').name('物体旋转');
|
||||||
|
// 控制相机的视角
|
||||||
|
const cameraGroup = gui.addFolder('相机');
|
||||||
|
const cameraProps = {
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
z: 50,
|
||||||
|
fov: 45,
|
||||||
|
helperVisiable: false,
|
||||||
|
};
|
||||||
|
// cameraGroup.open();
|
||||||
|
cameraGroup.add(animates, 'cameraMove').name('开始绕Y轴旋转');
|
||||||
|
cameraGroup
|
||||||
|
.add(cameraProps, 'helperVisiable')
|
||||||
|
.name('显示相机辅助')
|
||||||
|
.onChange((visible) => {
|
||||||
|
camerahelper.visible = visible;
|
||||||
|
});
|
||||||
|
cameraGroup
|
||||||
|
.add(cameraProps, 'fov', 0, 100)
|
||||||
|
.name('修改相机远近')
|
||||||
|
.onChange((num) => {
|
||||||
|
camera.fov = num;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
});
|
||||||
|
cameraGroup.add(cameraProps, 'x', -50, 100).onChange((e) => {
|
||||||
|
camera.position.x = e;
|
||||||
|
camera.lookAt(mesh.position);
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
});
|
||||||
|
cameraGroup.add(cameraProps, 'y', -50, 100).onChange((e) => {
|
||||||
|
camera.position.y = e;
|
||||||
|
camera.lookAt(mesh.position);
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
});
|
||||||
|
|
||||||
|
cameraGroup.add(cameraProps, 'z', -50, 100).onChange((e) => {
|
||||||
|
camera.position.z = e;
|
||||||
|
camera.lookAt(mesh.position);
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
});
|
||||||
|
// // 添加属性
|
||||||
|
// gui.add(controls, 'rotationSpeed', 0, 10, 0.01).onChange(e => {
|
||||||
|
// mesh.rotateX = e;
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染函数
|
||||||
|
function render() {
|
||||||
|
const spt = clock.getDelta() * 1000; // 毫秒
|
||||||
|
// console.log('两帧渲染时间间隔(毫秒)', spt);
|
||||||
|
// console.log('帧率FPS', 1000 / spt);
|
||||||
|
fps.value = Math.floor(1000 / spt);
|
||||||
|
raycaster.setFromCamera(mouse, camera);
|
||||||
|
const intersects = raycaster.intersectObjects(selectableGroups, true);
|
||||||
|
// // 如果有交集
|
||||||
|
// if (intersects.length > 0) {
|
||||||
|
// // 如果交集的对象不是上一次的对象
|
||||||
|
// if (INTERSECTED != intersects[0].object) {
|
||||||
|
// // 如果有上一次的对象,恢复其颜色
|
||||||
|
// if (INTERSECTED) INTERSECTED.material.color.set(INTERSECTED.currentHex);
|
||||||
|
|
||||||
|
// // 存储当前对象,并保存其颜色
|
||||||
|
// INTERSECTED = intersects[0].object;
|
||||||
|
// INTERSECTED.material = INTERSECTED.material.clone(); // 创建一个新的材质
|
||||||
|
// INTERSECTED.currentHex = INTERSECTED.material.color.getHex();
|
||||||
|
|
||||||
|
// // 设置当前对象的颜色为红色
|
||||||
|
// INTERSECTED.material.color.set(0xff0000);
|
||||||
|
// console.log(INTERSECTED);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // 如果没有交集,且有上一次的对象,恢复其颜色
|
||||||
|
// if (INTERSECTED) INTERSECTED.material.color.set(INTERSECTED.currentHex);
|
||||||
|
// INTERSECTED = null;
|
||||||
|
// }
|
||||||
|
chooseGroup(intersects);
|
||||||
|
initAnimate();
|
||||||
|
|
||||||
|
controls && controls.update(); // 每一帧都需要更新 为了阻尼效果
|
||||||
|
renderer.render(scene, camera); // 执行渲染操作
|
||||||
|
requestAnimationFrame(render); // 请求再次执行渲染函数render,渲染下一帧
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 50vh;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.main-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fps-container {
|
||||||
|
position: fixed;
|
||||||
|
color: #fff;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<div><ThreeBase></ThreeBase></div>
|
||||||
|
<!-- <a-drawer v-model:open="drawer" placement="right" :mask="false">
|
||||||
|
<RobotMenu @sliderInput="sliderInput" @switchChange="switchChange" />
|
||||||
|
</a-drawer>
|
||||||
|
<div class="btn" v-show="!drawer">
|
||||||
|
<a-button type="primary" circle size="large" @click="drawerSwitch" />
|
||||||
|
</div>
|
||||||
|
<Robot3d ref="Robot3dRef" /> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import ThreeBase from './base/index.vue'
|
||||||
|
import Robot3d from './Robot3d/index.vue';
|
||||||
|
import RobotMenu from './Menu/index.vue';
|
||||||
|
const Robot3dRef = ref();
|
||||||
|
const switchChange = (e) => {
|
||||||
|
Robot3dRef.value.setControlsEnabled(e);
|
||||||
|
};
|
||||||
|
const sliderInput = (e, name, direction) => {
|
||||||
|
Robot3dRef.value.setRobotRotation(e, name, direction);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawer = ref(false);
|
||||||
|
const drawerSwitch = () => {
|
||||||
|
drawer.value = !drawer.value;
|
||||||
|
};
|
||||||
|
defineOptions({
|
||||||
|
name: 'index',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
Loading…
Reference in New Issue