修改商品规格

This commit is contained in:
wangxiaowei
2025-05-29 23:41:48 +08:00
parent d9ffaa6b4e
commit c1cae803e0
16 changed files with 2782 additions and 713 deletions

258
package-lock.json generated
View File

@ -15,11 +15,13 @@
"@vueuse/core": "^12.7.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"@yipai-front-end/lib": "^0.0.27",
"axios": "^1.7.9",
"css-color-function": "^1.3.3",
"echarts": "^5.6.0",
"element-plus": "^2.9.4",
"highlight.js": "^11.11.1",
"jsonp": "^0.2.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"nprogress": "^0.2.0",
@ -27,7 +29,9 @@
"vue": "^3.5.13",
"vue-clipboard3": "^2.0.0",
"vue-echarts": "^6.7.3",
"vue-qqmap": "^1.1.1",
"vue-router": "^4.5.0",
"vue3-sku-form": "^1.0.1",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
@ -2772,6 +2776,19 @@
"node": ">= 8.0.0"
}
},
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
@ -3929,6 +3946,12 @@
"snabbdom": "^3.1.0"
}
},
"node_modules/@yipai-front-end/lib": {
"version": "0.0.27",
"resolved": "https://registry.npmmirror.com/@yipai-front-end/lib/-/lib-0.0.27.tgz",
"integrity": "sha512-X5llKyS/sImpDOQKkZU/BU2HB1N4mqFdAmAKoogO2I3gTHK65/KwvvfoqF/cLYVvnyfjFDONkPGfGwaUNWfKeg==",
"license": "ISC"
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz",
@ -4036,6 +4059,19 @@
"node": ">= 8"
}
},
"node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz",
@ -4455,7 +4491,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
@ -4757,7 +4792,6 @@
"version": "3.42.0",
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.42.0.tgz",
"integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==",
"dev": true,
"hasInstallScript": true,
"funding": {
"type": "opencollective",
@ -7547,6 +7581,29 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonp": {
"version": "0.2.1",
"resolved": "https://registry.npmmirror.com/jsonp/-/jsonp-0.2.1.tgz",
"integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==",
"dependencies": {
"debug": "^2.1.3"
}
},
"node_modules/jsonp/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/jsonp/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
@ -7837,6 +7894,19 @@
"node": ">=8.6"
}
},
"node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
@ -8256,7 +8326,6 @@
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -8514,12 +8583,13 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@ -8906,6 +8976,21 @@
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/quansync": {
"version": "0.2.10",
"resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.10.tgz",
@ -9550,7 +9635,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
@ -9569,7 +9653,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
@ -9585,7 +9668,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@ -9603,7 +9685,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@ -10539,6 +10620,19 @@
"node": ">=10.13.0"
}
},
"node_modules/tailwindcss/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tailwindcss/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
@ -10622,18 +10716,6 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-object-path": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/to-object-path/-/to-object-path-0.3.0.tgz",
@ -11076,18 +11158,6 @@
"@types/estree": "^1.0.0"
}
},
"node_modules/unimport/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/union-value/-/union-value-1.0.1.tgz",
@ -11158,18 +11228,6 @@
}
}
},
"node_modules/unplugin-auto-import/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/unplugin-utils": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.2.4.tgz",
@ -11186,18 +11244,6 @@
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/unplugin-utils/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/unplugin-vue-components": {
"version": "28.5.0",
"resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-28.5.0.tgz",
@ -11274,6 +11320,19 @@
}
}
},
"node_modules/unplugin-vue-components/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/unplugin-vue-components/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
@ -11286,18 +11345,6 @@
"node": ">=8.10.0"
}
},
"node_modules/unplugin/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/unset-value/-/unset-value-1.0.0.tgz",
@ -11633,18 +11680,6 @@
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
@ -11818,6 +11853,50 @@
"node": ">=10"
}
},
"node_modules/vue-jsonp": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/vue-jsonp/-/vue-jsonp-2.1.0.tgz",
"integrity": "sha512-kezmjaAcMWdieO3tWxniC+82DitYUYjR1e2GsWIKHCTf+zhWUt2nPhN3dnmnAVhDQ+po3BspM7sKjiQaIhijUg==",
"license": "MIT"
},
"node_modules/vue-qqmap": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/vue-qqmap/-/vue-qqmap-1.1.1.tgz",
"integrity": "sha512-fL58MO31pmXuADRc8eYYPdLTNl7b68pP0YbyR3CLja9D5PeFv7cF4K5DOR2mOb/lCHjRVUUF+Ft5f6KVDnPUog==",
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
"core-js": "^3.6.5",
"lodash-es": "^4.17.21",
"qs": "^6.10.1",
"typescript": "^4.3.5",
"vue": "^3.2.0",
"vue-jsonp": "^2.0.0",
"vue-qqmap": "^1.0.9"
}
},
"node_modules/vue-qqmap/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmmirror.com/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/vue-qqmap/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",
@ -11848,6 +11927,19 @@
"typescript": ">=5.0.0"
}
},
"node_modules/vue3-sku-form": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/vue3-sku-form/-/vue3-sku-form-1.0.1.tgz",
"integrity": "sha512-K54a7pYk6VKVhOpB/aG7syM9s4WXnAx4jdmbu2WUs0CoQDvvZ1TJONeybDyJB61k1Vv9UmGUhuc0PjZO194IEw==",
"license": "MIT",
"dependencies": {
"vue": "^3.3.4"
},
"peerDependencies": {
"element-plus": "^2.3.0",
"vue-router": "^4.0.0"
}
},
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz",

View File

@ -16,6 +16,7 @@
"@vueuse/core": "^12.7.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"@yipai-front-end/lib": "^0.0.27",
"axios": "^1.7.9",
"css-color-function": "^1.3.3",
"echarts": "^5.6.0",
@ -31,6 +32,7 @@
"vue-echarts": "^6.7.3",
"vue-qqmap": "^1.1.1",
"vue-router": "^4.5.0",
"vue3-sku-form": "^1.0.1",
"vuedraggable": "^4.1.0"
},
"devDependencies": {

12
pnpm-lock.yaml generated
View File

@ -24,8 +24,8 @@ importers:
specifier: ^5.1.23
version: 5.1.23
'@wangeditor/editor-for-vue':
specifier: ^5.1.12
version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.14(typescript@5.8.3))
specifier: ^1.0.2
version: 1.0.2(@wangeditor/editor@5.1.23)(vue@3.5.14(typescript@5.8.3))
axios:
specifier: ^1.7.9
version: 1.9.0
@ -1444,11 +1444,11 @@ packages:
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/editor-for-vue@5.1.12':
resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==}
'@wangeditor/editor-for-vue@1.0.2':
resolution: {integrity: sha512-BOENvAXJVtVXlE2X50AAvjV82YlCUeu5cbeR0cvEQHQjYtiVnJtq7HSoj85r2kTgGouI5OrpJG9BBEjSjUSPyA==}
peerDependencies:
'@wangeditor/editor': '>=5.1.0'
vue: ^3.0.5
vue: ^2.6.14
'@wangeditor/editor@5.1.23':
resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==}
@ -5345,7 +5345,7 @@ snapshots:
slate-history: 0.66.0(slate@0.72.8)
snabbdom: 3.6.2
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.14(typescript@5.8.3))':
'@wangeditor/editor-for-vue@1.0.2(@wangeditor/editor@5.1.23)(vue@3.5.14(typescript@5.8.3))':
dependencies:
'@wangeditor/editor': 5.1.23
vue: 3.5.14(typescript@5.8.3)

View File

