releaseProduct.vue 31 KB

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