goods.js 106 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260
  1. define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
  2. Fast.config.openArea = ['80%', '80%'];
  3. var vm, si;
  4. // 选项卡验证器模块
  5. var TabValidator = {
  6. steps: [
  7. {
  8. id: '#basics',
  9. name: '基础信息',
  10. required: ['row[type]', 'row[goods_sn]', 'row[title]', 'row[category_ids]', 'row[image]', 'row[images]'],
  11. conditionalRequired: {
  12. 'row[supplier_id]': function() { return true; },
  13. 'row[inspection_type_id]': function() { return true; }
  14. }
  15. },
  16. {
  17. id: '#skus',
  18. name: '价格库存',
  19. required: [],
  20. customValidation: 'validatePriceStock'
  21. },
  22. {
  23. id: '#delivery',
  24. name: '配送设置',
  25. required: ['row[delivery_type]'],
  26. conditionalRequired: {
  27. 'row[express_freight]': function() {
  28. return $('input[name="row[delivery_type]"]:checked').val() === 'EXPRESS' &&
  29. $('input[name="row[express_type]"]:checked').val() === '2';
  30. },
  31. 'row[express_template_id]': function() {
  32. return $('input[name="row[delivery_type]"]:checked').val() === 'EXPRESS' &&
  33. $('input[name="row[express_type]"]:checked').val() === '3';
  34. }
  35. }
  36. },
  37. {
  38. id: '#detail',
  39. name: '商品详情',
  40. required: [],
  41. optional: true
  42. },
  43. {
  44. id: '#params',
  45. name: '商品参数',
  46. required: [],
  47. optional: true
  48. },
  49. {
  50. id: '#sales',
  51. name: '销售设置',
  52. required: ['row[stock_show_type]', 'row[sales_show_type]']
  53. }
  54. ],
  55. currentStep: 0,
  56. completedSteps: [],
  57. stepValidationResults: {},
  58. init: function() {
  59. this.addValidationStyles();
  60. this.bindNiceValidatorEvents();
  61. this.bindTabEvents();
  62. this.initStepNavigation();
  63. },
  64. bindNiceValidatorEvents: function() {
  65. var self = this;
  66. // 监听 Nice-validator 的验证事件
  67. $(document).on('valid.form', 'form[role="form"]', function(e, obj) {
  68. // 表单验证通过时清除所有选项卡错误状态
  69. self.clearTabErrors();
  70. });
  71. $(document).on('invalid.form', 'form[role="form"]', function(e, obj) {
  72. // 表单验证失败时处理错误
  73. if (obj && obj.errors && obj.errors.length > 0) {
  74. self.handleNiceValidatorErrors(obj.errors);
  75. } else if (obj && obj.element) {
  76. // 单个字段错误
  77. var fieldName = obj.element.name || obj.element.id;
  78. var errorMessage = obj.msg || obj.message || '字段验证失败';
  79. self.handleFieldError(fieldName, errorMessage);
  80. }
  81. });
  82. $(document).on('invalid.field', 'form[role="form"]', function(e, obj) {
  83. // 单个字段验证失败时处理
  84. if (obj && obj.element) {
  85. var fieldName = obj.element.name || obj.element.id;
  86. var errorMessage = obj.msg || obj.message || '字段验证失败';
  87. self.handleFieldError(fieldName, errorMessage);
  88. }
  89. });
  90. // 监听FastAdmin的验证器事件 - 兼容性处理
  91. $(document).on('fa.event.validator', function(e, obj) {
  92. if (obj && obj.errors && obj.errors.length > 0) {
  93. self.handleNiceValidatorErrors(obj.errors);
  94. }
  95. });
  96. },
  97. addValidationStyles: function() {
  98. if ($('#goods-validation-styles').length > 0) return;
  99. var styles = `
  100. <style id="goods-validation-styles">
  101. .nav-tabs > li > a.tab-error { color: #d9534f !important; }
  102. .nav-tabs > li > a .error-icon { color: #d9534f; margin-left: 5px; }
  103. </style>
  104. `;
  105. $('head').append(styles);
  106. },
  107. bindTabEvents: function() {
  108. var self = this;
  109. $('.nav-tabs a[data-toggle="tab"]').on('shown.bs.tab', function(e) {
  110. self.clearTabErrors();
  111. var target = $(e.target).attr('href');
  112. for (var i = 0; i < self.steps.length; i++) {
  113. if (self.steps[i].id === target) {
  114. self.currentStep = i;
  115. self.updateButtons();
  116. break;
  117. }
  118. }
  119. });
  120. $(document).on('invalid.field invalid.form', 'form[data-toggle="validator"]', function(e, data) {
  121. if (data && data.element) {
  122. var fieldName = data.element.name || data.element.id;
  123. var errorMessage = data.message || '字段验证失败';
  124. self.handleValidationError(errorMessage, fieldName);
  125. }
  126. });
  127. },
  128. handleNiceValidatorErrors: function(errors) {
  129. var self = this;
  130. var errorsByTab = {};
  131. // 按选项卡分组错误
  132. for (var i = 0; i < errors.length; i++) {
  133. var error = errors[i];
  134. var fieldName = error.element ? error.element.name : '';
  135. var tabId = self.getTabByFieldName(fieldName);
  136. if (tabId && !errorsByTab[tabId]) {
  137. errorsByTab[tabId] = [];
  138. }
  139. if (tabId) {
  140. errorsByTab[tabId].push({
  141. field: fieldName,
  142. message: error.msg || '验证失败',
  143. element: error.element
  144. });
  145. }
  146. }
  147. // 为每个有错误的选项卡显示错误状态
  148. for (var tabId in errorsByTab) {
  149. var tabErrors = errorsByTab[tabId];
  150. var errorMessages = tabErrors.map(function(err) { return err.message; });
  151. this.showTabError(tabId, errorMessages.join('、'));
  152. }
  153. // 跳转到第一个有错误的选项卡
  154. var firstErrorTab = Object.keys(errorsByTab)[0];
  155. if (firstErrorTab) {
  156. var stepIndex = this.getStepIndexByTabId(firstErrorTab);
  157. if (stepIndex !== -1) {
  158. this.goToStep(stepIndex);
  159. }
  160. }
  161. },
  162. handleFieldError: function(fieldName, errorMessage) {
  163. var tabId = this.getTabByFieldName(fieldName);
  164. if (tabId) {
  165. this.showTabError(tabId, errorMessage);
  166. var stepIndex = this.getStepIndexByTabId(tabId);
  167. if (stepIndex !== -1) {
  168. this.goToStep(stepIndex);
  169. }
  170. }
  171. },
  172. getStepIndexByTabId: function(tabId) {
  173. for (var i = 0; i < this.steps.length; i++) {
  174. if (this.steps[i].id === tabId) {
  175. return i;
  176. }
  177. }
  178. return -1;
  179. },
  180. initStepNavigation: function() {
  181. var self = this;
  182. this.updateButtons();
  183. $('.btn-prev').off('click.step').on('click.step', function() {
  184. if (self.currentStep > 0) {
  185. self.goToStep(self.currentStep - 1);
  186. }
  187. });
  188. $('.btn-next').off('click.step').on('click.step', function() {
  189. // 直接跳转到下一步,无需验证
  190. self.clearTabError(self.steps[self.currentStep].id);
  191. if (self.completedSteps.indexOf(self.currentStep) === -1) {
  192. self.completedSteps.push(self.currentStep);
  193. }
  194. if (self.currentStep < self.steps.length - 1) {
  195. self.goToStep(self.currentStep + 1);
  196. }
  197. });
  198. $('.nav-tabs a[data-toggle="tab"]').off('click.step').on('click.step', function(e) {
  199. e.preventDefault();
  200. var targetTabId = $(this).attr('href');
  201. var targetStepIndex = self.steps.findIndex(function(step) {
  202. return step.id === targetTabId;
  203. });
  204. if (targetStepIndex !== -1) {
  205. // 允许自由切换选项卡,无需验证前置步骤
  206. self.goToStep(targetStepIndex);
  207. }
  208. });
  209. },
  210. goToStep: function(stepIndex) {
  211. if (stepIndex < 0 || stepIndex >= this.steps.length) return;
  212. $('.tab-pane').removeClass('active in');
  213. $(this.steps[stepIndex].id).addClass('active in');
  214. $('.nav-tabs li').removeClass('active');
  215. $('.nav-tabs a[href="' + this.steps[stepIndex].id + '"]').parent().addClass('active');
  216. this.currentStep = stepIndex;
  217. this.updateButtons();
  218. $('html, body').animate({scrollTop: 0}, 300);
  219. },
  220. updateButtons: function() {
  221. var $prevBtn = $('.btn-prev');
  222. var $nextBtn = $('.btn-next');
  223. var $submitBtn = $('.btn-submit');
  224. if (this.currentStep === 0) {
  225. $prevBtn.hide();
  226. } else {
  227. $prevBtn.show();
  228. }
  229. if (this.currentStep === this.steps.length - 1) {
  230. $nextBtn.hide();
  231. $submitBtn.show();
  232. } else {
  233. $nextBtn.show();
  234. $submitBtn.hide();
  235. }
  236. $('.step-current').text(this.currentStep + 1);
  237. $('.step-total').text(this.steps.length);
  238. $('.step-text').text(this.steps[this.currentStep].name);
  239. var progress = ((this.currentStep + 1) / this.steps.length) * 100;
  240. $('.step-progress-bar').css('width', progress + '%');
  241. },
  242. validateCurrentStep: function() {
  243. var step = this.steps[this.currentStep];
  244. var isValid = true;
  245. var errors = [];
  246. var errorFields = [];
  247. step.required.forEach(function(fieldName) {
  248. var fieldResult = Controller.validateField(fieldName);
  249. if (!fieldResult.isValid) {
  250. isValid = false;
  251. errors.push(fieldResult.label);
  252. errorFields.push(fieldResult.element);
  253. }
  254. });
  255. if (step.conditionalRequired) {
  256. for (var fieldName in step.conditionalRequired) {
  257. var condition = step.conditionalRequired[fieldName];
  258. if (condition && typeof condition === 'function' && condition()) {
  259. var fieldResult = Controller.validateField(fieldName);
  260. if (!fieldResult.isValid) {
  261. isValid = false;
  262. errors.push(fieldResult.label);
  263. errorFields.push(fieldResult.element);
  264. }
  265. }
  266. }
  267. }
  268. if (step.customValidation && Controller[step.customValidation]) {
  269. var customResult = Controller[step.customValidation]();
  270. if (!customResult.isValid) {
  271. isValid = false;
  272. errors = errors.concat(customResult.errors);
  273. if (customResult.errorFields) {
  274. errorFields = errorFields.concat(customResult.errorFields);
  275. }
  276. }
  277. }
  278. this.stepValidationResults[this.currentStep] = {
  279. isValid: isValid,
  280. errors: errors,
  281. errorFields: errorFields
  282. };
  283. if (!isValid) {
  284. var message = errors.length > 0 ?
  285. '请完善以下必填项:\n' + errors.join('、') :
  286. '当前步骤存在验证错误,请检查后重试';
  287. Toastr.error(message);
  288. this.highlightErrorFields(step.id, errorFields);
  289. this.showTabError(step.id, message);
  290. }
  291. return isValid;
  292. },
  293. validateAllSteps: function() {
  294. var result = {
  295. isValid: true,
  296. firstErrorStep: -1,
  297. errorSteps: [],
  298. allErrors: {}
  299. };
  300. for (var i = 0; i < this.steps.length; i++) {
  301. var step = this.steps[i];
  302. var stepValid = true;
  303. var stepErrors = [];
  304. step.required.forEach(function(fieldName) {
  305. var fieldResult = Controller.validateField(fieldName);
  306. if (!fieldResult.isValid) {
  307. stepValid = false;
  308. stepErrors.push(fieldResult.label);
  309. }
  310. });
  311. if (step.conditionalRequired) {
  312. for (var fieldName in step.conditionalRequired) {
  313. var condition = step.conditionalRequired[fieldName];
  314. if (condition && typeof condition === 'function' && condition()) {
  315. var fieldResult = Controller.validateField(fieldName);
  316. if (!fieldResult.isValid) {
  317. stepValid = false;
  318. stepErrors.push(fieldResult.label);
  319. }
  320. }
  321. }
  322. }
  323. if (step.customValidation && Controller[step.customValidation]) {
  324. var customResult = Controller[step.customValidation]();
  325. if (!customResult.isValid) {
  326. stepValid = false;
  327. stepErrors = stepErrors.concat(customResult.errors);
  328. }
  329. }
  330. if (!stepValid) {
  331. result.isValid = false;
  332. if (result.firstErrorStep === -1) {
  333. result.firstErrorStep = i;
  334. }
  335. result.errorSteps.push(i);
  336. result.allErrors[i] = {
  337. stepName: step.name,
  338. errors: stepErrors
  339. };
  340. this.showTabError(step.id, stepErrors.join('、'));
  341. }
  342. }
  343. return result;
  344. },
  345. getTabByFieldName: function(fieldName) {
  346. var fieldTabMap = {
  347. // 基础信息tab
  348. 'type': '#basics', 'goods_sn': '#basics', 'title': '#basics', 'sub_title': '#basics', 'subtitle': '#basics',
  349. 'category_id': '#basics', 'category_ids': '#basics', 'brand_id': '#basics',
  350. 'image': '#basics', 'images': '#basics', 'label_ids': '#basics', 'guarantee_ids': '#basics',
  351. 'unit_id': '#basics', 'supplier_id': '#basics', 'inspection_type_id': '#basics', 'weigh': '#basics',
  352. 'online_type': '#basics', 'scheduled_online_time': '#basics',
  353. 'is_auto_offline': '#basics', 'scheduled_offline_time': '#basics',
  354. // 价格库存tab
  355. 'spec_type': '#skus', 'price': '#skus', 'lineation_price': '#skus',
  356. 'cost_price': '#skus', 'stocks': '#skus', 'weight': '#skus', 'volume': '#skus',
  357. 'sku_sn': '#skus', 'skus': '#skus', 'spec': '#skus',
  358. // 配送设置tab
  359. 'delivery_type': '#delivery', 'express_type': '#delivery', 'express_freight': '#delivery',
  360. 'express_template_id': '#delivery',
  361. // 商品详情tab
  362. 'content': '#detail',
  363. // 商品参数tab
  364. 'params': '#params', 'attribute_ids': '#params',
  365. // 销售设置tab
  366. 'stock_show_type': '#sales', 'sales_show_type': '#sales', 'virtual_sales': '#sales',
  367. 'keywords': '#sales', 'description': '#sales', 'is_hot': '#sales'
  368. };
  369. // 清理字段名,支持多种格式
  370. var cleanFieldName = fieldName;
  371. if (fieldName.indexOf('row[') === 0) {
  372. cleanFieldName = fieldName.replace(/^row\[/, '').replace(/\]$/, '');
  373. } else if (fieldName.indexOf('c-') === 0) {
  374. cleanFieldName = fieldName.replace(/^c-/, '');
  375. }
  376. // 处理特殊情况
  377. if (cleanFieldName.indexOf('single-') === 0) {
  378. cleanFieldName = cleanFieldName.replace(/^single-/, '');
  379. }
  380. return fieldTabMap[cleanFieldName] || fieldTabMap[fieldName] || null;
  381. },
  382. showTabError: function(tabSelector, message) {
  383. var $tabLink = $('.nav-tabs a[href="' + tabSelector + '"]');
  384. if ($tabLink.length > 0) {
  385. $tabLink.addClass('tab-error');
  386. if ($tabLink.find('.error-icon').length === 0) {
  387. $tabLink.append(' <i class="fa fa-exclamation-circle error-icon"></i>');
  388. }
  389. if (message) {
  390. $tabLink.attr('title', message).tooltip('destroy').tooltip();
  391. }
  392. }
  393. },
  394. clearTabError: function(tabSelector) {
  395. var $tabLink = $('.nav-tabs a[href="' + tabSelector + '"]');
  396. if ($tabLink.length > 0) {
  397. $tabLink.removeClass('tab-error');
  398. $tabLink.find('.error-icon').remove();
  399. $tabLink.removeAttr('title').tooltip('destroy');
  400. }
  401. },
  402. clearTabErrors: function() {
  403. $('.nav-tabs a').removeClass('tab-error');
  404. $('.nav-tabs .error-icon').remove();
  405. $('.nav-tabs a').removeAttr('title').tooltip('destroy');
  406. },
  407. highlightErrorFields: function(tabId, errorFields) {
  408. if (!tabId) return;
  409. var $tabPane = $(tabId);
  410. if ($tabPane.length === 0) return;
  411. var $fieldsToHighlight = $();
  412. if (errorFields && errorFields.length > 0) {
  413. errorFields.forEach(function(element) {
  414. if (element) {
  415. $fieldsToHighlight = $fieldsToHighlight.add($(element));
  416. }
  417. });
  418. } else {
  419. // 使用FastAdmin默认的错误样式查找字段
  420. $fieldsToHighlight = $tabPane.find('.has-error input, .has-error select, .has-error textarea')
  421. .add($tabPane.find('.form-group.has-error input, .form-group.has-error select, .form-group.has-error textarea'))
  422. .add($tabPane.find('.is-invalid'))
  423. .add($tabPane.find('[aria-invalid="true"]'));
  424. if ($fieldsToHighlight.length === 0) {
  425. $fieldsToHighlight = $tabPane.find('.msg-box:visible').siblings('input, select, textarea');
  426. }
  427. }
  428. // 自动聚焦到第一个错误字段,不添加自定义样式
  429. if ($fieldsToHighlight.length > 0) {
  430. var $firstError = $fieldsToHighlight.first();
  431. setTimeout(function() {
  432. $firstError.focus();
  433. $('html, body').animate({
  434. scrollTop: $firstError.offset().top - 100
  435. }, 300);
  436. }, 100);
  437. }
  438. },
  439. handleValidationError: function(errorMessage, fieldName) {
  440. var targetTab = this.getTabByFieldName(fieldName);
  441. if (targetTab) {
  442. var stepIndex = this.steps.findIndex(function(step) {
  443. return step.id === targetTab;
  444. });
  445. if (stepIndex !== -1) {
  446. this.goToStep(stepIndex);
  447. setTimeout(function() {
  448. TabValidator.highlightErrorFields(targetTab);
  449. }, 300);
  450. }
  451. }
  452. }
  453. };
  454. var Controller = {
  455. index: function () {
  456. Table.api.init({
  457. extend: {
  458. index_url: 'shop/goods/index' + location.search,
  459. add_url: 'shop/goods/add',
  460. edit_url: 'shop/goods/edit?dialog=1',
  461. del_url: 'shop/goods/del',
  462. multi_url: 'shop/goods/multi',
  463. import_url: 'shop/goods/import',
  464. table: 'shop_goods',
  465. }
  466. });
  467. Table.config.dragsortfield = '';
  468. var table = $("#table");
  469. var goodsTypeList = Controller.api.parseConfigJson('goodsTypeList', {});
  470. var specTypeList = Controller.api.parseConfigJson('specTypeList', {});
  471. var statusList = Controller.api.parseConfigJson('statusList', {});
  472. table.bootstrapTable({
  473. url: $.fn.bootstrapTable.defaults.extend.index_url,
  474. pk: 'id',
  475. sortName: 'id',
  476. fixedColumns: true,
  477. fixedRightNumber: 1,
  478. columns: [
  479. [
  480. {checkbox: true},
  481. {field: 'id', title: 'ID', width: 60, sortable: true},
  482. {field: 'image', title: '图片', width: 100, operate: false, events: Table.api.events.image, formatter: Table.api.formatter.image},
  483. {
  484. field: 'title',
  485. title: '商品信息',
  486. width: 300,
  487. operate: 'LIKE',
  488. formatter: Controller.formatGoodsInfo
  489. },
  490. {
  491. field: 'type',
  492. title: '商品类型',
  493. width: 100,
  494. searchList: goodsTypeList,
  495. formatter: Controller.formatGoodsType
  496. },
  497. {
  498. field: 'spec_type',
  499. title: '规格',
  500. width: 80,
  501. searchList: specTypeList,
  502. formatter: Controller.formatSpecType
  503. },
  504. {field: 'price', title: '价格', width: 80, operate: 'BETWEEN', sortable: true},
  505. {field: 'stocks', title: '库存', width: 80, operate: 'BETWEEN', sortable: true},
  506. {field: 'sales', title: '销量', width: 80, operate: 'BETWEEN', sortable: true},
  507. {field: 'views', title: '浏览量', width: 80, operate: 'BETWEEN', sortable: true},
  508. {
  509. field: 'status',
  510. title: '状态',
  511. width: 80,
  512. searchList: statusList,
  513. formatter: Controller.formatGoodsStatus
  514. },
  515. {field: 'createtime', title: '创建时间', width: 120, operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, visible: false},
  516. {field: 'updatetime', title: '更新时间', width: 120, operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, visible: false},
  517. {field: 'brand_id', title: '品牌', visible: false, searchList: $.getJSON("shop/brand/getList")},
  518. {field: 'goods_sn', title: '商品编码', visible: false, operate: 'LIKE'},
  519. {field: 'title', title: '商品标题', visible: false, operate: 'LIKE'},
  520. {
  521. field: 'operate',
  522. title: '操作',
  523. width: 120,
  524. table: table,
  525. events: Table.api.events.operate,
  526. formatter: Table.api.formatter.operate,
  527. buttons: [
  528. {
  529. name: 'edit',
  530. title: '编辑',
  531. classname: 'btn btn-xs btn-success btn-dialog',
  532. icon: 'fa fa-pencil',
  533. url: 'shop/goods/edit'
  534. }
  535. ]
  536. }
  537. ]
  538. ]
  539. });
  540. Table.api.bindevent(table);
  541. },
  542. formatGoodsInfo: function(value, row, index) {
  543. var html = '<div style="width: 280px; max-width: 280px; padding: 8px 0; line-height: 1.4; overflow: hidden; box-sizing: border-box;">';
  544. html += '<div style="width: 100%; max-width: 100%; font-weight: 600; color: #2c3e50; font-size: 14px; line-height: 1.3; margin-bottom: 5px; word-wrap: break-word; word-break: break-all; overflow-wrap: break-word; white-space: normal; overflow: hidden; box-sizing: border-box;" title="' + (value || '') + '">' + (value || '') + '</div>';
  545. html += '<div style="width: 100%; max-width: 100%; font-size: 11px; color: #666; margin-bottom: 3px; line-height: 1.2; overflow: hidden; box-sizing: border-box;">';
  546. html += '<span style="display: inline-block; margin-right: 8px; max-width: 120px; overflow: hidden;">';
  547. html += '<i class="fa fa-barcode" style="margin-right: 3px; color: #999;"></i>';
  548. html += '<span style="word-break: break-all; overflow-wrap: break-word;">' + (row.goods_sn || '-') + '</span>';
  549. html += '</span>';
  550. if (row.brand && row.brand.name) {
  551. html += '<span style="display: inline-block; max-width: 120px; overflow: hidden;">';
  552. html += '<i class="fa fa-tag" style="margin-right: 3px; color: #999;"></i>';
  553. html += '<span style="word-break: break-all; overflow-wrap: break-word;">' + row.brand.name + '</span>';
  554. html += '</span>';
  555. }
  556. html += '</div>';
  557. if (row.subtitle) {
  558. html += '<div style="width: 100%; max-width: 100%; font-size: 11px; color: #999; line-height: 1.3; margin-bottom: 3px; word-wrap: break-word; word-break: break-all; overflow-wrap: break-word; white-space: normal; overflow: hidden; box-sizing: border-box;" title="' + row.subtitle + '">' + row.subtitle + '</div>';
  559. }
  560. if (row.spec_type == 1 && row.default_sku && row.default_sku.sku_attr) {
  561. html += '<div style="width: 100%; max-width: 100%; font-size: 10px; color: #888; margin-top: 4px; padding: 2px 6px; background-color: #f5f5f5; border-radius: 3px; word-wrap: break-word; word-break: break-all; overflow-wrap: break-word; white-space: normal; overflow: hidden; box-sizing: border-box;">';
  562. html += '<i class="fa fa-list-alt" style="margin-right: 3px;"></i>';
  563. html += '默认规格: ' + Controller.formatSkuAttr(row.default_sku.sku_attr);
  564. html += '</div>';
  565. }
  566. if (row.activity_name && row.activity_name.trim() !== '') {
  567. html += '<div style="width: 100%; max-width: 100%; margin-top: 6px; overflow: hidden; box-sizing: border-box;">';
  568. html += '<span style="display: inline-block; font-size: 10px; color: #fff; background-color: #ff6b35; padding: 2px 6px; border-radius: 12px; margin-right: 4px; word-wrap: break-word; word-break: break-all; overflow-wrap: break-word; white-space: normal;">';
  569. html += '<i class="fa fa-fire" style="margin-right: 2px;"></i>限时折扣活动</span>';
  570. html += '<span style="font-size: 11px; color: #ff6b35; font-weight: 600; word-wrap: break-word; word-break: break-all; overflow-wrap: break-word; white-space: normal;">' + row.activity_name + '</span>';
  571. html += '</div>';
  572. }
  573. html += '</div>';
  574. return html;
  575. },
  576. formatGoodsType: function(value, row, index) {
  577. var goodsTypeList = Controller.api.parseConfigJson('goodsTypeList', {});
  578. var text = (goodsTypeList && goodsTypeList[value]) ? goodsTypeList[value] : '未知';
  579. var typeStyles = {
  580. 1: {class: 'label-primary', icon: 'fa-cube'},
  581. 2: {class: 'label-info', icon: 'fa-key'},
  582. 3: {class: 'label-warning', icon: 'fa-cloud'},
  583. 4: {class: 'label-success', icon: 'fa-ticket'}
  584. };
  585. var style = typeStyles[parseInt(value)] || {class: 'label-primary', icon: 'fa-cube'};
  586. return '<span class="label ' + style.class + '"><i class="fa ' + style.icon + '"></i> ' + text + '</span>';
  587. },
  588. formatSpecType: function(value, row, index) {
  589. var specTypeList = Controller.api.parseConfigJson('specTypeList', {});
  590. var label = (specTypeList && specTypeList[value]) ? specTypeList[value] : (value == 1 ? '多规格' : '单规格');
  591. var className = value == 1 ? 'label-info' : 'label-success';
  592. return '<span class="label ' + className + '"><i class="fa ' + (value == 1 ? 'fa-list' : 'fa-square') + '"></i> ' + label + '</span>';
  593. },
  594. formatGoodsStatus: function(value, row, index) {
  595. var statusList = Controller.api.parseConfigJson('statusList', {});
  596. var statusStyles = {
  597. 0: {class: 'label-warning', icon: 'fa-warehouse'},
  598. 1: {class: 'label-success', icon: 'fa-check-circle'},
  599. 2: {class: 'label-danger', icon: 'fa-times-circle'},
  600. 3: {class: 'label-default', icon: 'fa-eye-slash'}
  601. };
  602. var text = statusList && statusList[value] ? statusList[value] : '未知';
  603. var style = statusStyles[value] || {class: 'label-default', icon: 'fa-question'};
  604. return '<span class="label ' + style.class + '"><i class="fa ' + style.icon + '"></i> ' + text + '</span>';
  605. },
  606. select: function () {
  607. var specTypeList = Controller.api.parseConfigJson('specTypeList', {0: "单规格", 1: "多规格"});
  608. var urlParams = new URLSearchParams(window.location.search);
  609. var selectMode = urlParams.get('selectMode') || 'multiple';
  610. var maxSelect = parseInt(urlParams.get('maxSelect')) || 0;
  611. Table.api.init({
  612. extend: {
  613. index_url: 'shop/goods/index' + location.search,
  614. table: 'shop_goods',
  615. }
  616. });
  617. Table.config.dragsortfield = '';
  618. var table = $("#table");
  619. var columns = [
  620. {field: 'id', title: 'ID', width: 60},
  621. {field: 'image', title: '图片', width: 120, operate: false, events: Table.api.events.image, formatter: Table.api.formatter.image},
  622. {
  623. field: 'title',
  624. title: '商品信息',
  625. operate: 'LIKE',
  626. formatter: function (value, row, index) {
  627. var html = '<div>';
  628. html += '<div style="font-weight: bold; margin-bottom: 2px;">' + value + '</div>';
  629. html += '<div style="font-size: 12px; color: #999;">';
  630. html += '编码: ' + (row.goods_sn || '-');
  631. if (row.category && row.category.name) {
  632. html += ' | 分类: ' + row.category.name;
  633. }
  634. html += '</div>';
  635. html += '</div>';
  636. return html;
  637. }
  638. },
  639. {field: 'spec_type', title: '规格', width: 80, searchList: specTypeList, formatter: Table.api.formatter.label},
  640. {
  641. field: 'price',
  642. title: '价格',
  643. width: 100,
  644. operate: 'BETWEEN',
  645. formatter: function (value, row, index) {
  646. return '<span style="color: #e74c3c; font-weight: bold;">¥' + (value || '0.00') + '</span>';
  647. }
  648. },
  649. {field: 'stocks', title: '库存', width: 80},
  650. {
  651. field: 'operate',
  652. title: selectMode === 'single' ? '选择' : '操作',
  653. width: selectMode === 'single' ? 80 : 100,
  654. operate: false,
  655. formatter: function (value, row, index) {
  656. var html = '';
  657. if (selectMode === 'single') {
  658. html = '<button class="btn btn-xs btn-primary btn-select-single" data-id="' + row.id + '" data-index="' + index + '" title="点击选择该商品">' +
  659. '<i class="fa fa-check"></i> 选择</button>';
  660. } else {
  661. html = '<button class="btn btn-xs btn-success btn-select-multi" data-id="' + row.id + '" data-index="' + index + '" data-selected="false" title="点击添加到选择列表">' +
  662. '<i class="fa fa-plus"></i> 选择</button>';
  663. }
  664. return html;
  665. }
  666. },
  667. {field: 'category_id', title: '分类', visible: false, searchList: $.getJSON("shop/category/getList")},
  668. {field: 'goods_sn', title: '商品编码', visible: false, operate: 'LIKE'},
  669. ];
  670. table.bootstrapTable({
  671. url: $.fn.bootstrapTable.defaults.extend.index_url,
  672. pk: 'id',
  673. sortName: 'id',
  674. clickToSelect: false,
  675. columns: [columns]
  676. });
  677. var selectedGoods = [];
  678. $(document).on('click', '.btn-select-single', function () {
  679. var $btn = $(this);
  680. var index = $btn.data('index');
  681. var rowData = table.bootstrapTable('getData')[index];
  682. Fast.api.close([rowData]);
  683. });
  684. $(document).on('click', '.btn-select-multi', function () {
  685. var $btn = $(this);
  686. var id = $btn.data('id');
  687. var index = $btn.data('index');
  688. var isSelected = $btn.data('selected');
  689. var rowData = table.bootstrapTable('getData')[index];
  690. if (!isSelected) {
  691. if (maxSelect > 0 && selectedGoods.length >= maxSelect) {
  692. Toastr.warning('最多只能选择 ' + maxSelect + ' 个商品');
  693. return;
  694. }
  695. selectedGoods.push(rowData);
  696. $btn.removeClass('btn-success').addClass('btn-danger')
  697. .html('<i class="fa fa-minus"></i> 取消选择')
  698. .attr('title', '点击从选择列表中移除')
  699. .data('selected', true);
  700. } else {
  701. selectedGoods = selectedGoods.filter(function(item) {
  702. return item.id != id;
  703. });
  704. $btn.removeClass('btn-danger').addClass('btn-success')
  705. .html('<i class="fa fa-plus"></i> 选择')
  706. .attr('title', '点击添加到选择列表')
  707. .data('selected', false);
  708. }
  709. updateSelectedCount();
  710. });
  711. function updateSelectedCount() {
  712. var count = selectedGoods.length;
  713. var countText = count > 0 ? '已选择 ' + count + ' 个商品' : '请选择商品';
  714. $('.btn-goods-select').text(countText === '请选择商品' ? '确认选择' : '确认选择 (' + count + ')');
  715. if (maxSelect > 0 && count >= maxSelect) {
  716. $('.btn-select-multi[data-selected="false"]').prop('disabled', true).addClass('disabled');
  717. } else {
  718. $('.btn-select-multi[data-selected="false"]').prop('disabled', false).removeClass('disabled');
  719. }
  720. }
  721. $(document).on('click', '.btn-goods-select', function () {
  722. if (selectedGoods.length === 0) {
  723. Layer.alert('请选择商品');
  724. return;
  725. }
  726. Fast.api.close(selectedGoods);
  727. });
  728. Table.api.bindevent(table);
  729. },
  730. recyclebin: function () {
  731. Table.api.init({
  732. extend: {
  733. 'dragsort_url': ''
  734. }
  735. });
  736. var table = $("#table");
  737. table.bootstrapTable({
  738. url: 'shop/goods/recyclebin' + location.search,
  739. pk: 'id',
  740. sortName: 'id',
  741. columns: [
  742. [{
  743. checkbox: true
  744. },
  745. {
  746. field: 'id',
  747. title: __('Id')
  748. },
  749. {
  750. field: 'title',
  751. title: __('Title'),
  752. align: 'left'
  753. },
  754. {
  755. field: 'deletetime',
  756. title: __('Deletetime'),
  757. operate: 'RANGE',
  758. addclass: 'datetimerange',
  759. formatter: Table.api.formatter.datetime
  760. },
  761. {
  762. field: 'operate',
  763. width: '130px',
  764. title: __('Operate'),
  765. table: table,
  766. events: Table.api.events.operate,
  767. buttons: [{
  768. name: 'Restore',
  769. text: __('Restore'),
  770. classname: 'btn btn-xs btn-info btn-ajax btn-restoreit',
  771. icon: 'fa fa-rotate-left',
  772. url: 'shop/goods/restore',
  773. refresh: true
  774. },
  775. {
  776. name: 'Destroy',
  777. text: __('Destroy'),
  778. classname: 'btn btn-xs btn-danger btn-ajax btn-destroyit',
  779. icon: 'fa fa-times',
  780. url: 'shop/goods/destroy',
  781. refresh: true
  782. }
  783. ],
  784. formatter: Table.api.formatter.operate
  785. }
  786. ]
  787. ]
  788. });
  789. Table.api.bindevent(table);
  790. },
  791. getAttribute: function (category_id, attribute_ids = []) {
  792. $.get('shop/attribute/attrs?category_id=' + category_id, function (res) {
  793. const {code, data, msg} = res;
  794. if (code) {
  795. $('#attributes').html(data.length ? Template('attributetpl', {row: data, attribute_ids}) : ' <input name="row[attribute_ids][0][]" type="hidden" value="">');
  796. var instance = $("form[data-toggle='validator']").data("validator");
  797. $('#attributes input').each(function () {
  798. instance._parse(this);
  799. });
  800. } else {
  801. Toastr.error(msg);
  802. }
  803. })
  804. },
  805. add: function () {
  806. var that = this;
  807. $(document).ready(function() {
  808. that.initializeForm();
  809. Controller.api.bindevent();
  810. Controller.api.add_sku();
  811. });
  812. },
  813. edit: function () {
  814. var that = this;
  815. $(document).ready(function() {
  816. that.initializeForm(true);
  817. that.initEditTypeState();
  818. setTimeout(function() {
  819. that.initSingleSpecData();
  820. }, 1000);
  821. Controller.api.bindevent();
  822. Controller.api.add_sku();
  823. });
  824. },
  825. initializeForm: function(isEdit) {
  826. var that = this;
  827. that.initGoodsTypeCards();
  828. that.initOnlineOfflineControl();
  829. that.initContentPreview();
  830. TabValidator.init();
  831. that.watchSingleSpecFormVisibility();
  832. if (isEdit) {
  833. $('#c-category_id, #c-category_ids').on('change', function () {
  834. var category_id = $(this).val();
  835. });
  836. }
  837. },
  838. watchSingleSpecFormVisibility: function() {
  839. var that = this;
  840. if (typeof MutationObserver !== 'undefined') {
  841. var observer = new MutationObserver(function(mutations) {
  842. mutations.forEach(function(mutation) {
  843. var $singleSpecForm = $('#single-spec-form');
  844. if ($singleSpecForm.length > 0 && $singleSpecForm.is(':visible')) {
  845. var $priceField = $('#c-price');
  846. var $stocksField = $('#c-stocks');
  847. if ($priceField.length > 0 && $stocksField.length > 0) {
  848. var currentPrice = $priceField.val();
  849. var currentStocks = $stocksField.val();
  850. if ((!currentPrice || currentPrice === '') &&
  851. (!currentStocks || currentStocks === '') &&
  852. Config.goods && Config.goods.spec_type == '0' &&
  853. Config.goods_skus && Config.goods_skus.length > 0) {
  854. setTimeout(function() {
  855. that.initSingleSpecData();
  856. }, 100);
  857. }
  858. }
  859. }
  860. });
  861. });
  862. observer.observe(document.body, {
  863. childList: true,
  864. subtree: true,
  865. attributes: true,
  866. attributeFilter: ['style', 'class']
  867. });
  868. this._singleSpecObserver = observer;
  869. }
  870. $('a[href="#skus"]').on('shown.bs.tab', function() {
  871. setTimeout(function() {
  872. var specType = $('input[name="row[spec_type]"]:checked').val();
  873. if (specType == '0') {
  874. var $priceField = $('#c-price');
  875. var $stocksField = $('#c-stocks');
  876. if ($priceField.length > 0 && $stocksField.length > 0) {
  877. var currentPrice = $priceField.val();
  878. var currentStocks = $stocksField.val();
  879. if ((!currentPrice || currentPrice === '') &&
  880. (!currentStocks || currentStocks === '')) {
  881. that.initSingleSpecData();
  882. }
  883. }
  884. }
  885. }, 200);
  886. });
  887. },
  888. isMultiSelectEmpty: function(value) {
  889. if (Array.isArray(value)) {
  890. return value.length === 0;
  891. } else if (typeof value === 'string') {
  892. var trimmed = value.trim();
  893. return trimmed === '' || trimmed === '0' || trimmed === 'null' || trimmed === 'undefined';
  894. } else {
  895. return !value || value === null || value === undefined;
  896. }
  897. },
  898. initGoodsTypeCards: function() {
  899. if ($('.goods-type-card').length === 0) {
  900. return;
  901. }
  902. function updateTypeCardState() {
  903. var $cards = $('.goods-type-card');
  904. if ($cards.length > 0) {
  905. $cards.removeClass('selected');
  906. $('.goods-type-card input[type="radio"]:checked').closest('.goods-type-card').addClass('selected');
  907. }
  908. }
  909. var $radioInputs = $('.goods-type-card input[type="radio"]');
  910. if ($radioInputs && $radioInputs.length > 0) {
  911. $radioInputs.off('change.typecard').on('change.typecard', function() {
  912. updateTypeCardState();
  913. });
  914. }
  915. updateTypeCardState();
  916. },
  917. initEditTypeState: function() {
  918. if (Config.goods && Config.goods.type) {
  919. var $typeRadio = $('input[name="row[type]"][value="' + Config.goods.type + '"]');
  920. if ($typeRadio.length > 0) {
  921. $typeRadio.prop('checked', true);
  922. $typeRadio.trigger('change');
  923. }
  924. }
  925. if (Config.goods && Config.goods.online_type !== undefined) {
  926. var $onlineTypeRadio = $('input[name="row[online_type]"][value="' + Config.goods.online_type + '"]');
  927. if ($onlineTypeRadio.length > 0) {
  928. $onlineTypeRadio.prop('checked', true);
  929. $onlineTypeRadio.trigger('change');
  930. }
  931. }
  932. if (Config.goods) {
  933. if (Config.goods.scheduled_online_time) {
  934. $('#c-scheduled_online_time').val(Config.goods.scheduled_online_time);
  935. }
  936. if (Config.goods.is_auto_offline) {
  937. $('#c-is_auto_offline').prop('checked', true);
  938. if (Config.goods.scheduled_offline_time) {
  939. $('#c-scheduled_offline_time').val(Config.goods.scheduled_offline_time);
  940. }
  941. }
  942. }
  943. if (Config.goods && Config.goods.spec_type !== undefined) {
  944. var $specTypeRadio = $('input[name="row[spec_type]"][value="' + Config.goods.spec_type + '"]');
  945. if ($specTypeRadio.length > 0) {
  946. $specTypeRadio.prop('checked', true);
  947. $specTypeRadio.trigger('change');
  948. setTimeout(function() {
  949. $(document).trigger('fa.event.favisible', $specTypeRadio);
  950. }, 100);
  951. }
  952. }
  953. this.initSingleSpecData();
  954. },
  955. initSingleSpecData: function() {
  956. var that = this;
  957. if (Config.goods && Config.goods.spec_type == '0' && Config.goods_skus && Config.goods_skus.length > 0) {
  958. var sku = Config.goods_skus[0];
  959. var fillAttempts = 0;
  960. var maxAttempts = 5;
  961. function attemptFill() {
  962. fillAttempts++;
  963. var $priceField = $('#c-price');
  964. var $stocksField = $('#c-stocks');
  965. if ($priceField.length === 0 || $stocksField.length === 0) {
  966. if (fillAttempts < maxAttempts) {
  967. setTimeout(attemptFill, 200 * fillAttempts);
  968. }
  969. return;
  970. }
  971. try {
  972. $priceField.val(sku.price || '0.01');
  973. $('#c-lineation_price').val(sku.lineation_price || '0.00');
  974. $('#c-cost_price').val(sku.cost_price || '0.00');
  975. $stocksField.val(sku.stocks || '1');
  976. $('#c-weight').val(sku.weight || '0.00');
  977. $('#c-volume').val(sku.volume || '0.00');
  978. $('#c-sku-sn').val(sku.sku_sn || '');
  979. $('#c-single-image').val(sku.image || '');
  980. if (sku.image) {
  981. var $preview = $('#p-single-image');
  982. if ($preview.length > 0) {
  983. var img = '<li><a href="' + Fast.api.cdnurl(sku.image, true) + '" data-url="' + sku.image + '" target="_blank"><img src="' + Fast.api.cdnurl(sku.image, true) + '"/></a></li>';
  984. $preview.html(img);
  985. }
  986. }
  987. $priceField.trigger('change');
  988. $stocksField.trigger('change');
  989. setTimeout(function() {
  990. var currentPrice = $priceField.val();
  991. var currentStocks = $stocksField.val();
  992. if (!currentPrice || currentPrice === '' || !currentStocks || currentStocks === '') {
  993. if (fillAttempts < maxAttempts) {
  994. setTimeout(attemptFill, 300);
  995. }
  996. }
  997. }, 100);
  998. } catch (error) {
  999. if (fillAttempts < maxAttempts) {
  1000. setTimeout(attemptFill, 200 * fillAttempts);
  1001. }
  1002. }
  1003. }
  1004. setTimeout(attemptFill, 300);
  1005. }
  1006. },
  1007. validateField: function(fieldName) {
  1008. var $field = $('[name="' + fieldName + '"]');
  1009. var result = {
  1010. isValid: true,
  1011. label: fieldName,
  1012. element: null,
  1013. message: ''
  1014. };
  1015. if ($field.length > 0) {
  1016. result.element = $field[0];
  1017. var $formGroup = $field.closest('.form-group');
  1018. var $label = $formGroup.find('.control-label');
  1019. if ($label.length > 0) {
  1020. result.label = $label.text().replace(/[::*]/g, '').trim();
  1021. }
  1022. // 使用 FastAdmin 的 Nice-validator 进行验证
  1023. var $form = $field.closest('form');
  1024. var validator = $form.data('validator');
  1025. if (validator && typeof validator.isValid === 'function') {
  1026. // 触发单个字段验证
  1027. var fieldResult = validator.isValid($field[0]);
  1028. if (!fieldResult) {
  1029. result.isValid = false;
  1030. // 获取错误消息
  1031. var $msgBox = $field.closest('.form-group').find('.msg-box');
  1032. if ($msgBox.length > 0) {
  1033. result.message = $msgBox.text() || result.label + '验证失败';
  1034. } else {
  1035. result.message = result.label + '验证失败';
  1036. }
  1037. }
  1038. } else {
  1039. // FastAdmin 的 Nice-validator 可能尚未初始化,使用基础检查
  1040. var value = $field.val();
  1041. var rules = $field.attr('data-rule');
  1042. if (rules && rules.indexOf('required') !== -1) {
  1043. if (!value || value.trim() === '') {
  1044. result.isValid = false;
  1045. result.message = result.label + '不能为空';
  1046. }
  1047. }
  1048. }
  1049. }
  1050. return result;
  1051. },
  1052. validatePriceStock: function() {
  1053. var result = {
  1054. isValid: true,
  1055. errors: [],
  1056. errorFields: []
  1057. };
  1058. var specType = $('input[name="row[spec_type]"]:checked').val();
  1059. if (specType == '0') {
  1060. var price = $('#c-price').val();
  1061. var stocks = $('#c-stocks').val();
  1062. if (!price || parseFloat(price) <= 0) {
  1063. result.isValid = false;
  1064. result.errors.push('销售价格');
  1065. result.errorFields.push(document.getElementById('c-price'));
  1066. }
  1067. if (!stocks || parseInt(stocks) <= 0) {
  1068. result.isValid = false;
  1069. result.errors.push('库存数量');
  1070. result.errorFields.push(document.getElementById('c-stocks'));
  1071. }
  1072. } else if (specType == '1') {
  1073. if (typeof vm !== 'undefined' && vm && vm.tableData && vm.tableData.length > 0) {
  1074. var validSkus = vm.tableData.filter(function(sku) {
  1075. return sku.status == 1 && sku.price > 0 && sku.stocks > 0;
  1076. });
  1077. if (validSkus.length === 0) {
  1078. result.isValid = false;
  1079. result.errors.push('至少需要一个有效的SKU规格(价格>0,库存>0,且状态为显示)');
  1080. }
  1081. } else {
  1082. result.isValid = false;
  1083. result.errors.push('请先设置商品规格');
  1084. }
  1085. }
  1086. return result;
  1087. },
  1088. initOnlineOfflineControl: function() {
  1089. $('input[name="row[online_type]"]').change(function() {
  1090. var value = $(this).val();
  1091. if (value == '3') {
  1092. $('#scheduled-online-time').show();
  1093. } else {
  1094. $('#scheduled-online-time').hide();
  1095. }
  1096. if (value == '1' || value == '3') {
  1097. $('#offline-time-setting').show();
  1098. } else {
  1099. $('#offline-time-setting').hide();
  1100. }
  1101. });
  1102. $('input[name="row[online_type]"]:checked').trigger('change');
  1103. $('#c-is_auto_offline').change(function() {
  1104. if ($(this).is(':checked')) {
  1105. $('#scheduled-offline-time').show();
  1106. } else {
  1107. $('#scheduled-offline-time').hide();
  1108. }
  1109. });
  1110. if ($('#c-is_auto_offline').is(':checked')) {
  1111. $('#scheduled-offline-time').show();
  1112. } else {
  1113. $('#scheduled-offline-time').hide();
  1114. }
  1115. },
  1116. initContentPreview: function() {
  1117. var previewTimer = null;
  1118. var lastContent = "";
  1119. $('a[href="#detail"]').on('shown.bs.tab', function (e) {
  1120. updatePreview();
  1121. if (previewTimer === null) {
  1122. previewTimer = setInterval(function() {
  1123. checkContentChange();
  1124. }, 1000);
  1125. }
  1126. });
  1127. $('a').not('a[href="#detail"]').on('shown.bs.tab', function (e) {
  1128. if (previewTimer) {
  1129. clearInterval(previewTimer);
  1130. previewTimer = null;
  1131. }
  1132. });
  1133. function checkContentChange() {
  1134. var editor = $("#c-content").data("nkeditor");
  1135. if (!editor) return;
  1136. var content = editor.html();
  1137. if (content !== lastContent) {
  1138. lastContent = content;
  1139. updatePreview();
  1140. }
  1141. }
  1142. function updatePreview() {
  1143. var editor = $("#c-content").data("nkeditor");
  1144. if (!editor) return;
  1145. var content = editor.html();
  1146. $('#content-preview').html(content || '<div class="text-center text-muted">暂无内容</div>');
  1147. adjustPreviewHeight();
  1148. }
  1149. function adjustPreviewHeight() {
  1150. var $editorContainer = $('.ke-container');
  1151. if ($editorContainer.length > 0) {
  1152. var editorHeight = $editorContainer.height();
  1153. $('#content-preview').css('height', editorHeight - 40 + 'px');
  1154. }
  1155. }
  1156. setTimeout(function() {
  1157. adjustPreviewHeight();
  1158. updatePreview();
  1159. }, 1000);
  1160. },
  1161. // 解析sku_attr数据的通用方法
  1162. parseSkuAttr: function(skuAttr) {
  1163. if (!skuAttr) {
  1164. return [];
  1165. }
  1166. try {
  1167. if (skuAttr.charAt(0) === '[' || skuAttr.charAt(0) === '{') {
  1168. // JSON格式
  1169. return JSON.parse(skuAttr);
  1170. } else {
  1171. // 字符串格式:规格名:规格值,规格名:规格值
  1172. var attrs = skuAttr.split(',');
  1173. var result = [];
  1174. attrs.forEach(function(attr) {
  1175. var parts = attr.split(':');
  1176. if (parts.length >= 2) {
  1177. result.push({
  1178. name: parts[0].trim(),
  1179. key: parts[0].trim(),
  1180. value: parts[1].trim(),
  1181. type: 'basic'
  1182. });
  1183. }
  1184. });
  1185. return result;
  1186. }
  1187. } catch (e) {
  1188. console.warn('解析sku_attr失败:', skuAttr, e);
  1189. return [];
  1190. }
  1191. },
  1192. // 生成sku_attr数据
  1193. generateSkuAttr: function(specValues) {
  1194. if (!specValues || !Array.isArray(specValues)) {
  1195. return '';
  1196. }
  1197. var attrs = [];
  1198. specValues.forEach(function(value) {
  1199. if (value.name && value.value) {
  1200. attrs.push(value.name + ':' + value.value);
  1201. }
  1202. });
  1203. return attrs.join(',');
  1204. },
  1205. formatSkuAttr: function(skuAttr) {
  1206. if (!skuAttr) {
  1207. return '';
  1208. }
  1209. var attrs = this.parseSkuAttr(skuAttr);
  1210. if (attrs.length > 0) {
  1211. var formatted = [];
  1212. attrs.forEach(function(attr) {
  1213. if (attr.name && attr.value) {
  1214. formatted.push(attr.name + ': ' + attr.value);
  1215. } else if (attr.key && attr.value) {
  1216. formatted.push(attr.key + ': ' + attr.value);
  1217. }
  1218. });
  1219. return formatted.join(' | ');
  1220. }
  1221. return skuAttr;
  1222. },
  1223. api: {
  1224. parseConfigJson: function(configKey, defaultValue) {
  1225. var configValue = Config[configKey] || defaultValue || {};
  1226. if (typeof configValue === 'string') {
  1227. try {
  1228. return JSON.parse(configValue);
  1229. } catch (e) {
  1230. return defaultValue || {};
  1231. }
  1232. }
  1233. return configValue;
  1234. },
  1235. initSpecTypeControl: function() {
  1236. $('input[name="row[spec_type]"]').off('change.spectype').on('change.spectype', function() {
  1237. var specType = $(this).val();
  1238. $(document).trigger('fa.event.favisible');
  1239. if (specType == '0') {
  1240. if (typeof vm !== 'undefined' && vm) {
  1241. // vm.specList = [];
  1242. // vm.tableData = [];
  1243. }
  1244. setTimeout(function() {
  1245. Controller.initSingleSpecData();
  1246. }, 200);
  1247. }
  1248. if (specType == '1' && typeof vm !== 'undefined' && vm) {
  1249. var $multiSpecForm = $('#multi-spec-form');
  1250. if ($multiSpecForm.length > 0 && !$multiSpecForm.is(':visible')) {
  1251. setTimeout(function() {
  1252. $(document).trigger('fa.event.favisible');
  1253. }, 100);
  1254. }
  1255. }
  1256. });
  1257. $('input[name="row[spec_type]"]:checked').trigger('change.spectype');
  1258. },
  1259. bindevent: function () {
  1260. $('[data-toggle="popover"]').popover({
  1261. trigger: 'hover',
  1262. html: true,
  1263. container: 'body'
  1264. });
  1265. $(document).on("click", ".btn-legal", function (a) {
  1266. Fast.api.ajax({
  1267. url: "shop/ajax/check_content_islegal",
  1268. data: {content: $("#c-content").val()}
  1269. }, function (data, ret) {
  1270. }, function (data, ret) {
  1271. if ($.isArray(data)) {
  1272. Layer.alert(__('Banned words') + ":" + data.join(","));
  1273. }
  1274. });
  1275. });
  1276. $(document).on("click", ".btn-keywords", function (a) {
  1277. Fast.api.ajax({
  1278. url: "shop/ajax/get_content_keywords",
  1279. data: {title: $("#c-title").val(), content: $("#c-content").val()}
  1280. }, function (data, ret) {
  1281. $("#c-keywords").val(data.keywords);
  1282. $("#c-description").val(data.description);
  1283. });
  1284. });
  1285. var $form = $("form[role=form]");
  1286. Form.api.bindevent($form, function (data, ret) {
  1287. TabValidator.clearTabErrors();
  1288. }, function (data, ret) {
  1289. var errorMessage = ret.msg || '提交失败';
  1290. var fieldName = null;
  1291. var extraData = null;
  1292. if (ret.data && typeof ret.data === 'object') {
  1293. if (ret.data.errors) {
  1294. extraData = { errors: ret.data.errors };
  1295. } else if (ret.data.field) {
  1296. fieldName = ret.data.field;
  1297. }
  1298. }
  1299. if (!fieldName && errorMessage) {
  1300. var fieldPatterns = [
  1301. /(\w+)\s*(字段|不能|必须|不可)/,
  1302. /row\[(\w+)\]/,
  1303. /'(\w+)'\s*(字段|必须|不能)/,
  1304. /(\w+)\s*is\s*(required|invalid)/,
  1305. /请输入\s*(\w+)/,
  1306. /(\w+)\s*格式不正确/
  1307. ];
  1308. for (var i = 0; i < fieldPatterns.length; i++) {
  1309. var match = errorMessage.match(fieldPatterns[i]);
  1310. if (match) {
  1311. fieldName = match[1];
  1312. break;
  1313. }
  1314. }
  1315. }
  1316. if (fieldName) {
  1317. var targetTab = TabValidator.getTabByFieldName(fieldName);
  1318. if (targetTab) {
  1319. var steps = [
  1320. { id: '#basics' }, { id: '#skus' }, { id: '#delivery' },
  1321. { id: '#detail' }, { id: '#params' }, { id: '#sales' }
  1322. ];
  1323. var stepIndex = steps.findIndex(function(step) {
  1324. return step.id === targetTab;
  1325. });
  1326. if (stepIndex !== -1) {
  1327. TabValidator.goToStep(stepIndex);
  1328. setTimeout(function() {
  1329. TabValidator.highlightErrorFields(targetTab);
  1330. }, 300);
  1331. }
  1332. }
  1333. }
  1334. TabValidator.handleValidationError(errorMessage, fieldName, extraData);
  1335. Toastr.error(errorMessage);
  1336. }, function (success, error) {
  1337. TabValidator.clearTabErrors();
  1338. if (typeof TabValidator.validateAllSteps === 'function') {
  1339. var allValidationPassed = TabValidator.validateAllSteps();
  1340. if (!allValidationPassed.isValid) {
  1341. if (allValidationPassed.firstErrorStep !== -1) {
  1342. TabValidator.goToStep(allValidationPassed.firstErrorStep);
  1343. var errorMessages = [];
  1344. for (var stepIndex in allValidationPassed.allErrors) {
  1345. var stepError = allValidationPassed.allErrors[stepIndex];
  1346. errorMessages.push(stepError.stepName + ': ' + stepError.errors.join('、'));
  1347. }
  1348. if (errorMessages.length > 0) {
  1349. Toastr.error('请完善以下内容:\n' + errorMessages.join('\n'));
  1350. }
  1351. }
  1352. return false;
  1353. }
  1354. }
  1355. let skus = '[]', spec = '[]';
  1356. var specType = $('input[name="row[spec_type]"]:checked').val();
  1357. if (specType == '1') {
  1358. if (typeof vm !== 'undefined' && vm && vm.tableData && vm.tableData.length > 0) {
  1359. try {
  1360. // 为每个SKU生成sku_attr字段
  1361. var processedSkus = vm.tableData.map(function(sku, index) {
  1362. var skuData = Object.assign({}, sku);
  1363. // 生成sku_attr字段
  1364. if (sku.skus && Array.isArray(sku.skus) && vm.specList) {
  1365. var skuAttrs = [];
  1366. sku.skus.forEach(function(specValue, specIndex) {
  1367. if (vm.specList[specIndex] && specValue) {
  1368. skuAttrs.push(vm.specList[specIndex].name + ':' + specValue);
  1369. }
  1370. });
  1371. skuData.sku_attr = skuAttrs.join(',');
  1372. } else {
  1373. skuData.sku_attr = '';
  1374. }
  1375. return skuData;
  1376. });
  1377. skus = JSON.stringify(processedSkus);
  1378. spec = JSON.stringify(vm.specList || []);
  1379. } catch (e) {
  1380. console.error('多规格数据处理失败:', e);
  1381. Toastr.error('多规格数据处理失败');
  1382. return false;
  1383. }
  1384. } else {
  1385. Toastr.error('请设置完整的多规格数据');
  1386. return false;
  1387. }
  1388. } else {
  1389. var singleSku = {
  1390. sku_sn: $('#c-sku-sn').val() || $('#c-goods_sn').val() || '',
  1391. price: parseFloat($('#c-price').val()) || 0,
  1392. lineation_price: parseFloat($('#c-lineation_price').val()) || 0,
  1393. cost_price: parseFloat($('#c-cost_price').val()) || 0,
  1394. stocks: parseInt($('#c-stocks').val()) || 0,
  1395. weight: parseFloat($('#c-weight').val()) || 0,
  1396. volume: parseFloat($('#c-volume').val()) || 0,
  1397. image: $('#c-single-image').val() || '',
  1398. sales: 0,
  1399. status: 1,
  1400. is_default: 1,
  1401. sku_attr: '', // 单规格没有sku_attr
  1402. skus: []
  1403. };
  1404. skus = JSON.stringify([singleSku]);
  1405. spec = JSON.stringify([]);
  1406. }
  1407. let html = `<textarea id="c-skus" class="form-control hide" rows="5" name="row[skus]" cols="50">${skus}</textarea>
  1408. <textarea id="c-spec" class="form-control hide" rows="5" name="row[spec]" cols="50">${spec}</textarea>`;
  1409. this.find('#goods-sku').html(html);
  1410. Form.api.submit(this, success, error);
  1411. return false;
  1412. });
  1413. require(['backend/shop/card'], function (Card) {
  1414. Card.api.bindcardevent();
  1415. });
  1416. },
  1417. bindUpload: function () {
  1418. if ($('.goods-sku-table table td.td-img button.faupload:not([initialized])').length === 0) {
  1419. return;
  1420. }
  1421. clearTimeout(si);
  1422. si = setTimeout(function () {
  1423. let doms = $('.goods-sku-table table td.td-img').toArray();
  1424. function uploadButtonTask(deadline) {
  1425. while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && doms.length > 0) {
  1426. bindEvent();
  1427. }
  1428. if (doms.length > 0)
  1429. requestIdleCallback(uploadButtonTask);
  1430. }
  1431. function bindEvent() {
  1432. var dom = doms.shift();
  1433. if (dom) {
  1434. var $elements = $(".faselect,.fachoose", dom);
  1435. if ($elements && $elements.length > 0) {
  1436. $elements.off('click');
  1437. }
  1438. Form.events.plupload(dom);
  1439. Form.events.faselect(dom);
  1440. }
  1441. }
  1442. requestIdleCallback(uploadButtonTask, {timeout: 1000});
  1443. }, 250);
  1444. },
  1445. add_sku: function () {
  1446. $('.td-img').find('.faupload').removeAttr('initialized');
  1447. require(['vue'], function (Vue) {
  1448. vm = new Vue({
  1449. el: '#vue-app',
  1450. computed: {
  1451. specValueText() {
  1452. return (skus, k) => {
  1453. return !skus || typeof skus[k] == 'undefined' ? '' : skus[k];
  1454. }
  1455. },
  1456. contentHtml() {
  1457. return field => {
  1458. return `<div class='input-group'>
  1459. <input class='form-control' type='text' value=''/>
  1460. <div class='input-group-btn'>
  1461. <span class='btn btn-success sku-confirm' data-field='${field}'>
  1462. 确定
  1463. </span>
  1464. </div>
  1465. </div>`;
  1466. }
  1467. }
  1468. },
  1469. watch: {
  1470. specList: {
  1471. handler: function (val) {
  1472. this.renderTableData(val);
  1473. var that = this;
  1474. that.$nextTick(function() {
  1475. Controller.api.bindSpecValueUpload();
  1476. Controller.api.rebindMultipleEdit();
  1477. setTimeout(function() {
  1478. Controller.api.bindUploadButtons();
  1479. that.updateSpecImagePreviews();
  1480. }, 300);
  1481. });
  1482. },
  1483. deep: true
  1484. }
  1485. },
  1486. data() {
  1487. return {
  1488. spec_name: '',
  1489. specList: [],
  1490. tableData: [],
  1491. result: [],
  1492. skus: [],
  1493. defaultSpecIndex: 0
  1494. }
  1495. },
  1496. mounted() {
  1497. let that = this;
  1498. that.init();
  1499. setTimeout(function() {
  1500. Controller.api.initSpecTypeControl();
  1501. Controller.api.bindSpecValueUpload();
  1502. if (Config.goods && Config.goods.spec_type == '1') {
  1503. var $multiSpecForm = $('#multi-spec-form');
  1504. if ($multiSpecForm.length > 0) {
  1505. $multiSpecForm.show();
  1506. }
  1507. }
  1508. }, 100);
  1509. this.$nextTick(function () {
  1510. $('body').on('click', function (e) {
  1511. if (!$(e.target).hasClass('multiple-edit') &&
  1512. $(e.target).parents('.multiple-edit').length === 0 &&
  1513. $(e.target).parents('.popover.in').length === 0) {
  1514. $('.multiple-edit').popover('hide');
  1515. }
  1516. });
  1517. $(".multiple-edit").popover({
  1518. sanitize: false,
  1519. container: "body",
  1520. html: true,
  1521. placement: "top",
  1522. trigger: 'manual'
  1523. }).on('click', function (e) {
  1524. $(".popover").hide();
  1525. $(this).popover('show');
  1526. });
  1527. $(document).on('click', '.sku-confirm', function () {
  1528. let value = $(this).parent().prev().val().trim();
  1529. let field = $(this).data('field');
  1530. if (field != 'sku_sn' && Number.isNaN(parseFloat(value))) {
  1531. Toastr.error('请输入数字');
  1532. return;
  1533. }
  1534. for (let [index] of that.tableData.entries()) {
  1535. that.$set(that.tableData[index], field, value);
  1536. }
  1537. });
  1538. require(['selectpage'], function () {
  1539. $('.selectpage', $('.spec-template')).selectPage({
  1540. eAjaxSuccess: function (data) {
  1541. data.totalRow = data.total;
  1542. return data;
  1543. },
  1544. eSelect: function (row) {
  1545. let spec_names = row.spec_names.split(';');
  1546. let spec_values = row.spec_values.split(';');
  1547. let list = [];
  1548. for (let [i, v] of spec_names.entries()) {
  1549. let valueList = spec_values[i].split(',').map(val => ({
  1550. name: val,
  1551. image: '',
  1552. description: ''
  1553. }));
  1554. list.push({
  1555. name: v,
  1556. type: 'basic',
  1557. value: valueList
  1558. });
  1559. }
  1560. that.tableData = [];
  1561. that.skus = [];
  1562. setTimeout(function () {
  1563. that.specList = list;
  1564. }, 100);
  1565. }
  1566. });
  1567. });
  1568. })
  1569. },
  1570. methods: {
  1571. init() {
  1572. let skus = [];
  1573. if (Config.goods_skus && Config.goods_skus.length) {
  1574. if (Config.goods && Config.goods.spec_type == '1') {
  1575. let specList = [];
  1576. if (Config.spec_data && Config.spec_data.length > 0) {
  1577. specList = Config.spec_data.map(spec => {
  1578. return {
  1579. name: spec.name,
  1580. type: spec.type || 'basic',
  1581. value: spec.value || []
  1582. };
  1583. });
  1584. for (let item of Config.goods_skus) {
  1585. if (item.sku_attr) {
  1586. let attr = [];
  1587. let skuAttrs = [];
  1588. // 使用统一的sku_attr解析方法
  1589. skuAttrs = Controller.parseSkuAttr(item.sku_attr);
  1590. for (let attrObj of skuAttrs) {
  1591. let specName_key = attrObj.key || attrObj.name;
  1592. let specValue = attrObj.value;
  1593. if (specName_key && specValue) {
  1594. attr.push(specValue);
  1595. }
  1596. }
  1597. let attrKey = attr.join(',');
  1598. skus[attrKey] = item;
  1599. }
  1600. }
  1601. } else {
  1602. let specName = {};
  1603. let specTypeMap = {};
  1604. for (let item of Config.goods_skus) {
  1605. if (item.sku_attr) {
  1606. let attr = [];
  1607. let skuAttrs = [];
  1608. // 使用统一的sku_attr解析方法
  1609. skuAttrs = Controller.parseSkuAttr(item.sku_attr);
  1610. for (let attrObj of skuAttrs) {
  1611. let specName_key = attrObj.key || attrObj.name;
  1612. let specValue = attrObj.value;
  1613. let specType = attrObj.type || 'basic';
  1614. if (specName_key && specValue) {
  1615. attr.push(specValue);
  1616. if (!specName[specName_key]) {
  1617. specName[specName_key] = [];
  1618. }
  1619. if (!specName[specName_key].includes(specValue)) {
  1620. specName[specName_key].push(specValue);
  1621. }
  1622. specTypeMap[specName_key] = specType;
  1623. }
  1624. }
  1625. let attrKey = attr.join(',');
  1626. skus[attrKey] = item;
  1627. }
  1628. }
  1629. let specValueMap = {};
  1630. if (Config.spec_values && Config.spec_values.length > 0) {
  1631. Config.spec_values.forEach(function(item) {
  1632. if (!specValueMap[item.spec_name]) {
  1633. specValueMap[item.spec_name] = {};
  1634. }
  1635. specValueMap[item.spec_name][item.value] = {
  1636. name: item.value,
  1637. image: item.image || '',
  1638. description: item.description || ''
  1639. };
  1640. });
  1641. }
  1642. for (let i in specName) {
  1643. let valueList = specName[i].map(val => {
  1644. if (typeof val === 'string') {
  1645. if (specValueMap[i] && specValueMap[i][val]) {
  1646. return {
  1647. name: specValueMap[i][val].name,
  1648. image: specValueMap[i][val].image || '',
  1649. description: specValueMap[i][val].description || ''
  1650. };
  1651. } else {
  1652. return {
  1653. name: val,
  1654. image: '',
  1655. description: ''
  1656. };
  1657. }
  1658. }
  1659. return {
  1660. name: val.name || val,
  1661. image: val.image || '',
  1662. description: val.description || ''
  1663. };
  1664. });
  1665. specList.push({
  1666. name: i,
  1667. type: specTypeMap[i] || 'basic',
  1668. value: valueList
  1669. });
  1670. }
  1671. }
  1672. this.skus = skus;
  1673. this.specList = specList;
  1674. this.$nextTick(() => {
  1675. if (this.specList.length > 0) {
  1676. this.renderTableData(this.specList);
  1677. setTimeout(() => {
  1678. this.updateSpecImagePreviews();
  1679. }, 500);
  1680. }
  1681. });
  1682. }
  1683. }
  1684. },
  1685. addSpec() {
  1686. if (!this.spec_name.trim()) {
  1687. Toastr.error('请输入规格名称');
  1688. return;
  1689. }
  1690. if (this.specList.some(item => item.name == this.spec_name)) {
  1691. Toastr.error('已存在规格名称');
  1692. return;
  1693. }
  1694. this.specList.push({
  1695. name: this.spec_name,
  1696. type: 'basic',
  1697. value: [{
  1698. name: '',
  1699. image: '',
  1700. description: ''
  1701. }]
  1702. });
  1703. this.spec_name = '';
  1704. },
  1705. showAddSpecForm() {
  1706. this.specList.push({
  1707. name: '',
  1708. type: 'basic',
  1709. value: [{
  1710. name: '',
  1711. image: '',
  1712. description: ''
  1713. }]
  1714. });
  1715. },
  1716. addSpecValue(key) {
  1717. this.specList[key].value.push({
  1718. name: '',
  1719. image: '',
  1720. description: ''
  1721. });
  1722. this.$nextTick(function() {
  1723. setTimeout(function() {
  1724. Controller.api.bindUploadButtons();
  1725. }, 100);
  1726. });
  1727. },
  1728. removeSpecValue(key, index) {
  1729. this.specList[key].value.splice(index, 1);
  1730. },
  1731. renderTableData(list) {
  1732. const isEditMode = Config.goods && Config.goods.id;
  1733. const defaultValues = {
  1734. goods_sn: $('#c-goods_sn').val() || '',
  1735. price: '0.01',
  1736. lineation_price: '0.00',
  1737. cost_price: '0.00',
  1738. weight: '0.00',
  1739. volume: '0.00',
  1740. stocks: '1'
  1741. };
  1742. let columns = [];
  1743. this.result = [];
  1744. this.resetSpec(list, 0);
  1745. this.result.forEach((item, index) => {
  1746. let su = this.skus[item];
  1747. let row;
  1748. if (isEditMode && su) {
  1749. row = {
  1750. skus: item ? item.split(',') : [],
  1751. sku_sn: su.sku_sn || '',
  1752. image: su.image || '',
  1753. price: parseFloat(su.price) || 0,
  1754. lineation_price: parseFloat(su.lineation_price) || 0,
  1755. cost_price: parseFloat(su.cost_price) || 0,
  1756. weight: parseFloat(su.weight) || 0,
  1757. volume: parseFloat(su.volume) || 0,
  1758. stocks: parseInt(su.stocks) || 0,
  1759. sales: parseInt(su.sales) || 0,
  1760. status: su.status !== undefined ? parseInt(su.status) : 1,
  1761. is_default: su.is_default !== undefined ? parseInt(su.is_default) : 0
  1762. };
  1763. } else {
  1764. row = {
  1765. skus: item ? item.split(',') : [],
  1766. sku_sn: defaultValues.goods_sn,
  1767. image: '',
  1768. price: defaultValues.price,
  1769. lineation_price: defaultValues.lineation_price,
  1770. cost_price: defaultValues.cost_price,
  1771. weight: defaultValues.weight,
  1772. volume: defaultValues.volume,
  1773. stocks: defaultValues.stocks,
  1774. sales: 0,
  1775. status: 1,
  1776. is_default: index === 0 ? 1 : 0
  1777. };
  1778. }
  1779. if (!isEditMode) {
  1780. let old = this.tableData[index];
  1781. if (old) {
  1782. for (let i in row) {
  1783. if ((row[i] === '' || row[i] === 0 || row[i] === '0.00') && old[i]) {
  1784. row[i] = old[i];
  1785. }
  1786. }
  1787. }
  1788. }
  1789. columns.push(row);
  1790. });
  1791. this.tableData = columns;
  1792. if (isEditMode) {
  1793. this.defaultSpecIndex = this.tableData.findIndex(item => item.is_default === 1);
  1794. }
  1795. this.$nextTick(function () {
  1796. Controller.api.bindUpload();
  1797. Controller.api.bindSpecValueUpload();
  1798. Controller.api.rebindMultipleEdit();
  1799. });
  1800. },
  1801. removeSpec(key) {
  1802. this.specList.splice(key, 1);
  1803. },
  1804. updateSpecImagePreviews() {
  1805. this.specList.forEach((spec, specKey) => {
  1806. if (spec.value && spec.value.length > 0) {
  1807. spec.value.forEach((value, valueIndex) => {
  1808. if (value.image) {
  1809. const previewId = 'p-spec-image-' + specKey + '-' + valueIndex;
  1810. const $preview = $('#' + previewId);
  1811. if ($preview.length > 0) {
  1812. const img = '<img src="' + Fast.api.cdnurl(value.image) + '" style="width:40px;height:40px;object-fit:cover;border-radius:4px;border:1px solid #ddd;">';
  1813. $preview.html(img);
  1814. }
  1815. }
  1816. });
  1817. }
  1818. });
  1819. },
  1820. resetSpec(list, index) {
  1821. if (list[index] != undefined) {
  1822. let value = list[index].value.map(item => typeof item === 'string' ? item : item.name);
  1823. if (!index) {
  1824. this.result = value;
  1825. } else {
  1826. let res = [];
  1827. for (let i of this.result) {
  1828. for (let j of value) {
  1829. res.push(i + ',' + j);
  1830. }
  1831. }
  1832. if (res.length) {
  1833. this.result = res;
  1834. }
  1835. }
  1836. this.resetSpec(list, ++index);
  1837. }
  1838. },
  1839. }
  1840. });
  1841. });
  1842. $(document).on('click', '.btn-del-sku', function () {
  1843. vm.specList = [];
  1844. vm.tableData = [];
  1845. });
  1846. $(document).on('change', '.sku-images', function () {
  1847. let index = $(this).data('index');
  1848. let value = $(this).val();
  1849. vm.tableData[index].image = value;
  1850. });
  1851. },
  1852. rebindMultipleEdit: function() {
  1853. var $multipleEdit = $(".multiple-edit");
  1854. if (!$multipleEdit || $multipleEdit.length === 0) {
  1855. return;
  1856. }
  1857. $multipleEdit.popover('destroy');
  1858. $multipleEdit.popover({
  1859. sanitize: false,
  1860. container: "body",
  1861. html: true,
  1862. placement: "top",
  1863. trigger: 'manual'
  1864. });
  1865. if ($multipleEdit && $multipleEdit.length > 0) {
  1866. $multipleEdit.off('click.multiple').on('click.multiple', function (e) {
  1867. $(".popover").hide();
  1868. $(this).popover('show');
  1869. });
  1870. }
  1871. },
  1872. bindSpecValueUpload: function() {
  1873. if ($('.spec-image-input').length === 0) {
  1874. return;
  1875. }
  1876. $(document).off('change.specvalue', '.spec-image-input');
  1877. $(document).on('change.specvalue', '.spec-image-input', function () {
  1878. var specKey = parseInt($(this).data('spec-key'));
  1879. var valueIndex = parseInt($(this).data('value-index'));
  1880. var imageUrl = $(this).val();
  1881. if (vm && vm.specList && vm.specList[specKey] && vm.specList[specKey].value[valueIndex]) {
  1882. vm.$set(vm.specList[specKey].value[valueIndex], 'image', imageUrl);
  1883. }
  1884. });
  1885. setTimeout(function() {
  1886. Controller.api.bindUploadButtons();
  1887. }, 200);
  1888. },
  1889. bindUploadButtons: function() {
  1890. if ($('.spec-upload-btn').length === 0) {
  1891. return;
  1892. }
  1893. setTimeout(function() {
  1894. require(['upload'], function (Upload) {
  1895. $('.spec-upload-btn:not([initialized])').each(function() {
  1896. var $btn = $(this);
  1897. var specKey = $btn.attr('data-spec-key');
  1898. var valueIndex = $btn.attr('data-value-index');
  1899. if (specKey !== undefined && valueIndex !== undefined) {
  1900. $btn.attr('initialized', true);
  1901. if ($btn && $btn.length > 0) {
  1902. $btn.off('click.specUpload').on('click.specUpload', function() {
  1903. var inputId = 'c-spec-image-' + specKey + '-' + valueIndex;
  1904. var previewId = 'p-spec-image-' + specKey + '-' + valueIndex;
  1905. var tempBtn = $('<button type="button" class="faupload" style="display:none;"></button>');
  1906. tempBtn.attr('data-input-id', inputId);
  1907. tempBtn.attr('data-preview-id', previewId);
  1908. tempBtn.attr('data-mimetype', $btn.attr('data-mimetype'));
  1909. tempBtn.attr('data-multiple', $btn.attr('data-multiple'));
  1910. $('body').append(tempBtn);
  1911. Upload.api.upload(tempBtn, function(data, ret) {
  1912. var url = data.url || '';
  1913. if (vm && vm.specList && vm.specList[specKey] && vm.specList[specKey].value[valueIndex]) {
  1914. vm.$set(vm.specList[specKey].value[valueIndex], 'image', url);
  1915. }
  1916. $('#' + inputId).val(url);
  1917. var $preview = $('#' + previewId);
  1918. if (url) {
  1919. var img = '<img src="' + Fast.api.cdnurl(url) + '" style="width:100%;height:100%;object-fit:cover;">';
  1920. $preview.html(img);
  1921. } else {
  1922. $preview.empty();
  1923. }
  1924. tempBtn.remove();
  1925. }, function(data, ret) {
  1926. Toastr.error(ret.msg || '上传失败');
  1927. tempBtn.remove();
  1928. });
  1929. tempBtn.click();
  1930. });
  1931. }
  1932. }
  1933. });
  1934. $('.spec-choose-btn:not([initialized])').each(function() {
  1935. var $btn = $(this);
  1936. if (!$btn || $btn.length === 0) {
  1937. return;
  1938. }
  1939. var specKey = $btn.attr('data-spec-key');
  1940. var valueIndex = $btn.attr('data-value-index');
  1941. if (specKey !== undefined && valueIndex !== undefined) {
  1942. $btn.attr('initialized', true);
  1943. if ($btn && $btn.length > 0) {
  1944. $btn.off('click.specChoose').on('click.specChoose', function() {
  1945. var inputId = 'c-spec-image-' + specKey + '-' + valueIndex;
  1946. var previewId = 'p-spec-image-' + specKey + '-' + valueIndex;
  1947. parent.Fast.api.open("general/attachment/select?element_id=" + inputId + "&multiple=false&mimetype=image/*", __('Choose'), {
  1948. callback: function(data) {
  1949. var url = data.url || '';
  1950. if (vm && vm.specList && vm.specList[specKey] && vm.specList[specKey].value[valueIndex]) {
  1951. vm.$set(vm.specList[specKey].value[valueIndex], 'image', url);
  1952. }
  1953. $('#' + inputId).val(url);
  1954. var $preview = $('#' + previewId);
  1955. if (url) {
  1956. var img = '<img src="' + Fast.api.cdnurl(url) + '" style="width:100%;height:100%;object-fit:cover;">';
  1957. $preview.html(img);
  1958. } else {
  1959. $preview.empty();
  1960. }
  1961. }
  1962. });
  1963. });
  1964. }
  1965. }
  1966. });
  1967. });
  1968. }, 100);
  1969. },
  1970. content: function (value, row, index) {
  1971. var width = this.width != undefined ? (this.width.match(/^\d+$/) ? this.width + "px" : this.width) : "350px";
  1972. return "<div style='white-space: nowrap; text-align:left; text-overflow:ellipsis; overflow: hidden; max-width:" + width + ";' title='" + value + "' data-toggle='tooltip' data-placement='right'>" + value + "</div>";
  1973. },
  1974. }
  1975. };
  1976. return Controller;
  1977. });