爱简收旧衣按件回收前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1311 lines
39 KiB

3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 weeks ago
3 months ago
2 weeks ago
2 weeks ago
3 months ago
2 weeks ago
2 weeks ago
3 months ago
3 months ago
3 months ago
2 weeks ago
3 months ago
3 months ago
3 months ago
3 months ago
2 weeks ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 weeks ago
2 weeks ago
2 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
2 weeks ago
2 weeks ago
3 months ago
3 months ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
3 months ago
2 weeks ago
2 weeks ago
2 weeks ago
2 months ago
2 months ago
3 months ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
3 months ago
3 months ago
2 weeks ago
3 months ago
3 months ago
3 months ago
2 weeks ago
2 weeks ago
2 weeks ago
3 months ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 weeks ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 weeks ago
3 months ago
3 months ago
3 months ago
3 months ago
2 weeks ago
3 months ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 weeks ago
3 months ago
3 months ago
3 months ago
3 months ago
2 weeks ago
3 months ago
3 months ago
3 months ago
2 weeks ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
  1. <template>
  2. <view class="inspect-result-container" :class="{ 'popup-open': isPopupOpen }">
  3. <!-- 顶部导航栏 -->
  4. <view class="nav-bar">
  5. <view class="back" @tap="goBack">
  6. <uni-icons type="left" size="20" color="#222" />
  7. </view>
  8. <text class="nav-title">步骤二合格质检</text>
  9. <view class="nav-icons">
  10. <uni-icons type="more" size="24" color="#222" />
  11. </view>
  12. </view>
  13. <view class="main-content">
  14. <!-- 合格产品卡片 -->
  15. <view v-if="hasQualifiedItems" class="result-card">
  16. <view class="card-title">合格产品</view>
  17. <view v-for="(item, index) in qualifiedList" :key="item.uniqueKey || index" class="result-group">
  18. <view class="row-main">
  19. <view class="goods-name-section">
  20. <text class="goods-name">{{ getGoodsName(item.shopId) }}</text>
  21. <text class="goods-brand">{{ getBrandStyleInfo(item) }}</text>
  22. <text class="goods-style-price" v-if="getStylePriceInfo(item)">{{ getStylePriceInfo(item) }}</text>
  23. </view>
  24. <text class="row-count">x{{ item.count }}</text>
  25. </view>
  26. <view class="price-input-row">
  27. <text class="price-label">总金额</text>
  28. <input class="price-input" v-model="item.total" type="digit" placeholder="请输入金额" />
  29. </view>
  30. <!-- 选择理由模块 -->
  31. <view v-if="item.items && item.items.length > 0">
  32. <view v-for="(commonItem, itemIndex) in item.items" :key="commonItem.id || itemIndex" class="row-reason">
  33. <text class="reason-label">{{ getQualifiedItemLabel(item, itemIndex) }}</text>
  34. <view class="reason-select" @click="selectReasonForQualified(commonItem, item)">
  35. <text class="reason-placeholder" :class="{ 'selected': hasSelectedReason(commonItem) }">
  36. {{ hasSelectedReason(commonItem) ? '已选择' : '请选择理由(选填)' }}
  37. </text>
  38. <uni-icons type="right" size="18" color="#bbb" />
  39. </view>
  40. </view>
  41. </view>
  42. </view>
  43. </view>
  44. <!-- 不合格产品卡片 -->
  45. <view v-if="hasUnqualifiedItems" class="result-card">
  46. <view class="card-title">不合格产品</view>
  47. <view v-for="group in unqualifiedGroups" :key="group.shopId" class="result-group">
  48. <view class="row-main">
  49. <view class="goods-name-section">
  50. <text class="goods-name">质量问题</text>
  51. <text class="goods-brand"></text>
  52. </view>
  53. <text class="row-count">x{{ group.count }}</text>
  54. </view>
  55. <view class="price-input-row">
  56. <text class="price-label">总金额</text>
  57. <input class="price-input" v-model="group.total" type="digit" placeholder="请输入金额" />
  58. </view>
  59. <!-- 选择理由模块 -->
  60. <view v-if="inspectData && inspectData.list">
  61. <view v-for="(item, index) in (inspectData.list.find(i => i.id === 'quality_issue')?.commonOrderList || [])"
  62. :key="item.id || item.testingStatus" class="row-reason">
  63. <text class="reason-label">质量问题{{ index + 1 }}</text>
  64. <view class="reason-select" @click="selectReason(item)">
  65. <text class="reason-placeholder" :class="{ 'selected': hasSelectedReason(item) }">
  66. {{ hasSelectedReason(item) ? '已选择' : '请选择理由' }}
  67. </text>
  68. <uni-icons type="right" size="18" color="#bbb" />
  69. </view>
  70. </view>
  71. </view>
  72. </view>
  73. </view>
  74. <!-- 不可回收产品卡片 -->
  75. <view v-if="hasUnrecyclableItems" class="result-card">
  76. <view class="card-title">不可回收产品</view>
  77. <view v-for="item in unrecyclableList" :key="item.id" class="result-group">
  78. <view class="row-main">
  79. <view class="goods-name-section">
  80. <text class="goods-name">不可回收</text>
  81. <text class="goods-brand"></text>
  82. </view>
  83. <text class="row-count">x{{ item.count }}</text>
  84. </view>
  85. <view class="price-input-row">
  86. <text class="price-label">总金额</text>
  87. <input class="price-input" v-model="item.total" type="digit" placeholder="请输入金额" />
  88. </view>
  89. <!-- 选择理由模块 -->
  90. <view v-if="inspectData && inspectData.list">
  91. <view v-for="(commonItem,index) in (inspectData.list.find(i => i.id === 'unrecyclable')?.commonOrderList || [])"
  92. :key="commonItem.id || commonItem.testingStatus" class="row-reason">
  93. <text class="reason-label">不可回收{{ index +1 }}</text>
  94. <view class="reason-select" @click="selectReason(commonItem)">
  95. <text class="reason-placeholder" :class="{ 'selected': hasSelectedReason(commonItem) }">
  96. {{ hasSelectedReason(commonItem) ? '已选择' : '请选择理由' }}
  97. </text>
  98. <uni-icons type="right" size="18" color="#bbb" />
  99. </view>
  100. </view>
  101. </view>
  102. </view>
  103. </view>
  104. <!-- 回收信息卡片 -->
  105. <view class="result-card info-card">
  106. <view class="card-title-row">
  107. <text class="card-title">回收信息</text>
  108. <view class="status-tag">待质检</view>
  109. </view>
  110. <view class="info-row">
  111. <text class="info-label">订单编号</text>
  112. <text class="info-value copy-btn"
  113. @click="$utils.copyText(order?.ordeNo)"
  114. >{{ order?.ordeNo }} 复制</text>
  115. </view>
  116. <view class="info-row">
  117. <text class="info-label">取件时间</text>
  118. <text class="info-value">{{ order?.goTime || '2025-03-20 11:00' }}</text>
  119. </view>
  120. </view>
  121. </view>
  122. <!-- 底部操作按钮 -->
  123. <view class="footer-btns">
  124. <button class="btn-outline" @tap="goPrev">上一步</button>
  125. <button class="btn-main" @tap="finishInspect">完成质检</button>
  126. </view>
  127. <!-- 理由弹窗 -->
  128. <uni-popup ref="reasonPopup" type="bottom" :mask-click="false" :safe-area="false" class="reason-popup-wrapper">
  129. <view class="reason-popup">
  130. <view class="popup-header">
  131. <text class="popup-close" @tap="closeReasonPopup">关闭</text>
  132. <text class="popup-title">{{ currentReasonTitle }}</text>
  133. </view>
  134. <scroll-view class="popup-content" scroll-y="true">
  135. <view class="popup-section">
  136. <text class="section-label">上传图片</text>
  137. <view class="img-list">
  138. <view class="img-item add" @tap="addReasonImg">
  139. <uni-icons type="plusempty" size="32" color="#bbb" />
  140. </view>
  141. <view v-for="(img, idx) in reasonImages" :key="idx" class="img-item">
  142. <image :src="img" class="img" />
  143. <view class="img-del" @tap="removeReasonImg(idx)">×</view>
  144. </view>
  145. </view>
  146. </view>
  147. <view class="popup-section">
  148. <text class="section-label">自定义理由</text>
  149. <textarea class="custom-reason-input" v-model="customReason" placeholder="请输入自定义理由..." maxlength="200" />
  150. </view>
  151. <view class="popup-section">
  152. <text class="section-label">质检级别</text>
  153. <view class="level-options">
  154. <view v-for="level in ['S', 'A', 'B', 'C']" :key="level"
  155. :class="['level-option', { 'selected': currentQualityLevel === level }]"
  156. @tap="selectQualityLevel(level)">
  157. <text class="level-text">{{ level }}</text>
  158. </view>
  159. </view>
  160. </view>
  161. <view class="popup-section">
  162. <text class="section-label">选择理由</text>
  163. <view v-for="(item, index) in reasonOptions" :key="item.id" class="reason-row" @tap="toggleReason(index)">
  164. <view :class="['checkbox', { checked: reasonChecked.includes(item.reason) }]" />
  165. <text class="reason-text">{{ item.reason }}</text>
  166. </view>
  167. </view>
  168. </scroll-view>
  169. <button class="popup-save-btn" @tap="saveReason">保存</button>
  170. </view>
  171. </uni-popup>
  172. </view>
  173. </template>
  174. <script>
  175. import pullRefreshMixin from '../mixins/pullRefreshMixin.js'
  176. import OSS from '@/utils/oss-upload/oss/index.js'
  177. export default {
  178. mixins: [pullRefreshMixin],
  179. data() {
  180. return {
  181. inspectData: null, // 从步骤一传过来的质检数据
  182. order: null, // 订单数据
  183. qualifiedList: [],
  184. unqualifiedGroups: [],
  185. unrecyclableList: [],
  186. goodsNameMap: {}, // 商品id => 名称
  187. reasonPopupVisible: false,
  188. currentReasonTitle: '',
  189. reasonImages: [],
  190. reasonOptions: [],
  191. reasonChecked: [],
  192. customReason: '', // 自定义理由输入
  193. currentReasonItem: null,
  194. currentQualityLevel: '', // 当前选择的质检级别
  195. isPopupOpen: false, // 控制弹窗状态
  196. scrollTop: 0 // 记录滚动位置
  197. }
  198. },
  199. computed: {
  200. hasQualifiedItems() {
  201. return this.qualifiedList && this.qualifiedList.length > 0
  202. },
  203. hasUnqualifiedItems() {
  204. return this.unqualifiedGroups && this.unqualifiedGroups.length > 0
  205. },
  206. hasUnrecyclableItems() {
  207. return this.unrecyclableList && this.unrecyclableList.length > 0
  208. }
  209. },
  210. onLoad(options) {
  211. // 接收质检数据和订单数据
  212. if (options && options.resultData) {
  213. try {
  214. const resultData = JSON.parse(decodeURIComponent(options.resultData))
  215. this.inspectData = resultData.inspectResult
  216. this.order = resultData.order
  217. this.processInspectData()
  218. } catch (error) {
  219. console.error('解析数据失败:', error)
  220. }
  221. }
  222. if (options && options.inspectData && !options.resultData) {
  223. try {
  224. this.inspectData = JSON.parse(decodeURIComponent(options.inspectData))
  225. this.processInspectData()
  226. } catch (error) {
  227. console.error('解析质检数据失败:', error)
  228. }
  229. }
  230. },
  231. methods: {
  232. async processInspectData() {
  233. if (!this.inspectData || !this.inspectData["list"]) return
  234. this.qualifiedList = []
  235. this.unqualifiedGroups = []
  236. this.unrecyclableList = []
  237. // 只收集普通商品的 shopId 作为 goodsId
  238. const allIds = []
  239. this.inspectData["list"].forEach(item => {
  240. if (item.shopId && item.id !== 'unrecyclable' && item.id !== 'quality_issue') {
  241. allIds.push(item.shopId)
  242. }
  243. })
  244. console.log('allIds', allIds)
  245. const uniqueIds = Array.from(new Set(allIds.filter(Boolean)))
  246. console.log('uniqueIds', uniqueIds)
  247. await this.fetchGoodsNames(uniqueIds)
  248. // 合格商品 - 按照商品、品牌、款式作为唯一标识生成独立数据
  249. this.inspectData["list"].forEach(item => {
  250. if (item.qualifiedNum > 0 && item.id !== 'unrecyclable' && item.id !== 'quality_issue') {
  251. // 为每个品牌款式组合创建独立的条目
  252. const uniqueKey = item.uniqueKey || `${item.shopId}_${item.pinId || 'no-brand'}_${item.styleId || 'no-style'}`
  253. this.qualifiedList.push({
  254. shopId: item.shopId,
  255. count: item.qualifiedNum,
  256. total: item.price || 0,
  257. classId: item.categoryId,
  258. brandName: item.brandName || '',
  259. styleName: item.styleName || '',
  260. pinId: item.pinId || '',
  261. styleId: item.styleId || '',
  262. uniqueKey: uniqueKey,
  263. qualityLevel: item.qualityLevel || '', // 质检级别
  264. // 添加款式价格信息
  265. styleMinPrice: item.styleMinPrice || 0,
  266. styleMaxPrice: item.styleMaxPrice || 0,
  267. // 添加商品本身的价格信息
  268. productMinPrice: item.productMinPrice || 0,
  269. productMaxPrice: item.productMaxPrice || 0,
  270. originalItem: item, // 保存原始数据引用
  271. items: item.commonOrderList ? [...item.commonOrderList] : [{
  272. testingInstructions: '',
  273. testingImages: '',
  274. testingStatus: 0
  275. }]
  276. })
  277. console.log('qualifiedList', this.qualifiedList)
  278. }
  279. })
  280. // 不合格商品(只展示质量问题)
  281. this.inspectData["list"].forEach(item => {
  282. if (item.id === 'quality_issue' && item.noQualifiedNum > 0) {
  283. this.unqualifiedGroups.push({
  284. shopId: item.id,
  285. count: item.noQualifiedNum,
  286. total: item.price,
  287. items: item.commonOrderList ? [...item.commonOrderList] : []
  288. })
  289. }
  290. })
  291. // 不可回收商品
  292. this.inspectData["list"].forEach(item => {
  293. if (item.id === 'unrecyclable' && item.unrecyclable > 0) {
  294. this.unrecyclableList.push({
  295. id: item.id,
  296. count: item.unrecyclable,
  297. total: item.price,
  298. items: item.commonOrderList ? [...item.commonOrderList] : []
  299. })
  300. }
  301. })
  302. },
  303. async fetchGoodsNames(ids) {
  304. // 批量获取商品名称
  305. const nameMap = {}
  306. for (const id of ids) {
  307. try {
  308. const res = await this.$api('getGoodsDetail', { goodsId: id })
  309. if (res && res.code === 200 && res.result) {
  310. nameMap[id] = res.result.name
  311. }
  312. } catch (e) {
  313. nameMap[id] = '未知商品'
  314. }
  315. }
  316. this.goodsNameMap = nameMap
  317. },
  318. getGoodsName(id) {
  319. return this.goodsNameMap[id] || '未知商品'
  320. },
  321. goBack() {
  322. uni.navigateBack()
  323. },
  324. goPrev() {
  325. uni.navigateBack()
  326. },
  327. finishInspect() {
  328. // 校验金额必填性
  329. // 检查合格产品金额
  330. for (const item of this.qualifiedList) {
  331. if (!item.total || item.total === '' || parseFloat(item.total) <= 0) {
  332. uni.showToast({ title: '请填写合格产品金额', icon: 'none' })
  333. return
  334. }
  335. }
  336. // 检查不合格产品金额
  337. // for (const group of this.unqualifiedGroups) {
  338. // if (!group.total || group.total === '' || parseFloat(group.total) <= 0) {
  339. // uni.showToast({ title: '请填写不合格产品金额', icon: 'none' })
  340. // return
  341. // }
  342. // }
  343. // // 检查不可回收产品金额
  344. // for (const item of this.unrecyclableList) {
  345. // if (!item.total || item.total === '' || parseFloat(item.total) <= 0) {
  346. // uni.showToast({ title: '请填写不可回收产品金额', icon: 'none' })
  347. // return
  348. // }
  349. // }
  350. // 校验理由必填性
  351. // 1. 合格产品理由可选填
  352. // 2. 质量问题和不可回收理由必填
  353. // 检查质量问题
  354. const qualityItem = this.inspectData["list"].find(item => item.id === 'quality_issue')
  355. if (qualityItem && qualityItem.noQualifiedNum > 0) {
  356. const hasReason = qualityItem.commonOrderList && qualityItem.commonOrderList.some(c => {
  357. // 只要填写了理由或图片即可
  358. return (c.testingInstructions && c.testingInstructions.trim() !== '') ||
  359. (c.testingImages && c.testingImages.trim() !== '')
  360. })
  361. if (!hasReason) {
  362. uni.showToast({ title: '质量问题理由必填', icon: 'none' })
  363. return
  364. }
  365. }
  366. // 检查不可回收
  367. const unrecyclableItem = this.inspectData["list"].find(item => item.id === 'unrecyclable')
  368. if (unrecyclableItem && unrecyclableItem.unrecyclable > 0) {
  369. const hasReason = unrecyclableItem.commonOrderList && unrecyclableItem.commonOrderList.some(c => {
  370. return (c.testingInstructions && c.testingInstructions.trim() !== '') ||
  371. (c.testingImages && c.testingImages.trim() !== '')
  372. })
  373. if (!hasReason) {
  374. uni.showToast({ title: '不可回收理由必填', icon: 'none' })
  375. return
  376. }
  377. }
  378. // 同步用户输入的金额到inspectData
  379. this.syncPriceToInspectData()
  380. // 在提交前将质量问题和不可回收中的id值置为空
  381. const processedList = this.inspectData["list"].map(item => {
  382. if (item.id === 'quality_issue' || item.id === 'unrecyclable') {
  383. // 对于质量问题和不可回收,将id置为空
  384. return { ...item, id: '' }
  385. }
  386. return item
  387. })
  388. const inspectDatas = {...this.inspectData, list: JSON.stringify(processedList, null, 2)}
  389. console.log('最终的质检数据:', inspectDatas);
  390. this.$api('submitQualityInfo', inspectDatas , res => {
  391. if (res && res.code === 200) {
  392. uni.showToast({ title: '质检完成', icon: 'success' })
  393. uni.reLaunch({ url: '/pages/component/home' })
  394. }
  395. })
  396. uni.showToast({ title: '完成质检', icon: 'success' })
  397. },
  398. selectReason(item) {
  399. // 直接使用传入的item,因为它已经是commonOrderList中的具体项
  400. let popupItem = item
  401. let type = 0 // 默认类型
  402. // 根据testingStatus确定类型
  403. if (item.testingStatus === 1) {
  404. type = 1 // 质量问题
  405. } else if (item.testingStatus === 2) {
  406. type = 2 // 不可回收
  407. }
  408. this.currentReasonItem = popupItem
  409. // 判断类型,动态设置标题
  410. if (popupItem.testingStatus === 1) {
  411. this.currentReasonTitle = '质量问题'
  412. } else if (popupItem.testingStatus === 2) {
  413. this.currentReasonTitle = '不可回收'
  414. } else {
  415. this.currentReasonTitle = this.getGoodsName(popupItem.shopId || popupItem.id)
  416. }
  417. // 只使用testingImages字段
  418. if (popupItem.testingImages && popupItem.testingImages.trim() !== '') {
  419. this.reasonImages = popupItem.testingImages.split(',').filter(img => img.trim() !== '')
  420. } else {
  421. this.reasonImages = []
  422. }
  423. // 初始化质检级别
  424. this.currentQualityLevel = popupItem.qualityLevel || ''
  425. // 不在这里设置reasonChecked
  426. this.selectReasonForUnqualified(popupItem, type)
  427. this.lockScroll()
  428. this.$refs.reasonPopup.open()
  429. },
  430. selectReasonForQualified(commonItem, parentItem) {
  431. this.currentReasonItem = commonItem
  432. this.currentReasonTitle = this.getGoodsName(parentItem.shopId || parentItem.id)
  433. // 只使用testingImages字段
  434. if (commonItem.testingImages && commonItem.testingImages.trim() !== '') {
  435. this.reasonImages = commonItem.testingImages.split(',').filter(img => img.trim() !== '')
  436. } else {
  437. this.reasonImages = []
  438. }
  439. // 初始化质检级别
  440. this.currentQualityLevel = commonItem.qualityLevel || ''
  441. // 使用从上一个页面传过来的categoryId
  442. this.selectReasonForUnqualified(commonItem, 0, parentItem.classId)
  443. this.lockScroll()
  444. this.$refs.reasonPopup.open()
  445. },
  446. selectReasonForUnqualified(item, type, classId = '') {
  447. console.log(item)
  448. // 根据类型确定classId
  449. if (!classId) {
  450. if (type === 1) {
  451. // 质量问题
  452. classId = ''
  453. } else if (type === 2) {
  454. // 不可回收
  455. classId = ''
  456. } else {
  457. // classId
  458. classId = item.classId || ''
  459. }
  460. }
  461. console.log('[selectReasonForUnqualified] classId:', classId, 'type:', type)
  462. this.$api('getcheckoutReasons', {
  463. classId: classId,
  464. type: type
  465. }, res => {
  466. if (res.code == 200) {
  467. this.reasonOptions = res.result;
  468. if (item.testingInstructions && item.testingInstructions.trim() !== '') {
  469. const instructions = item.testingInstructions.split(',').map(s => s.trim()).filter(Boolean)
  470. // 分离预设理由和自定义理由
  471. this.reasonChecked = []
  472. this.customReason = ''
  473. instructions.forEach(instruction => {
  474. const foundOption = this.reasonOptions.find(option => option.reason === instruction)
  475. if (foundOption) {
  476. this.reasonChecked.push(instruction)
  477. } else {
  478. // 如果不在预设选项中,认为是自定义理由
  479. this.customReason = this.customReason ? `${this.customReason},${instruction}` : instruction
  480. }
  481. })
  482. } else {
  483. this.reasonChecked = []
  484. this.customReason = ''
  485. }
  486. this.$nextTick(() => {
  487. this.reasonChecked = [...this.reasonChecked]
  488. console.log('[selectReasonForUnqualified] reasonOptions:', this.reasonOptions)
  489. console.log('[selectReasonForUnqualified] reasonChecked:', this.reasonChecked)
  490. console.log('[selectReasonForUnqualified] customReason:', this.customReason)
  491. })
  492. } else {
  493. console.error('[selectReasonForUnqualified] API返回错误:', res)
  494. uni.showToast({
  495. title: '获取理由列表失败',
  496. icon: 'none'
  497. })
  498. }
  499. }, err => {
  500. console.error('[selectReasonForUnqualified] API调用失败:', err)
  501. uni.showToast({
  502. title: '获取理由列表失败',
  503. icon: 'none'
  504. })
  505. })
  506. },
  507. closeReasonPopup() {
  508. this.unlockScroll()
  509. this.$refs.reasonPopup.close()
  510. },
  511. addReasonImg() {
  512. // 检查是否已达到最大数量限制
  513. if (this.reasonImages.length >= 9) {
  514. uni.showToast({
  515. title: '最多只能上传9张图片',
  516. icon: 'none'
  517. })
  518. return
  519. }
  520. uni.chooseMedia({
  521. count: 3 - this.reasonImages.length, // 剩余可选择数量
  522. mediaType: ['image'], // 只选择图片
  523. sourceType: ['camera'], // 直接使用相机拍照
  524. maxDuration: 30,
  525. sizeType: [ 'compressed'],
  526. camera: 'back',
  527. success: (res) => {
  528. console.log('选择的图片:', res.tempFiles)
  529. // 显示上传进度
  530. uni.showLoading({
  531. title: '上传中...'
  532. })
  533. // 处理选择的图片
  534. const uploadPromises = res.tempFiles.map(file => {
  535. return this.uploadImageToOSS(file.tempFilePath)
  536. })
  537. // 批量上传
  538. Promise.all(uploadPromises).then(urls => {
  539. this.reasonImages = this.reasonImages.concat(urls)
  540. uni.hideLoading()
  541. uni.showToast({
  542. title: '上传成功',
  543. icon: 'success'
  544. })
  545. }).catch(error => {
  546. console.error('上传失败:', error)
  547. uni.hideLoading()
  548. uni.showToast({
  549. title: '上传失败,请重试',
  550. icon: 'none'
  551. })
  552. })
  553. },
  554. fail: (error) => {
  555. console.error('选择图片失败:', error)
  556. uni.showToast({
  557. title: '选择图片失败',
  558. icon: 'none'
  559. })
  560. }
  561. })
  562. },
  563. async uploadImageToOSS(filePath) {
  564. try {
  565. // 使用项目中的OSS上传工具
  566. const url = await OSS.ossUpload(filePath)
  567. return url
  568. } catch (error) {
  569. console.error('OSS上传失败:', error)
  570. throw error
  571. }
  572. },
  573. removeReasonImg(idx) {
  574. this.reasonImages.splice(idx, 1)
  575. },
  576. toggleReason(idx) {
  577. const val = this.reasonOptions[idx].reason.toString()
  578. const i = this.reasonChecked.indexOf(val)
  579. if (i > -1) {
  580. this.reasonChecked.splice(i, 1)
  581. } else {
  582. this.reasonChecked.push(val)
  583. }
  584. this.reasonChecked = [...this.reasonChecked]
  585. console.log('[toggleReason] reasonChecked:', this.reasonChecked)
  586. },
  587. saveReason() {
  588. if (this.currentReasonItem) {
  589. console.log('this.currentReasonItem', this.currentReasonItem)
  590. // 合并选择的理由和自定义理由
  591. const allReasons = [...this.reasonChecked]
  592. if (this.customReason && this.customReason.trim() !== '') {
  593. allReasons.push(this.customReason.trim())
  594. }
  595. // 直接更新当前项
  596. this.currentReasonItem.testingInstructions = allReasons.join(',')
  597. this.currentReasonItem.testingImages = this.reasonImages.join(',')
  598. this.currentReasonItem.qualityLevel = this.currentQualityLevel
  599. }
  600. this.closeReasonPopup()
  601. },
  602. lockScroll() {
  603. // 获取当前页面滚动位置
  604. uni.createSelectorQuery().selectViewport().scrollOffset((res) => {
  605. this.scrollTop = res.scrollTop
  606. }).exec()
  607. // 禁用页面滚动
  608. this.isPopupOpen = true
  609. },
  610. unlockScroll() {
  611. // 恢复页面滚动
  612. this.isPopupOpen = false
  613. // 恢复滚动位置
  614. this.$nextTick(() => {
  615. if (this.scrollTop > 0) {
  616. uni.pageScrollTo({
  617. scrollTop: this.scrollTop,
  618. duration: 0
  619. })
  620. }
  621. })
  622. },
  623. async onRefresh() {
  624. await this.refreshData && this.refreshData()
  625. },
  626. refreshData() {
  627. // 可根据实际需求刷新质检结果数据
  628. // 例如重新请求接口或重置数据
  629. },
  630. getItemBrand(id) {
  631. if (this.order && this.order.commonOrderList) {
  632. const orderItem = this.order.commonOrderList.find(item => item.id == id)
  633. return orderItem ? (orderItem.pinName || '') : ''
  634. }
  635. return ''
  636. },
  637. parseTestingInstructions() {
  638. return []
  639. },
  640. hasSelectedReason(item) {
  641. // console.log('item', item)
  642. // 检查该商品是否已选择理由或上传图片
  643. return (item.testingInstructions && item.testingInstructions.trim() !== '') ||
  644. (item.testingImages && item.testingImages.trim() !== '')
  645. },
  646. getBrandStyleInfo(item) {
  647. // 获取品牌款式信息
  648. const brandName = item.brandName || ''
  649. const styleName = item.styleName || ''
  650. if (brandName && styleName) {
  651. return `品牌:${brandName} | 款式:${styleName}`
  652. } else if (brandName) {
  653. return `品牌:${brandName}`
  654. } else if (styleName) {
  655. return `款式:${styleName}`
  656. } else {
  657. return this.getItemBrand(item.shopId)
  658. }
  659. },
  660. getQualifiedItemLabel(item, index) {
  661. // 生成合格产品的标签
  662. const goodsName = this.getGoodsName(item.shopId)
  663. const brandName = item.brandName || ''
  664. const styleName = item.styleName || ''
  665. if (brandName && styleName) {
  666. return `(${brandName}-${styleName})`
  667. } else if (brandName) {
  668. return `(${brandName})`
  669. } else {
  670. return `${goodsName}`
  671. }
  672. },
  673. getStylePriceInfo(item) {
  674. // 优先显示款式价格信息
  675. if (item.styleMinPrice && item.styleMaxPrice) {
  676. const minPrice = parseFloat(item.styleMinPrice)
  677. const maxPrice = parseFloat(item.styleMaxPrice)
  678. if (minPrice === maxPrice) {
  679. return `款式价格:¥${minPrice}`
  680. } else {
  681. return `款式价格:¥${minPrice}-${maxPrice}`
  682. }
  683. }
  684. // 如果没有款式价格,显示商品本身的价格
  685. if (item.productMinPrice && item.productMaxPrice) {
  686. const minPrice = parseFloat(item.productMinPrice)
  687. const maxPrice = parseFloat(item.productMaxPrice)
  688. if (minPrice === maxPrice) {
  689. return `商品价格:¥${minPrice}`
  690. } else {
  691. return `商品价格:¥${minPrice}-${maxPrice}`
  692. }
  693. }
  694. return ''
  695. },
  696. selectQualityLevel(level) {
  697. // 设置当前选择的质检级别
  698. this.currentQualityLevel = level
  699. },
  700. syncPriceToInspectData() {
  701. // 同步合格产品金额 - 按品牌款式唯一标识
  702. this.qualifiedList.forEach(qualifiedItem => {
  703. // 使用uniqueKey或者shopId+pinId+styleId来匹配
  704. const inspectItem = this.inspectData.list.find(item => {
  705. if (qualifiedItem.uniqueKey && item.uniqueKey) {
  706. return item.uniqueKey === qualifiedItem.uniqueKey
  707. }
  708. // 备用匹配方式
  709. return item.shopId === qualifiedItem.shopId &&
  710. (item.pinId || '') === (qualifiedItem.pinId || '') &&
  711. (item.styleId || '') === (qualifiedItem.styleId || '') &&
  712. item.id !== 'unrecyclable' &&
  713. item.id !== 'quality_issue'
  714. })
  715. if (inspectItem) {
  716. inspectItem.price = parseFloat(qualifiedItem.total) || 0
  717. // 同步质检级别 - 从items中获取第一个有级别的项目
  718. const itemWithLevel = qualifiedItem.items && qualifiedItem.items.find(item => item.qualityLevel)
  719. inspectItem.qualityLevel = itemWithLevel ? itemWithLevel.qualityLevel : (qualifiedItem.qualityLevel || '')
  720. // 同步理由信息
  721. if (qualifiedItem.items && qualifiedItem.items.length > 0) {
  722. inspectItem.commonOrderList = qualifiedItem.items
  723. }
  724. }
  725. })
  726. // 同步不合格产品金额和质检级别
  727. this.unqualifiedGroups.forEach(group => {
  728. const inspectItem = this.inspectData.list.find(item => item.id === 'quality_issue')
  729. if (inspectItem) {
  730. inspectItem.price = parseFloat(group.total) || 0
  731. // 同步质检级别
  732. if (inspectItem.commonOrderList && inspectItem.commonOrderList.length > 0) {
  733. const itemWithLevel = inspectItem.commonOrderList.find(item => item.qualityLevel)
  734. if (itemWithLevel) {
  735. inspectItem.qualityLevel = itemWithLevel.qualityLevel
  736. }
  737. }
  738. }
  739. })
  740. // 同步不可回收产品金额和质检级别
  741. this.unrecyclableList.forEach(unrecyclableItem => {
  742. const inspectItem = this.inspectData.list.find(item => item.id === 'unrecyclable')
  743. if (inspectItem) {
  744. inspectItem.price = parseFloat(unrecyclableItem.total) || 0
  745. // 同步质检级别
  746. if (inspectItem.commonOrderList && inspectItem.commonOrderList.length > 0) {
  747. const itemWithLevel = inspectItem.commonOrderList.find(item => item.qualityLevel)
  748. if (itemWithLevel) {
  749. inspectItem.qualityLevel = itemWithLevel.qualityLevel
  750. }
  751. }
  752. }
  753. })
  754. }
  755. }
  756. }
  757. </script>
  758. <style lang="scss" scoped>
  759. .inspect-result-container {
  760. min-height: 100vh;
  761. background: #f8f8f8;
  762. display: flex;
  763. flex-direction: column;
  764. &.popup-open {
  765. overflow: hidden;
  766. height: 100vh;
  767. }
  768. }
  769. .nav-bar {
  770. display: flex;
  771. align-items: center;
  772. height: calc(150rpx + var(--status-bar-height));
  773. padding: 0 32rpx;
  774. padding-top: var(--status-bar-height);
  775. background: #fff;
  776. position: fixed;
  777. top: 0;
  778. left: 0;
  779. right: 0;
  780. z-index: 999;
  781. box-sizing: border-box;
  782. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
  783. .back {
  784. padding: 22rpx;
  785. margin-left: -22rpx;
  786. }
  787. .nav-title {
  788. flex: 1;
  789. text-align: center;
  790. font-size: 32rpx;
  791. font-weight: 500;
  792. color: #222;
  793. }
  794. .nav-icons {
  795. display: flex;
  796. align-items: center;
  797. gap: 12px;
  798. }
  799. }
  800. .main-content {
  801. margin-top: calc(200rpx + var(--status-bar-height));
  802. display: flex;
  803. flex-direction: column;
  804. background: none;
  805. padding-bottom: 240rpx;
  806. }
  807. .result-card {
  808. background: #fff;
  809. border-radius: 48rpx;
  810. box-shadow: 0 8rpx 48rpx rgba(0, 0, 0, 0.06);
  811. margin: 0 24rpx 24rpx 24rpx;
  812. padding: 48rpx 48rpx 0 48rpx;
  813. }
  814. .card-title {
  815. font-size: 32rpx;
  816. font-weight: bold;
  817. color: #222;
  818. margin-bottom: 36rpx;
  819. }
  820. .card-title-row {
  821. display: flex;
  822. align-items: center;
  823. justify-content: space-between;
  824. margin-bottom: 36rpx;
  825. }
  826. .status-tag {
  827. background: #fff7e6;
  828. color: #ffb400;
  829. font-size: 24rpx;
  830. border-radius: 24rpx;
  831. padding: 4rpx 26rpx;
  832. font-weight: 400;
  833. height: 44rpx;
  834. display: flex;
  835. align-items: center;
  836. }
  837. .result-row,
  838. .result-group {
  839. margin-bottom: 36rpx;
  840. }
  841. .row-main {
  842. display: flex;
  843. align-items: center;
  844. .goods-name-section {
  845. display: flex;
  846. flex-direction: column;
  847. margin-right: 8px;
  848. .goods-name {
  849. font-size: 32rpx;
  850. font-weight: bold;
  851. color: #333;
  852. line-height: 1.3;
  853. }
  854. .goods-brand {
  855. font-size: 26rpx;
  856. color: #666;
  857. font-weight: normal;
  858. line-height: 1.3;
  859. margin-top: 4rpx;
  860. }
  861. .goods-style-price {
  862. font-size: 26rpx;
  863. color: #ff6b35;
  864. font-weight: 600;
  865. line-height: 1.3;
  866. margin-top: 4rpx;
  867. }
  868. }
  869. .row-name {
  870. font-size: 26rpx;
  871. font-weight: bold;
  872. color: #222;
  873. margin-right: 8px;
  874. }
  875. .row-count {
  876. font-size: 13px;
  877. color: #888;
  878. margin-right: 8px;
  879. }
  880. .row-total {
  881. font-size: 26rpx;
  882. color: #222;
  883. font-weight: bold;
  884. margin-left: auto;
  885. }
  886. }
  887. .price-input-row {
  888. display: flex;
  889. align-items: center;
  890. margin-top: 24rpx;
  891. margin-bottom: 16rpx;
  892. .price-label {
  893. font-size: 26rpx;
  894. color: #888;
  895. min-width: 160rpx;
  896. flex-shrink: 0;
  897. }
  898. .price-input {
  899. flex: 1;
  900. height: 80rpx;
  901. border-radius: 24rpx;
  902. background: #f6f6f6;
  903. border: none;
  904. font-size: 32rpx;
  905. color: #222;
  906. padding: 0 32rpx;
  907. margin-left: 24rpx;
  908. font-weight: bold;
  909. }
  910. }
  911. .row-reason {
  912. display: flex;
  913. align-items: center;
  914. margin-top: 16rpx;
  915. .reason-label {
  916. font-size: 26rpx;
  917. color: #bbb;
  918. min-width: 160rpx;
  919. max-width: 240rpx;
  920. overflow: hidden;
  921. text-overflow: ellipsis;
  922. white-space: nowrap;
  923. }
  924. .reason-input {
  925. flex: 1;
  926. height: 72rpx;
  927. border-radius: 24rpx;
  928. background: #f6f6f6;
  929. border: none;
  930. font-size: 26rpx;
  931. color: #222;
  932. padding-left: 24rpx;
  933. margin-left: 16rpx;
  934. }
  935. .reason-select {
  936. flex: 1;
  937. display: flex;
  938. align-items: center;
  939. height: 72rpx;
  940. border-radius: 24rpx;
  941. background: #f6f6f6;
  942. font-size: 26rpx;
  943. color: #bbb;
  944. padding-left: 24rpx;
  945. margin-left: 16rpx;
  946. justify-content: flex-end;
  947. }
  948. .reason-placeholder {
  949. color: #bbb;
  950. font-size: 26rpx;
  951. flex-shrink: 0;
  952. &.selected {
  953. color: #52c41a;
  954. font-weight: 500;
  955. }
  956. }
  957. }
  958. .info-card {
  959. background: #fff;
  960. border-radius: 48rpx;
  961. box-shadow: 0 8rpx 48rpx rgba(0, 0, 0, 0.06);
  962. }
  963. .info-row {
  964. display: flex;
  965. align-items: center;
  966. margin-bottom: 32rpx;
  967. .info-label {
  968. font-size: 26rpx;
  969. color: #bbb;
  970. min-width: 160rpx;
  971. }
  972. .info-value {
  973. font-size: 26rpx;
  974. color: #222;
  975. margin-left: 16rpx;
  976. }
  977. .copy-btn {
  978. color: #ffb400;
  979. margin-left: 16rpx;
  980. }
  981. }
  982. .footer-btns {
  983. position: fixed;
  984. left: 0;
  985. right: 0;
  986. bottom: 0;
  987. background: #fff;
  988. display: flex;
  989. gap: 32rpx;
  990. padding: 24rpx 32rpx 48rpx 32rpx;
  991. z-index: 101;
  992. .btn-outline {
  993. flex: 1;
  994. height: 80rpx;
  995. border-radius: 32rpx;
  996. border: 2rpx solid #ffe09a;
  997. color: #ffb400;
  998. background: #fff0d2;
  999. font-size: 30rpx;
  1000. font-weight: 500;
  1001. box-shadow: none;
  1002. padding: 0 36rpx;
  1003. }
  1004. .btn-main {
  1005. flex: 1;
  1006. height: 80rpx;
  1007. border-radius: 32rpx;
  1008. background: linear-gradient(90deg, #ffd01e 0%, #ffac04 100%);
  1009. color: #fff;
  1010. border: none;
  1011. font-size: 30rpx;
  1012. font-weight: 500;
  1013. box-shadow: none;
  1014. padding: 0 36rpx;
  1015. }
  1016. }
  1017. .reason-popup-wrapper {
  1018. z-index: 10000 !important;
  1019. }
  1020. .reason-popup-wrapper .uni-popup__wrapper {
  1021. z-index: 10000 !important;
  1022. }
  1023. .reason-popup {
  1024. background: #fff;
  1025. border-radius: 48rpx 48rpx 0 0;
  1026. padding: 0;
  1027. height: 75vh;
  1028. position: relative;
  1029. z-index: 9999;
  1030. .popup-header {
  1031. display: flex;
  1032. align-items: center;
  1033. justify-content: center;
  1034. height: 120rpx;
  1035. border-bottom: 2rpx solid #f0f0f0;
  1036. position: relative;
  1037. .popup-close {
  1038. position: absolute;
  1039. left: 40rpx;
  1040. color: #666;
  1041. font-size: 32rpx;
  1042. }
  1043. .popup-title {
  1044. font-size: 36rpx;
  1045. font-weight: 600;
  1046. color: #222;
  1047. }
  1048. }
  1049. .popup-content {
  1050. height: calc(75vh - 120rpx - 164rpx);
  1051. overflow-y: auto;
  1052. }
  1053. .popup-section {
  1054. padding: 0 40rpx 48rpx 40rpx;
  1055. &:first-child {
  1056. padding-top: 48rpx;
  1057. }
  1058. .section-label {
  1059. font-size: 32rpx;
  1060. color: #222;
  1061. margin-bottom: 32rpx;
  1062. display: block;
  1063. font-weight: 500;
  1064. }
  1065. .level-options {
  1066. display: flex;
  1067. gap: 24rpx;
  1068. margin-bottom: 32rpx;
  1069. .level-option {
  1070. width: 80rpx;
  1071. height: 80rpx;
  1072. border-radius: 16rpx;
  1073. border: 4rpx solid #ddd;
  1074. display: flex;
  1075. align-items: center;
  1076. justify-content: center;
  1077. background: #fff;
  1078. cursor: pointer;
  1079. transition: all 0.2s;
  1080. &.selected {
  1081. border-color: #ffb400;
  1082. background: #ffb400;
  1083. .level-text {
  1084. color: #fff;
  1085. font-weight: bold;
  1086. }
  1087. }
  1088. .level-text {
  1089. font-size: 32rpx;
  1090. color: #666;
  1091. font-weight: 500;
  1092. }
  1093. }
  1094. }
  1095. .custom-reason-input {
  1096. width: 100%;
  1097. min-height: 160rpx;
  1098. padding: 24rpx;
  1099. border: 2rpx solid #e0e0e0;
  1100. border-radius: 16rpx;
  1101. font-size: 28rpx;
  1102. color: #222;
  1103. background: #fff;
  1104. resize: none;
  1105. box-sizing: border-box;
  1106. &::placeholder {
  1107. color: #999;
  1108. }
  1109. &:focus {
  1110. border-color: #ffb400;
  1111. outline: none;
  1112. }
  1113. }
  1114. .img-list {
  1115. display: flex;
  1116. flex-wrap: wrap;
  1117. gap: 20rpx;
  1118. margin-bottom: 64rpx;
  1119. .img-item {
  1120. width: 150rpx;
  1121. height: 150rpx;
  1122. border-radius: 24rpx;
  1123. background: #f8f8f8;
  1124. position: relative;
  1125. .img {
  1126. width: 100%;
  1127. height: 100%;
  1128. border-radius: 24rpx;
  1129. object-fit: cover;
  1130. }
  1131. .img-del {
  1132. position: absolute;
  1133. top: -12rpx;
  1134. right: -12rpx;
  1135. width: 48rpx;
  1136. height: 48rpx;
  1137. background: rgba(0, 0, 0, 0.6);
  1138. color: #fff;
  1139. border-radius: 50%;
  1140. text-align: center;
  1141. line-height: 48rpx;
  1142. font-size: 32rpx;
  1143. font-weight: bold;
  1144. }
  1145. &.add {
  1146. display: flex;
  1147. align-items: center;
  1148. justify-content: center;
  1149. background: #f8f8f8;
  1150. border: 4rpx dashed #ddd;
  1151. color: #bbb;
  1152. }
  1153. }
  1154. }
  1155. .reason-row {
  1156. display: flex;
  1157. align-items: center;
  1158. padding: 32rpx 0;
  1159. border-bottom: 2rpx solid #f0f0f0;
  1160. &:last-child {
  1161. border-bottom: none;
  1162. }
  1163. .checkbox {
  1164. width: 40rpx;
  1165. height: 40rpx;
  1166. border-radius: 8rpx;
  1167. border: 4rpx solid #ddd;
  1168. margin-right: 24rpx;
  1169. background: #fff;
  1170. position: relative;
  1171. &.checked {
  1172. border-color: #ffb400;
  1173. background: #ffb400;
  1174. &::after {
  1175. content: '✓';
  1176. position: absolute;
  1177. top: 50%;
  1178. left: 50%;
  1179. transform: translate(-50%, -50%);
  1180. color: #fff;
  1181. font-size: 24rpx;
  1182. font-weight: bold;
  1183. }
  1184. }
  1185. }
  1186. .reason-text {
  1187. font-size: 32rpx;
  1188. color: #222;
  1189. line-height: 1.4;
  1190. }
  1191. }
  1192. }
  1193. .popup-save-btn {
  1194. position: fixed;
  1195. bottom: 68rpx;
  1196. left: 40rpx;
  1197. right: 40rpx;
  1198. height: 96rpx;
  1199. border-radius: 48rpx;
  1200. background: linear-gradient(90deg, #ffd01e 0%, #ffac04 100%);
  1201. color: #fff;
  1202. font-size: 36rpx;
  1203. font-weight: 600;
  1204. border: none;
  1205. display: flex;
  1206. align-items: center;
  1207. justify-content: center;
  1208. box-shadow: 0 8rpx 24rpx rgba(255, 172, 4, 0.3);
  1209. }
  1210. }
  1211. </style>