Explorar o código

feat(web): 订单详情页 + 订单/商品管理完善

Sisyphus hai 4 meses
pai
achega
a1e12ceefa

+ 3 - 3
cfc-backend/.classpath

@@ -36,18 +36,18 @@
 			<attribute name="maven.pomderived" value="true"/>
 		</attributes>
 	</classpathentry>
-	<classpathentry kind="src" path="target/generated-sources/annotations">
+	<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
 		<attributes>
 			<attribute name="optional" value="true"/>
+			<attribute name="test" value="true"/>
 			<attribute name="maven.pomderived" value="true"/>
 			<attribute name="ignore_optional_problems" value="true"/>
 			<attribute name="m2e-apt" value="true"/>
 		</attributes>
 	</classpathentry>
-	<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
+	<classpathentry kind="src" path="target/generated-sources/annotations">
 		<attributes>
 			<attribute name="optional" value="true"/>
-			<attribute name="test" value="true"/>
 		</attributes>
 	</classpathentry>
 	<classpathentry kind="output" path="target/classes"/>

+ 488 - 1
cfc-web/package-lock.json

@@ -8,6 +8,8 @@
       "name": "cfc-web",
       "version": "1.0.0",
       "dependencies": {
+        "@wangeditor/editor": "^5.1.23",
+        "@wangeditor/editor-for-vue": "^1.0.2",
         "axios": "^1.6.0",
         "core-js": "^3.8.3",
         "echarts": "^6.1.0",
@@ -1564,6 +1566,14 @@
         "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
       }
     },
+    "node_modules/@babel/runtime": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+      "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@babel/template": {
       "version": "7.28.6",
       "dev": true,
@@ -2743,6 +2753,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@transloadit/prettier-bytes": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
+      "integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA=="
+    },
     "node_modules/@types/babel__core": {
       "version": "7.20.5",
       "dev": true,
@@ -2837,6 +2852,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@types/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ=="
+    },
     "node_modules/@types/express": {
       "version": "4.17.25",
       "dev": true,
@@ -3097,6 +3117,56 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@uppy/companion-client": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-2.2.2.tgz",
+      "integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
+      "dependencies": {
+        "@uppy/utils": "^4.1.2",
+        "namespace-emitter": "^2.0.1"
+      }
+    },
+    "node_modules/@uppy/core": {
+      "version": "2.3.4",
+      "resolved": "https://registry.npmjs.org/@uppy/core/-/core-2.3.4.tgz",
+      "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
+      "dependencies": {
+        "@transloadit/prettier-bytes": "0.0.7",
+        "@uppy/store-default": "^2.1.1",
+        "@uppy/utils": "^4.1.3",
+        "lodash.throttle": "^4.1.1",
+        "mime-match": "^1.0.2",
+        "namespace-emitter": "^2.0.1",
+        "nanoid": "^3.1.25",
+        "preact": "^10.5.13"
+      }
+    },
+    "node_modules/@uppy/store-default": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-2.1.1.tgz",
+      "integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ=="
+    },
+    "node_modules/@uppy/utils": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-4.1.3.tgz",
+      "integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
+      "dependencies": {
+        "lodash.throttle": "^4.1.1"
+      }
+    },
+    "node_modules/@uppy/xhr-upload": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
+      "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
+      "dependencies": {
+        "@uppy/companion-client": "^2.2.2",
+        "@uppy/utils": "^4.1.2",
+        "nanoid": "^3.1.25"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^2.3.3"
+      }
+    },
     "node_modules/@vue/cli-overlay": {
       "version": "5.0.9",
       "dev": true,
@@ -3401,6 +3471,156 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@wangeditor/basic-modules": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
+      "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
+      "dependencies": {
+        "is-url": "^1.2.4"
+      },
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "lodash.throttle": "^4.1.1",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/code-highlight": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
+      "integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
+      "dependencies": {
+        "prismjs": "^1.23.0"
+      },
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/core": {
+      "version": "1.1.19",
+      "resolved": "https://registry.npmjs.org/@wangeditor/core/-/core-1.1.19.tgz",
+      "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
+      "dependencies": {
+        "@types/event-emitter": "^0.3.3",
+        "event-emitter": "^0.3.5",
+        "html-void-elements": "^2.0.0",
+        "i18next": "^20.4.0",
+        "scroll-into-view-if-needed": "^2.2.28",
+        "slate-history": "^0.66.0"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^2.1.1",
+        "@uppy/xhr-upload": "^2.0.3",
+        "dom7": "^3.0.0",
+        "is-hotkey": "^0.2.0",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.debounce": "^4.0.8",
+        "lodash.foreach": "^4.5.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "lodash.toarray": "^4.4.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/editor": {
+      "version": "5.1.23",
+      "resolved": "https://registry.npmjs.org/@wangeditor/editor/-/editor-5.1.23.tgz",
+      "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
+      "dependencies": {
+        "@uppy/core": "^2.1.1",
+        "@uppy/xhr-upload": "^2.0.3",
+        "@wangeditor/basic-modules": "^1.1.7",
+        "@wangeditor/code-highlight": "^1.0.3",
+        "@wangeditor/core": "^1.1.19",
+        "@wangeditor/list-module": "^1.0.5",
+        "@wangeditor/table-module": "^1.1.4",
+        "@wangeditor/upload-image-module": "^1.0.2",
+        "@wangeditor/video-module": "^1.1.4",
+        "dom7": "^3.0.0",
+        "is-hotkey": "^0.2.0",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.debounce": "^4.0.8",
+        "lodash.foreach": "^4.5.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "lodash.toarray": "^4.4.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/editor-for-vue": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@wangeditor/editor-for-vue/-/editor-for-vue-1.0.2.tgz",
+      "integrity": "sha512-BOENvAXJVtVXlE2X50AAvjV82YlCUeu5cbeR0cvEQHQjYtiVnJtq7HSoj85r2kTgGouI5OrpJG9BBEjSjUSPyA==",
+      "peerDependencies": {
+        "@wangeditor/editor": ">=5.1.0",
+        "vue": "^2.6.14"
+      }
+    },
+    "node_modules/@wangeditor/list-module": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/@wangeditor/list-module/-/list-module-1.0.5.tgz",
+      "integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/table-module": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@wangeditor/table-module/-/table-module-1.1.4.tgz",
+      "integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/upload-image-module": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
+      "integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
+      "peerDependencies": {
+        "@uppy/core": "^2.0.3",
+        "@uppy/xhr-upload": "^2.0.3",
+        "@wangeditor/basic-modules": "1.x",
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "lodash.foreach": "^4.5.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/video-module": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@wangeditor/video-module/-/video-module-1.1.4.tgz",
+      "integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
+      "peerDependencies": {
+        "@uppy/core": "^2.1.4",
+        "@uppy/xhr-upload": "^2.0.7",
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
     "node_modules/@webassemblyjs/ast": {
       "version": "1.14.1",
       "dev": true,
@@ -4848,6 +5068,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/compute-scroll-into-view": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+      "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
+    },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "dev": true,
@@ -5359,6 +5584,18 @@
       "version": "3.2.3",
       "license": "MIT"
     },
+    "node_modules/d": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+      "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+      "dependencies": {
+        "es5-ext": "^0.10.64",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.12"
+      }
+    },
     "node_modules/data-urls": {
       "version": "5.0.0",
       "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-5.0.0.tgz",
@@ -5724,6 +5961,14 @@
         "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
       }
     },
