validation-manager.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. // 工具方法
  2. const findParentTd = (element) => {
  3. let parent = element.parentElement;
  4. while (parent && parent.tagName !== 'TD') {
  5. parent = parent.parentElement;
  6. }
  7. return parent;
  8. };
  9. const isSameTd = (element1, element2) => {
  10. const td1 = findParentTd(element1);
  11. const td2 = findParentTd(element2);
  12. return td1 && td1 === td2;
  13. };
  14. class ValidationManager {
  15. constructor() {
  16. this.validations = new Map();
  17. this.dependRules = new Map(); // 存储依赖关系
  18. }
  19. // 添加验证规则
  20. add(ruleName, fields, options = {}, priority = 1) {
  21. if (!Array.isArray(fields)) {
  22. fields = [fields];
  23. }
  24. const rule = this._parseRule(ruleName);
  25. if (!rule) return;
  26. fields.forEach(field => {
  27. if (!this.validations.has(field)) {
  28. this.validations.set(field, []);
  29. }
  30. this.validations.get(field).push({
  31. rule: rule,
  32. options: {
  33. msgPrfx: options.msgPrfx || field,
  34. ...options
  35. },
  36. priority: priority
  37. });
  38. });
  39. if (options.relField) {
  40. const relElement = document.querySelector(`[name="${options.relField}"]`);
  41. if (relElement) {
  42. relElement.addEventListener('change', () => {
  43. this.validateField(field);
  44. });
  45. }
  46. }
  47. this.initRequiredMarks();
  48. // 通知移动端组件重新检查必填状态
  49. this.notifyComponentsUpdate();
  50. ssVm.bindForm(".form-container");
  51. }
  52. // 验证单个字段
  53. validateField(field) {
  54. const element = document.querySelector(`[name="${field}"]`);
  55. if (!element) return { valid: true };
  56. const td = findParentTd(element);
  57. if (!td) return { valid: true };
  58. // 获取同一个 td 内的所有需要验证的字段
  59. const tdFields = Array.from(td.querySelectorAll('[name]'))
  60. .map(el => el.getAttribute('name'))
  61. .filter(name => this.validations.has(name));
  62. // 验证所有字段并合并错误信息
  63. let errors = [];
  64. let hasEmptyRequired = false; // 是否有空的必填字段
  65. for (const f of tdFields) {
  66. const element = document.querySelector(`[name="${f}"]`);
  67. const value = element.value;
  68. // 检查是否为空
  69. if (!value || value.trim() === '') {
  70. hasEmptyRequired = true;
  71. }
  72. const result = this._doValidate(f);
  73. if (!result.valid && errors.indexOf(result.message) === -1) {
  74. errors.push(result.message);
  75. }
  76. }
  77. // 检查是否有依赖规则
  78. const dependRule = this.dependRules.get(field);
  79. if (dependRule) {
  80. const { dependField, rules } = dependRule;
  81. const dependElement = document.querySelector(`[name="${dependField}"]`);
  82. if (dependElement) {
  83. const dependValue = dependElement.value;
  84. const validatorName = rules[dependValue];
  85. if (validatorName) {
  86. // 更新验证规则
  87. this.validations.set(field, [{
  88. rule: window.ValidatorRules[validatorName],
  89. options: {
  90. msgPrfx: field,
  91. required: true
  92. }
  93. }]);
  94. }
  95. }
  96. }
  97. // 管理必填标记
  98. let requiredMark = td.querySelector('.required');
  99. // console.log(hasEmptyRequired,errors.length,requiredMark)
  100. if ((hasEmptyRequired || errors.length > 0) && !requiredMark) {
  101. requiredMark = document.createElement('div');
  102. requiredMark.className = 'required';
  103. td.appendChild(requiredMark);
  104. } else if (!hasEmptyRequired && errors.length === 0 && requiredMark) {
  105. requiredMark.remove();
  106. }
  107. // 动态管理错误提示 - 仅PC端使用,移动端通过组件内部显示
  108. // 检查是否为移动端环境(通过检查是否有ss-input或ss-mobile-component组件)
  109. const isMobileEnv = td.querySelector('.ss-input') !== null || td.querySelector('.ss-mobile-component') !== null;
  110. if (!isMobileEnv) {
  111. // PC端:使用.err-tip元素显示错误
  112. let errTip = td.querySelector('.err-tip');
  113. if (!errTip && errors.length > 0) {
  114. errTip = document.createElement('div');
  115. errTip.className = 'err-tip';
  116. errTip.style.position = 'absolute';
  117. errTip.style.zIndex = '1';
  118. errTip.innerHTML = `
  119. <div class="tip">${errors.join(';')}</div>
  120. <div class="tip-more">${errors.join(';')}</div>
  121. `;
  122. td.appendChild(errTip);
  123. } else if (errTip && errors.length === 0) {
  124. errTip.remove();
  125. } else if (errTip && errors.length > 0) {
  126. errTip.querySelector('.tip').textContent = errors.join(';');
  127. errTip.querySelector('.tip-more').textContent = errors.join(';');
  128. }
  129. }
  130. // 移动端:错误提示由ss-input组件内部处理,这里不生成.err-tip元素
  131. return {
  132. valid: errors.length === 0,
  133. message: errors.join(';')
  134. };
  135. }
  136. // 验证所有字段
  137. validateAll() {
  138. const errors = [];
  139. for (const field of this.validations.keys()) {
  140. const result = this.validateField(field);
  141. if (!result.valid) {
  142. errors.push({
  143. field: field,
  144. message: result.message
  145. });
  146. }
  147. }
  148. return {
  149. valid: errors.length === 0,
  150. errors: errors
  151. };
  152. }
  153. // 解析规则名称并获取对应的验证规则
  154. _parseRule(ruleName) {
  155. const parts = ruleName.split('.');
  156. const actualRuleName = parts[parts.length - 1];
  157. return ValidatorRules[actualRuleName];
  158. }
  159. // 绑定到表单提交
  160. bindForm(formClass) {
  161. // 创建一个观察器实例
  162. const observer = new MutationObserver((mutations) => {
  163. const form = document.querySelector(formClass);
  164. if (form && !form._bound) {
  165. // console.log("Found form, binding submit event");
  166. $(form).on('submit', (e) => {
  167. e.preventDefault();
  168. const result = this.validateAll();
  169. if (!result.valid) {
  170. // 触发所有字段的校验,让移动端组件显示错误
  171. result.errors.forEach(error => {
  172. const element = document.querySelector(`[name="${error.field}"]`);
  173. if (element) {
  174. // 触发input事件,让移动端组件更新状态
  175. const inputEvent = new Event('input', { bubbles: true });
  176. element.dispatchEvent(inputEvent);
  177. // PC端的错误提示处理
  178. const validateComponent = element.closest('.input-container')?.querySelector('.err-tip');
  179. if (validateComponent) {
  180. validateComponent.querySelector('.tip').textContent = error.message;
  181. validateComponent.querySelector('.tip-more').textContent = error.message;
  182. }
  183. }
  184. });
  185. return false;
  186. } else {
  187. alert('表单校验通过!');
  188. }
  189. return true;
  190. });
  191. form._bound = true; // 标记已绑定
  192. observer.disconnect(); // 停止观察
  193. }
  194. });
  195. // 开始观察
  196. observer.observe(document.body, {
  197. childList: true,
  198. subtree: true
  199. });
  200. }
  201. remove(fields) {
  202. // 如果传入单个字段,转换为数组
  203. if (!Array.isArray(fields)) {
  204. fields = [fields];
  205. }
  206. // 遍历字段并移除验证规则
  207. fields.forEach(field => {
  208. if (this.validations.has(field)) {
  209. this.validations.delete(field);
  210. }
  211. });
  212. }
  213. // 私有验证方法
  214. _doValidate(field) {
  215. const rules = this.validations.get(field);
  216. if (!rules) return { valid: true };
  217. const element = document.querySelector(`[name="${field}"]`);
  218. if (!element) return { valid: true };
  219. const value = element.value;
  220. // console.log(element,value)
  221. for (const {rule, options} of rules) {
  222. // console.log(rule,options)
  223. // 如果设置了 required 且值为空,进行必填验证
  224. if (options.required && (!value || value.trim() === '')) {
  225. return {
  226. valid: false,
  227. message: `${options.msgPrfx}不能为空`
  228. };
  229. }
  230. // 执行规则验证,传入完整的 options 以支持关联字段验证
  231. const isValid = rule.validate(value, options);
  232. if (!isValid) {
  233. let message = rule.message;
  234. message = message.replace('{field}', options.msgPrfx);
  235. Object.keys(options).forEach(key => {
  236. message = message.replace(`{${key}}`, options[key]);
  237. });
  238. return {
  239. valid: false,
  240. message: message
  241. };
  242. }
  243. }
  244. return { valid: true };
  245. }
  246. // 初始化必填标记
  247. initRequiredMarks() {
  248. // 先收集所有需要处理的 td
  249. const processedTds = new Set();
  250. // 遍历所有带验证规则的字段
  251. for (const [field, rules] of this.validations.entries()) {
  252. const element = document.querySelector(`[name="${field}"]`);
  253. if (!element) continue;
  254. const td = findParentTd(element);
  255. if (!td || processedTds.has(td)) continue; // 跳过已处理的 td
  256. processedTds.add(td); // 标记该 td 已处理
  257. // 获取同一个 td 内的所有需要验证的字段
  258. const tdFields = Array.from(td.querySelectorAll('[name]'))
  259. .map(el => el.getAttribute('name'))
  260. .filter(name => this.validations.has(name));
  261. // 只要有验证规则就添加必填标记
  262. if (tdFields.length > 0) {
  263. let requiredMark = td.querySelector('.required');
  264. if (!requiredMark) {
  265. requiredMark = document.createElement('div');
  266. requiredMark.className = 'required';
  267. td.appendChild(requiredMark);
  268. }
  269. }
  270. }
  271. }
  272. // 验证字段但不显示错误信息
  273. validateFieldSilent(field) {
  274. const rules = this.validations.get(field);
  275. if (!rules) return { valid: true };
  276. const element = document.querySelector(`[name="${field}"]`);
  277. if (!element) return { valid: true };
  278. const value = element.value;
  279. for (const {rule, options} of rules) {
  280. if (options.required && (!value || value.trim() === '')) {
  281. return { valid: false };
  282. }
  283. if (!value && !options.required) {
  284. return { valid: true };
  285. }
  286. const isValid = rule.validate(value, options);
  287. if (!isValid) {
  288. return { valid: false };
  289. }
  290. }
  291. return { valid: true };
  292. }
  293. // 检查字段是否为必填项 - 为移动端组件提供支持
  294. isRequired(field) {
  295. const rules = this.validations.get(field);
  296. if (!rules) return false;
  297. // 检查是否有任何规则设置了required或者是notNull规则
  298. return rules.some(({rule, options}) => {
  299. return options.required || rule === ValidatorRules.notNull;
  300. });
  301. }
  302. // 通知移动端组件更新状态
  303. notifyComponentsUpdate() {
  304. // 触发所有已挂载的ss-input组件重新检查状态
  305. const inputs = document.querySelectorAll('input[name]');
  306. inputs.forEach(input => {
  307. // 触发一个自定义事件,让组件重新检查必填状态
  308. const event = new CustomEvent('ssvm-rules-updated', {
  309. bubbles: true,
  310. detail: { fieldName: input.getAttribute('name') }
  311. });
  312. input.dispatchEvent(event);
  313. });
  314. }
  315. }
  316. // 创建全局实例
  317. window.ssVm = window.ssVm || new ValidationManager();
  318. // 在 DOM 加载完成后初始化必填标记