evaluation.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. <template>
  2. <div class="evaluation-page">
  3. <div class="evaluation-header">
  4. <div class="header-left">
  5. <span class="header-title">评价订单</span>
  6. <span class="header-info">订单号:{{ orderInfo.orderNo }}</span>
  7. <span class="header-info">{{ orderInfo.orderTime }}</span>
  8. </div>
  9. <div class="header-right">
  10. <el-button v-if="evaluateForm.evaluationType !== 3" type="primary" @click="handleSubmitEvaluate">提交评价</el-button>
  11. <el-button type="info" @click="handleBack">返回</el-button>
  12. </div>
  13. </div>
  14. <div class="evaluate-content">
  15. <!-- 物流服务评价 -->
  16. <div class="logistics-evaluation">
  17. <div class="section-title">物流服务评价</div>
  18. <div class="logistics-ratings">
  19. <div class="rating-item">
  20. <span class="rating-label">快递包装</span>
  21. <el-rate v-model="evaluateForm.expressPacking" :colors="rateColors" :disabled="evaluateForm.evaluationType === 3" />
  22. </div>
  23. <div class="rating-item">
  24. <span class="rating-label">送货速度</span>
  25. <el-rate v-model="evaluateForm.deliverySpeed" :colors="rateColors" :disabled="evaluateForm.evaluationType === 3" />
  26. </div>
  27. <div class="rating-item">
  28. <span class="rating-label">配送员评价</span>
  29. <el-rate v-model="evaluateForm.deliveryManRating" :colors="rateColors" :disabled="evaluateForm.evaluationType === 3" />
  30. </div>
  31. </div>
  32. </div>
  33. <!-- 商品评价列表 -->
  34. <div v-for="(item, index) in evaluateForm.productEvaluations" :key="index" class="product-evaluation-item">
  35. <div class="product-left">
  36. <div class="product-image">
  37. <el-image :src="item.image" fit="contain">
  38. <template #error>
  39. <div class="image-placeholder">
  40. <el-icon :size="30" color="#ccc"><Picture /></el-icon>
  41. </div>
  42. </template>
  43. </el-image>
  44. </div>
  45. <div class="product-name">{{ item.name }}</div>
  46. <div class="product-code">{{ item.productNo }}</div>
  47. </div>
  48. <div class="product-right">
  49. <div class="evaluation-hint" v-if="evaluateForm.evaluationType !== 3">
  50. <el-icon color="#e6a23c"><WarningFilled /></el-icon>
  51. <span>请至少填写一件商品的评价</span>
  52. </div>
  53. <div class="rating-row">
  54. <span class="field-label">商品评价</span>
  55. <el-rate v-model="item.rating" :colors="rateColors" :disabled="evaluateForm.evaluationType === 3" />
  56. </div>
  57. <div class="content-row">
  58. <span class="field-label">评价晒单</span>
  59. <el-input v-model="item.content" type="textarea" :rows="4" placeholder="请输入评价内容" maxlength="200" :disabled="evaluateForm.evaluationType === 3" />
  60. </div>
  61. <div class="upload-row" v-if="evaluateForm.evaluationType !== 3">
  62. <el-upload
  63. class="upload-box"
  64. :action="action"
  65. :show-file-list="false"
  66. :on-success="(res) => handleProductUploadSuccess(res, index)"
  67. :before-upload="beforeAvatarUpload"
  68. accept="image/*"
  69. multiple
  70. list-type="picture-card"
  71. >
  72. <div class="upload-placeholder">
  73. <el-icon :size="24"><Plus /></el-icon>
  74. </div>
  75. </el-upload>
  76. <div v-if="item.images && item.images.length > 0" class="image-list">
  77. <div v-for="(img, imgIndex) in item.images" :key="imgIndex" class="image-item">
  78. <el-image :src="img" fit="cover" style="width: 100px; height: 100px; border-radius: 6px;" />
  79. <el-icon class="delete-icon" @click="handleDeleteImage(index, imgIndex)"><Close /></el-icon>
  80. </div>
  81. </div>
  82. </div>
  83. <div class="upload-row" v-else>
  84. <div v-if="item.images && item.images.length > 0" class="image-list">
  85. <div v-for="(img, imgIndex) in item.images" :key="imgIndex" class="image-item">
  86. <el-image :src="img" fit="cover" style="width: 100px; height: 100px; border-radius: 6px;" />
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. </template>
  95. <script setup lang="ts">
  96. import { ref, reactive, onMounted } from 'vue';
  97. import { useRouter, useRoute } from 'vue-router';
  98. import { Picture, Plus, WarningFilled, Close } from '@element-plus/icons-vue';
  99. import { ElMessage } from 'element-plus';
  100. import type { UploadProps } from 'element-plus';
  101. import { getOrderProducts, addOrderEvaluation, getOrderEvaluation, getOrderEvaluationHeader } from '@/api/pc/enterprise/order';
  102. const action = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
  103. const router = useRouter();
  104. const route = useRoute();
  105. const rateColors = ['#f7ba2a', '#f7ba2a', '#f7ba2a'];
  106. const orderInfo = reactive({
  107. orderId: '',
  108. orderNo: '',
  109. orderTime: ''
  110. });
  111. const evaluateForm = reactive({
  112. expressPacking: 5,
  113. deliverySpeed: 5,
  114. deliveryManRating: 5,
  115. evaluationType: 1 as number,
  116. productEvaluations: [] as any[]
  117. });
  118. // 加载订单商品
  119. const loadOrderProducts = async (orderId: string) => {
  120. try {
  121. const res = await getOrderProducts([Number(orderId)]);
  122. if (res.code === 200 && res.rows) {
  123. return res.rows.map((product: any) => ({
  124. orderItemId: product.id,
  125. productId: product.productId,
  126. name: product.productName || '',
  127. productName: product.productName || '',
  128. productNo: product.productNo || '',
  129. image: product.productImage || '',
  130. productImg: product.productImage || '',
  131. rating: 5,
  132. content: '',
  133. images: []
  134. }));
  135. }
  136. return [];
  137. } catch (error) {
  138. console.error('加载订单商品失败:', error);
  139. return [];
  140. }
  141. };
  142. // 加载已有评价数据(追评/查看)
  143. const loadExistingEvaluation = async (orderId: string) => {
  144. try {
  145. const res = await getOrderEvaluationHeader(orderId);
  146. if (res.code === 200 && res.data) {
  147. // 回显物流评分
  148. evaluateForm.expressPacking = res.data.logisticsPackScore || 5;
  149. evaluateForm.deliverySpeed = res.data.deliverSpeedScore || 5;
  150. evaluateForm.deliveryManRating = res.data.courierServiceScore || 5;
  151. // 回显商品评价内容
  152. const itemList = res.data.orderEvaluationItemList || [];
  153. if (itemList.length > 0 && evaluateForm.productEvaluations.length > 0) {
  154. // 按 orderItemId 或 productId 匹配回显
  155. itemList.forEach((evalItem: any) => {
  156. const productIndex = evaluateForm.productEvaluations.findIndex(
  157. (p: any) => p.orderItemId === evalItem.orderItemId || p.productId === evalItem.productId
  158. );
  159. if (productIndex !== -1) {
  160. evaluateForm.productEvaluations[productIndex].rating = evalItem.productScore || 5;
  161. evaluateForm.productEvaluations[productIndex].content = evalItem.content || '';
  162. const imgStr = evalItem.images || '';
  163. evaluateForm.productEvaluations[productIndex].images = imgStr ? imgStr.split(',').filter((url: string) => url) : [];
  164. }
  165. });
  166. }
  167. }
  168. } catch (error) {
  169. console.error('加载评价数据失败:', error);
  170. }
  171. };
  172. //上传成功(按商品索引)
  173. const handleProductUploadSuccess = (res: any, index: number) => {
  174. if (res.code == 200) {
  175. if (!evaluateForm.productEvaluations[index].images) {
  176. evaluateForm.productEvaluations[index].images = [];
  177. }
  178. evaluateForm.productEvaluations[index].images.push(res.data.url);
  179. } else {
  180. ElMessage({ message: res.msg, type: 'warning' });
  181. }
  182. };
  183. // 删除图片
  184. const handleDeleteImage = (productIndex: number, imageIndex: number) => {
  185. evaluateForm.productEvaluations[productIndex].images.splice(imageIndex, 1);
  186. };
  187. const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
  188. const isImage = rawFile.type.startsWith('image/');
  189. if (!isImage) {
  190. ElMessage.error('只能上传图片文件!');
  191. return false;
  192. }
  193. if (rawFile.size / 1024 / 1024 > 2) {
  194. ElMessage.error('图片大小不能超过2MB!');
  195. return false;
  196. }
  197. return true;
  198. };
  199. const handleSubmitEvaluate = async () => {
  200. const hasEvaluation = evaluateForm.productEvaluations.some((item: any) => item.content);
  201. if (!hasEvaluation) {
  202. ElMessage.warning('请至少填写一件商品的评价');
  203. return;
  204. }
  205. try {
  206. const submitData = {
  207. orderId: orderInfo.orderId,
  208. orderNo: orderInfo.orderNo,
  209. logisticsPackScore: evaluateForm.expressPacking,
  210. deliverSpeedScore: evaluateForm.deliverySpeed,
  211. courierServiceScore: evaluateForm.deliveryManRating,
  212. evaluationType: evaluateForm.evaluationType,
  213. orderEvaluationItemList: evaluateForm.productEvaluations
  214. .filter((item: any) => item.content)
  215. .map((item: any) => ({
  216. orderItemId: item.orderItemId,
  217. productId: item.productId,
  218. productName: item.productName || item.name,
  219. productImg: item.productImg || item.image,
  220. productScore: item.rating,
  221. content: item.content,
  222. images: item.images && item.images.length > 0 ? item.images.join(',') : ''
  223. }))
  224. };
  225. const res = await addOrderEvaluation(submitData);
  226. if (res.code === 200) {
  227. ElMessage.success('评价提交成功');
  228. router.back();
  229. } else {
  230. ElMessage.error(res.msg || '评价提交失败');
  231. }
  232. } catch (error) {
  233. console.error('评价提交失败:', error);
  234. ElMessage.error('评价提交失败');
  235. }
  236. };
  237. const handleBack = () => {
  238. router.back();
  239. };
  240. onMounted(async () => {
  241. const orderId = String(route.query.orderId || '');
  242. const type = Number(route.query.type) || 1;
  243. const orderNo = (route.query.orderNo as string) || '';
  244. const orderTime = (route.query.orderTime as string) || '';
  245. orderInfo.orderId = orderId;
  246. orderInfo.orderNo = orderNo;
  247. orderInfo.orderTime = orderTime;
  248. evaluateForm.evaluationType = type;
  249. if (orderId) {
  250. // 优先从 history state 获取已加载的商品数据
  251. const stateProducts = history.state?.products;
  252. let products: any[] = [];
  253. if (stateProducts) {
  254. try {
  255. const parsed = JSON.parse(stateProducts);
  256. products = parsed.map((p: any) => ({
  257. orderItemId: p.id,
  258. productId: p.productId,
  259. name: p.productName || p.name,
  260. productName: p.productName || p.name,
  261. productNo: p.spec2,
  262. image: p.productImg || p.image,
  263. productImg: p.productImg || p.image,
  264. rating: 5,
  265. content: '',
  266. images: []
  267. }));
  268. } catch (e) {
  269. products = await loadOrderProducts(orderId);
  270. }
  271. } else {
  272. products = await loadOrderProducts(orderId);
  273. }
  274. evaluateForm.productEvaluations = products;
  275. // 追评或查看评价时,加载已有评价
  276. if (type === 2 || type === 3) {
  277. await loadExistingEvaluation(orderId);
  278. }
  279. }
  280. });
  281. </script>
  282. <style scoped lang="scss">
  283. .evaluation-page {
  284. padding: 20px;
  285. background: #fff;
  286. min-height: 100%;
  287. width: 100%;
  288. }
  289. .evaluation-header {
  290. display: flex;
  291. justify-content: space-between;
  292. align-items: center;
  293. margin-bottom: 20px;
  294. .header-left {
  295. display: flex;
  296. align-items: center;
  297. gap: 12px;
  298. .header-title {
  299. font-size: 16px;
  300. font-weight: bold;
  301. color: #333;
  302. }
  303. .header-info {
  304. font-size: 14px;
  305. color: #666;
  306. }
  307. }
  308. .header-right {
  309. display: flex;
  310. gap: 8px;
  311. }
  312. }
  313. .evaluate-content {
  314. .logistics-evaluation {
  315. border: 1px solid #eee;
  316. border-radius: 4px;
  317. padding: 20px;
  318. margin-bottom: 20px;
  319. .section-title {
  320. font-size: 14px;
  321. font-weight: bold;
  322. color: #e6a23c;
  323. margin-bottom: 15px;
  324. }
  325. .logistics-ratings {
  326. display: flex;
  327. gap: 40px;
  328. .rating-item {
  329. display: flex;
  330. align-items: center;
  331. gap: 10px;
  332. .rating-label {
  333. font-size: 13px;
  334. color: #666;
  335. white-space: nowrap;
  336. }
  337. }
  338. }
  339. }
  340. .product-evaluation-item {
  341. display: flex;
  342. border: 1px solid #eee;
  343. border-radius: 4px;
  344. padding: 20px;
  345. margin-bottom: 15px;
  346. gap: 30px;
  347. .product-left {
  348. width: 180px;
  349. flex-shrink: 0;
  350. .product-image {
  351. width: 100px;
  352. height: 100px;
  353. background: #f5f5f5;
  354. border-radius: 4px;
  355. overflow: hidden;
  356. margin-bottom: 10px;
  357. .el-image {
  358. width: 100%;
  359. height: 100%;
  360. }
  361. .image-placeholder {
  362. width: 100%;
  363. height: 100%;
  364. display: flex;
  365. align-items: center;
  366. justify-content: center;
  367. }
  368. }
  369. .product-name {
  370. font-size: 13px;
  371. color: #333;
  372. margin-bottom: 6px;
  373. line-height: 1.4;
  374. }
  375. .product-code {
  376. font-size: 12px;
  377. color: #e6a23c;
  378. }
  379. }
  380. .product-right {
  381. flex: 1;
  382. .evaluation-hint {
  383. display: flex;
  384. align-items: center;
  385. gap: 5px;
  386. font-size: 13px;
  387. color: #e6a23c;
  388. margin-bottom: 15px;
  389. }
  390. .rating-row {
  391. display: flex;
  392. align-items: center;
  393. gap: 10px;
  394. margin-bottom: 15px;
  395. }
  396. .content-row {
  397. display: flex;
  398. gap: 10px;
  399. margin-bottom: 15px;
  400. }
  401. .field-label {
  402. font-size: 13px;
  403. color: #666;
  404. white-space: nowrap;
  405. line-height: 32px;
  406. }
  407. .upload-row {
  408. padding-left: 65px;
  409. }
  410. }
  411. }
  412. }
  413. .upload-box {
  414. :deep(.el-upload) {
  415. width: 100px;
  416. height: 100px;
  417. border: 1px dashed #d9d9d9;
  418. border-radius: 6px;
  419. cursor: pointer;
  420. overflow: hidden;
  421. &:hover {
  422. border-color: #e60012;
  423. }
  424. }
  425. .upload-placeholder {
  426. width: 100%;
  427. height: 100%;
  428. display: flex;
  429. flex-direction: column;
  430. align-items: center;
  431. justify-content: center;
  432. color: #999;
  433. font-size: 12px;
  434. gap: 5px;
  435. }
  436. .upload-preview {
  437. width: 100%;
  438. height: 100%;
  439. .el-image {
  440. width: 100%;
  441. height: 100%;
  442. }
  443. }
  444. }
  445. .image-list {
  446. display: flex;
  447. gap: 10px;
  448. flex-wrap: wrap;
  449. margin-top: 10px;
  450. .image-item {
  451. position: relative;
  452. width: 100px;
  453. height: 100px;
  454. .delete-icon {
  455. position: absolute;
  456. top: 2px;
  457. right: 2px;
  458. width: 20px;
  459. height: 20px;
  460. background: rgba(0, 0, 0, 0.5);
  461. color: #fff;
  462. border-radius: 50%;
  463. cursor: pointer;
  464. display: flex;
  465. align-items: center;
  466. justify-content: center;
  467. font-size: 14px;
  468. &:hover {
  469. background: rgba(0, 0, 0, 0.7);
  470. }
  471. }
  472. }
  473. }
  474. </style>