+    "node_modules/dom7": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz",
+      "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
+      "dependencies": {
+        "ssr-window": "^3.0.0-alpha.1"
+      }
+    },
     "node_modules/domelementtype": {
       "version": "2.3.0",
       "dev": true,
@@ -6031,6 +6276,43 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/es5-ext": {
+      "version": "0.10.64",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+      "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "es6-iterator": "^2.0.3",
+        "es6-symbol": "^3.1.3",
+        "esniff": "^2.0.1",
+        "next-tick": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+      "dependencies": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "node_modules/es6-symbol": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+      "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+      "dependencies": {
+        "d": "^1.0.2",
+        "ext": "^1.7.0"
+      },
+      "engines": {
+        "node": ">=0.12"
+      }
+    },
     "node_modules/escalade": {
       "version": "3.2.0",
       "dev": true,
@@ -6064,6 +6346,20 @@
         "node": ">=8.0.0"
       }
     },
+    "node_modules/esniff": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+      "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+      "dependencies": {
+        "d": "^1.0.1",
+        "es5-ext": "^0.10.62",
+        "event-emitter": "^0.3.5",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/esprima": {
       "version": "4.0.1",
       "dev": true,
@@ -6119,6 +6415,15 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+      "dependencies": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
     "node_modules/event-pubsub": {
       "version": "4.3.0",
       "dev": true,
@@ -6237,6 +6542,14 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/ext": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+      "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+      "dependencies": {
+        "type": "^2.7.2"
+      }
+    },
     "node_modules/extend-shallow": {
       "version": "2.0.1",
       "dev": true,
@@ -6966,6 +7279,15 @@
         "node": ">=12"
       }
     },
+    "node_modules/html-void-elements": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
+      "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
     "node_modules/html-webpack-plugin": {
       "version": "5.6.7",
       "dev": true,
@@ -7116,6 +7438,14 @@
         "node": ">=10.17.0"
       }
     },
+    "node_modules/i18next": {
+      "version": "20.6.1",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz",
+      "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
+      "dependencies": {
+        "@babel/runtime": "^7.12.0"
+      }
+    },
     "node_modules/iconv-lite": {
       "version": "0.4.24",
       "dev": true,
@@ -7165,6 +7495,15 @@
         "node": ">= 4"
       }
     },
+    "node_modules/immer": {
+      "version": "9.0.21",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
+      "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/immer"
+      }
+    },
     "node_modules/import-fresh": {
       "version": "3.3.1",
       "dev": true,
@@ -7366,6 +7705,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-hotkey": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz",
+      "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="
+    },
     "node_modules/is-interactive": {
       "version": "1.0.0",
       "dev": true,
@@ -7430,6 +7774,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/is-url": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
+      "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
+    },
     "node_modules/is-whitespace": {
       "version": "0.3.0",
       "dev": true,
@@ -9232,9 +9581,18 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
+    },
+    "node_modules/lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
+    },
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/lodash.defaultsdeep": {
@@ -9242,6 +9600,17 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lodash.foreach": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
+      "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ=="
+    },
+    "node_modules/lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+      "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
+    },
     "node_modules/lodash.mapvalues": {
       "version": "4.6.0",
       "dev": true,
@@ -9252,6 +9621,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lodash.throttle": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+      "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
+    },
+    "node_modules/lodash.toarray": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
+      "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw=="
+    },
     "node_modules/lodash.uniq": {
       "version": "4.5.0",
       "dev": true,
@@ -9559,6 +9938,19 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/mime-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz",
+      "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
+      "dependencies": {
+        "wildcard": "^1.1.0"
+      }
+    },
+    "node_modules/mime-match/node_modules/wildcard": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz",
+      "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng=="
+    },
     "node_modules/mime-types": {
       "version": "2.1.35",
       "license": "MIT",
@@ -9725,6 +10117,11 @@
         "thenify-all": "^1.0.0"
       }
     },
+    "node_modules/namespace-emitter": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
+      "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g=="
+    },
     "node_modules/nanoid": {
       "version": "3.3.12",
       "funding": [
@@ -9759,6 +10156,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/next-tick": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+      "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+    },
     "node_modules/nice-try": {
       "version": "1.0.5",
       "dev": true,
@@ -10848,6 +11250,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/preact": {
+      "version": "10.29.4",
+      "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.4.tgz",
+      "integrity": "sha512-GMpwh9+NJ8tSmqwIaVyFRQkiKfBEzQ+k7r7tle4W+kaJ+7wJiB9hFz9BixAomMtenPPSBfM4bZhXozGxhf0uFQ==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/preact"
+      }
+    },
     "node_modules/prettier": {
       "version": "2.8.8",
       "license": "MIT",
@@ -10908,6 +11319,14 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/prismjs": {
+      "version": "1.30.0",
+      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+      "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/process-nextick-args": {
       "version": "2.0.1",
       "dev": true,
@@ -11503,6 +11922,14 @@
         "url": "https://opencollective.com/webpack"
       }
     },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "2.2.31",
