releaseProduct.vue 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289
  1. <template>
  2. <view class="container">
  3. <view class="form-container">
  4. <up-form labelPosition="left" :model="formData" :rules="rules" ref="formRef" :labelWidth="100">
  5. <!-- 商品分类 -->
  6. <view class="card-title" v-if="!isProductCenter">商品分类</view>
  7. <up-form-item class="form-item" label="商品分类" prop="categoryIds" :borderBottom="false"
  8. @click="showCategory = true" :required="true" v-if="!isProductCenter">
  9. <up-input v-model="formData.categoryDisplayName" disabled disabledColor="#ffffff"
  10. placeholder="请选择商品分类" inputAlign="right" border="none"></up-input>
  11. <template #right>
  12. <up-icon name="arrow-right"></up-icon>
  13. </template>
  14. </up-form-item>
  15. <!-- 商品信息 -->
  16. <view class="card-title">商品信息</view>
  17. <up-form-item label="商品名称" prop="storeName" :borderBottom="false" :required="true">
  18. <up-input v-model="formData.storeName" placeholder="请输入商品名称" inputAlign="right"
  19. border="none"></up-input>
  20. </up-form-item>
  21. <up-form-item label="商品关键字" prop="keyword" :borderBottom="false" :required="true"
  22. v-if="!isProductCenter">
  23. <up-input v-model="formData.keyword" placeholder="请输入商品关键字" inputAlign="right"
  24. border="none"></up-input>
  25. </up-form-item>
  26. <up-form-item label="商品简介" prop="storeInfo" :borderBottom="false" :required="true"
  27. v-if="!isProductCenter">
  28. <up-textarea v-model="formData.storeInfo" placeholder="请输入商品简介" inputAlign="right"
  29. border="none"></up-textarea>
  30. </up-form-item>
  31. <up-form-item label="单位" prop="unitName" :borderBottom="false" :required="true" v-if="!isProductCenter">
  32. <up-input v-model="formData.unitName" placeholder="请输入单位" inputAlign="right"
  33. border="none"></up-input>
  34. </up-form-item>
  35. <!-- 图片上传 -->
  36. <view class="upload-section" v-if="!isProductCenter">
  37. <view class="upload-item">
  38. <view class="upload-label"><text class="required">*</text>商品封面图</view>
  39. <up-upload @afterRead="async (e) => {
  40. await afterRead(e);
  41. getImage();
  42. }" @delete="onPreviewImageDelete" name="preview" max-count="1" :fileList="previewImages">
  43. <view class="upload-btn">
  44. <up-icon name="plus" size="20" color="#ccc"></up-icon>
  45. <text class="upload-tip">点击上传</text>
  46. </view>
  47. </up-upload>
  48. <text class="format-tip">支持上传PNG、JPG格式的图片,每张不超过5MB。</text>
  49. </view>
  50. </view>
  51. <view class="upload-section" v-if="!isProductCenter">
  52. <view class="upload-item">
  53. <view class="upload-label"><text class="required">*</text>商品图片</view>
  54. <up-upload @afterRead="async (e) => {
  55. await afterRead(e);
  56. getImageProduct();
  57. }" @delete="onProductImageDelete" name="product" multiple :maxCount="10" :fileList="productImages">
  58. <view class="upload-btn">
  59. <up-icon name="plus" size="20" color="#ccc"></up-icon>
  60. <text class="upload-tip">点击上传</text>
  61. </view>
  62. </up-upload>
  63. <text class="format-tip">支持上传PNG、JPG格式图片,每张不超过5MB,最多可上传10张。</text>
  64. </view>
  65. </view>
  66. <!-- 商品属性 -->
  67. <view class="card-title">商品属性</view>
  68. <up-form-item label="材质" prop="metalType" :borderBottom="false" :required="true"
  69. v-if="!isProductCenter">
  70. <up-radio-group v-model="formData.metalType" placement="row">
  71. <up-radio :customStyle="{marginRight: '20rpx'}" v-for="(item, index) in materialList"
  72. :key="index" :label="item.name" :name="item.code" activeColor="#F8C008">
  73. </up-radio>
  74. </up-radio-group>
  75. </up-form-item>
  76. <up-form-item class="form-item" label="运费模板" prop="tempIds" :borderBottom="false"
  77. @click="showTemp = true" :required="true">
  78. <up-input v-model="formData.tempName" disabled disabledColor="#ffffff" placeholder="请选择运费模板"
  79. inputAlign="right" border="none"></up-input>
  80. <template #right>
  81. <up-icon name="arrow-right"></up-icon>
  82. </template>
  83. </up-form-item>
  84. <!-- 商品描述 -->
  85. <view class="editor-section" v-if="!isProductCenter">
  86. <view class="editor-header">
  87. <text class="editor-label">商品描述</text>
  88. <text class="word-count">{{ descriptionText.length }}/500</text>
  89. </view>
  90. <sp-editor editorId="editor" :toolbar-config="toolbarConfig" :readOnly="readOnly"
  91. @input="onEditorInput" @upinImage="onUpinImage" @init="onEditorInit"
  92. @overMax="onOverMax"></sp-editor>
  93. </view>
  94. <!-- 商品规格 -->
  95. <view class="pro-cate-container">
  96. <view class="cate-title" v-if="!isProductCenter"><text style="color:red;">*</text>商品规格</view>
  97. <view class="cate-list">
  98. <view class="cate-item">
  99. <view class="cate-pic-line">
  100. <image src="@/static/avator.png" class="cate-pic" mode="widthFix"></image>
  101. <view class="cate-name">
  102. 属性1;属性2
  103. </view>
  104. </view>
  105. <view class="cate-ipt-line">
  106. <up-input class="cate-ipt" style="margin-right: 16rpx;" v-model="formData.laborCost" placeholder="请输入工费"
  107. inputAlign="left" border="none" type="digit" labelWidth="">
  108. <template #suffix>
  109. <text class="unit">元/g</text>
  110. </template>
  111. </up-input>
  112. <up-input class="cate-ipt" v-model="formData.stock" placeholder="请输入库存"
  113. inputAlign="left" border="none" type="number" labelWidth="">
  114. <template #suffix>
  115. <text class="unit">件</text>
  116. </template>
  117. </up-input>
  118. </view>
  119. <view class="cate-ipt-line">
  120. <up-input class="cate-ipt" style="margin-right: 16rpx;" v-model="formData.weight" placeholder="请输入重量"
  121. inputAlign="left" border="none" type="digit" labelWidth="">
  122. <template #suffix>
  123. <text class="unit">g</text>
  124. </template>
  125. </up-input>
  126. <up-input class="cate-ipt" v-model="formData.additionalFee" placeholder="请输入附加金额"
  127. inputAlign="left" border="none" type="number" labelWidth="">
  128. <template #suffix>
  129. <text class="unit">元</text>
  130. </template>
  131. </up-input>
  132. </view>
  133. <view class="cate-ipt-line" style="padding: 0;">
  134. <up-input class="cate-ipt" v-model="formData.barCode" placeholder="请输入商品编号"
  135. inputAlign="left" border="none" type="digit" labelWidth="">
  136. </up-input>
  137. </view>
  138. </view>
  139. </view>
  140. <view class="cate-add" @click="editCatePage">
  141. 管理规格
  142. </view>
  143. </view>
  144. <view class="card-title">商品规格</view>
  145. <up-form-item label="工费" prop="laborCost" :borderBottom="false" :required="true">
  146. <up-input v-model="formData.laborCost" placeholder="请输入工费" inputAlign="right" border="none"
  147. type="digit"></up-input>
  148. <template #right>
  149. <text class="unit">元/g</text>
  150. </template>
  151. </up-form-item>
  152. <up-form-item label="库存" prop="stock" :borderBottom="false" :required="true">
  153. <up-input v-model="formData.stock" placeholder="请输入库存" inputAlign="right" border="none"
  154. type="number"></up-input>
  155. </up-form-item>
  156. <up-form-item label="重量" prop="weight" :borderBottom="false" :required="true">
  157. <up-input v-model="formData.weight" placeholder="请输入重量" inputAlign="right" border="none"
  158. type="digit"></up-input>
  159. <template #right>
  160. <text class="unit">g</text>
  161. </template>
  162. </up-form-item>
  163. <up-form-item label="附加费" prop="additionalFee" :borderBottom="false" :required="true">
  164. <up-input v-model="formData.additionalFee" placeholder="请输入附加费" inputAlign="right" border="none"
  165. type="digit"></up-input>
  166. <template #right>
  167. <text class="unit">元</text>
  168. </template>
  169. </up-form-item>
  170. <up-form-item label="商品编号" prop="barCode" :borderBottom="false" :required="true">
  171. <up-input v-model="formData.barCode" placeholder="请输入商品编号" inputAlign="right" border="none"
  172. type="number"></up-input>
  173. </up-form-item>
  174. <view class="upload-section">
  175. <view class="upload-item">
  176. <view class="upload-label"><text class="required">*</text>规格图片</view>
  177. <up-upload :fileList="productImagesGg" @afterRead="async (e) => {
  178. await afterRead(e);
  179. getImageProductGg();
  180. }" @delete="onProductImageGgDelete" name="productGg" :maxCount="1">
  181. <view class="upload-btn">
  182. <up-icon name="plus" size="20" color="#ccc"></up-icon>
  183. <text class="upload-tip">点击上传</text>
  184. </view>
  185. </up-upload>
  186. <text class="format-tip">支持上传PNG、JPG格式图片,每张不超过5MB,最多可上传1张。</text>
  187. </view>
  188. </view>
  189. <up-form-item label="商品排序" prop="sort" :borderBottom="false">
  190. <up-input v-model="formData.sort" placeholder="请输入排序号" inputAlign="right" border="none"
  191. type="number"></up-input>
  192. </up-form-item>
  193. </up-form>
  194. </view>
  195. <!-- 发布按钮 -->
  196. <view class="btn-view">
  197. <button class="submit" @click="submitForm">立即发布</button>
  198. </view>
  199. <!-- 类目选择弹窗 -->
  200. <up-popup :show="showCategory" @close="showCategory = false" mode="bottom" round="20" :closeable="true">
  201. <view class="popup-content">
  202. <view class="popup-header">
  203. <text class="popup-title">选择商品分类</text>
  204. </view>
  205. <category-selector :categoryList="categoryData" :selectedIds="formData.categoryIds"
  206. @change="onCategoryChange" ref="categoryRef" />
  207. <view class="popup-actions">
  208. <button class="action-btn cancel" @click="showCategory = false">取消</button>
  209. <button class="action-btn confirm" @click="confirmCategory">确定</button>
  210. </view>
  211. </view>
  212. </up-popup>
  213. <!-- 运费模板-->
  214. <up-picker :show="showTemp" v-model="formData.tempIds" :columns="tempColumns" keyName="name" valueName="id"
  215. confirmColor="#F8C008" @close="showTemp = false" @confirm="tempConfirm"
  216. @cancel="showTemp = false"></up-picker>
  217. </view>
  218. </template>
  219. <script setup>
  220. import {
  221. ref,
  222. computed,
  223. watch,
  224. nextTick
  225. } from 'vue';
  226. import {
  227. onShow,
  228. onLoad
  229. } from "@dcloudio/uni-app";
  230. import {
  231. productCategory,
  232. productSave,
  233. productUpdate,
  234. templatesList,
  235. productInfo
  236. } from "@/api/merchant";
  237. import CategorySelector from '@/components/CategorySelector';
  238. import {
  239. useAppStore
  240. } from "@/stores/app";
  241. import {
  242. useImageUpload
  243. } from "@/hooks/useImageUpload";
  244. import {
  245. useToast
  246. } from "@/hooks/useToast";
  247. const {
  248. Toast
  249. } = useToast();
  250. const {
  251. imageList,
  252. afterRead,
  253. deletePic,
  254. uploadLoading
  255. } = useImageUpload({
  256. pid: 1,
  257. model: "product",
  258. });
  259. const appStore = useAppStore();
  260. const merchantInfo = appStore.userInfo.merchant
  261. // 表单验证和提交
  262. const formRef = ref(null);
  263. // 表单数据
  264. const formData = ref({
  265. categoryIds: [], // 选中的分类ID数组
  266. categoryDisplayName: '', // 显示的分类名称
  267. tempName: '', // 显示的运费模板名称
  268. tempIds: [],
  269. storeName: '',
  270. keyword: '',
  271. storeInfo: '',
  272. unitName: '',
  273. metalType: '',
  274. weight: '',
  275. laborCost: '',
  276. additionalFee: '',
  277. sort: '',
  278. content: '',
  279. stock: '',
  280. barCode: ''
  281. });
  282. // 编辑器相关
  283. const editorIns = ref(null);
  284. const readOnly = ref(false);
  285. const descriptionText = ref('');
  286. const toolbarConfig = ref({
  287. iconSize: '20px',
  288. iconColumns: 10,
  289. excludeKeys: ['direction', 'date', 'lineHeight', 'letterSpacing', 'listCheck']
  290. });
  291. // 分类相关
  292. const showCategory = ref(false);
  293. const categoryData = ref([]);
  294. const categoryRef = ref();
  295. const showTemp = ref(false);
  296. const tempColumns = ref([])
  297. // 图片列表
  298. const previewImages = ref([]);
  299. const productImages = ref([]);
  300. const productImagesGg = ref([]);
  301. const productId = ref(null);
  302. const isProductCenter = ref(null);
  303. // 材质列表
  304. const materialList = ref([{
  305. name: '黄金',
  306. code: 'au'
  307. },
  308. {
  309. name: '铂金',
  310. code: 'pt'
  311. },
  312. {
  313. name: '白银',
  314. code: 'ag'
  315. }
  316. ]);
  317. // 验证规则
  318. const rules = ref({
  319. categoryIds: {
  320. type: 'array',
  321. required: true,
  322. message: '请选择商品分类',
  323. trigger: ['blur', 'change']
  324. },
  325. storeName: {
  326. type: 'string',
  327. required: true,
  328. message: '请输入商品名称',
  329. trigger: ['blur', 'change']
  330. },
  331. keyword: {
  332. type: 'string',
  333. required: true,
  334. message: '请输入商品关键字',
  335. trigger: ['blur', 'change']
  336. },
  337. storeInfo: {
  338. type: 'string',
  339. required: true,
  340. message: '请输入商品关键字',
  341. trigger: ['blur', 'change']
  342. },
  343. unitName: {
  344. type: 'string',
  345. required: true,
  346. message: '请输入单位',
  347. trigger: ['blur', 'change']
  348. },
  349. tempIds: {
  350. type: 'array',
  351. required: true,
  352. message: '请选择运费模板',
  353. trigger: ['blur', 'change']
  354. },
  355. metalType: {
  356. type: 'string',
  357. required: true,
  358. message: '请选择材质',
  359. trigger: ['blur', 'change']
  360. },
  361. weight: {
  362. type: 'string',
  363. required: true,
  364. pattern: /^\d+(\.\d+)?$/,
  365. message: '重量必须是数字,可以是小数',
  366. trigger: ['blur', 'change']
  367. },
  368. laborCost: {
  369. type: 'string',
  370. required: true,
  371. pattern: /^\d+(\.\d+)?$/,
  372. message: '工费必须是数字,可以是小数',
  373. trigger: ['blur', 'change']
  374. },
  375. additionalFee: {
  376. type: 'string',
  377. required: true,
  378. pattern: /^\d+(\.\d+)?$/,
  379. message: '附加费必须是数字,可以是小数',
  380. trigger: ['blur', 'change']
  381. },
  382. sort: {
  383. type: 'string',
  384. pattern: /^\d*$/,
  385. message: '排序号必须是整数',
  386. trigger: ['blur', 'change'],
  387. validator: (rule, value) => {
  388. if (!value || value.trim().length === 0) return true; // 可选字段,为空时通过
  389. return /^\d+$/.test(value);
  390. }
  391. },
  392. stock: {
  393. type: 'string',
  394. required: true,
  395. pattern: /^\d*$/,
  396. message: '库存必须是整数',
  397. trigger: ['blur', 'change'],
  398. validator: (rule, value) => {
  399. if (!value || value.trim().length === 0) return false;
  400. return /^\d+$/.test(value);
  401. }
  402. },
  403. barCode: {
  404. type: 'string',
  405. required: true,
  406. pattern: /^\d*$/,
  407. message: '商品编号必须是整数',
  408. trigger: ['blur', 'change'],
  409. validator: (rule, value) => {
  410. if (!value || value.trim().length === 0) return false;
  411. return /^\d+$/.test(value);
  412. }
  413. },
  414. });
  415. // 页面加载
  416. onShow(() => {
  417. })
  418. onLoad(async (options) => {
  419. await getProductCategory();
  420. await getTempData();
  421. console.log(options)
  422. if (options.id) {
  423. productId.value = options.id;
  424. isProductCenter.value = options.isProductCenter || null;
  425. await getProductDetail(options.id);
  426. } else {
  427. resetForm();
  428. }
  429. })
  430. // 获取商品分类
  431. async function getProductCategory() {
  432. let obj = {
  433. type: 1,
  434. status: 1
  435. }
  436. try {
  437. const {
  438. data
  439. } = await productCategory(obj)
  440. console.log('原始分类数据:', data);
  441. const newArr = []
  442. data.forEach((value, index) => {
  443. newArr[index] = value
  444. if (value.child) newArr[index].child = value.child.filter(item => item.status === true)
  445. })
  446. // 过滤商品分类设置为隐藏的子分类不出现在树形列表里
  447. categoryData.value = newArr.filter(item => item.code !== 'bb_mall')
  448. console.log('转换后的分类数据:', categoryData.value);
  449. } catch (error) {
  450. console.error('获取商品分类失败:', error);
  451. uni.showToast({
  452. title: '获取分类失败',
  453. icon: 'none'
  454. });
  455. }
  456. }
  457. // 获取运费模板
  458. async function getTempData() {
  459. let obj = {
  460. page: 1,
  461. limit: 9999
  462. }
  463. try {
  464. const {
  465. data
  466. } = await templatesList()
  467. tempColumns.value[0] = data.list;
  468. } catch (error) {
  469. console.error('获取商品分类失败:', error);
  470. uni.showToast({
  471. title: '获取分类失败',
  472. icon: 'none'
  473. });
  474. }
  475. }
  476. const initFormValidation = async () => {
  477. await nextTick();
  478. if (!formRef.value) {
  479. console.error('表单引用不存在');
  480. return;
  481. }
  482. try {
  483. // 方法1:清除所有验证状态,然后重新验证
  484. formRef.value.clearValidate();
  485. // 方法2:延迟触发字段验证
  486. setTimeout(async () => {
  487. // 逐个触发必填字段的验证
  488. const requiredFields = ['categoryIds', 'storeName', 'keyword', 'storeInfo', 'unitName',
  489. 'tempIds', 'metalType', 'weight', 'laborCost', 'additionalFee',
  490. 'stock', 'barCode'
  491. ];
  492. for (const field of requiredFields) {
  493. try {
  494. await formRef.value.validateField(field);
  495. } catch (error) {
  496. console.log(`⚠️ 字段 ${field} 验证状态:`, error);
  497. }
  498. }
  499. }, 300);
  500. } catch (error) {
  501. console.error('初始化表单验证状态失败:', error);
  502. }
  503. };
  504. // 获取商品详情
  505. async function getProductDetail(id) {
  506. try {
  507. const {
  508. data
  509. } = await productInfo(id)
  510. console.log('获取商品详情:', data);
  511. // 使用Object.assign确保响应式更新
  512. Object.assign(formData.value, {
  513. ...data,
  514. // 确保categoryIds是数组格式
  515. categoryIds: data.cateId ? (Array.isArray(data.cateId) ? data.cateId : data.cateId.split(
  516. ',')) : [],
  517. // 确保tempIds是数组格式
  518. tempIds: data.tempId ? [data.tempId] : []
  519. });
  520. productImages.value = [];
  521. console.log('分类IDs:', formData.value.categoryIds);
  522. console.log('运费模板IDs:', formData.value.tempIds);
  523. // 等待DOM更新
  524. await nextTick();
  525. // 更新显示名称
  526. formData.value.categoryDisplayName = getCategoryDisplayName(formData.value.categoryIds);
  527. formData.value.tempName = formatterTemp(data.tempId);
  528. console.log('分类显示名称:', formData.value.categoryDisplayName);
  529. // 图片处理
  530. previewImages.value = data.image ? [{
  531. url: data.image
  532. }] : [];
  533. // 商品轮播图
  534. if (data.sliderImage) {
  535. try {
  536. const urlArr = typeof data.sliderImage === 'string' ? JSON.parse(data.sliderImage) : data
  537. .sliderImage;
  538. if (urlArr && urlArr.length > 0) {
  539. productImages.value = urlArr.map(url => ({
  540. url
  541. }));
  542. }
  543. } catch (error) {
  544. console.error('解析轮播图失败:', error);
  545. productImages.value = [];
  546. }
  547. }
  548. // 商品属性
  549. if (data.attrValue && data.attrValue.length > 0) {
  550. formData.value.additionalFee = data.attrValue[0].additionalAmount;
  551. formData.value.laborCost = data.attrValue[0].price;
  552. formData.value.barCode = data.attrValue[0].barCode;
  553. formData.value.stock = data.attrValue[0].stock;
  554. formData.value.weight = data.attrValue[0].weight;
  555. // 规格图片
  556. productImagesGg.value = data.attrValue[0].image ? [{
  557. url: data.attrValue[0].image
  558. }] : [];
  559. }
  560. // 商品描述
  561. descriptionText.value = data.content ? data.content.replace(/<[^>]*>/g, '').substring(0, 500) : '';
  562. formData.value.content = data.content || '';
  563. // 设置分类选择器
  564. if (categoryRef.value && formData.value.categoryIds.length > 0) {
  565. // 使用nextTick确保组件已渲染
  566. await nextTick();
  567. if (categoryRef.value.setSelectedIds) {
  568. categoryRef.value.setSelectedIds(formData.value.categoryIds);
  569. }
  570. }
  571. // 重要:手动初始化表单验证状态
  572. await initFormValidation();
  573. } catch (error) {
  574. console.error('获取商品详情失败:', error);
  575. uni.showToast({
  576. title: '获取商品详情失败',
  577. icon: 'none'
  578. });
  579. }
  580. }
  581. // 运费模板id获取中文名
  582. function formatterTemp(id) {
  583. // 假设 tempColumns 是一个数组
  584. const foundItem = tempColumns.value[0].find(item => item.id == id);
  585. return foundItem ? foundItem.name : '';
  586. }
  587. function tempConfirm(obj) {
  588. formData.value.tempId = obj.value[0].id;
  589. formData.value.tempName = obj.value[0].name;
  590. showTemp.value = false;
  591. setTimeout(() => {
  592. if (formRef.value) {
  593. formRef.value.validateField('tempIds');
  594. }
  595. }, 100);
  596. }
  597. // 分类选择变化
  598. const onCategoryChange = (result) => {
  599. console.log('分类选择变化:', result);
  600. // 这里只更新显示,不直接更新表单数据,等用户点击确定
  601. }
  602. // 确认分类选择
  603. const confirmCategory = () => {
  604. if (categoryRef.value) {
  605. const selectedIds = categoryRef.value.getSelectedIds()
  606. if (selectedIds.length === 0) {
  607. uni.showToast({
  608. title: '请至少选择一个分类',
  609. icon: 'none'
  610. })
  611. return
  612. }
  613. // 更新表单数据
  614. formData.value.categoryIds = selectedIds
  615. formData.value.categoryDisplayName = getCategoryDisplayName(selectedIds)
  616. showCategory.value = false
  617. console.log('最终选中的分类ID:', selectedIds)
  618. // 触发校验
  619. setTimeout(() => {
  620. if (formRef.value) {
  621. formRef.value.validateField('categoryIds');
  622. }
  623. }, 100);
  624. }
  625. }
  626. // 根据选中的ID生成显示名称
  627. const getCategoryDisplayName = (selectedIds) => {
  628. if (!selectedIds || selectedIds.length === 0) return ''
  629. const names = []
  630. selectedIds.forEach(id => {
  631. // 检查是否是一级分类(表示全选)
  632. const firstLevel = categoryData.value.find(item => item.id == id)
  633. if (firstLevel) {
  634. // 如果是一级分类,显示"分类名称(全部)"
  635. names.push(`${firstLevel.name}`)
  636. } else {
  637. // 查找二级分类
  638. for (const parent of categoryData.value) {
  639. if (parent.child) {
  640. const secondLevel = parent.child.find(child => child.id == id)
  641. if (secondLevel) {
  642. names.push(`${parent.name}-${secondLevel.name}`)
  643. break
  644. }
  645. }
  646. }
  647. // 如果没有子分类的一级分类
  648. const singleLevel = categoryData.value.find(item =>
  649. !item.child && item.id == id
  650. )
  651. if (singleLevel) {
  652. names.push(singleLevel.name)
  653. }
  654. }
  655. })
  656. return names.join('、')
  657. }
  658. async function getImage() {
  659. console.log(imageList.value)
  660. if (imageList.value.length > 0) {
  661. if (imageList.value[0].status == "success") {
  662. previewImages.value = imageList.value;
  663. console.log('previewImages.value', previewImages.value)
  664. // change();
  665. } else {
  666. Toast({
  667. title: "上传失败"
  668. });
  669. }
  670. }
  671. imageList.value = [];
  672. }
  673. async function getImageProduct() {
  674. if (imageList.value.length > 0) {
  675. if (imageList.value[0].status == "success") {
  676. productImages.value = [...productImages.value, ...imageList.value];
  677. // change();
  678. } else {
  679. Toast({
  680. title: "上传失败"
  681. });
  682. }
  683. }
  684. imageList.value = [];
  685. }
  686. async function getImageProductGg() {
  687. if (imageList.value.length > 0) {
  688. if (imageList.value[0].status == "success") {
  689. productImagesGg.value = imageList.value;
  690. // change();
  691. } else {
  692. Toast({
  693. title: "上传失败"
  694. });
  695. }
  696. }
  697. imageList.value = [];
  698. }
  699. const onPreviewImageDelete = (e) => {
  700. previewImages.value.splice(e.index, 1);
  701. };
  702. const onProductImageDelete = (e) => {
  703. productImages.value.splice(e.index, 1);
  704. };
  705. const onProductImageGgDelete = (e) => {
  706. productImagesGg.value.splice(e.index, 1);
  707. };
  708. // 编辑器相关方法
  709. const onEditorInput = (e) => {
  710. descriptionText.value = e.text || '';
  711. formData.value.content = e.html || '';
  712. };
  713. const onEditorInit = (editor) => {
  714. editorIns.value = editor;
  715. };
  716. const onOverMax = (e) => {
  717. uni.showToast({
  718. title: '内容长度超出限制',
  719. icon: 'none'
  720. });
  721. };
  722. const onUpinImage = (tempFiles, editorCtx) => {
  723. const filePath = tempFiles[0].tempFilePath || tempFiles[0].path;
  724. editorCtx.insertImage({
  725. src: filePath,
  726. width: '80%',
  727. success: () => {
  728. uni.showToast({
  729. title: '图片插入成功',
  730. icon: 'success'
  731. });
  732. }
  733. });
  734. };
  735. const validateForm = () => {
  736. try {
  737. if (!isProductCenter.value) {
  738. // 检查图片上传
  739. if (previewImages.value.length === 0) {
  740. uni.showToast({
  741. title: '请上传商品预览图',
  742. icon: 'none'
  743. });
  744. return;
  745. }
  746. if (productImages.value.length === 0) {
  747. uni.showToast({
  748. title: '请上传商品图片',
  749. icon: 'none'
  750. });
  751. return;
  752. }
  753. if (formData.value.sort && !/^\d+$/.test(formData.value.sort)) {
  754. uni.showToast({
  755. title: '排序号必须是整数',
  756. icon: 'none'
  757. });
  758. return;
  759. }
  760. }
  761. if (productImagesGg.value.length === 0) {
  762. uni.showToast({
  763. title: '请上传商品规格图片',
  764. icon: 'none'
  765. });
  766. return;
  767. }
  768. // 验证数字字段
  769. if (!/^\d+(\.\d+)?$/.test(formData.value.weight)) {
  770. uni.showToast({
  771. title: '重量格式不正确',
  772. icon: 'none'
  773. });
  774. return;
  775. }
  776. if (!/^\d+(\.\d+)?$/.test(formData.value.laborCost)) {
  777. uni.showToast({
  778. title: '工费格式不正确',
  779. icon: 'none'
  780. });
  781. return;
  782. }
  783. if (!/^\d+(\.\d+)?$/.test(formData.value.additionalFee)) {
  784. uni.showToast({
  785. title: '附加费格式不正确',
  786. icon: 'none'
  787. });
  788. return;
  789. }
  790. if (!/^\d+$/.test(formData.value.stock)) {
  791. uni.showToast({
  792. title: '库存必须是整数',
  793. icon: 'none'
  794. });
  795. return;
  796. }
  797. if (!/^\d+$/.test(formData.value.barCode)) {
  798. uni.showToast({
  799. title: '商品编号必须是整数',
  800. icon: 'none'
  801. });
  802. return;
  803. }
  804. const valid = formRef.value.validate();
  805. return valid;
  806. } catch (error) {
  807. console.error('表单验证失败:', error);
  808. return false;
  809. }
  810. };
  811. const submitForm = async () => {
  812. const valid = validateForm();
  813. if (valid) {
  814. const submitData = {
  815. ...formData.value
  816. };
  817. let urlString = '';
  818. if (isProductCenter.value) {
  819. submitData.image = previewImages.value[0].url;
  820. urlString = JSON.stringify(productImages.value.map(item => item.url));
  821. } else {
  822. submitData.image = previewImages.value[0].info.url;
  823. urlString = JSON.stringify(productImages.value.map(item => item.info.url));
  824. }
  825. submitData.sliderImage = urlString;
  826. submitData.cateId = formData.value.categoryIds.join(',');
  827. submitData.merchantId = parseInt(merchantInfo.id);
  828. submitData.specType = 0;
  829. submitData.isSub = false;
  830. submitData.attr = [{
  831. "attrName": "规格",
  832. "attrValues": "默认",
  833. "id": 0
  834. }];
  835. submitData.attrValue = [{
  836. additionalAmount: formData.value.additionalFee,
  837. attrValue: "{\"规格\":\"默认\"}",
  838. barCode: formData.value.barCode,
  839. image: productImagesGg.value[0]?.info?.url || productImagesGg.value[0].url,
  840. price: formData.value.laborCost,
  841. stock: formData.value.stock,
  842. weight: formData.value.weight
  843. }];
  844. if (productId.value && !isProductCenter.value) {
  845. const {
  846. data
  847. } = await productUpdate(submitData);
  848. uni.showToast({
  849. title: '修改成功',
  850. icon: 'success'
  851. });
  852. } else {
  853. const {
  854. data
  855. } = await productSave(submitData);
  856. uni.showToast({
  857. title: '发布成功',
  858. icon: 'success'
  859. });
  860. }
  861. uni.navigateTo({
  862. url: '/pages/merchantCenters/productManagement'
  863. })
  864. }
  865. }
  866. // 重置表单(用于新建场景)
  867. const resetForm = () => {
  868. formData.value = {
  869. categoryIds: [],
  870. categoryDisplayName: '',
  871. tempName: '',
  872. tempIds: [],
  873. storeName: '',
  874. keyword: '',
  875. storeInfo: '',
  876. unitName: '',
  877. metalType: '',
  878. weight: '',
  879. laborCost: '',
  880. additionalFee: '',
  881. sort: '',
  882. content: '',
  883. stock: '',
  884. barCode: ''
  885. };
  886. previewImages.value = [];
  887. productImages.value = [];
  888. productImagesGg.value = [];
  889. descriptionText.value = '';
  890. // 清除校验状态
  891. if (formRef.value) {
  892. formRef.value.clearValidate();
  893. }
  894. }
  895. const editCatePage = ()=>{
  896. uni.navigateTo({
  897. url: "/pages/merchantCenters/productCate/productCate"
  898. })
  899. }
  900. </script>
  901. <style scoped lang="scss">
  902. .container {
  903. background-color: #f9f7f0;
  904. min-height: 100vh;
  905. padding-bottom: 140rpx;
  906. }
  907. .form-container {
  908. padding: 16rpx;
  909. }
  910. .upload-section {
  911. margin-top: 20rpx;
  912. margin-bottom: 20rpx;
  913. background: white;
  914. border-radius: 16rpx;
  915. padding: 30rpx;
  916. }
  917. .upload-item {
  918. margin-bottom: 40rpx;
  919. &:last-child {
  920. margin-bottom: 0;
  921. }
  922. }
  923. .upload-label {
  924. display: block;
  925. font-size: 28rpx;
  926. color: #333;
  927. margin-bottom: 20rpx;
  928. position: relative;
  929. }
  930. .required {
  931. position: absolute;
  932. left: -9px;
  933. color: #f56c6c;
  934. line-height: 20px;
  935. font-size: 20px;
  936. top: 3px;
  937. }
  938. .upload-btn {
  939. display: flex;
  940. flex-direction: column;
  941. align-items: center;
  942. justify-content: center;
  943. width: 200rpx;
  944. height: 200rpx;
  945. border: 2rpx dashed #ccc;
  946. border-radius: 12rpx;
  947. background: #fafafa;
  948. }
  949. .upload-tip {
  950. font-size: 24rpx;
  951. color: #999;
  952. margin-top: 10rpx;
  953. }
  954. .format-tip {
  955. display: block;
  956. font-size: 24rpx;
  957. color: #999;
  958. margin-top: 15rpx;
  959. }
  960. .card-title {
  961. font-size: 36rpx;
  962. color: #333;
  963. line-height: 60rpx;
  964. margin-bottom: 20rpx;
  965. }
  966. :deep(.u-form-item) {
  967. background-color: #fff;
  968. border-radius: 16rpx;
  969. padding: 8rpx 30rpx;
  970. box-sizing: border-box;
  971. margin-bottom: 20rpx;
  972. }
  973. :deep(.u-form-item__body__left__content__label) {
  974. font-size: 28rpx !important;
  975. color: #333 !important;
  976. }
  977. :deep(.u-radio-group--row) {
  978. justify-content: flex-end;
  979. }
  980. .btn-view {
  981. position: fixed;
  982. bottom: 0;
  983. left: 0;
  984. right: 0;
  985. background-color: #FFF;
  986. padding: 20rpx 30rpx;
  987. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
  988. z-index: 5;
  989. }
  990. .submit {
  991. height: 88rpx;
  992. line-height: 88rpx;
  993. background: #F8C008;
  994. border-radius: 16rpx;
  995. font-size: 32rpx;
  996. color: #333333;
  997. border: none;
  998. width: 100%;
  999. &:active {
  1000. opacity: 0.8;
  1001. }
  1002. }
  1003. .unit {
  1004. color: #999;
  1005. font-size: 28rpx;
  1006. margin-left: 10rpx;
  1007. }
  1008. /* 编辑器样式 */
  1009. .editor-section {
  1010. background: white;
  1011. border-radius: 16rpx;
  1012. margin-bottom: 20rpx;
  1013. overflow: hidden;
  1014. }
  1015. .editor-header {
  1016. display: flex;
  1017. justify-content: space-between;
  1018. align-items: center;
  1019. padding: 30rpx;
  1020. border-bottom: 1rpx solid #f0f0f0;
  1021. }
  1022. .editor-label {
  1023. font-size: 28rpx;
  1024. color: #333;
  1025. font-weight: 500;
  1026. }
  1027. .word-count {
  1028. font-size: 24rpx;
  1029. color: #999;
  1030. }
  1031. :deep(.sp-editor) {
  1032. min-height: 400rpx;
  1033. }
  1034. /* 弹窗样式 */
  1035. .popup-content {
  1036. background: #fff;
  1037. border-radius: 20rpx 20rpx 0 0;
  1038. padding-bottom: env(safe-area-inset-bottom);
  1039. }
  1040. .popup-header {
  1041. padding: 30rpx;
  1042. text-align: center;
  1043. border-bottom: 1rpx solid #f0f0f0;
  1044. position: relative;
  1045. }
  1046. .popup-title {
  1047. font-size: 32rpx;
  1048. font-weight: 600;
  1049. color: #333;
  1050. }
  1051. .popup-actions {
  1052. display: flex;
  1053. padding: 30rpx;
  1054. gap: 20rpx;
  1055. }
  1056. .action-btn {
  1057. flex: 1;
  1058. height: 80rpx;
  1059. line-height: 80rpx;
  1060. border-radius: 12rpx;
  1061. font-size: 28rpx;
  1062. border: none;
  1063. &.cancel {
  1064. background: #f0f0f0;
  1065. color: #666;
  1066. }
  1067. &.confirm {
  1068. background: #F8C008;
  1069. color: #333;
  1070. }
  1071. }
  1072. :deep(.u-tabs__wrapper__nav__line) {
  1073. background-color: #F8C008 !important;
  1074. }
  1075. :deep(.u-form-item__body__right__message) {
  1076. text-align: right;
  1077. }
  1078. :deep(.u-textarea__field) {
  1079. text-align: right;
  1080. }
  1081. .pro-cate-container {
  1082. width: 100%;
  1083. padding: 16rpx;
  1084. background-color: #fff;
  1085. border-radius: 16rpx;
  1086. margin-bottom: 16rpx;
  1087. .cate-title {
  1088. font-size: 28rpx;
  1089. line-height: 44rpx;
  1090. color: #333;
  1091. margin-bottom: 16rpx;
  1092. }
  1093. .cate-add{
  1094. width: 100%;
  1095. height: 88rpx;
  1096. line-height: 88rpx;
  1097. text-align: center;
  1098. border-radius: 16rpx;
  1099. background-color: #FEF8E6;
  1100. color: #F8C008;
  1101. font-size: 32rpx;
  1102. font-weight: bold;
  1103. margin-top: 16rpx;
  1104. }
  1105. .cate-list {
  1106. .cate-item {
  1107. width: 100%;
  1108. background-color: #F9F7F0;
  1109. border-radius: 16rpx;
  1110. padding: 16rpx;
  1111. .cate-pic-line {
  1112. width: 100%;
  1113. height: 100rpx;
  1114. display: flex;
  1115. justify-content: space-between;
  1116. align-items: center;
  1117. margin-bottom: 16rpx;
  1118. .cate-pic {
  1119. width: 100rpx;
  1120. height: 100rpx;
  1121. border-radius: 8rpx;
  1122. background-color: #fff;
  1123. margin-right: 16rpx;
  1124. }
  1125. .cate-name {
  1126. width: 100rpx;
  1127. flex: 1;
  1128. color: #333;
  1129. font-size: 28rpx;
  1130. line-height: 44rpx;
  1131. }
  1132. }
  1133. .cate-ipt-line {
  1134. width: 100%;
  1135. display: flex;
  1136. align-items: center;
  1137. justify-content: space-between;
  1138. flex-wrap: nowrap;
  1139. padding-bottom: 16rpx;
  1140. .cate-ipt {
  1141. width: 48%;
  1142. flex: 1;
  1143. height: 100rpx;
  1144. background-color: #fff;
  1145. border-radius: 16rpx;
  1146. padding: 0 16rpx !important;
  1147. font-size: 28rpx;
  1148. .unit{
  1149. color:#333;
  1150. }
  1151. }
  1152. }
  1153. }
  1154. }
  1155. }
  1156. </style>