releaseProduct.vue 31 KB

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