+      "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
+      "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
+      "dependencies": {
+        "compute-scroll-into-view": "^1.0.20"
+      }
+    },
     "node_modules/select-hose": {
       "version": "2.0.0",
       "dev": true,
@@ -11796,6 +12223,51 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/slate": {
+      "version": "0.72.8",
+      "resolved": "https://registry.npmjs.org/slate/-/slate-0.72.8.tgz",
+      "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
+      "dependencies": {
+        "immer": "^9.0.6",
+        "is-plain-object": "^5.0.0",
+        "tiny-warning": "^1.0.3"
+      }
+    },
+    "node_modules/slate-history": {
+      "version": "0.66.0",
+      "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.66.0.tgz",
+      "integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
+      "dependencies": {
+        "is-plain-object": "^5.0.0"
+      },
+      "peerDependencies": {
+        "slate": ">=0.65.3"
+      }
+    },
+    "node_modules/slate-history/node_modules/is-plain-object": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/slate/node_modules/is-plain-object": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/snabbdom": {
+      "version": "3.6.4",
+      "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.6.4.tgz",
+      "integrity": "sha512-VmxEfuw1/Y/eFj5VtMhYnukExpYiPkNzoo3+N3qwAOUDMl8wXgbli5ebR+j0knE3lZ/0eYskLxNcX64uy10N9w==",
+      "engines": {
+        "node": ">=12.17.0"
+      }
+    },
     "node_modules/sockjs": {
       "version": "0.3.24",
       "dev": true,
@@ -11898,6 +12370,11 @@
       "dev": true,
       "license": "BSD-3-Clause"
     },