@ -29,3 +29,8 @@ export function apiGoodsDetail(params: any) {
export function checkCategory() {
return request.post({ url: '/goodsCategory/checkCategory' })
}
// 商品上传图片
export function uploadImage(params: any) {
return request.post({ url: '/upload/image', params })
}

View File

@ -1,25 +1,9 @@
<template>
<div class="border border-br flex flex-col" :style="styles">
<toolbar
class="border-b border-br"
:editor="editorRef"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<w-editor
class="flex-1 overflow-hidden"
v-model="valueHtml"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="handleCreated"
/>
<material-picker
ref="materialPickerRef"
:type="fileType"
:limit="-1"
hidden-upload
@change="selectChange"
/>
<toolbar class="border-b border-br" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
<w-editor class="flex-1 overflow-hidden" v-model="valueHtml" :defaultConfig="editorConfig" :mode="mode"
@onCreated="handleCreated" />
<material-picker ref="materialPickerRef" :type="fileType" :limit="-1" hidden-upload @change="selectChange" />
</div>
</template>
<script setup lang="ts">
@ -114,27 +98,35 @@ const handleCreated = (editor: any) => {
.w-e-full-screen-container {
z-index: 999;
}
.w-e-text-container [data-slate-editor] ul {
list-style: disc;
}
.w-e-text-container [data-slate-editor] ol {
list-style: decimal;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1.17em;
}
h4 {
font-size: 1em;
}
h5 {
font-size: 0.83em;
}
h1,
h2,
h3,

View File

@ -4,66 +4,31 @@
<el-page-header :content="$route.meta.title" @back="$router.back()" />
</el-card>
<el-card class="mt-4 !border-none" shadow="never">
<el-form
ref="formRef"
class="ls-form"
:model="formData"
label-width="85px"
:rules="rules"
>
<el-form ref="formRef" class="ls-form" :model="formData" label-width="85px" :rules="rules">
<div class="xl:flex">
<div>
<el-form-item label="文章标题" prop="title">
<div class="w-80">
<el-input
v-model="formData.title"
placeholder="请输入文章标题"
type="textarea"
:autosize="{ minRows: 3, maxRows: 3 }"
maxlength="64"
show-word-limit
clearable
/>
<el-input v-model="formData.title" placeholder="请输入文章标题" type="textarea"
:autosize="{ minRows: 3, maxRows: 3 }" maxlength="64" show-word-limit clearable />
</div>
</el-form-item>
<el-form-item label="文章栏目" prop="cid">
<el-select
class="w-80"
v-model="formData.cid"
placeholder="请选择文章栏目"
clearable
>
<el-option
v-for="item in optionsData.article_cate"
:key="item.id"
:label="item.name"
:value="item.id"
/>
<el-select class="w-80" v-model="formData.cid" placeholder="请选择文章栏目" clearable>
<el-option v-for="item in optionsData.article_cate" :key="item.id" :label="item.name"
:value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="文章简介" prop="desc">
<div class="w-80">
<el-input
v-model="formData.desc"
placeholder="请输入文章简介"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
:maxlength="200"
show-word-limit
clearable
/>
<el-input v-model="formData.desc" placeholder="请输入文章简介" type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }" :maxlength="200" show-word-limit clearable />
</div>
</el-form-item>
<el-form-item label="摘要" prop="abstract">
<div class="w-80">
<el-input
type="textarea"
:autosize="{ minRows: 6, maxRows: 6 }"
v-model="formData.abstract"
maxlength="200"
show-word-limit
clearable
/>
<el-input type="textarea" :autosize="{ minRows: 6, maxRows: 6 }"
v-model="formData.abstract" maxlength="200" show-word-limit clearable />
</div>
</el-form-item>
<el-form-item label="文章封面" prop="image">

View File

@ -0,0 +1,794 @@
<template>
<div>
<div class="sku-form-container" :class="`sku-form-container-${theme}`" v-if="!disabled">
<div class="sku-form-section" v-for="(attrItem, attrIndex) in myAttribute" :key="attrIndex">
<div class="flex items-center">
<div class="sku-form-title">{{ attrItem.name }}</div>
<div @click="deleteAttrItemName(attrIndex)" class="mt-1 ml-2 cursor-pointer">
<el-icon>
<Delete />
</el-icon>
</div>
</div>
<div class="sku-form-tags-box">
<el-checkbox-group v-model="checked[attrIndex]" :disabled="disabled" class="checkbox-group">
<el-checkbox v-for="(item, index) in attrItem.item" :key="index" :disabled="disabled"
:label="item.name" @change="checked => onCheckedChange(attrIndex, index, checked)">
<div class="sku-checkbox-content">
<img v-if="item.image" :src="item.image" class="sku-option-image" :alt="item.name" />
<span>{{ item.name }}</span>
</div>
</el-checkbox>
</el-checkbox-group>
</div>
<div class="sku-form-add-tags" v-if="attrItem.canAddAttribute">
<el-input v-model="inputValues[attrIndex]" placeholder="请输入规格名称" size="small"
@keyup.enter="onAddAttribute(attrIndex)">
<template #append>
<el-button :icon="Plus" @click="onAddAttribute(attrIndex)">添加</el-button>
</template>
</el-input>
</div>
</div>
</div>
<el-form v-if="myAttribute.length > 0" ref="formRef" :model="form" :rules="rules" class="sku-form-table"
:class="disabled ? 'sku-form-table-disabled' : ''">
<el-table :data="form.skuData" border style="width: 100%;overflow-x: auto;" :key="form.skuData.length">
<el-table-column v-for="(col, colIndex) in emitAttribute" :key="colIndex" :label="col.name"
align="center">
<template #default="{ row }">
<div class="sku-table-cell">
<img v-if="getAttributeImage(col.name, row[col.name])"
:src="getAttributeImage(col.name, row[col.name])" class="sku-table-image" />
<span>{{ row[col.name] }} --{{ row[col.price] }}</span>
</div>
</template>
</el-table-column>
<el-table-column v-for="(col, colIndex) in structure" :key="colIndex" :label="col.label" align="center">
<template #header v-if="col.batch !== false && col.type === 'input' && isBatch">
<div class="sku-form-batch">
<el-input v-model="batch[col.name]" :placeholder="`批量${col.label}`"
@change="onBatchSet(col.name)">
<!-- <template #append>
<el-button @click="onBatchSet(col.name)">设置</el-button>
</template> -->
</el-input>
</div>
</template>
<template #default="{ row, $index }">
<div v-if="col.type === 'slot'">
<slot :name="col.name" :row="row" :index="$index" />
</div>
<el-form-item v-else :prop="`skuData.${$index}.${col.name}`"
:class="`sku-form-${$index}-${col.name}`">
<el-tooltip v-if="col.tips" :content="col.tips" placement="top" :hide-after="0">
<el-icon class="sku-form-tips">
<InfoFilled />
</el-icon>
</el-tooltip>
<el-input v-model="row[col.name]" size="small" :placeholder="col.placeholder"
:disabled="col.disabled" />
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, toRefs, nextTick } from 'vue'
import { Plus, InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
/**
* 原始规格数据
* sourceAttribute: [
* { name: '颜色', item: ['黑', '金', '白'] },
* { name: '内存', item: ['16G', '32G'] },
* { name: '运营商', item: ['电信', '移动', '联通'] }
* ]
*/
sourceAttribute: {
type: Array,
default: () => []
},
/**
* 已使用的规格数据用于复原数据支持v-model:attribute修饰符
* attribute: [
* { name: '颜色', item: ['黑'] },
* { name: '运营商', item: ['电信', '移动', '联通'] }
* ]
*/
attribute: {
type: Array,
default: () => []
},
/**
* 用于复原sku数据支持v-model:sku修饰符
* sku: [
* { sku: '黑;电信', price: 1, stock: 1 },
* { sku: '黑;移动', price: 2, stock: 2 },
* { sku: '黑;联通', price: 3, stock: 3 }
* ]
*/
sku: {
type: Array,
default: () => []
},
/**
* 表格结构注意name字段用于输出sku数据
*/
structure: {
type: Array,
default: () => [
{ name: 'price', type: 'input', label: '价格' },
{ name: 'cost_price', type: 'input', label: '成本价' },
{ name: 'stock', type: 'input', label: '库存' }
]
},
// sku 字段分隔符
separator: {
type: String,
default: ';'
},
// 无规格的 sku
emptySku: {
type: String,
default: ''
},
// 是否显示 sku 选择栏
disabled: {
type: Boolean,
default: false
},
// 主题风格
theme: {
type: Number,
default: 1
},
// 是否开启异步加载
async: {
type: Boolean,
default: false
},
// 是否可添加属性值
canAddAttribute: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['update:attribute', 'update:sku', 'validate'])
// 使用toRefs优化props解构保持响应性
const { sourceAttribute, attribute, sku, structure, separator, emptySku, async: isAsync, canAddAttribute } = toRefs(props)
// 表单引用
const formRef = ref(null)
const isInit = ref(false)
// 输入框的值独立管理,避免深层次响应式问题
const inputValues = ref([])
// 数据
const form = reactive({
skuData: []
})
// 批量设置暂存数据
const batch = reactive({})
// 属性数据(包含选中状态)
const myAttribute = ref([])
// 用于管理checkbox组的选中状态
const checked = ref([])
// 计算规则
const rules = computed(() => {
const result = {}
structure.value.forEach(item => {
if (item.required) {
const rule = { required: true, message: `请输入${item.label}`, trigger: 'blur' }
if (item.validator) {
rule.validator = item.validator
}
result[item.name] = [rule]
} else if (item.validator) {
result[item.name] = [{ validator: item.validator, trigger: 'blur' }]
}
})
return result
})
// 是否显示批量设置
const isBatch = computed(() => {
return structure.value.some(item => item.type === 'input' && item.batch !== false)
})
// 仅输出勾选的属性
const emitAttribute = computed(() => {
return myAttribute.value.map(attr => {
// 过滤选中的项目并包含完整的属性信息
const selectedItems = attr.item
.filter(item => item.checked)
.map(item => {
// 仅移除checked状态保留其他所有属性
const { checked, ...itemDetail } = item;
return itemDetail;
});
return {
name: attr.name,
item: selectedItems
};
}).filter(attr => attr.item.length > 0);
})
// 初始化方法
const init = () => {
nextTick(() => {
isInit.value = true
// 初始化 myAttribute
const newMyAttribute = []
// 根据 sourceAttribute 复原 myAttribute 的结构
sourceAttribute.value.forEach(v => {
const temp = {
name: v.name,
canAddAttribute: typeof v.canAddAttribute !== 'undefined' ? v.canAddAttribute : canAddAttribute.value,
addAttribute: '',
item: []
}
// 处理规格项,支持字符串或对象格式
v.item.forEach(itemValue => {
if (typeof itemValue === 'string') {
// 处理字符串类型的规格项(向后兼容)
temp.item.push({
name: itemValue,
checked: false
})
} else {
// 处理对象类型的规格项(新格式)
temp.item.push({
...itemValue,
checked: false
})
}
})
newMyAttribute.push(temp)
})
// 初始化输入值数组
inputValues.value = Array(sourceAttribute.value.length).fill('');
// 根据 attribute 更新 myAttribute处理已选中的属性
attribute.value.forEach(attrVal => {
newMyAttribute.forEach(myAttrVal => {
if (attrVal.name === myAttrVal.name) {
attrVal.item.forEach(attrItem => {
const attrName = typeof attrItem === 'string' ? attrItem : attrItem.name;
// 查找匹配的属性项
const existingItem = myAttrVal.item.find(myAttrItem => myAttrItem.name === attrName);
if (existingItem) {
// 如果找到匹配项,标记为选中
existingItem.checked = true;
} else {
// 如果没找到,添加新项
myAttrVal.item.push({
...(typeof attrItem === 'string' ? { name: attrItem } : attrItem),
checked: true
});
}
});
}
});
});
myAttribute.value = newMyAttribute
// 因为 skuData 是实时监听 myAttribute 变化并自动生成,使用微任务确保已生成
nextTick(() => {
console.log("sku.value>>>", sku.value);
console.log(" form.skuData2>>>", form.skuData);
sku.value.forEach(skuItem => {
form.skuData.forEach(skuDataItem => {
console.log("form.skuDataItem>>>", skuDataItem);
if (skuItem.sku === skuDataItem.sku) {
structure.value.forEach(structureItem => {
skuDataItem[structureItem.name] = skuItem[structureItem.name]
})
}
})
})
isInit.value = false
})
})
}
// 获取属性的图片路径
const getAttributeImage = (attrName, attrValue) => {
if (!attrName || !attrValue) return null;
const attrGroup = myAttribute.value.find(attr => attr.name === attrName);
if (!attrGroup) return null;
const attrItem = attrGroup.item.find(item => item.name === attrValue);
return attrItem?.image || null;
}
// 初始化属性
watch(
attribute,
() => {
if (!isAsync.value) {
init()
}
},
{ immediate: true, deep: true }
)
// 监听选中属性的变化
watch(myAttribute, () => {
if (!isInit.value) {
// 更新父组件
emit('update:attribute', emitAttribute.value)
}
// 解决通过 $emit 更新后无法拿到 attribute 最新数据的问题
nextTick(() => {
if (emitAttribute.value.length !== 0) {
combinationAttribute()
} else {
form.skuData = []
const obj = {
sku: emptySku.value
}
structure.value.forEach(v => {
if (!(v.type === 'slot' && v.skuProperty === false)) {
obj[v.name] = typeof v.defaultValue !== 'undefined' ? v.defaultValue : ''
}
})
console.log('obj=', obj);
form.skuData.push(obj)
}
clearValidate()
})
}, { deep: true })
// 监听skuData变化
watch(() => form.skuData, (newValue, oldValue) => {
if (!isInit.value || (newValue.length === 1 && newValue[0].sku === emptySku.value)) {
// 如果有老数据,或者 sku 数据为空,则更新父级 sku 数据
if (oldValue.length || !sku.value.length) {
// 更新父组件
const arr = [];
newValue.forEach(v1 => {
const obj = {
sku: v1.sku,
skuData: v1.skuData || {} // 完整的SKU数据
};
structure.value.forEach(v2 => {
if (!(v2.type === 'slot' && v2.skuProperty === false)) {
obj[v2.name] = v1[v2.name] || (typeof v2.defaultValue !== 'undefined' ? v2.defaultValue : '');
}
});
arr.push(obj);
});
emit('update:sku', arr);
}
}
}, { deep: true })
// 组合属性生成SKU表格数据
const combinationAttribute = (index = 0, dataTemp = []) => {
if (index === 0) {
for (let i = 0; i < emitAttribute.value[0].item.length; i++) {
const attrItem = emitAttribute.value[0].item[i];
const attrName = attrItem.name;
const obj = {
sku: attrName,
[emitAttribute.value[0].name]: attrName,
skuData: { // 存储完整的SKU对象数据
[emitAttribute.value[0].name]: attrItem
}
};
structure.value.forEach(v => {
if (!(v.type === 'slot' && v.skuProperty === false)) {
obj[v.name] = typeof v.defaultValue !== 'undefined' ? v.defaultValue : '';
}
});
dataTemp.push(obj);
}
} else {
const temp = [];
for (let i = 0; i < dataTemp.length; i++) {
for (let j = 0; j < emitAttribute.value[index].item.length; j++) {
const attrItem = emitAttribute.value[index].item[j];
const attrName = attrItem.name;
const newItem = JSON.parse(JSON.stringify(dataTemp[i]));
// 添加新的属性名称
newItem[emitAttribute.value[index].name] = attrName;
// 更新SKU编码
newItem['sku'] = [newItem['sku'], attrName].join(separator.value);
// 更新完整的SKU对象数据
newItem.skuData = {
...newItem.skuData,
[emitAttribute.value[index].name]: attrItem
};
temp.push(newItem);
}
}
dataTemp = temp;
}
// if (index !== emitAttribute.value.length - 1) {
// combinationAttribute(index + 1, dataTemp);
// } else {
// if (!isInit.value || isAsync.value) {
// // 将原有的 sku 数据和新的 sku 数据比较,相同的 sku 则把原有的 sku 数据覆盖到新的 sku 数据里
// for (let i = 0; i < form.skuData.length; i++) {
// for (let j = 0; j < dataTemp.length; j++) {
// if (form.skuData[i].sku === dataTemp[j].sku) {
// // 保留原SKU数据中的属性值
// structure.value.forEach(structureItem => {
// dataTemp[j][structureItem.name] = form.skuData[i][structureItem.name];
// });
// // 保留原SKU数据中的完整对象信息
// dataTemp[j].skuData = {
// ...dataTemp[j].skuData,
// ...form.skuData[i].skuData
// };
// }
// }
// }
// }
// form.skuData = dataTemp;
// }
if (index !== emitAttribute.value.length - 1) {
combinationAttribute(index + 1, dataTemp);
} else {
// 先合并外部传入的 sku 数据
if (sku.value && sku.value.length) {
const skuMap = new Map();
sku.value.forEach(skuItem => {
skuMap.set(skuItem.sku, skuItem);
});
dataTemp.forEach(dataItem => {
const matched = skuMap.get(dataItem.sku);
if (matched) {
structure.value.forEach(structureItem => {
dataItem[structureItem.name] = matched[structureItem.name];
});
// 合并skuData对象
dataItem.skuData = {
...dataItem.skuData,
...matched.skuData
};
}
});
}
form.skuData = dataTemp;
}
}
// 查找属性的详细信息
const findAttributeDetail = (attrName, itemName) => {
const attrGroup = myAttribute.value.find(attr => attr.name === attrName);
if (!attrGroup) return { name: itemName };
const item = attrGroup.item.find(item => item.name === itemName);
if (!item) return { name: itemName };
// 返回完整的属性对象仅过滤掉checked状态
const { checked, ...itemDetail } = item;
return itemDetail;
}
// 添加新属性值
const onAddAttribute = (index) => {
const newValue = inputValues.value[index]?.trim();
if (!newValue) {
ElMessage.warning('请输入规格名称');
return;
}
// 检查分隔符
if (newValue.includes(separator.value)) {
ElMessage.warning(`规格里不允许出现「 ${separator.value} 」字符,请检查后重新添加`);
return;
}
// 检查重复
if (myAttribute.value[index].item.some(item => item.name === newValue)) {
ElMessage.warning('请勿添加相同规格');
return;
}
// 添加新属性,并默认选中
myAttribute.value[index].item.push({
name: newValue,
checked: true
});
// 清空输入框
inputValues.value[index] = '';
}
// 批量设置
const onBatchSet = (field) => {
if (batch[field] !== '') {
form.skuData.forEach(row => {
row[field] = batch[field]
})
batch[field] = ''
}
}
// 表单验证
const validate = (callback) => {
if (formRef.value) {
formRef.value.validate(valid => {
callback && callback(valid)
})
} else {
callback && callback(false)
}
}
// 自定义验证
const validateFieldByColumns = (columns, callback) => {
if (!formRef.value) {
callback && callback(false)
return
}
const propPaths = []
form.skuData.forEach((_, i) => {
columns.forEach(col => {
propPaths.push(`skuData.${i}.${col}`)
})
})
formRef.value.validateField(propPaths, valid => {
callback && callback(valid)
})
}
// 按行验证
const validateFieldByRows = (index, prop, callback) => {
if (formRef.value) {
formRef.value.validateField([`skuData.${index}.${prop}`], valid => {
callback && callback(valid)
})
} else {
callback && callback(false)
}
}
// 清除验证
const clearValidate = () => {
if (formRef.value) {
formRef.value.clearValidate()
}
}
// 删除规格先
const deleteAttrItemName = (attrIndex) => {
myAttribute.value.splice(attrIndex, 1)
emit('update:attribute', emitAttribute.value)
}
// 添加新属性(带图片)
const onAddAttributeWithImage = (index, name, imagePath) => {
if (!name || typeof name !== 'string') {
ElMessage.warning('请提供有效的规格名称');
return;
}
const newValue = name.trim();
if (!newValue) {
ElMessage.warning('规格名称不能为空');
return;
}
// 检查分隔符
if (newValue.includes(separator.value)) {
ElMessage.warning(`规格里不允许出现「 ${separator.value} 」字符,请检查后重新添加`);
return;
}
// 检查重复
if (myAttribute.value[index].item.some(item => item.name === newValue)) {
ElMessage.warning('请勿添加相同规格');
return;
}
// 添加新属性(带图片),并默认选中
myAttribute.value[index].item.push({
name: newValue,
image: imagePath || '',
checked: true
});
return true;
}
// 监听属性变化初始化checked数组
watch(() => myAttribute.value, () => {
// 初始化checked数组
checked.value = myAttribute.value.map(attr =>
attr.item.filter(item => item.checked).map(item => item.name)
)
}, { deep: true, immediate: true })
watch(sourceAttribute, () => {
if (!isAsync.value) {
init()
}
}, { immediate: true, deep: true })
// 处理checkbox变化
const onCheckedChange = (attrIndex, itemIndex, isChecked) => {
// 更新原始item的checked状态
myAttribute.value[attrIndex].item[itemIndex].checked = isChecked
}
// 暴露方法
defineExpose({
init,
validate,
validateFieldByColumns,
validateFieldByRows,
clearValidate,
onAddAttributeWithImage
})
</script>
<style lang="scss" scoped>
.sku-form {
&-container {
margin-bottom: 10px;
&-1 {
.sku-form-section {
margin-bottom: 10px;
}
.sku-form-title {
font-weight: bold;
}
.sku-form-tags-box {
margin-bottom: 10px;
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
}
&-2 {
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 4px;
.sku-form-section {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
.sku-form-title {
min-width: 70px;
margin-right: 10px;
font-weight: bold;
}
.sku-form-tags-box {
flex: 1;
margin-right: 10px;
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.sku-form-add-tags {
width: 230px;
}
}
}
&-table {
:deep(.el-table .cell) {
padding: 0 5px;
}
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-form-item__content) {
min-height: 32px;
display: flex;
align-items: center;
}
&-disabled {
.el-table {
pointer-events: none;
}
}
}
&-batch {
margin-bottom: 5px;
}
&-tips {
margin-right: 5px;
color: #409eff;
cursor: pointer;
}
}
// 新增样式 - 规格选项相关
.sku-checkbox-content {
display: flex;
align-items: center;
gap: 4px;
}
.sku-option-image {
width: 24px;
height: 24px;
object-fit: cover;
border-radius: 2px;
}
.sku-table-cell {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.sku-table-image {
width: 20px;
height: 20px;
object-fit: cover;
border-radius: 2px;
}
</style>

View File

@ -0,0 +1,25 @@
import SkuForm from './SkuForm.vue'
// 版本信息
const version = '1.0.0'
// 组件安装函数
const install = (app) => {
app.component('SkuForm', SkuForm)
}
// 组件库对象
const SkuFormLib = {
version,
install,
SkuForm
}
// Vue插件安装函数
SkuForm.install = app => {
app.component('SkuForm', SkuForm)
}
// 导出方式支持 ES Module、CommonJS 和全局变量
export { version, SkuForm }
export default SkuFormLib

View File

@ -0,0 +1,307 @@
<template>
<el-form>
<el-form-item label="销售属性" name="skuAttributes">
<div class="mb-[10px] bg-[#fafafa] p-[8px] max-w-[1000px]" v-for="(item, index) in skuAttributes">
<div class="flex items-center relative">
<!-- <a-popconfirm title="确定删除吗?" @confirm="deleteSkuAttr(index)" placement="right">
<a-button class="absolute top-[5px] right-[5px]" type="text">删除</a-button>
</a-popconfirm> -->
<div class="absolute top-[5px] right-[5px]" @confirm="deleteSkuAttr(index)">删除</div>
<div class="w-[60px] text-right">属性名称:</div>
<el-input class="ml-5 w-[130px]" v-model="item.title" placeholder="请输入属性名称"></el-input>
</div>
<div class="flex items-start mt-[10px] overflow-x-auto">
<div class="w-[60px] text-right leading-[32px]">属性值:</div>
<div class="ml-5 relative sku_item" v-for="(text, cindex) in item.values">
<!-- <DeleteIcon class="sku_item_delete absolute top-[-4px] right-[4px] z-50"
@click="deleteSkuAttrName(index, cindex)"></DeleteIcon> -->
<el-icon class="sku_item_delete absolute top-[-4px] right-[4px] z-50" color="red"
@click="deleteSkuAttrName(index, cindex)">
<CircleCloseFilled />
</el-icon>
<div class="flex flex-col items-center mr-[10px]">
<el-input placeholder="请输入属性值" class="w-[130px]" v-model="text.attributeValue"></el-input>
<div v-if="item.isAddImage"
class="relative w-[100px] h-[100px] border-solid border-[#eee] border-[1px] mt-[10px] cursor-pointer flex justify-center items-center bg-white"
@click="addSkuAttrImage(index, cindex)">
<img v-if="text.thumbnailUrl" class="w-[100%] h-[100%]" :src="text.thumbnailUrl" />
<!-- <svgIcon v-else class="w-[40px] h-[40px]" name="add" color="#eee"></svgIcon> -->
<el-icon v-else class="w-[40px] h-[40px]" name="add" color="#eee">
<Plus />
</el-icon>
</div>
</div>
</div>
<el-button :disabled="item.values.length >= 3" type="text" @click="addSkuAttrName(index)">
增加字段
</el-button>
<!-- <el-button class="ml-[10px]" v-if="isAddImg" @click="toggleSkuImg(index)">上传图片</el-button>
<el-button class="ml-[10px]" v-if="item.isAddImage" @click="toggleSkuImg(index)">
取消上传
</el-button> -->
</div>
</div>
<el-button type="primary" @click="addSkuAttr">增加销售属性</el-button>
</el-form-item>
<el-form-item label="销售规格" name="stockKeepUnits">
<el-table :data="stockKeepUnits" class="mt-[10px] max-w-[1000px] w-auto" border>
<!-- 销售规格 -->
<el-table-column prop="attributeValue" label="销售规格">
<template #default="{ row }">
<span>{{ row.attributeValue }}</span>
</template>
</el-table-column>
<!-- 售价 -->
<el-table-column prop="price" label="*售价">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.price" />
</template>
</el-table-column>
<!-- 市场价 -->
<el-table-column prop="marketPrice" label="*市场价">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.marketPrice" />
</template>
</el-table-column>
<!-- 库存 -->
<el-table-column prop="stock" label="*库存">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.stock" />
</template>
</el-table-column>
</el-table>
<!-- <el-table class="mt-[10px] max-w-[1000px] w-auto" bordered :data="stockKeepUnits">
<template #bodyCell="{ column, record, index }">
<div v-if="column.dataIndex === 'attributeValue'">
<span>{{ record.attributeValue }}</span>
</div>
<div v-else-if="column.dataIndex === 'thumbnailUrl'">
<img v-if="record.thumbnailUrl" class="w-[90px] h-[90px]" :src="record.thumbnailUrl" />
</div>
<div v-else>
<el-input class="w-[80px]" v-model:value="record[column.dataIndex]"></el-input>
</div>
</template>
</el-table> -->
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, watch, type Ref, computed } from 'vue'
import type { skuType, skuAttrItemType } from './type.d'
import type { TableColumnCtx } from 'element-plus'
import { deepClone } from '@yipai-front-end/lib'
import { ElMessage } from 'element-plus'
const skuAttributes: Ref<skuAttrItemType[]> = ref([])
const stockKeepUnits: Ref<skuType[]> = ref([])
let afterSku: skuType[] = []
const emit = defineEmits(['update:attribute'])
const skuAttrItem: skuAttrItemType = {
title: '',
isAddImage: false,
values: [{ thumbnailUrl: '', attributeValue: '' }],
}
const columns = [
{
dataIndex: 'attributeValue',
title: '销售规格',
},
{
dataIndex: 'thumbnailUrl',
title: '图片',
},
{
dataIndex: 'price',
title: '*售价',
},
{
dataIndex: 'marketPrice',
title: '*市场价',
},
{
dataIndex: 'stock',
title: '*库存',
},
{
dataIndex: 'specificationBarCode',
title: '商品条码',
},
]
// 是否可以增加图片
const isAddImg = computed(() => {
return skuAttributes.value.findIndex((e) => e.isAddImage == true) == -1 ? true : false
})
// 监听sku本身的变化,并将当前sku进行备份
watch(
() => stockKeepUnits.value,
(value) => {
afterSku = deepClone(value)
},
{ deep: true }
)
// 监听销售属性的变化,并构建sku
watch(
() => skuAttributes.value,
(value) => {
if (value.length) {
generateSku(deepClone(value))
}
},
{ deep: true }
)
/**
* 更新销售属性构建sku
* @param skuAttribute
*/
function generateSku(skuAttribute: skuAttrItemType[]) {
let attrValue: any[] = []
skuAttribute.map((item) => {
attrValue.push(item.values)
})
let skus: any[] = []
if (attrValue.length === 0) {
stockKeepUnits.value = []
return
}
skus = attrValue.reduce((col: any[], set) => {
let res: any[] = []
col.forEach((c) => {
set.forEach((s) => {
let t = c.attributeValue + ',' + s.attributeValue
res.push({ attributeValue: t, thumbnailUrl: c.thumbnailUrl || s.thumbnailUrl || '' })
})
})
return res
})
// 增加,回显相关字段
skus.map((e: skuType) => {
// 寻找销售规格一致的副本数据
let old = afterSku.find((item) => item.attributeValue == e.attributeValue)
console.log('单项', e)
e.id = old == null ? '' : old.id
e.price = old == null ? '' : old.price
e.marketPrice = old == null ? '' : old.marketPrice
e.stock = old == null ? '' : old.stock
e.specificationBarCode = old == null ? '' : old.specificationBarCode
return e
})
console.log(skus, 'skus')
stockKeepUnits.value = skus
}
/**
* 删除销售属性
* @param index
*/
function deleteSkuAttr(index) {
skuAttributes.value.splice(index, 1)
}
/**
* 删除销售属性字段
* @param index
* @param cindex
*/
function deleteSkuAttrName(index: number, cindex: number) {
skuAttributes.value[index].values.splice(cindex, 1)
}
/**
* 增加sku属性图片
* @param index
* @param cindex
*/
async function addSkuAttrImage(index: number, cindex: number) {
let res = await chooseToFile()
console.log(res)
// 生产环境此处应该是上传到服务端,获取线上url
// 此处写法仅限测试,上传大图片可能造成卡顿
_blobToDataUrl(res[0], (res) => {
skuAttributes.value[index].values[cindex].thumbnailUrl = res
})
}
/**
* 增加销售属性字段
* @param index
*/
function addSkuAttrName(index: number) {
skuAttributes.value[index].values.push({ attributeValue: '', thumbnailUrl: '' })
}
/**
* 切换首个sku是否上传图片的状态
*/
function toggleSkuImg(index) {
let { isAddImage } = skuAttributes.value[index]
if (isAddImage) {
skuAttributes.value[index].values.map((e) => {
e.thumbnailUrl = ''
return e
})
}
skuAttributes.value[index].isAddImage = !isAddImage
}
/**
* 增加销售属性
*/
function addSkuAttr() {
skuAttributes.value.push(deepClone(skuAttrItem))
}
/**
* 动态更新表格字段
* @param index
* @param dataIndex
*/
function changeSkuData(index: number, dataIndex: string, evt: any) {
let value = evt.target.value
stockKeepUnits.value[index][dataIndex] = value
}
function save() {
message.success('请查看控制台输出')
console.log('销售属性:', skuAttributes.value)
console.log('sku:', stockKeepUnits.value)
}
function _blobToDataUrl(file, callback) {
const reader = new FileReader()
reader.onload = () => {
const url = URL.createObjectURL(file) // 获取临时访问链接
callback(url)
}
reader.readAsDataURL(file)
}
defineExpose({
skuAttributes,
stockKeepUnits
})
</script>
<style lang="scss" scoped>
.sku_item {
&:hover {
.sku_item_delete {
opacity: 1;
}
}
.sku_item_delete {
opacity: 0;
}
}
</style>

39
src/views/goods/components/type.d.ts vendored Normal file
View File

@ -0,0 +1,39 @@
/**
* sku表格字段
*/
export type skuType = {
attributeValue: string
id?: string
marketPrice: string
price: string
specificationBarCode: string
stock: string
thumbnailUrl: string
}
/**
* 销售属性类型
*/
export type skuAttrItemType = {
/**
* 是否上传图片
*/
isAddImage: boolean
/**
* 名称
*/
title: string
/**
* 具体数据
*/
values: {
/**
* 属性图片
* */
thumbnailUrl?: string
/**
* 属性名称
*/
attributeValue: string
}[]
}

View File

@ -15,26 +15,26 @@
<el-form-item label="商品分类" prop="first_category_id" required>
<div class="flex w-full">
<div class="w-4/12">
<div class="w-4/12 mr-1">
<el-select v-model="formData.first_category_id" placeholder="请选择分类"
@change="selectFirstCategory">
<el-option :label="item.name" :value="item.id"
v-for="(item, index) in firstCategory" />
<el-option :label="item.name" :value="item.id" :key="item.id"
v-for="item in firstCategory" />
</el-select>
</div>
<div class="w-4/12">
<div class="w-4/12 mr-1">
<el-select v-model="formData.second_category_id" placeholder="请选择分类"
@change="selectSecondCategory">
<el-option :label="item.name" :value="item.id"
v-for="(item, index) in secondCategory" />
<el-option :label="item.name" :value="item.id" :key="item.id"
v-for="item in secondCategory" />
</el-select>
</div>
<div class="w-4/12">
<el-select v-model="formData.third_category_id" placeholder="请选择分类">
<el-option :label="item.name" :value="item.id"
v-for="(item, index) in thirdCategory" />
<el-option :label="item.name" :value="item.id" :key="item.id"
v-for="item in thirdCategory" />
</el-select>
</div>
</div>
@ -47,18 +47,17 @@
<el-form-item label="商品主图" prop="image" required>
<material-picker v-model="formData.image" :limit="1" />
</el-form-item>
<el-form-item label="商品轮播图" prop="goods_image" required>
<material-picker v-model="formData.goods_image" :limit="8" />
</el-form-item>
</el-tab-pane>
<el-tab-pane label="价格库存">
<el-radio-group v-model="goodsSpec">
<el-radio-group v-model="formData.spec_type">
<el-radio :value="1">统一规格</el-radio>
<el-radio :value="2">多规格</el-radio>
</el-radio-group>
<!-- 单规格 -->
<div v-if="goodsSpec === 1">
<div v-if="formData.spec_type === 1">
<el-table :data="[{}]" style="width: 100%">
<el-table-column label="价格(元)" prop="one_price">
<template #default="scope">
@ -77,188 +76,113 @@
</el-table-column>
</el-table>
</div>
<!-- 规格 -->
<div v-if="goodsSpec === 2">
<div>
<el-form-item label="规格项" prop="code">
<el-input v-model="formData.code" clearable placeholder="请填写规格名" />
<div>
<el-input v-model="specValue" clearable />
</div>
<div @click="addSpecValue" style="cursor: pointer;"> + 添加规格值</div>
<!-- 规格 -->
<div v-if="formData.spec_type === 2">
<div v-for="(spec, specIdx) in specs" :key="specIdx" class="mb-2">
<el-form-item :label="'规格名'">
<el-input v-model="spec.name" placeholder="如口味/尺寸" style="width: 100px;"
@input="onSpecNameChange" />
</el-form-item>
<el-form-item :label="'规格值'" style="flex:1;">
<el-input v-model="spec.valuesStr" type="textarea" :rows="2" placeholder="每行一个规格值"
@change="onSpecValueChange(specIdx)" />
</el-form-item>
<div class="flex justify-end">
<el-button type="danger" @click="removeSpec(specIdx)">删除</el-button>
</div>
<el-button type="primary">添加规格项目</el-button>
</div>
<el-button type="primary" @click="addSpec" :disabled="specs.length >= 3"
class="mb-2">添加规格项</el-button>
<!-- <el-table :data="[{}]" style="width: 100%">
<el-table-column label="*价格(元)" prop="one_price">
<el-table :data="skuTable" style="width: 100%; margin-top: 10px;">
<el-table-column v-for="(spec, idx) in specs" :key="idx"
:label="spec.name || `规格${idx + 1}`" :prop="'spec' + idx" />
<el-table-column label="价格(元)">
<template #default="scope">
<el-input v-model="formData.one_price" clearable placeholder="请输入价格(元)" />
<el-input v-model="scope.row.price" placeholder="价格" />
</template>
</el-table-column>
<el-table-column label="*成本价(元)" prop="one_cost_price">
<el-table-column label="成本价(元)">
<template #default="scope">
<el-input v-model="formData.one_cost_price" clearable placeholder="请输入成本价(元)" />
<el-input v-model="scope.row.cost_price" placeholder="成本价" />
</template>
</el-table-column>
<el-table-column label="*请输入库存" prop="one_stock">
<el-table-column label="库存">
<template #default="scope">
<el-input v-model="formData.one_stock" clearable placeholder="请输入库存" />
<el-input v-model="scope.row.stock" placeholder="库存" />
</template>
</el-table-column>
</el-table> -->
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="商品详情"></el-tab-pane>
<el-tab-pane label="销售设置"></el-tab-pane>
<el-tab-pane label="商品详情">
<editor v-model="formData.content" :height="667" />
</el-tab-pane>
<el-tab-pane label="销售设置">
<el-form-item label="虚拟销量" prop="virtual_sales_sum">
<el-input v-model="formData.virtual_sales_sum" placeholder="请输入虚拟销量" />
</el-form-item>
<el-form-item label="虚拟浏览量" prop="virtual_click">
<el-input v-model="formData.virtual_click" placeholder="请输入虚拟浏览量" />
</el-form-item>
<el-form-item label="库存显示" prop="is_show_stock">
<el-radio-group v-model="formData.is_show_stock">
<el-radio :value="1">显示</el-radio>
<el-radio :value="0">不显示</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="库存预警" prop="stock_warn">
<el-input v-model="formData.stock_warn" placeholder="请输入库存预警" />
<span>设置最低库存预警值当库存低于预警值时会出现在库存预警商品列表页0为不预警。</span>
</el-form-item> -->
<!-- <el-form-item label="配送方式" prop="goods_image" required>
<el-checkbox v-model="formData.is_express" label="快递" size="large" />
<el-checkbox v-model="formData.is_selffetch" label="门店自提" size="large" />
</el-form-item> -->
<!-- <el-form-item label="积分抵扣" prop="is_integral">
<el-radio-group v-model="formData.is_integral">
<el-radio value="1">允许积分抵扣</el-radio>
<el-radio value="0">不能使用积分抵扣</el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="赠送积分" prop="give_integral_type">
<el-radio-group v-model="formData.give_integral_type">
<el-radio :value="1">
赠送固定积分
<el-input v-model="formData.give_integral_num" style="width: 120px"
placeholder="请输入赠送积分" />
</el-radio>
<el-radio :value="2" class="mt-1">
按比例赠送积分
<el-input v-model="formData.give_integral_ratio" style="width: 120px"
placeholder="请输入赠送积分比例" />
<span class="ml-1">%</span>
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="会员价" prop="is_member">
<el-radio-group v-model="formData.is_member">
<el-radio value="0">不参与会员价</el-radio>
<el-radio value="1">根据会员等级折扣计算会员价</el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="销售状态" prop="status" required>
<el-radio-group v-model="formData.status">
<el-radio :value="1">立即上架</el-radio>
<el-radio :value="0">放入仓库</el-radio>
</el-radio-group>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<el-dialog v-model="showSpec" title="输入规格值,多个请换行" width="500">
<el-input v-model="specValue" type="textarea" clearable :rows="3" />
<div class="mt-4">
<el-button type="default" @click="closeSpecPopup">取消</el-button>
<el-button type="primary" @click="confirmSpec">确定</el-button>
</div>
</el-dialog>
<!-- <el-form ref="formRef" :model="formData" label-width="90px" :rules="formRules">
<el-form-item label="一级分类id" prop="first_category_id">
<el-input v-model="formData.first_category_id" clearable placeholder="请输入一级分类id" />
</el-form-item>
<el-form-item label="二级分类id" prop="second_category_id">
<el-input v-model="formData.second_category_id" clearable placeholder="请输入二级分类id" />
</el-form-item>
<el-form-item label="三级分类id" prop="third_category_id">
<el-input v-model="formData.third_category_id" clearable placeholder="请输入三级分类id" />
</el-form-item>
<el-form-item label="品牌id" prop="brand_id">
<el-input v-model="formData.brand_id" clearable placeholder="请输入品牌id" />
</el-form-item>
<el-form-item label="供应商id" prop="supplier_id">
<el-input v-model="formData.supplier_id" clearable placeholder="请输入供应商id" />
</el-form-item>
<el-form-item label="商品状态:-1-回收站0-下架1-上架" prop="status">
<el-input v-model="formData.status" clearable placeholder="请输入商品状态:-1-回收站0-下架1-上架" />
</el-form-item>
<el-form-item label="商品主图" prop="image">
<el-input v-model="formData.image" clearable placeholder="请输入商品主图" />
</el-form-item>
<el-form-item label="商品视频" prop="video">
<el-input v-model="formData.video" clearable placeholder="请输入商品视频" />
</el-form-item>
<el-form-item label="商品自定义海报" prop="poster">
<el-input v-model="formData.poster" clearable placeholder="请输入商品自定义海报" />
</el-form-item>
<el-form-item label="商品简介" prop="remark">
<el-input v-model="formData.remark" clearable placeholder="请输入商品简介" />
</el-form-item>
<el-form-item label="商品详细描述" prop="content">
<el-input v-model="formData.content" clearable placeholder="请输入商品详细描述" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="formData.sort" clearable placeholder="请输入排序" />
</el-form-item>
<el-form-item label="商品销量" prop="sales_sum">
<el-input v-model="formData.sales_sum" clearable placeholder="请输入商品销量" />
</el-form-item>
<el-form-item label="虚拟销量" prop="virtual_sales_sum">
<el-input v-model="formData.virtual_sales_sum" clearable placeholder="请输入虚拟销量" />
</el-form-item>
<el-form-item label="商品点击量" prop="click_count">
<el-input v-model="formData.click_count" clearable placeholder="请输入商品点击量" />
</el-form-item>
<el-form-item label="虚拟点击量" prop="virtual_click">
<el-input v-model="formData.virtual_click" clearable placeholder="请输入虚拟点击量" />
</el-form-item>
<el-form-item label="商品规格:1-统一规格2-多规格;" prop="spec_type">
<el-input v-model="formData.spec_type" clearable placeholder="请输入商品规格:1-统一规格2-多规格;" />
</el-form-item>
<el-form-item label="最高价格" prop="max_price">
<el-input v-model="formData.max_price" clearable placeholder="请输入最高价格" />
</el-form-item>
<el-form-item label="最低价格" prop="min_price">
<el-input v-model="formData.min_price" clearable placeholder="请输入最低价格" />
</el-form-item>
<el-form-item label="市场价sku中最高的市场价" prop="market_price">
<el-input v-model="formData.market_price" clearable placeholder="请输入市场价sku中最高的市场价" />
</el-form-item>
<el-form-item label="总库存" prop="stock">
<el-input v-model="formData.stock" clearable placeholder="请输入总库存" />
</el-form-item>
<el-form-item label="库存预警" prop="stock_warn">
<el-input v-model="formData.stock_warn" clearable placeholder="请输入库存预警" />
</el-form-item>
<el-form-item label="是否显示库存1-是0-否" prop="is_show_stock">
<el-input v-model="formData.is_show_stock" clearable placeholder="请输入是否显示库存1-是0-否" />
</el-form-item>
<el-form-item label="运费类型1-包邮2-统一运费3-运费模板" prop="free_shipping_type">
<el-input v-model="formData.free_shipping_type" clearable
placeholder="请输入运费类型1-包邮2-统一运费3-运费模板" />
</el-form-item>
<el-form-item label="统一运费金额" prop="free_shipping">
<el-input v-model="formData.free_shipping" clearable placeholder="请输入统一运费金额" />
</el-form-item>
<el-form-item label="运费模板" prop="free_shipping_template_id">
<el-input v-model="formData.free_shipping_template_id" clearable placeholder="请输入运费模板" />
</el-form-item>
<el-form-item label="分销佣金1-开启0-不开启" prop="is_commission">
<el-input v-model="formData.is_commission" clearable placeholder="请输入分销佣金1-开启0-不开启" />
</el-form-item>
<el-form-item label="一级分销比例" prop="first_ratio">
<el-input v-model="formData.first_ratio" clearable placeholder="请输入一级分销比例" />
</el-form-item>
<el-form-item label="二级分销比例" prop="second_ratio">
<el-input v-model="formData.second_ratio" clearable placeholder="请输入二级分销比例" />
</el-form-item>
<el-form-item label="三级分销比例" prop="three_ratio">
<el-input v-model="formData.three_ratio" clearable placeholder="请输入三级分销比例" />
</el-form-item>
<el-form-item label="区域股东分红1-开启0-不开启" prop="is_share_bouns">
<el-input v-model="formData.is_share_bouns" clearable placeholder="请输入区域股东分红1-开启0-不开启" />
</el-form-item>
<el-form-item label="区域分红比例" prop="region_ratio">
<el-input v-model="formData.region_ratio" clearable placeholder="请输入区域分红比例" />
</el-form-item>
<el-form-item label="股东分红比例" prop="shareholder_ratio">
<el-input v-model="formData.shareholder_ratio" clearable placeholder="请输入股东分红比例" />
</el-form-item>
<el-form-item label="新品推荐1-是0-否" prop="is_new">
<el-input v-model="formData.is_new" clearable placeholder="请输入新品推荐1-是0-否" />
</el-form-item>
<el-form-item label="好物优选1-是0-否" prop="is_best">
<el-input v-model="formData.is_best" clearable placeholder="请输入好物优选1-是0-否" />
</el-form-item>
<el-form-item label="猜你喜欢1-是0-否" prop="is_like">
<el-input v-model="formData.is_like" clearable placeholder="请输入猜你喜欢1-是0-否" />
</el-form-item>
<el-form-item label="是否开启拼团[0=否, 1=是]" prop="is_team">
<el-input v-model="formData.is_team" clearable placeholder="请输入是否开启拼团[0=否, 1=是]" />
</el-form-item>
<el-form-item label="积分抵扣1-开启0-不开启" prop="is_integral">
<el-input v-model="formData.is_integral" clearable placeholder="请输入积分抵扣1-开启0-不开启" />
</el-form-item>
<el-form-item label="会员价1-开启0-不开启" prop="is_member">
<el-input v-model="formData.is_member" clearable placeholder="请输入会员价1-开启0-不开启" />
</el-form-item>
<el-form-item label="赠送积分类型0-不赠送1-赠送固定积分2-按比例赠送积分" prop="give_integral_type">
<el-input v-model="formData.give_integral_type" clearable
placeholder="请输入赠送积分类型0-不赠送1-赠送固定积分2-按比例赠送积分" />
</el-form-item>
<el-form-item label="赠送积分;" prop="give_integral">
<el-input v-model="formData.give_integral" clearable placeholder="请输入赠送积分;" />
</el-form-item>
<el-form-item label="是否删除1-是0-否" prop="del">
<el-input v-model="formData.del" clearable placeholder="请输入是否删除1-是0-否" />
</el-form-item>
<el-form-item label="是否开启快递配送:1-是;0-否;" prop="is_express">
<el-input v-model="formData.is_express" clearable placeholder="请输入是否开启快递配送:1-是;0-否;" />
</el-form-item>
<el-form-item label="是否开启上门自提:1-是;0-否;" prop="is_selffetch">
<el-input v-model="formData.is_selffetch" clearable placeholder="请输入是否开启上门自提:1-是;0-否;" />
</el-form-item>
</el-form> -->
</popup>
</div>
</template>
@ -267,8 +191,10 @@
import type { FormInstance } from 'element-plus'
import Popup from '@/components/popup/index.vue'
import { apiGoodsAdd, apiGoodsEdit, apiGoodsDetail, checkCategory } from '@/api/goods'
import { timeFormat } from '@/utils/util'
import type { PropType } from 'vue'
import feedback from '@/utils/feedback'
import type { AnyAaaaRecord } from 'dns'
defineProps({
dictData: {
type: Object as PropType<Record<string, any[]>>,
@ -279,9 +205,7 @@ const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const mode = ref('add')
const goodsSpec = ref(1)
let showSpec = ref(false)
let specValue = ref("")
const goodsSpec = ref(2)
// 弹窗标题
const popupTitle = computed(() => {
@ -300,55 +224,58 @@ const formData = reactive({
remark: '',
image: '',
goods_image: '',
video: '',
poster: '',
one_price: '',
one_cost_price: '',
one_stock: '',
one_market_price: '',
one_spec_image: '',
one_volume: '',
one_weight: '',
one_bar_code: '',
spec_name: '',
spec_type: 1,
content: '',
sales_sum: 0,
click_count: 0,
virtual_sales_sum: '',
virtual_click: '',
stock_warn: 0,
is_show_stock: 1,
is_express: 1,
is_integral: 0,
give_integral_type: 0,
give_integral_num: '',
give_integral_ratio: '',
status: 0,
brand_id: '',
supplier_id: '',
status: '',
video: '',
one_spec_image: '',
poster: '',
content: '',
sort: '',
sales_sum: '',
virtual_sales_sum: '',
click_count: '',
virtual_click: '',
spec_type: '',
max_price: '',
min_price: '',
market_price: '',
stock: '',
stock_warn: '',
is_show_stock: '',
free_shipping_type: '',
free_shipping: '',
free_shipping_template_id: '',
is_commission: '',
first_ratio: '',
second_ratio: '',
three_ratio: '',
is_share_bouns: '',
region_ratio: '',
shareholder_ratio: '',
max_price: 0,
min_price: 0,
market_price: 0,
free_shipping_type: 0,
free_shipping: 0,
free_shipping_template_id: 0,
is_commission: 0,
first_ratio: 0,
second_ratio: 0,
three_ratio: 0,
is_share_bouns: 0,
region_ratio: 0,
shareholder_ratio: 0,
is_new: '',
is_best: '',
is_like: '',
is_team: '',
is_integral: '',
is_member: '',
give_integral_type: '',
give_integral: '',
del: '',
is_express: '',
is_team: 0,
is_member: 0,
give_integral: 0,
is_selffetch: '',
})
// 表单验证
// // 表单验证
const formRules = reactive<any>({
name: [{
required: true,
@ -360,60 +287,20 @@ const formRules = reactive<any>({
message: '请选择商品分类',
trigger: ['blur']
}],
// status: [{
// required: true,
// message: '请输入商品状态:-1-回收站0-下架1-上架',
// trigger: ['blur']
// }],
// image: [{
// required: true,
// message: '请输入商品主图',
// trigger: ['blur']
// }],
// is_show_stock: [{
// required: true,
// message: '请输入是否显示库存1-是0-否',
// trigger: ['blur']
// }],
// free_shipping_type: [{
// required: true,
// message: '请输入运费类型1-包邮2-统一运费3-运费模板',
// trigger: ['blur']
// }],
// is_commission: [{
// required: true,
// message: '请输入分销佣金1-开启0-不开启',
// trigger: ['blur']
// }],
// is_share_bouns: [{
// required: true,
// message: '请输入区域股东分红1-开启0-不开启',
// trigger: ['blur']
// }],
// is_team: [{
// required: true,
// message: '请输入是否开启拼团[0=否, 1=是]',
// trigger: ['blur']
// }],
// is_integral: [{
// required: true,
// message: '请输入积分抵扣1-开启0-不开启',
// trigger: ['blur']
// }],
// is_member: [{
// required: true,
// message: '请输入会员价1-开启0-不开启',
// trigger: ['blur']
// }],
// give_integral_type: [{
// required: true,
// message: '请输入赠送积分类型0-不赠送1-赠送固定积分2-按比例赠送积分',
// trigger: ['blur']
// }]
image: [{
required: true,
message: '请上传商品主图',
trigger: ['blur']
}],
goods_image: [{
required: true,
message: '请上传商品轮播图',
trigger: ['blur']
}],
})
onMounted(() => {
category()
getCategory()
});
// 获取详情
@ -430,15 +317,60 @@ const getDetail = async (row: Record<string, any>) => {
const data = await apiGoodsDetail({
id: row.id
})
setFormData(data)
// 提取 abs_image 并重新赋值给 goods_image
if (Array.isArray(data['base']['goods_image'])) {
data['base']['goods_image'] = data['base']['goods_image'].map(item => item.abs_image)
}
setFormData(data['base'])
// 编辑--重新组成数据
const { base, item, spec } = data
if (base.spec_type == 1) {
// 单规格
formData['one_price'] = item[0].price
formData['one_cost_price'] = item[0].cost_price
formData['one_stock'] = item[0].stock
formData['one_volume'] = item[0].volume
formData['one_weight'] = item[0].weight
formData['one_bar_code'] = item[0].bar_code
} else if (base.spec_type == 2) {
// 多规格
// 1. 渲染规格名和规格值
specs.value = spec.map((s: any) => ({
name: s.name,
valuesStr: s.values.map((v: any) => v.value).join('\n'),
values: s.values.map((v: any) => v.value)
}))
// 2. 渲染规格表格
skuTable.value = item.map((it: any) => {
const row: any = {
price: it.price,
cost_price: it.cost_price,
stock: it.stock
}
// 动态添加 spec0, spec1, spec2
if (it.spec_value_str) {
it.spec_value_str.split(',').forEach((v: string, idx: number) => {
row[`spec${idx}`] = v
})
}
return row
})
syncSpecToFormData()
}
// 设置分类
selectFirstCategory(data.first_category_id)
selectSecondCategory(data.second_category_id)
}
//
// 创建分类列表
let goodsCategory = reactive<any[]>([])
let firstCategory = reactive<any[]>([])
let secondCategory = reactive<any[]>([])
let thirdCategory = reactive<any[]>([])
const category = async () => {
const getCategory = async () => {
const res = await checkCategory()
goodsCategory = res
firstCategory = res.filter((item: { pid: number }) => {
@ -477,10 +409,30 @@ const selectSecondCategory = (id: number) => {
// 提交按钮
const handleSubmit = async () => {
// await formRef.value?.validate()
const data = { ...formData, }
console.log("data>>>", data);
return false;
await formRef.value?.validate()
const data = { ...formData }
if (data.spec_type == 1 && !data.one_price && !data.one_cost_price && !data.one_stock) {
feedback.notifyError('请填写商品规格')
return false
}
data.price = skuTable.value.map(item => item.price)
data.cost_price = skuTable.value.map(item => item.cost_price)
data.stock = skuTable.value.map(item => item.stock)
data.spec_value_str = skuTable.value.map(item => {
const arr = []
if ('spec0' in item) arr.push(item.spec0)
if ('spec1' in item) arr.push(item.spec1)
if ('spec2' in item) arr.push(item.spec2)
return arr.join(',')
})
if (data.spec_type == 2 && !data.price && !data.cost_price && !data.stock && !data.spec_value_str) {
feedback.notifyError('请填写商品多规格字段')
return false
}
mode.value == 'edit'
? await apiGoodsEdit(data)
: await apiGoodsAdd(data)
@ -499,33 +451,141 @@ const handleClose = () => {
emit('close')
}
// 多规格相关
// const specs = ref([
// { name: '', valuesStr: '', values: [] }
// ])
const specs = ref([])
const skuTable = ref<any[]>([])
// 添加规格值
const addSpecValue = () => {
showSpec.value = true
const onSpecNameChange = () => {
syncSpecToFormData()
}
// 关闭规格值弹窗
const closeSpecPopup = () => {
specValue.value = ''
showSpec.value = false
}
const confirmSpec = () => {
if (specValue.value) {
let specs = specValue.value.split('\n');
for (let i in specs) {
specs[i] = specs[i].trim();
}
// 添加规格项最多3个
const addSpec = () => {
if (specs.value.length < 3) {
specs.value.push({ name: '', valuesStr: '', values: [] })
syncSpecToFormData()
}
}
// 删除规格项
const removeSpec = (idx: number) => {
specs.value.splice(idx, 1)
updateSkuTable()
syncSpecToFormData()
}
// 规格值变更
const onSpecValueChange = (idx: number) => {
const spec = specs.value[idx]
spec.values = spec.valuesStr
.split('\n')
.map(v => v.trim())
.filter(v => v)
updateSkuTable()
syncSpecToFormData()
}
// 计算所有规格组合(笛卡尔积)
function cartesianProduct(arr: any[][]) {
if (arr.length === 0) return []
return arr.reduce((a, b) =>
a.flatMap(d => b.map(e => [].concat(d, e)))
)
}
// 同步规格数据到formData
const syncSpecToFormData = () => {
formData.spec_name = specs.value.map(item => item.name)
formData.spec_values = specs.value.map(item => item.valuesStr.replace(/\n/g, ','))
}
// 更新SKU表格
const updateSkuTable = () => {
const valueArr = specs.value.map(s => s.values)
if (valueArr.some(arr => arr.length === 0)) {
skuTable.value = []
return
}
let result: any[] = []
if (valueArr.length === 1) {
result = valueArr[0].map(v1 => {
const old = skuTable.value.find(row => row.spec0 === v1)
return {
spec0: v1,
price: old ? old.price : '',
cost_price: old ? old.cost_price : '',
stock: old ? old.stock : ''
}
})
} else if (valueArr.length === 2) {
result = cartesianProduct(valueArr).map(([v1, v2]) => {
const old = skuTable.value.find(row => row.spec0 === v1 && row.spec1 === v2)
return {
spec0: v1,
spec1: v2,
price: old ? old.price : '',
cost_price: old ? old.cost_price : '',
stock: old ? old.stock : ''
}
})
} else if (valueArr.length === 3) {
result = cartesianProduct(valueArr).map(([v1, v2, v3]) => {
const old = skuTable.value.find(row => row.spec0 === v1 && row.spec1 === v2 && row.spec2 === v3)
return {
spec0: v1,
spec1: v2,
spec2: v3,
price: old ? old.price : '',
cost_price: old ? old.cost_price : '',
stock: old ? old.stock : ''
}
})
}
// 合并已有 skuTable 的价格等数据
result.forEach(row => {
// 1. 精确匹配
let old = skuTable.value.find(oldRow =>
['spec0', 'spec1', 'spec2'].every(key => row[key] === oldRow[key])
)
// 2. 如果精确找不到,尝试模糊匹配(只要有一项不同也算)
if (!old) {
old = skuTable.value.find(oldRow => {
let sameCount = 0
let total = 0
for (const key of ['spec0', 'spec1', 'spec2']) {
if (row[key] !== undefined && oldRow[key] !== undefined) {
total++
if (row[key] === oldRow[key]) sameCount++
}
}
// 至少有一项相同(比如只改了一个规格值)
return total > 0 && sameCount === total - 1
})
}
if (old) {
row.price = old.price
row.cost_price = old.cost_price
row.stock = old.stock
}
})
skuTable.value = result
}
// 监听规格变化自动生成表格
watch(specs, updateSkuTable, { deep: true })
defineExpose({
open,
setFormData,
getDetail,
selectFirstCategory,
selectSecondCategory
selectSecondCategory,
mode: 'default', // 或 'simple'
})
</script>

View File

@ -15,26 +15,26 @@
<el-form-item label="商品分类" prop="first_category_id" required>
<div class="flex w-full">
<div class="w-4/12">
<div class="w-4/12 mr-1">
<el-select v-model="formData.first_category_id" placeholder="请选择分类"
@change="selectFirstCategory">
<el-option :label="item.name" :value="item.id"
v-for="(item, index) in firstCategory" />
<el-option :label="item.name" :value="item.id" :key="item.id"
v-for="item in firstCategory" />
</el-select>
</div>
<div class="w-4/12">
<div class="w-4/12 mr-1">
<el-select v-model="formData.second_category_id" placeholder="请选择分类"
@change="selectSecondCategory">
<el-option :label="item.name" :value="item.id"
v-for="(item, index) in secondCategory" />
<el-option :label="item.name" :value="item.id" :key="item.id"
v-for="item in secondCategory" />
</el-select>
</div>
<div class="w-4/12">
<el-select v-model="formData.third_category_id" placeholder="请选择分类">
<el-option :label="item.name" :value="item.id"
v-for="(item, index) in thirdCategory" />
<el-option :label="item.name" :value="item.id" :key="item.id"
v-for="item in thirdCategory" />
</el-select>
</div>
</div>
@ -47,7 +47,6 @@
<el-form-item label="商品主图" prop="image" required>
<material-picker v-model="formData.image" :limit="1" />
</el-form-item>
<el-form-item label="商品轮播图" prop="goods_image" required>
<material-picker v-model="formData.goods_image" :limit="8" />
</el-form-item>
@ -79,9 +78,115 @@
</div>
<!-- 多规格 -->
<div v-if="formData.spec_type === 2">
<div v-for="(spec, specIdx) in specs" :key="specIdx" class="mb-2">
<el-form-item label="销售属性" name="skuAttributes">
<div class="mb-[10px] bg-[#fafafa] p-[8px] max-w-[1000px]"
v-for="(item, index) in skuAttributes">
<div class="flex items-center relative">
<!-- <a-popconfirm title="确定删除吗?" @confirm="deleteSkuAttr(index)" placement="right">
<a-button class="absolute top-[5px] right-[5px]" type="text">删除</a-button>
</a-popconfirm> -->
<div class="absolute top-[5px] right-[5px]" @confirm="deleteSkuAttr(index)">删除
</div>
<div class="w-[60px] text-right">属性名称:</div>
<el-input class="ml-5 w-[130px]" v-model="item.title"
placeholder="请输入属性名称"></el-input>
</div>
<div class="flex items-start mt-[10px] overflow-x-auto">
<div class="w-[60px] text-right leading-[32px]">属性值:</div>
<div class="ml-5 relative sku_item" v-for="(text, cindex) in item.values">
<!-- <DeleteIcon class="sku_item_delete absolute top-[-4px] right-[4px] z-50"
@click="deleteSkuAttrName(index, cindex)"></DeleteIcon> -->
<el-icon class="sku_item_delete absolute top-[-4px] right-[4px] z-50"
color="red" @click="deleteSkuAttrName(index, cindex)">
<CircleCloseFilled />
</el-icon>
<div class="flex flex-col items-center mr-[10px]">
<el-input placeholder="请输入属性值" class="w-[130px]"
v-model="text.attributeValue"></el-input>
<div v-if="item.isAddImage"
class="relative w-[100px] h-[100px] border-solid border-[#eee] border-[1px] mt-[10px] cursor-pointer flex justify-center items-center bg-white"
@click="addSkuAttrImage(index, cindex)">
<img v-if="text.thumbnailUrl" class="w-[100%] h-[100%]"
:src="text.thumbnailUrl" />
<!-- <svgIcon v-else class="w-[40px] h-[40px]" name="add" color="#eee"></svgIcon> -->
<el-icon v-else class="w-[40px] h-[40px]" name="add" color="#eee">
<Plus />
</el-icon>
</div>
</div>
</div>
<el-button :disabled="item.values.length >= 3" type="text"
@click="addSkuAttrName(index)">
增加字段
</el-button>
<!-- <el-button class="ml-[10px]" v-if="isAddImg" @click="toggleSkuImg(index)">上传图片</el-button>
<el-button class="ml-[10px]" v-if="item.isAddImage" @click="toggleSkuImg(index)">
取消上传
</el-button> -->
</div>
</div>
<el-button type="primary" @click="addSkuAttr">增加销售属性</el-button>
</el-form-item>
<el-form-item label="销售规格" name="stockKeepUnits">
{{ stockKeepUnits }}
<el-table :data="stockKeepUnits" class="mt-[10px] max-w-[1000px] w-auto" border>
<!-- 销售规格 -->
<el-table-column prop="attributeValue" label="销售规格">
<template #default="{ row }">
<span>{{ row.attributeValue }}</span>
</template>
</el-table-column>
<!-- 售价 -->
<el-table-column prop="price" label="*售价">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.price" />
</template>
</el-table-column>
<!-- 市场价 -->
<el-table-column prop="marketPrice" label="*市场价">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.marketPrice" />
</template>
</el-table-column>
<!-- 库存 -->
<el-table-column prop="stock" label="*库存">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.stock" />
</template>
</el-table-column>
</el-table>
<!-- <el-table class="mt-[10px] max-w-[1000px] w-auto" bordered :data="stockKeepUnits">
<template #bodyCell="{ column, record, index }">
<div v-if="column.dataIndex === 'attributeValue'">
<span>{{ record.attributeValue }}</span>
</div>
<div v-else-if="column.dataIndex === 'thumbnailUrl'">
<img v-if="record.thumbnailUrl" class="w-[90px] h-[90px]" :src="record.thumbnailUrl" />
</div>
<div v-else>
<el-input class="w-[80px]" v-model:value="record[column.dataIndex]"></el-input>
</div>
</template>
</el-table> -->
</el-form-item>
<!-- <div>
<div>
<el-input v-model="attributes.name" placeholder="添加规格项: 如口味/尺寸" />
</div>
<div class="mt-2">
<el-button type="primary" @click="addSourceAttribute" :disabled="specs.length >= 3"
class="mb-2">添加规格项</el-button>
<span style="color: #999;margin: 10px;">备注:商品规格只能添加三个</span>
</div>
</div> -->
<!-- <el-button type="primary" @click="addSourceAttribute" :disabled="specs.length >= 3"
class="mb-2">添加规格项</el-button> -->
<!-- <div v-for="(spec, specIdx) in specs" :key="specIdx" class="mb-2">
<el-form-item :label="'规格名'">
<el-input v-model="spec.name" placeholder="如口味/尺寸" style="width: 100px;" />
<el-input v-model="spec.name" placeholder="如口味/尺寸" style="width: 100px;"
@change="onSpecNameChange" />
</el-form-item>
<el-form-item :label="'规格值'" style="flex:1;">
<el-input v-model="spec.valuesStr" type="textarea" :rows="2" placeholder="每行一个规格值"
@ -112,23 +217,77 @@
<el-input v-model="scope.row.stock" placeholder="库存" />
</template>
</el-table-column>
</el-table>
</el-table> -->
</div>
</el-tab-pane>
<el-tab-pane label="商品详情">
<Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef"
:defaultConfig="toolbarConfig" :mode="mode" />
<Editor style="height: 500px; overflow-y: hidden;" v-model="formData.content"
:defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" />
<editor v-model="formData.content" :height="667" />
</el-tab-pane>
<el-tab-pane label="销售设置">
<el-form-item label="虚拟销量" prop="virtual_sales_sum">
<el-input v-model="formData.virtual_sales_sum" placeholder="请输入虚拟销量" />
</el-form-item>
<el-form-item label="虚拟浏览量" prop="virtual_click">
<el-input v-model="formData.virtual_click" placeholder="请输入虚拟浏览量" />
</el-form-item>
<el-form-item label="库存显示" prop="is_show_stock">
<el-radio-group v-model="formData.is_show_stock">
<el-radio :value="1">显示</el-radio>
<el-radio :value="0">不显示</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="库存预警" prop="stock_warn">
<el-input v-model="formData.stock_warn" placeholder="请输入库存预警" />
<span>设置最低库存预警值当库存低于预警值时会出现在库存预警商品列表页0为不预警。</span>
</el-form-item> -->
<!-- <el-form-item label="配送方式" prop="goods_image" required>
<el-checkbox v-model="formData.is_express" label="快递" size="large" />
<el-checkbox v-model="formData.is_selffetch" label="门店自提" size="large" />
</el-form-item> -->
<!-- <el-form-item label="积分抵扣" prop="is_integral">
<el-radio-group v-model="formData.is_integral">
<el-radio value="1">允许积分抵扣</el-radio>
<el-radio value="0">不能使用积分抵扣</el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="赠送积分" prop="give_integral_type">
<el-radio-group v-model="formData.give_integral_type">
<el-radio :value="1">
赠送固定积分
<el-input v-model="formData.give_integral_num" style="width: 120px"
placeholder="请输入赠送积分" />
</el-radio>
<el-radio :value="2" class="mt-1">
按比例赠送积分
<el-input v-model="formData.give_integral_ratio" style="width: 120px"
placeholder="请输入赠送积分比例" />
<span class="ml-1">%</span>
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="会员价" prop="is_member">
<el-radio-group v-model="formData.is_member">
<el-radio value="0">不参与会员价</el-radio>
<el-radio value="1">根据会员等级折扣计算会员价</el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="销售状态" prop="status" required>
<el-radio-group v-model="formData.status">
<el-radio :value="1">立即上架</el-radio>
<el-radio :value="0">放入仓库</el-radio>
</el-radio-group>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="销售设置"></el-tab-pane>
</el-tabs>
</el-form>
<!-- <el-dialog v-model="showImagePicker" title="选择图片" width="800px" @close="showImagePicker = false">
<material-picker :limit="10" @change="onImageSelected" />
</el-dialog> -->
<!-- <material-picker ref="materialPickerRef" :limit="10" @change="onImageSelected" style="display: none;" /> -->
</popup>
</div>
</template>
@ -138,8 +297,9 @@ import type { FormInstance } from 'element-plus'
import Popup from '@/components/popup/index.vue'
import { apiGoodsAdd, apiGoodsEdit, apiGoodsDetail, checkCategory } from '@/api/goods'
import type { PropType } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import feedback from '@/utils/feedback'
import { ref, watch, type Ref, computed } from 'vue'
import sku from './components/sku.vue'
defineProps({
dictData: {
@ -170,56 +330,58 @@ const formData = reactive({
remark: '',
image: '',
goods_image: '',
video: '',
poster: '',
one_price: '',
one_cost_price: '',
one_stock: '',
one_market_price: '',
one_spec_image: '',
one_volume: '',
one_weight: '',
one_bar_code: '',
spec_name: '',
spec_type: 1,
spec_type: 2,
content: '',
sales_sum: 0,
click_count: 0,
virtual_sales_sum: '',
virtual_click: '',
stock_warn: 0,
is_show_stock: 1,
is_express: 1,
is_integral: 0,
give_integral_type: 0,
give_integral_num: '',
give_integral_ratio: '',
status: 0,
brand_id: '',
supplier_id: '',
status: '',
video: '',
one_spec_image: '',
poster: '',
sort: '',
sales_sum: '',
virtual_sales_sum: '',
click_count: '',
virtual_click: '',
max_price: '',
min_price: '',
market_price: '',
stock: '',
stock_warn: '',
is_show_stock: '',
free_shipping_type: '',
free_shipping: '',
free_shipping_template_id: '',
is_commission: '',
first_ratio: '',
second_ratio: '',
three_ratio: '',
is_share_bouns: '',
region_ratio: '',
shareholder_ratio: '',
max_price: 0,
min_price: 0,
market_price: 0,
free_shipping_type: 0,
free_shipping: 0,
free_shipping_template_id: 0,
is_commission: 0,
first_ratio: 0,
second_ratio: 0,
three_ratio: 0,
is_share_bouns: 0,
region_ratio: 0,
shareholder_ratio: 0,
is_new: '',
is_best: '',
is_like: '',
is_team: '',
is_integral: '',
is_member: '',
give_integral_type: '',
give_integral: '',
del: '',
is_express: '',
is_team: 0,
is_member: 0,
give_integral: 0,
is_selffetch: '',
})
// 表单验证
// // 表单验证
const formRules = reactive<any>({
name: [{
required: true,
@ -231,61 +393,186 @@ const formRules = reactive<any>({
message: '请选择商品分类',
trigger: ['blur']
}],
// status: [{
// required: true,
// message: '请输入商品状态:-1-回收站0-下架1-上架',
// trigger: ['blur']
// }],
// image: [{
// required: true,
// message: '请输入商品主图',
// trigger: ['blur']
// }],
// is_show_stock: [{
// required: true,
// message: '请输入是否显示库存1-是0-否',
// trigger: ['blur']
// }],
// free_shipping_type: [{
// required: true,
// message: '请输入运费类型1-包邮2-统一运费3-运费模板',
// trigger: ['blur']
// }],
// is_commission: [{
// required: true,
// message: '请输入分销佣金1-开启0-不开启',
// trigger: ['blur']
// }],
// is_share_bouns: [{
// required: true,
// message: '请输入区域股东分红1-开启0-不开启',
// trigger: ['blur']
// }],
// is_team: [{
// required: true,
// message: '请输入是否开启拼团[0=否, 1=是]',
// trigger: ['blur']
// }],
// is_integral: [{
// required: true,
// message: '请输入积分抵扣1-开启0-不开启',
// trigger: ['blur']
// }],
// is_member: [{
// required: true,
// message: '请输入会员价1-开启0-不开启',
// trigger: ['blur']
// }],
// give_integral_type: [{
// required: true,
// message: '请输入赠送积分类型0-不赠送1-赠送固定积分2-按比例赠送积分',
// trigger: ['blur']
// }]
image: [{
required: true,
message: '请上传商品主图',
trigger: ['blur']
}],
goods_image: [{
required: true,
message: '请上传商品轮播图',
trigger: ['blur']
}],
})
onMounted(() => {
category()
});
/*******************************多规格开始**************************************/
import type { skuType, skuAttrItemType } from './components/type.d'
import { deepClone } from '@yipai-front-end/lib'
const skuAttributes: Ref<skuAttrItemType[]> = ref([])
const stockKeepUnits: Ref<skuType[]> = ref([])
let afterSku: skuType[] = []
const skuAttrItem: skuAttrItemType = {
title: '',
isAddImage: false,
values: [{ thumbnailUrl: '', attributeValue: '' }],
}
// 监听sku本身的变化,并将当前sku进行备份
watch(
() => stockKeepUnits.value,
(value) => {
console.log("value>>>", value);
afterSku = deepClone(value)
},
{ deep: true }
)
// 监听销售属性的变化,并构建sku
watch(
() => skuAttributes.value,
(value) => {
if (value.length) {
console.log("123>>>", 123);
generateSku(deepClone(value))
}
},
{ deep: true }
)
/**
* 更新销售属性构建sku
* @param skuAttribute
*/
function generateSku(skuAttribute: skuAttrItemType[]) {
let attrValue: any[] = []
skuAttribute.map((item) => {
attrValue.push(item.values)
})
let skus: any[] = []
if (attrValue.length === 0) {
stockKeepUnits.value = []
return
}
skus = attrValue.reduce((col: any[], set) => {
let res: any[] = []
col.forEach((c) => {
set.forEach((s) => {
let t = c.attributeValue + ',' + s.attributeValue
res.push({ attributeValue: t, thumbnailUrl: c.thumbnailUrl || s.thumbnailUrl || '' })
})
})
return res
})
// 增加,回显相关字段
skus.map((e: skuType) => {
// 寻找销售规格一致的副本数据
let old = afterSku.find((item) => item.attributeValue == e.attributeValue)
console.log('old=', old)
console.log('单项', e)
e.id = old == null ? '' : old.id
e.price = old == null ? '' : old.price
e.marketPrice = old == null ? '' : old.marketPrice
e.stock = old == null ? '' : old.stock
e.specificationBarCode = old == null ? '' : old.specificationBarCode
return e
})
console.log(skus, 'skus')
stockKeepUnits.value = skus
}
/**
* 删除销售属性
* @param index
*/
function deleteSkuAttr(index) {
skuAttributes.value.splice(index, 1)
}
/**
* 删除销售属性字段
* @param index
* @param cindex
*/
function deleteSkuAttrName(index: number, cindex: number) {
skuAttributes.value[index].values.splice(cindex, 1)
}
/**
* 增加sku属性图片
* @param index
* @param cindex
*/
async function addSkuAttrImage(index: number, cindex: number) {
let res = await chooseToFile()
console.log(res)
// 生产环境此处应该是上传到服务端,获取线上url
// 此处写法仅限测试,上传大图片可能造成卡顿
_blobToDataUrl(res[0], (res) => {
skuAttributes.value[index].values[cindex].thumbnailUrl = res
})
}
/**
* 增加销售属性字段
* @param index
*/
function addSkuAttrName(index: number) {
skuAttributes.value[index].values.push({ attributeValue: '', thumbnailUrl: '' })
}
/**
* 切换首个sku是否上传图片的状态
*/
function toggleSkuImg(index) {
let { isAddImage } = skuAttributes.value[index]
if (isAddImage) {
skuAttributes.value[index].values.map((e) => {
e.thumbnailUrl = ''
return e
})
}
skuAttributes.value[index].isAddImage = !isAddImage
}
/**
* 增加销售属性
*/
function addSkuAttr() {
skuAttributes.value.push(deepClone(skuAttrItem))
}
/**
* 动态更新表格字段
* @param index
* @param dataIndex
*/
function changeSkuData(index: number, dataIndex: string, evt: any) {
let value = evt.target.value
stockKeepUnits.value[index][dataIndex] = value
}
function save() {
// message.success('请查看控制台输出')
console.log('销售属性:', skuAttributes.value)
console.log('sku:', stockKeepUnits.value)
}
function _blobToDataUrl(file, callback) {
const reader = new FileReader()
reader.onload = () => {
const url = URL.createObjectURL(file) // 获取临时访问链接
callback(url)
}
reader.readAsDataURL(file)
}
/*******************************多规格结束**************************************/
// 获取详情
const setFormData = async (data: Record<any, any>) => {
@ -301,15 +588,106 @@ const getDetail = async (row: Record<string, any>) => {
const data = await apiGoodsDetail({
id: row.id
})
setFormData(data)
// 提取 abs_image 并重新赋值给 goods_image
if (Array.isArray(data['base']['goods_image'])) {
data['base']['goods_image'] = data['base']['goods_image'].map(item => item.abs_image)
}
setFormData(data['base'])
// 编辑--重新组成数据
const { base, item, spec } = data
if (base.spec_type == 1) {
// 单规格
formData['one_price'] = item[0].price
formData['one_cost_price'] = item[0].cost_price
formData['one_stock'] = item[0].stock
formData['one_volume'] = item[0].volume
formData['one_weight'] = item[0].weight
formData['one_bar_code'] = item[0].bar_code
} else if (base.spec_type == 2) {
console.log("456>>>", 456);
// 多规格
skuAttributes.value = [
{
isAddImage: false,
title: '口味',
values: [
{
attributeValue: '甜',
thumbnailUrl: ''
},
{
attributeValue: '辣',
thumbnailUrl: ''
}
]
},
{
isAddImage: false,
title: '尺寸',
values: [
{
attributeValue: '大',
thumbnailUrl: ''
},
]
}
]
stockKeepUnits.value = [
{
attributeValue: '甜,大',
id: '',
marketPrice: "2",
price: "1",
specificationBarCode: '',
stock: '3',
thumbnailUrl: ''
},
{
attributeValue: '辣,大',
id: '',
marketPrice: "2",
price: "1",
specificationBarCode: '',
stock: '3',
thumbnailUrl: ''
}
]
afterSku = [
{
attributeValue: '甜,大',
id: '',
marketPrice: "2",
price: "1",
specificationBarCode: '',
stock: '3',
thumbnailUrl: ''
},
{
attributeValue: '辣,大',
id: '',
marketPrice: "2",
price: "1",
specificationBarCode: '',
stock: '3',
thumbnailUrl: ''
}
]
// generateSku(deepClone(skuAttributes.value))
}
// 设置分类
selectFirstCategory(data.first_category_id)
selectSecondCategory(data.second_category_id)
}
//
// 创建分类列表
let goodsCategory = reactive<any[]>([])
let firstCategory = reactive<any[]>([])
let secondCategory = reactive<any[]>([])
let thirdCategory = reactive<any[]>([])
const category = async () => {
const getCategory = async () => {
const res = await checkCategory()
goodsCategory = res
firstCategory = res.filter((item: { pid: number }) => {
@ -348,26 +726,29 @@ const selectSecondCategory = (id: number) => {
// 提交按钮
const handleSubmit = async () => {
console.log('销售属性:', skuAttributes.value)
console.log('sku:', stockKeepUnits.value)
return false
// await formRef.value?.validate()
const data = { ...formData, }
data.price = skuTable.value.map(item => item.price)
data.cost_price = skuTable.value.map(item => item.cost_price)
data.stock = skuTable.value.map(item => item.stock)
data.spec_value_str = skuTable.value.map(item => {
const arr = []
if ('spec0' in item) arr.push(item.spec0)
if ('spec1' in item) arr.push(item.spec1)
if ('spec2' in item) arr.push(item.spec2)
return arr.join(',')
})
// data.spec_name = specs.value.map(item => item.name);
// data.spec_values = specs.value.map(item => item.valuesStr.replace(/\n/g, ','));
console.log("data>>>", data);
const data = { ...formData }
if (data.spec_type == 1 && !data.one_price && !data.one_cost_price && !data.one_stock) {
feedback.notifyError('请填写商品规格')
return false
}
console.log("attribute>>>", attribute);
data.spec_name = attribute.spec.map(item => item.name) // 规格名称
data.spec_values = attribute.spec.map(item => item.item.map(i => i.name).join(',')) // 规格项名称
data.spec_value_str = attribute.sku.map(item => item.sku) // 规格项笛卡尔积
data.price = attribute.sku.map(item => item.price)
data.cost_price = attribute.sku.map(item => item.cost_price)
data.stock = attribute.sku.map(item => item.stock)
if (data.spec_type == 2 && !data.price && !data.cost_price && !data.stock && !data.spec_value_str) {
feedback.notifyError('请完善商品多规格字段')
return false
}
// console.log("specs>>>", specs);
// console.log("specs>>>", skuTable);
return false;
mode.value == 'edit'
? await apiGoodsEdit(data)
: await apiGoodsAdd(data)
@ -386,151 +767,112 @@ const handleClose = () => {
emit('close')
}
// 多规格相关
// const specs = ref([
// { name: '', valuesStr: '', values: [] }
// ])
const specs = ref([])
const skuTable = ref<any[]>([])
// 添加规格项最多3个
const addSpec = () => {
if (specs.value.length < 3) {
specs.value.push({ name: '', valuesStr: '', values: [] })
syncSpecToFormData()
}
}
onMounted(() => {
getCategory()
// 删除规格项
const removeSpec = (idx: number) => {
specs.value.splice(idx, 1)
updateSkuTable()
syncSpecToFormData()
}
// console.log('销售属性:', skuAttributes.value)
// console.log('sku:', stockKeepUnits.value)
// 规格值变更
const onSpecValueChange = (idx: number) => {
const spec = specs.value[idx]
spec.values = spec.valuesStr
.split('\n')
.map(v => v.trim())
.filter(v => v)
updateSkuTable()
syncSpecToFormData()
}
// skuAttributes.value = [
// {
// isAddImage: false,
// title: '口味',
// values: [
// {
// attributeValue: '甜',
// thumbnailUrl: ''
// },
// {
// attributeValue: '辣',
// thumbnailUrl: ''
// }
// ]
// },
// {
// isAddImage: false,
// title: '尺寸',
// values: [
// {
// attributeValue: '大',
// thumbnailUrl: ''
// },
// ]
// }
// ]
// 计算所有规格组合(笛卡尔积)
function cartesianProduct(arr: any[][]) {
if (arr.length === 0) return []
return arr.reduce((a, b) =>
a.flatMap(d => b.map(e => [].concat(d, e)))
)
}
// 同步规格数据到formData
const syncSpecToFormData = () => {
formData.spec_name = specs.value.map(item => item.name)
formData.spec_values = specs.value.map(item => item.valuesStr.replace(/\n/g, ','))
}
// stockKeepUnits.value = [
// {
// attributeValue: '甜,大',
// id: '',
// marketPrice: "2",
// price: "1",
// specificationBarCode: '',
// stock: '3',
// thumbnailUrl: ''
// },
// {
// attributeValue: '辣,大',
// id: '',
// marketPrice: "2",
// price: "1",
// specificationBarCode: '',
// stock: '3',
// thumbnailUrl: ''
// }
// ]
// generateSku(deepClone(skuAttributes.value))
// skuAttributes = {
// 0: {
// 'isAddImage': false,
// 'title': '口味',
// 'values': {
// {
// 'attributeValue': '甜',
// 'thumbnailUrl': ''
// },
// {
// 'attributeValue': '辣',
// 'thumbnailUrl': ''
// }
// }
// },
// 1: {
// 'isAddImage': false,
// 'title': '尺寸',
// 'values': {
// {
// 'attributeValue': '大',
// 'thumbnailUrl': ''
// }
// }
// }
// }
// 更新SKU表格
const updateSkuTable = () => {
const valueArr = specs.value.map(s => s.values)
if (valueArr.some(arr => arr.length === 0)) {
skuTable.value = []
return
}
let result: any[] = []
if (valueArr.length === 1) {
result = valueArr[0].map(v1 => ({
spec0: v1,
price: '',
cost_price: '',
stock: ''
}))
} else if (valueArr.length === 2) {
result = cartesianProduct(valueArr).map(([v1, v2]) => ({
spec0: v1,
spec1: v2,
price: '',
cost_price: '',
stock: ''
}))
} else if (valueArr.length === 3) {
result = cartesianProduct(valueArr).map(([v1, v2, v3]) => ({
spec0: v1,
spec1: v2,
spec2: v3,
price: '',
cost_price: '',
stock: ''
}))
}
skuTable.value = result
}
});
// 编辑器
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
const toolbarConfig = {}
const materialPickerRef = ref()
const showImagePicker = ref(false)
let imageInsertFn: ((url: string) => void) | null = null
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {
uploadImage: {
// 自定义上传图片
customBrowseAndUpload(insertFn: (url: string, alt: string, href: string) => void) {
showImagePicker.value = true
imageInsertFn = (url: string) => {
insertFn(url, '', '')
// 直接调用 material-picker 的打开方法
materialPickerRef.value?.open(-1)
}
}
}
}
}
// 图片选择后回调
function onImageSelected(val: string | string[]) {
const urls = Array.isArray(val) ? val : [val]
if (imageInsertFn && urls.length) {
urls.forEach(url => {
imageInsertFn!(url)
})
imageInsertFn = null
}
showImagePicker.value = false
}
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor // 记录 editor 实例,重要!
}
// 监听规格变化自动生成表格
watch(specs, updateSkuTable, { deep: true })
defineExpose({
open,
setFormData,
getDetail,
selectFirstCategory,
selectSecondCategory,
editorRef,
mode: 'default', // 或 'simple'
toolbarConfig,
editorConfig,
handleCreated,
})
</script>
<style lang="scss" scoped>
.sku_item {
&:hover {
.sku_item_delete {
opacity: 1;
}
}
.sku_item_delete {
opacity: 0;
}
}
</style>

178
src/views/goods/goods.vue Normal file
View File

@ -0,0 +1,178 @@
<template>
<div class="goods-sku" v-if="goods">
<dl v-for="item in goods.specs" :key="item.id">
<dt>{{ item.name }}</dt>
<dd>
<template v-for="val in item.values" :key="val.name">
<img v-if="val.picture" :class="{ selected: val.selected, disabled: val.disabled }"
@click="changeSelectedStatus(item, val)" :src="val.picture" :title="val.name" />
<span v-else :class="{ selected: val.selected, disabled: val.disabled }"
@click="changeSelectedStatus(item, val)">{{ val.name }}</span>
</template>
</dd>
</dl>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
import powerSet from "./power-set.js";
const goods = ref();
let pathMap = {};
const getGoods = async () => {
//1135076无库存规格
//1369155859933827074
const res = await axios.get(
"http://pcapi-xiaotuxian-front-devtest.itheima.net/goods?id=1369155859933827074"
);
goods.value = res.data.result;
pathMap = getPathMap(goods.value);
initDosabledStatus(goods.value.specs, pathMap);
};
onMounted(() => {
getGoods();
});
//切换选中状态
const changeSelectedStatus = (item, val) => {
if (val.disabled) {
return;
}
//item同一排对象,val当前点击项
if (val.selected) {
val.selected = false;
} else {
item.values.forEach((val) => (val.selected = false));
val.selected = true;
}
updateDisabledStatus(goods.value.specs, pathMap);
//产出sku对象
const index = getSelectedValues(goods.value.specs).findIndex(
(item) => item === undefined
);
if (index > -1) {
} else {
const key = getSelectedValues(goods.value.specs).join("-");
const skuIds = pathMap[key];
const skuObj = goods.value.skus.find((item) => item.id === skuIds[0]);
console.log("skuObj", skuObj);
}
};
//生成有效路径字典对象
const getPathMap = (goods) => {
const pathMap = {};
// 1.根据库存字段得到有效的sku数组
const effectiveSkus = goods.skus.filter((sku) => sku.inventory > 0);
// 2.根据有效的sku数组使用powerSet算法得到所有子集
effectiveSkus.forEach((sku) => {
//2.1获取匹配的valueName组成的数组
const selectedValArr = sku.specs.map((val) => val.valueName);
//2.2使用算法获取子集
const valueNamePowerSet = powerSet(selectedValArr);
// 3.根据子集生成路径字典对象
valueNamePowerSet.forEach((arr) => {
//初始化key
const key = arr.join("-");
//如果已经存在当前key就往数组中直接添加skuId如果不存在直接做赋值
if (pathMap[key]) {
pathMap[key].push(sku.id);
} else {
pathMap[key] = [sku.id];
}
});
});
return pathMap;
};
//初始化禁用状态
const initDosabledStatus = (specs, pathMap) => {
specs.forEach((spe) => {
spe.values.forEach((val) => {
if (pathMap[val.name]) {
val.disabled = false;
} else {
val.disabled = true;
}
});
});
};
//获取相中项的数组
const getSelectedValues = (specs) => {
const arr = [];
specs.forEach((spe) => {
//找到values中selected为true的项然后把它的name字段添加到对应的位置
const selectedVal = spe.values.find((item) => item.selected);
arr.push(selectedVal ? selectedVal.name : undefined);
});
return arr;
};
//切换时更新禁用状态
const updateDisabledStatus = (specs, pathMap) => {
specs.forEach((spe, index) => {
const selectedValues = getSelectedValues(specs);
spe.values.forEach((val) => {
selectedValues[index] = val.name;
const key = selectedValues.filter((value) => value).join("-");
console.log("key", key);
if (pathMap[key]) {
val.disabled = false;
} else {
val.disabled = true;
}
});
});
};
</script>
<style scoped lang="scss">
@mixin sku-state-mixin {
border: 1px solid #e4e4e4;
margin-right: 10px;
cursor: pointer;
&.selected {
border-color: #27ba9b;
}
&.disabled {
opacity: 0.6;
border-style: dashed;
cursor: not-allowed;
}
}
.goods-sku {
padding-left: 10px;
padding-top: 20px;
dl {
display: flex;
padding-bottom: 20px;
align-items: center;
dt {
width: 50px;
color: #999;
}
dd {
flex: 1;
color: #666;
>img {
width: 50px;
height: 50px;
margin-bottom: 4px;
@include sku-state-mixin;
}
>span {
display: inline-block;
height: 30px;
line-height: 28px;
padding: 0 20px;
margin-bottom: 4px;
@include sku-state-mixin;
}
}
}
}
</style>

View File

@ -106,7 +106,7 @@ const handleEdit = async (data: any) => {
showEdit.value = true
await nextTick()
editRef.value?.open('edit')
editRef.value?.setFormData(data)
editRef.value?.getDetail(data)
}
// 删除

View File

@ -0,0 +1,28 @@
export default function bwPowerSet(originalSet) {
const subSets = []
// We will have 2^n possible combinations (where n is a length of original set).
// It is because for every element of original set we will decide whether to include
// it or not (2 options for each set element).
const numberOfCombinations = 2 ** originalSet.length
// Each number in binary representation in a range from 0 to 2^n does exactly what we need:
// it shows by its bits (0 or 1) whether to include related element from the set or not.
// For example, for the set {1, 2, 3} the binary number of 0b010 would mean that we need to
// include only "2" to the current set.
for (let combinationIndex = 0; combinationIndex < numberOfCombinations; combinationIndex += 1) {
const subSet = []
for (let setElementIndex = 0; setElementIndex < originalSet.length; setElementIndex += 1) {
// Decide whether we need to include current element into the subset or not.
if (combinationIndex & (1 << setElementIndex)) {
subSet.push(originalSet[setElementIndex])
}
}
// Add current subset to the list of all subsets.
subSets.push(subSet)
}
return subSets
}

240
src/views/goods/sku.vue Normal file
View File

@ -0,0 +1,240 @@
<template>
<div>
<!-- 属性配置 -->
<el-button @click="addAttribute" type="success" size="mini">添加属性</el-button>
<el-table :data="attributes" border style="width: 100%">
<!-- <el-table-column prop="attributeName" label="属性名称">
<template v-slot="{ row }">
<el-input v-model="row.attributeValues" placeholder="用竖线(|)分隔,如:蓝色|白色|灰色"></el-input>
</template>
</el-table-column> -->
<el-table-column label="属性值">
<template v-slot="{ row }">
<el-input v-model="row.attributeValues" placeholder="用竖线(|)分隔,如:蓝色|白色|灰色"></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template v-slot="{ $index }">
<el-button size="mini" type="danger" @click="removeAttribute($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- SKU列表 -->
<el-button @click="handleMakeSku" size="mini" style="margin-top: 20px" type="warning">生成SKU</el-button>
<el-button @click="batchSetVisible = true" size="mini">批量设置</el-button>
<el-table :data="skus" border style="width: 100%;" @cell-change="handleSkuChange">
<el-table-column prop="spec_key" label="规格键名" header-align="center"></el-table-column>
<el-table-column prop="spec_name" label="规格名称" header-align="center"></el-table-column>
<el-table-column prop="store_count" label="库存" header-align="center">
<template v-slot="{ row }">
<el-input size="mini" v-model.number="row.stock" @input="handleSkuChange(row)"></el-input>
</template>
</el-table-column>
<el-table-column prop="price" label="销售价格" header-align="center">
<template v-slot="{ row }">
<el-input-number v-model="row.price" size="mini" controls-position="right" :precision="2"
:step="0.01" :min="0" @change="handleSkuChange(row)"></el-input-number>
</template>
</el-table-column>
<el-table-column prop="cost_price" label="成本价" header-align="center">
<template v-slot="{ row }">
<el-input-number v-model="row.cost_price" size="mini" controls-position="right" :precision="2"
:step="0.01" :min="0" @change="handleSkuChange(row)"></el-input-number>
</template>
</el-table-column>
<el-table-column label="操作" width="100" header-align="center">
<template v-slot="{ $index }">
<el-button size="mini" type="danger" @click="removeSku($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 批量设置 -->
<!-- <el-button @click="batchSetVisible = true">批量设置</el-button>-->
<!-- <el-dialog title="批量设置" :visible.sync="batchSetVisible">
<el-form :model="batchForm">
<el-form-item label="价格" :label-width="formLabelWidth">
<el-input v-model="batchForm.price"></el-input>
</el-form-item>
<el-form-item label="库存" :label-width="formLabelWidth">
<el-input v-model.number="batchForm.store_count"></el-input>
</el-form-item>
<el-form-item label="成本价" :label-width="formLabelWidth">
<el-input v-model="batchForm.cost_price"></el-input>
</el-form-item>
<el-form-item label="市场价" :label-width="formLabelWidth">
<el-input v-model="batchForm.market_price"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="batchSetVisible = false">取消</el-button>
<el-button type="primary" @click="applyBatchSet">确定</el-button>
</span>
</el-dialog> -->
</div>
</template>
<script>
export default {
props: {
initialSkus: {
type: Array,
default: () => []
}
},
data() {
return {
attributes: [], // 存储属性及其值
skus: [], // 存储生成的SKU列表
batchSetVisible: false, // 控制批量设置对话框显示状态
batchForm: {
price: '',
store_count: null,
cost_price: null,
market_price: null
},
formLabelWidth: '80px'
}
},
watch: {
skus: {
deep: true,
handler(newValue) {
this.$emit('sku-updated', newValue)
}
}
},
created() {
// 初始化时从props中获取已有SKU数据并解析出属性和SKU列表
if (this.initialSkus.length > 0) {
this.parseInitialSkus(this.initialSkus)
}
},
methods: {
addAttribute() {
this.attributes.push({ attributeName: '', attributeValues: '' })
},
removeAttribute(index) {
this.attributes.splice(index, 1)
},
handleMakeSku() {
console.log('handleMakeSku')
if (this.attributes.length === 0) {
this.$message.error('请先添加属性')
return
}
if (this.skus.length > 0) {
this.$confirm('已存在SKU是否覆盖', '操作提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.generateSkus()
}).catch(() => {
// 取消操作
})
} else {
this.generateSkus()
}
},
generateSkus() {
// 清空现有的SKU列表
this.skus = []
// 获取所有属性值并创建所有可能的组合
const combinations = this.createCombinations(this.attributes)
// 根据组合生成SKUs
combinations.forEach(combination => {
const specKey = combination.join('_')
const specName = combination.join(';')
this.skus.push({
spec_key: specKey,
spec_name: specName,
store_count: 0,
price: 0.00,
cost_price: 0.00,
market_price: 0.00
})
})
},
createCombinations(attributes) {
const valueLists = attributes.map(attr => attr.attributeValues.split('|').map(val => val.trim()))
const result = []
function combine(tempArray, index) {
if (index === valueLists.length) {
result.push(tempArray.slice())
return
}
for (let i = 0; i < valueLists[index].length; i++) {
tempArray[index] = valueLists[index][i]
combine(tempArray, index + 1)
}
}
combine([], 0)
return result
},
removeSku(index) {
this.skus.splice(index, 1)
},
applyBatchSet() {
const { price, store_count, cost_price, market_price } = this.batchForm
// 更新所有SKU对应的字段
this.skus.forEach(sku => {
if (price !== '') sku.price = parseFloat(price)
if (store_count !== null) sku.store_count = store_count
if (cost_price !== null) sku.cost_price = cost_price
if (market_price !== null) sku.market_price = market_price
})
// 关闭对话框
this.batchSetVisible = false
},
handleSkuChange(row) {
// 触发更新事件将最新的SKU数据传递给父组件
this.$emit('sku-updated', this.skus)
},
parseInitialSkus(initialSkus) {
// 创建一个映射来保存每个属性名称及其对应的值集合
const attributeMap = new Map()
// 遍历初始SKU数据提取所有属性和对应的值
initialSkus.forEach(sku => {
// 提取属性名称
const [color, size] = sku.spec_key.split('_')
// 提取属性值
const values = sku.spec_name.split(';')
// 添加属性值到集合中
values.forEach(value => {
if (!attributeMap.has(color)) {
attributeMap.set(color, [])
}
if (!attributeMap.has(size)) {
attributeMap.set(size, [])
}
attributeMap.get(color).push(value.trim())
attributeMap.get(size).push(value.trim())
})
})
// 设置初始SKU列表
this.skus = initialSkus
// 构建属性对象数组,并确保每个属性值是按规则分隔的字符串,fixme 这里有问题,不是和生成一致的
/* this.attributes = Array.from(attributeMap.entries()).map(([key, values]) => ({
attributeName: key,
// 将数组转换为字符串,使用' | '作为分隔符
attributeValues: values.sort().join(' | ')
})) */
}
}
}
</script>