+    "node_modules/ssr-window": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-3.0.0.tgz",
+      "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA=="
+    },
     "node_modules/ssri": {
       "version": "8.0.1",
       "dev": true,
@@ -12353,6 +12830,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/tiny-warning": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+    },
     "node_modules/tldts": {
       "version": "6.1.86",
       "resolved": "https://registry.npmmirror.com/tldts/-/tldts-6.1.86.tgz",
@@ -12463,6 +12945,11 @@
       "dev": true,
       "license": "0BSD"
     },
+    "node_modules/type": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
+      "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
+    },
     "node_modules/type-detect": {
       "version": "4.0.8",
       "dev": true,

+ 2 - 0
cfc-web/package.json

@@ -10,6 +10,8 @@
     "test:coverage": "jest --coverage"
   },
   "dependencies": {
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "^1.0.2",
     "axios": "^1.6.0",
     "core-js": "^3.8.3",
     "echarts": "^6.1.0",

+ 36 - 4
cfc-web/src/api/admin.js

@@ -551,17 +551,49 @@ export function toggleCategory(id) {
 
 export function getOrderList(params) {
   return request({
-    url: '/api/admin/order/list',
+    url: '/api/admin/product/order/list',
     method: 'post',
     data: params
   })
 }
 
-export function getOrderDetail(id) {
+export function getOrderDetail(orderNo) {
   return request({
-    url: `/api/product/order/detail`,
+    url: '/api/admin/product/order/detail',
     method: 'post',
-    data: { orderId: id }
+    data: { orderNo }
+  })
+}
+
+export function shipOrder(orderNo, trackingNumber, expressCompany) {
+  return request({
+    url: '/api/admin/product/order/ship',
+    method: 'post',
+    data: { orderNo, trackingNumber, expressCompany }
+  })
+}
+
+export function closeOrder(orderNo) {
+  return request({
+    url: '/api/admin/product/order/close',
+    method: 'post',
+    data: { orderNo }
+  })
+}
+
+export function approveRefund(orderNo) {
+  return request({
+    url: '/api/admin/product/order/refund/approve',
+    method: 'post',
+    data: { orderNo }
+  })
+}
+
+export function rejectRefund(orderNo, reason) {
+  return request({
+    url: '/api/admin/product/order/refund/reject',
+    method: 'post',
+    data: { orderNo, reason }
   })
 }
 

+ 3 - 0
cfc-web/src/main.js

@@ -10,6 +10,7 @@ import {
   Menu, Submenu, MenuItem, Empty, Drawer, Timeline, TimelineItem,
   Progress, Container, Header, Aside, Main, Avatar, Scrollbar, Divider,
   Tab, Badge, Tree, ColorPicker, Tooltip, Popover,
+  Slider, InputNumber,
   MessageBox
 } from 'element-ui'
 import 'element-ui/lib/theme-chalk/index.css'
@@ -64,6 +65,8 @@ Vue.use(Tree)
 Vue.use(ColorPicker)
 Vue.use(Tooltip)
 Vue.use(Popover)
+Vue.use(Slider)
+Vue.use(InputNumber)
 // MessageBox 没有 install 方法,Vue.use(MessageBox) ——这是错误的,不会注册 $confirm
 // 需要手动挂载到 prototype 上
 Vue.prototype.$confirm = MessageBox.confirm

+ 236 - 0
cfc-web/src/views/admin/OrderDetail.vue

@@ -0,0 +1,236 @@
+<template>
+  <div class="order-detail" v-loading="loading">
+    <div class="page-header">
+      <el-button type="text" @click="$router.push('/order-manage')">&lt; 返回订单管理</el-button>
+      <h2>订单详情</h2>
+    </div>
+
+    <template v-if="order">
+      <el-card style="margin-bottom:20px;">
+        <div slot="header">
+          <span>基本信息</span>
+          <el-tag :type="statusType(order.status)" style="margin-left:10px;">{{ statusLabel(order.status) }}</el-tag>
+        </div>
+        <el-descriptions :column="3" border>
+          <el-descriptions-item label="订单编号">{{ order.orderNo }}</el-descriptions-item>
+          <el-descriptions-item label="商品名称">{{ order.productName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="商品类型">{{ typeLabel(order.productType) }}</el-descriptions-item>
+          <el-descriptions-item label="数量">{{ order.quantity }}</el-descriptions-item>
+          <el-descriptions-item label="单价">{{ formatPrice(order.unitPrice) }}</el-descriptions-item>
+          <el-descriptions-item label="总金额">{{ formatPrice(order.totalAmount) }}</el-descriptions-item>
+          <el-descriptions-item label="优惠金额">{{ order.discountAmount ? formatPrice(order.discountAmount) : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="支付方式">{{ payMethodLabel(order.paymentMethod) }}</el-descriptions-item>
+          <el-descriptions-item label="支付时间">{{ order.paidAt ? formatTime(order.paidAt) : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="下单时间">{{ formatTime(order.createdAt) }}</el-descriptions-item>
+          <el-descriptions-item label="买家备注">{{ order.remark || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="交易号">{{ order.transactionId || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </el-card>
+
+      <el-card style="margin-bottom:20px;">
+        <div slot="header"><span>收货信息</span></div>
+        <el-descriptions :column="3" border>
+          <el-descriptions-item label="配送方式">{{ order.deliveryMethod == 2 ? '自提' : '快递' }}</el-descriptions-item>
+          <el-descriptions-item label="自提码">{{ order.pickupCode || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="收货地址">{{ order.addressSnapshot || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="物流公司">{{ order.logisticsCompany || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="物流单号">{{ order.logisticsNo || order.trackingNumber || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </el-card>
+
+      <el-card v-if="order.refundStatus" style="margin-bottom:20px;">
+        <div slot="header"><span>退款信息</span></div>
+        <el-descriptions :column="3" border>
+          <el-descriptions-item label="退款状态">{{ refundStatusLabel(order.refundStatus) }}</el-descriptions-item>
+          <el-descriptions-item label="退款原因">{{ order.refundReason || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="退款金额">{{ order.refundAmount ? formatPrice(order.refundAmount) : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="退款时间">{{ order.refundTime ? formatTime(order.refundTime) : '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </el-card>
+
+      <div style="margin-top:20px;">
+        <el-button type="success" @click="handleShip" v-if="order.status === 'paid'">发货</el-button>
+        <el-button type="danger" @click="handleClose" v-if="['paid','pending_receipt','refunding'].includes(order.status)">关闭订单</el-button>
+        <el-button type="warning" @click="showRefundDialog" v-if="order.status === 'refunding'">退款审核</el-button>
+      </div>
+    </template>
+
+    <div v-else-if="!loading" style="text-align:center;padding:60px;color:#999;">
+      <p>未找到订单信息</p>
+    </div>
+
+    <!-- Ship Dialog -->
+    <el-dialog title="发货" :visible.sync="shipDialogVisible" width="450px">
+      <el-alert title="确认发货" type="info" :closable="false" style="margin-bottom:15px;">
+        <template slot>订单号:<strong>{{ order?.orderNo }}</strong></template>
+      </el-alert>
+      <el-form :model="shipForm" label-width="90px">
+        <el-form-item label="物流公司">
+          <el-input v-model="shipForm.expressCompany" placeholder="如:顺丰、中通" />
+        </el-form-item>
+        <el-form-item label="物流单号">
+          <el-input v-model="shipForm.trackingNumber" placeholder="请输入物流单号" />
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="shipDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmShip" :loading="shipLoading">确认发货</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- Refund Dialog -->
+    <el-dialog title="退款审核" :visible.sync="refundDialogVisible" width="450px">
+      <el-alert title="退款申请" type="warning" :closable="false" style="margin-bottom:15px;">
+        <template slot>
+          订单号:<strong>{{ order?.orderNo }}</strong><br>
+          退款原因:{{ order?.refundReason || '-' }}
+        </template>
+      </el-alert>
+      <el-input v-model="refundRejectReason" type="textarea" placeholder="拒绝原因(选填)" :rows="3" />
+      <span slot="footer">
+        <el-button @click="refundDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleRejectRefund" :loading="refundLoading">拒绝退款</el-button>
+        <el-button type="success" @click="handleApproveRefundConfirm" :loading="refundLoading">同意退款</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getOrderDetail, shipOrder, closeOrder, approveRefund, rejectRefund } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      order: null,
+      loading: false,
+      shipDialogVisible: false,
+      shipLoading: false,
+      shipForm: { expressCompany: '', trackingNumber: '' },
+      refundDialogVisible: false,
+      refundLoading: false,
+      refundRejectReason: ''
+    }
+  },
+  mounted() {
+    this.loadOrder()
+  },
+  methods: {
+    async loadOrder() {
+      var orderNo = this.$route.params.orderNo
+      if (!orderNo) return
+      this.loading = true
+      try {
+        var res = await getOrderDetail(orderNo)
+        this.order = res.data
+      } catch (e) {
+        this.$message.error('加载订单详情失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleShip() {
+      this.shipForm = { expressCompany: '', trackingNumber: '' }
+      this.shipDialogVisible = true
+    },
+    async confirmShip() {
+      if (!this.shipForm.trackingNumber.trim()) {
+        return this.$message.warning('请填写物流单号')
+      }
+      this.shipLoading = true
+      try {
+        await shipOrder(this.order.orderNo, this.shipForm.trackingNumber, this.shipForm.expressCompany)
+        this.$message.success('发货成功')
+        this.shipDialogVisible = false
+        this.loadOrder()
+      } catch (e) {
+        this.$message.error(e.message || '发货失败')
+      } finally {
+        this.shipLoading = false
+      }
+    },
+    async handleClose() {
+      try {
+        await this.$confirm('确定关闭订单 ' + this.order.orderNo + '?', '提示', { type: 'warning' })
+        await closeOrder(this.order.orderNo)
+        this.$message.success('订单已关闭')
+        this.loadOrder()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error(e.message || '关闭失败')
+        }
+      }
+    },
+    showRefundDialog() {
+      this.refundRejectReason = ''
+      this.refundDialogVisible = true
+    },
+    async handleApproveRefundConfirm() {
+      this.refundLoading = true
+      try {
+        await this.$confirm('确认同意退款?', '提示', { type: 'warning' })
+        await approveRefund(this.order.orderNo)
+        this.$message.success('已同意退款')
+        this.refundDialogVisible = false
+        this.loadOrder()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error(e.message || '操作失败')
+        }
+      } finally {
+        this.refundLoading = false
+      }
+    },
+    async handleRejectRefund() {
+      this.refundLoading = true
+      try {
+        await this.$confirm('确认拒绝退款?', '提示', { type: 'warning' })
+        await rejectRefund(this.order.orderNo, this.refundRejectReason)
+        this.$message.success('已拒绝退款')
+        this.refundDialogVisible = false
+        this.loadOrder()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error(e.message || '操作失败')
+        }
+      } finally {
+        this.refundLoading = false
+      }
+    },
+    statusType(status) {
+      var map = { pending: 'warning', paid: 'primary', pending_receipt: '', completed: 'success', cancelled: 'info', refunding: 'danger', refunded: 'danger', closed: 'info' }
+      return map[status] || 'info'
+    },
+    statusLabel(status) {
+      var map = { pending: '待支付', paid: '待发货', pending_receipt: '待收货', completed: '已完成', cancelled: '已取消', refunding: '退款中', refunded: '已退款', closed: '已关闭' }
+      return map[status] || status
+    },
+    payMethodLabel(method) {
+      var map = { wechat: '微信支付', balance: '余额支付', points: '积分兑换', test: '测试支付' }
+      return map[method] || method || '-'
+    },
+    typeLabel(type) {
+      var map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
+      return map[type] || type || '-'
+    },
+    refundStatusLabel(rs) {
+      var map = { 0: '无退款', 1: '申请中', 2: '已退款', 3: '已拒绝' }
+      return map[rs] || rs || '-'
+    },
+    formatPrice(cents) {
+      if (cents == null) return '-'
+      return '¥' + (cents / 100).toFixed(2)
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      return new Date(time).toLocaleString()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.order-detail { padding: 20px; }
+.page-header { margin-bottom: 20px; }
+.page-header h2 { margin: 10px 0 0; }
+</style>

+ 176 - 101
cfc-web/src/views/admin/OrderManage.vue

@@ -3,43 +3,34 @@
     <div class="header">
       <h2>订单管理</h2>
       <div class="filters">
-        <el-radio-group v-model="sourceTab" size="small" @change="onSourceChange" style="margin-right: 10px;">
-          </el-radio-group>
-        <el-select v-model="statusFilter" placeholder="订单状态" @change="loadList" style="width: 150px;">
-          <el-option label="全部" value="" />
+        <el-select v-model="statusFilter" placeholder="订单状态" @change="loadList" style="width: 180px;">
+          <el-option label="全部状态" value="" />
           <el-option label="待支付" value="pending" />
-          <el-option label="已支付" value="paid" />
+          <el-option label="已支付(待发货)" value="paid" />
+          <el-option label="待收货" value="pending_receipt" />
           <el-option label="已完成" value="completed" />
           <el-option label="已取消" value="cancelled" />
+          <el-option label="退款中" value="refunding" />
           <el-option label="已退款" value="refunded" />
+          <el-option label="已关闭" value="closed" />
         </el-select>
       </div>
     </div>
 
     <el-table :data="list" v-loading="loading" border stripe>
-      <el-table-column prop="id" label="订单ID" width="80" />
-      <el-table-column label="商品" min-width="180">
+      <el-table-column prop="orderNo" label="订单号" width="180" />
+      <el-table-column label="商品" min-width="160">
         <template slot-scope="{ row }">
-          <div v-if="row.productName">{{ row.productName }}</div>
-          <div v-else-if="row.items && row.items.length">
-            <span v-for="(item, idx) in row.items" :key="idx">{{ item.productName }} x{{ item.quantity }}<span v-if="idx < row.items.length - 1">,</span></span>
+          <div style="display:flex;align-items:center;gap:8px;">
+            <img v-if="row.coverImage" :src="row.coverImage" style="width:40px;height:40px;object-fit:cover;border-radius:4px;" />
+            <span>{{ row.productName || '-' }}</span>
           </div>
-          <div v-else>-</div>
         </template>
       </el-table-column>
-      <el-table-column label="买家" width="100">
+      <el-table-column prop="quantity" label="数量" width="60" align="center" />
+      <el-table-column label="金额" width="100">
         <template slot-scope="{ row }">
-          {{ row.buyerNickname || row.nickname || '-' }}
-        </template>
-      </el-table-column>
-      <el-table-column label="供应商" width="100">
-        <template slot-scope="{ row }">
-          {{ row.vendorName || '-' }}
-        </template>
-      </el-table-column>
-      <el-table-column label="订单金额" width="100">
-        <template slot-scope="{ row }">
-          {{ formatPriceWithSymbol(row.amount || 0) }}
+          {{ formatPrice(row.totalAmount) }}
         </template>
       </el-table-column>
       <el-table-column label="状态" width="100">
@@ -47,69 +38,75 @@
           <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="支付方式" width="100">
+      <el-table-column prop="paymentMethod" label="支付方式" width="100">
         <template slot-scope="{ row }">
-          {{ payMethodLabel(row.payMethod) }}
+          {{ payMethodLabel(row.paymentMethod) }}
         </template>
       </el-table-column>
-      <el-table-column prop="createTime" label="下单时间" width="160">
+      <el-table-column label="下单时间" width="155">
         <template slot-scope="{ row }">
-          {{ formatTime(row.createTime || row.createdAt) }}
+          {{ formatTime(row.createdAt) }}
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="200" fixed="right">
+      <el-table-column label="操作" width="300" fixed="right">
         <template slot-scope="{ row }">
-          <el-button type="info" size="small" @click="viewDetail(row)">详情</el-button>
-          
+          <el-button type="primary" size="small" @click="viewDetail(row)">详情</el-button>
+          <el-button type="success" size="small" @click="showShipDialog(row)" v-if="row.status === 'paid'">发货</el-button>
+          <el-button type="danger" size="small" @click="handleClose(row)" v-if="row.status === 'paid' || row.status === 'refunding'">关闭</el-button>
+          <el-button type="warning" size="small" @click="showRefundDialog(row)" v-if="row.status === 'refunding'">退款审核</el-button>
         </template>
       </el-table-column>
     </el-table>
 
     <el-pagination
-      @size-change="onPageChange"
+      @size-change="onSizeChange"
       @current-change="onPageChange"
       :current-page="page"
       :page-size="size"
       :total="total"
-      layout="total, prev, pager, next"
+      layout="total, sizes, prev, pager, next"
+      :page-sizes="[10, 20, 50]"
       style="margin-top: 20px; text-align: right;"
     />
 
-    <el-dialog title="订单详情" :visible.sync="detailDialogVisible" width="600px">
-      <el-descriptions :column="2" border v-if="detail">
-        <el-descriptions-item label="订单ID">{{ detail.id }}</el-descriptions-item>
-        <el-descriptions-item label="订单编号">{{ detail.orderNo || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="商品名称">{{ detail.productName || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
-        <el-descriptions-item label="订单金额">{{ formatPriceWithSymbol(detail.amount || 0) }}</el-descriptions-item>
-        <el-descriptions-item label="实际支付">{{ formatPriceWithSymbol(detail.paidAmount || 0) }}</el-descriptions-item>
-        <el-descriptions-item label="买家昵称">{{ detail.buyerNickname || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="买家手机">{{ detail.buyerPhone || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="供应商">{{ detail.vendorName || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="供应商手机">{{ detail.vendorPhone || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="订单状态">
-          <el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="支付方式">{{ payMethodLabel(detail.payMethod) }}</el-descriptions-item>
-        <el-descriptions-item label="收货人">{{ detail.receiverName || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="收货电话">{{ detail.receiverPhone || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="收货地址" :span="2">{{ detail.shippingAddress || detail.receiverAddress || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="支付时间" :span="2">{{ detail.paidTime || detail.paymentTime ? formatTime(detail.paidTime || detail.paymentTime) : '-' }}</el-descriptions-item>
-        <el-descriptions-item label="买家留言">{{ detail.buyerNote || detail.remark || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="下单时间">{{ formatTime(detail.createTime || detail.createdAt) }}</el-descriptions-item>
-      </el-descriptions>
+    <!-- Detail Dialog -->
+    <el-dialog title="订单详情" :visible.sync="detailDialogVisible" width="700px" top="5vh">
+      <template v-if="detail">
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="订单编号" :span="2">{{ detail.orderNo }}</el-descriptions-item>
+          <el-descriptions-item label="商品名称">{{ detail.productName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
+          <el-descriptions-item label="数量">{{ detail.quantity }}</el-descriptions-item>
+          <el-descriptions-item label="单价">{{ formatPrice(detail.unitPrice) }}</el-descriptions-item>
+          <el-descriptions-item label="总金额">{{ formatPrice(detail.totalAmount) }}</el-descriptions-item>
+          <el-descriptions-item label="优惠金额">{{ detail.discountAmount ? formatPrice(detail.discountAmount) : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="订单状态">
+            <el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="支付方式">{{ payMethodLabel(detail.paymentMethod) }}</el-descriptions-item>
+          <el-descriptions-item label="支付时间">{{ detail.paidAt ? formatTime(detail.paidAt) : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="买家备注">{{ detail.remark || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="下单时间">{{ formatTime(detail.createdAt) }}</el-descriptions-item>
+        </el-descriptions>
+      </template>
       <span slot="footer">
         <el-button @click="detailDialogVisible = false">关闭</el-button>
       </span>
     </el-dialog>
 
+    <!-- Ship Dialog -->
     <el-dialog title="发货" :visible.sync="shipDialogVisible" width="450px">
-      <el-form :model="shipForm" label-width="100px">
+      <el-alert title="确认给以下订单发货" type="info" :closable="false" style="margin-bottom:15px;">
+        <template slot>
+          订单号:<strong>{{ shipTarget?.orderNo }}</strong>
+        </template>
+      </el-alert>
+      <el-form :model="shipForm" label-width="90px">
         <el-form-item label="物流公司">
-          <el-input v-model="shipForm.company" placeholder="如:顺丰、中通" />
+          <el-input v-model="shipForm.expressCompany" placeholder="如:顺丰、中通" />
         </el-form-item>
         <el-form-item label="物流单号">
-          <el-input v-model="shipForm.trackingNo" placeholder="请输入物流单号" />
+          <el-input v-model="shipForm.trackingNumber" placeholder="请输入物流单号" />
         </el-form-item>
       </el-form>
       <span slot="footer">
@@ -117,16 +114,33 @@
         <el-button type="primary" @click="confirmShip" :loading="shipLoading">确认发货</el-button>
       </span>
     </el-dialog>
+
+    <!-- Refund Dialog -->
+    <el-dialog title="退款审核" :visible.sync="refundDialogVisible" width="450px">
+      <template v-if="refundTarget">
+        <el-alert title="退款申请" type="warning" :closable="false" style="margin-bottom:15px;">
+          <template slot>
+            订单号:<strong>{{ refundTarget.orderNo }}</strong><br>
+            退款原因:{{ refundTarget.refundReason || '-' }}
+          </template>
+        </el-alert>
+        <el-input v-model="refundRejectReason" type="textarea" placeholder="拒绝原因(选填)" :rows="3" />
+      </template>
+      <span slot="footer">
+        <el-button @click="refundDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleRejectRefund" :loading="refundLoading">拒绝退款</el-button>
+        <el-button type="success" @click="handleApproveRefundConfirm" :loading="refundLoading">同意退款</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { getOrderList } from '@/api/admin'
+import { getOrderList, shipOrder, closeOrder, approveRefund, rejectRefund } from '@/api/admin'
 
 export default {
   data() {
     return {
-      sourceTab: 'cfc',
       list: [],
       loading: false,
       page: 1,
@@ -138,27 +152,31 @@ export default {
       shipDialogVisible: false,
       shipTarget: null,
       shipLoading: false,
-      shipForm: { company: '', trackingNo: '' }
+      shipForm: { expressCompany: '', trackingNumber: '' },
+      refundDialogVisible: false,
+      refundTarget: null,
+      refundLoading: false,
+      refundRejectReason: ''
     }
   },
   mounted() {
     this.loadList()
   },
   methods: {
-    onSourceChange() {
-      this.page = 1
-      this.list = []
-      this.statusFilter = ''
-      this.loadList()
-    },
     async loadList() {
       this.loading = true
       try {
-        var res = await getOrderList({ status: this.statusFilter })
-        this.list = res.data || []
-        this.total = (res.data || []).length
+        var params = { page: this.page, size: this.size }
+        if (this.statusFilter) {
+          params.status = this.statusFilter
+        }
+        var res = await getOrderList(params)
+        this.list = res.data.records || []
+        this.total = res.data.total || 0
+        this.page = res.data.page || this.page
+        this.size = res.data.size || this.size
       } catch (e) {
-        this.$message.error('加载失败')
+        this.$message.error('加载订单列表失败')
       } finally {
         this.loading = false
       }
@@ -167,25 +185,10 @@ export default {
       this.page = val
       this.loadList()
     },
-    statusType(status) {
-      var map = { pending: 'warning', paid: 'primary', completed: 'success', cancelled: 'info', refunded: 'danger' }
-      return map[status] || 'info'
-    },
-    statusLabel(status) {
-      var map = { pending: '待支付', paid: '已支付', completed: '已完成', cancelled: '已取消', refunded: '已退款' }
-      return map[status] || status
-    },
-    payMethodLabel(method) {
-      var map = { wechat: '微信支付', balance: '余额支付', points: '积分兑换', test: '测试支付' }
-      return map[method] || method || '-'
-    },
-    typeLabel(type) {
-      var map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
-      return map[type] || type || '-'
-    },
-    formatTime(time) {
-      if (!time) return '-'
-      return new Date(time).toLocaleString()
+    onSizeChange(val) {
+      this.size = val
+      this.page = 1
+      this.loadList()
     },
     viewDetail(row) {
       this.detail = row
@@ -193,33 +196,107 @@ export default {
     },
     showShipDialog(row) {
       this.shipTarget = row
-      this.shipForm.company = ''
-      this.shipForm.trackingNo = ''
+      this.shipForm = { expressCompany: '', trackingNumber: '' }
       this.shipDialogVisible = true
     },
     async confirmShip() {
-      if (!this.shipForm.company.trim() || !this.shipForm.trackingNo.trim()) {
-        return this.$message.warning('请填写物流公司和物流单号')
+      if (!this.shipForm.trackingNumber.trim()) {
+        return this.$message.warning('请填写物流单号')
       }
       this.shipLoading = true
       try {
+        await shipOrder(this.shipTarget.orderNo, this.shipForm.trackingNumber, this.shipForm.expressCompany)
         this.$message.success('发货成功')
         this.shipDialogVisible = false
         this.loadList()
       } catch (e) {
-        this.$message.error('发货失败')
+        this.$message.error(e.message || '发货失败')
       } finally {
         this.shipLoading = false
       }
+    },
+    async handleClose(row) {
+      try {
+        await this.$confirm('确定关闭订单 ' + row.orderNo + '?', '提示', { type: 'warning' })
+        await closeOrder(row.orderNo)
+        this.$message.success('订单已关闭')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error(e.message || '关闭失败')
+        }
+      }
+    },
+    showRefundDialog(row) {
+      this.refundTarget = row
+      this.refundRejectReason = ''
+      this.refundDialogVisible = true
+    },
+    async handleApproveRefundConfirm() {
+      this.refundLoading = true
+      try {
+        await this.$confirm('确认同意退款?', '提示', { type: 'warning' })
+        await approveRefund(this.refundTarget.orderNo)
+        this.$message.success('已同意退款')
+        this.refundDialogVisible = false
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error(e.message || '操作失败')
+        }
+      } finally {
+        this.refundLoading = false
+      }
+    },
+    async handleRejectRefund() {
+      this.refundLoading = true
+      try {
+        await this.$confirm('确认拒绝退款?', '提示', { type: 'warning' })
+        await rejectRefund(this.refundTarget.orderNo, this.refundRejectReason)
+        this.$message.success('已拒绝退款')
+        this.refundDialogVisible = false
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error(e.message || '操作失败')
+        }
+      } finally {
+        this.refundLoading = false
+      }
+    },
+    canClose(status) {
+      return ['pending', 'paid', 'pending_receipt', 'refunding'].includes(status)
+    },
+    statusType(status) {
+      var map = { pending: 'warning', paid: 'primary', pending_receipt: '', completed: 'success', cancelled: 'info', refunding: 'danger', refunded: 'danger', closed: 'info' }
+      return map[status] || 'info'
+    },
+    statusLabel(status) {
+      var map = { pending: '待支付', paid: '待发货', pending_receipt: '待收货', completed: '已完成', cancelled: '已取消', refunding: '退款中', refunded: '已退款', closed: '已关闭' }
+      return map[status] || status
+    },
+    payMethodLabel(method) {
+      var map = { wechat: '微信支付', balance: '余额支付', points: '积分兑换', test: '测试支付' }
+      return map[method] || method || '-'
+    },
+    typeLabel(type) {
+      var map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
+      return map[type] || type || '-'
+    },
+    formatPrice(cents) {
+      if (cents == null) return '-'
+      return '¥' + (cents / 100).toFixed(2)
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      return new Date(time).toLocaleString()
     }
   }
 }
 </script>
 
 <style scoped>
-.order-manage {
-  padding: 20px;
-}
+.order-manage { padding: 20px; }
 .header {
   display: flex;
   justify-content: space-between;
@@ -227,9 +304,7 @@ export default {
   margin-bottom: 20px;
   flex-wrap: wrap;
 }
-.header h2 {
-  margin: 0;
-}
+.header h2 { margin: 0; }
 .filters {
   display: flex;
   gap: 10px;

+ 95 - 24
cfc-web/src/views/admin/ProductEdit.vue

@@ -24,7 +24,7 @@
         </el-form-item>
 
         <el-form-item label="商品描述" prop="description">
-          <el-input v-model="form.description" type="textarea" :rows="4" placeholder="详细描述" />
+          <div ref="editorRef" class="rich-editor"></div>
         </el-form-item>
 
         <el-form-item label="封面图片" prop="coverImage">
@@ -66,17 +66,17 @@
         <el-row :gutter="20">
           <el-col :span="8">
             <el-form-item label="售价(元)" prop="price">
-              <el-input-number v-model="form.price" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" />
+              <el-input-number v-model="form.price" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" controls />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="会员价(元)">
-              <el-input-number v-model="form.memberPrice" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" />
+              <el-input-number v-model="form.memberPrice" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" controls />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="库存" prop="stock">
-              <el-input-number v-model="form.stock" :min="0" :precision="0" placeholder="0" style="width: 100%;" />
+              <el-input-number v-model="form.stock" :min="0" :precision="0" placeholder="0" style="width: 100%;" controls />
             </el-form-item>
           </el-col>
         </el-row>
@@ -101,13 +101,7 @@
         </el-form-item>
 
         <el-form-item label="商品维度">
-          <el-select v-model="form.domain" placeholder="选择关联维度" clearable style="width: 200px;">
-            <el-option label="身" value="body" />
-            <el-option label="智" value="mind" />
-            <el-option label="富" value="wealth" />
-            <el-option label="行" value="action" />
-            <el-option label="心" value="heart" />
-          </el-select>
+          <dimension-weight-picker v-model="form.dimensionWeights" :show-presets="true" />
         </el-form-item>
 
         <el-form-item v-if="isEdit" label="SKU规格">
@@ -127,11 +121,14 @@
 
 <script>
 import { getProduct, createProduct, updateProduct } from '@/api/admin'
+import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight'
+import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
 import ProductSkuEditor from './components/ProductSkuEditor.vue'
+import E from '@wangeditor/editor'
 
 export default {
   name: 'ProductEdit',
-  components: { ProductSkuEditor },
+  components: { DimensionWeightPicker, ProductSkuEditor },
   data() {
     return {
       form: {
@@ -141,14 +138,14 @@ export default {
         coverImage: '',
         images: '',
         productType: 'physical',
-        price: 0,
-        memberPrice: 0,
-        stock: 0,
+        price: null,
+        memberPrice: null,
+        stock: null,
         vendorName: '',
         vendorId: null,
         deliveryMethod: 1,
         memberEligible: 'no',
-        domain: ''
+        dimensionWeights: []
       },
       imageList: [],
       rules: {
@@ -157,7 +154,8 @@ export default {
         price: [{ required: true, message: '请输入售价', trigger: 'blur' }],
         stock: [{ required: true, message: '请输入库存', trigger: 'blur' }]
       },
-      saving: false
+      saving: false,
+      editor: null
     }
   },
   computed: {
@@ -174,10 +172,17 @@ export default {
     }
   },
   mounted() {
+    this.initEditor()
     if (this.isEdit) {
       this.loadProduct(this.$route.query.id)
     }
   },
+  beforeDestroy() {
+    if (this.editor) {
+      this.editor.destroy()
+      this.editor = null
+    }
+  },
   methods: {
     async loadProduct(id) {
       try {
@@ -192,14 +197,14 @@ export default {
             coverImage: p.coverImage || '',
             images: p.images || '',
             productType: p.productType || 'physical',
-            price: p.price ? p.price / 100 : 0,
-            memberPrice: p.memberPrice ? p.memberPrice / 100 : 0,
-            stock: p.stock || 0,
+            price: (p.price != null) ? p.price / 100 : null,
+            memberPrice: (p.memberPrice != null) ? p.memberPrice / 100 : null,
+            stock: (p.stock != null) ? p.stock : null,
             vendorName: p.vendorName || '',
             vendorId: p.vendorId || null,
             deliveryMethod: p.deliveryMethod || 1,
             memberEligible: p.memberEligible || 'no',
-            domain: p.domain || ''
+            dimensionWeights: []
           }
           if (p.images) {
             try {
@@ -208,11 +213,49 @@ export default {
               this.imageList = []
             }
           }
+          // Load dimension weights
+          this.loadDimensionWeights(p.id)
+          // Set editor content after DOM ready
+          this.$nextTick(() => {
+            if (this.editor) {
+              this.editor.txt.html(p.description || '')
+            }
+          })
         }
       } catch (e) {
         this.$message.error('加载商品失败')
       }
     },
+    async loadDimensionWeights(productId) {
+      try {
+        const res = await getDimensionWeights('product', productId)
+        if (res.code === 200 && res.data) {
+          this.form.dimensionWeights = res.data.map(w => ({
+            dimension: w.dimension,
+            weight: w.weight,
+            enabled: w.enabled
+          }))
+        }
+      } catch (e) {
+        // ignore
+      }
+    },
+    initEditor() {
+      this.editor = new E(this.$refs.editorRef)
+      this.editor.config.uploadImgServer = this.uploadActionUrl
+      this.editor.config.uploadImgHeaders = this.uploadHeaders
+      this.editor.config.uploadImgMaxLength = 1
+      this.editor.config.uploadFileName = 'file'
+      this.editor.config.customAlert = (msg, type) => {
+        this.$message({ message: msg, type: type || 'error' })
+      }
+      this.editor.config.uploadImgSuccess = (res, insertFn) => {
+        if (res.code === 200) {
+          insertFn(res.data.url || res.data)
+        }
+      }
+      this.editor.create()
+    },
     beforeUpload(file) {
       const isImage = file.type.startsWith('image/')
       const isLt5M = file.size / 1024 / 1024 < 5
@@ -253,19 +296,39 @@ export default {
         if (!valid) return
         this.saving = true
         try {
+          // Get HTML content from editor
+          this.form.description = this.editor ? this.editor.txt.html() : ''
           const payload = {
             ...this.form,
-            price: Math.round(this.form.price * 100),
-            memberPrice: this.form.memberPrice ? Math.round(this.form.memberPrice * 100) : null,
+            price: (this.form.price != null) ? Math.round(this.form.price * 100) : 0,
+            memberPrice: (this.form.memberPrice != null) ? Math.round(this.form.memberPrice * 100) : null,
             images: JSON.stringify(this.imageList)
           }
+          // Extract dimensionWeights before sending to product API
+          const dimensionWeights = payload.dimensionWeights
+          delete payload.dimensionWeights
+
           let res
+          let productId = this.form.id
           if (this.isEdit) {
             res = await updateProduct(payload)
+            productId = this.form.id
           } else {
             res = await createProduct(payload)
+            // Get the created product id
+            if (res.code === 200 && res.data) {
+              productId = res.data.id || res.data
+            }
           }
           if (res.code === 200) {
+            // Save dimension weights
+            if (productId && Array.isArray(dimensionWeights)) {
+              try {
+                await saveDimensionWeights('product', productId, dimensionWeights)
+              } catch (e) {
+                console.warn('保存维度权重失败', e)
+              }
+            }
             this.$message.success(this.isEdit ? '保存成功' : '创建成功')
             this.$router.push('/product-manage')
           } else {
@@ -285,7 +348,7 @@ export default {
 <style scoped>
 .product-edit {
   padding: 20px;
-  max-width: 800px;
+  max-width: 900px;
 }
 .cover-uploader {
   display: flex;
@@ -366,4 +429,12 @@ export default {
   border-color: #409eff;
   color: #409eff;
 }
+.rich-editor {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  min-height: 300px;
+}
+.rich-editor /deep/ .w-e-toolbar {
+  flex-wrap: wrap;
+}
 </style>