announcement-form-mixin.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867
  1. // @ts-check
  2. /**
  3. * 公告表单混入组件
  4. * @fileoverview 提供公告表单的核心业务逻辑,包括数据转换、表单验证、API调用等
  5. */
  6. /**
  7. * @typedef {import('./types').AnnouncementFormMixinComponent} AnnouncementFormMixinComponent
  8. * @typedef {import('./types').AnnouncementFormModel} AnnouncementFormModel
  9. * @typedef {import('./types').RoleType} RoleType
  10. * @typedef {import('./types').CustomerOption} CustomerOption
  11. * @typedef {import('./types').BrandOption} BrandOption
  12. * @typedef {import('./types').CategoryOption} CategoryOption
  13. * @typedef {import('./types').BrandScopeItem} BrandScopeItem
  14. * @typedef {import('./types').CustomerBlacklistItem} CustomerBlacklistItem
  15. * @typedef {import('@/api/types/announcement').NoticeRecord} NoticeRecord
  16. * @typedef {import('@/api/types/announcement').NoticeFormData} NoticeFormData
  17. */
  18. import {
  19. getAnnouncement,
  20. add as addAnnouncement,
  21. update as updateAnnouncement
  22. } from '@/api/announcement';
  23. import { getCategoryList } from '@/api/announcement/category';
  24. import { getCustomerList } from '@/api/common/index';
  25. import {
  26. ROLE_OPTIONS,
  27. STATUS_OPTIONS,
  28. calculateRolesMask,
  29. parseRolesMask
  30. } from '@/views/announcement/constants';
  31. /**
  32. * @typedef {import('./types').AnnouncementFormMixinData} AnnouncementFormMixinData
  33. * @typedef {import('./types').DataConverter} DataConverter
  34. */
  35. /**
  36. * 数据转换工具类
  37. * @type {DataConverter}
  38. */
  39. const dataConverter = {
  40. /**
  41. * 将API数据转换为功能模型
  42. * @param {NoticeRecord} apiData API返回的公告数据
  43. * @returns {AnnouncementFormModel} 功能模型数据
  44. */
  45. apiToFormModel(apiData) {
  46. return {
  47. id: apiData.id,
  48. title: apiData.title || '',
  49. content: apiData.content || '',
  50. categoryId: apiData.categoryId || '',
  51. categoryName: apiData.categoryName,
  52. orgId: apiData.orgId || 0,
  53. orgCode: apiData.orgCode || '',
  54. orgName: apiData.orgName || '',
  55. visibleRoles: this.parseVisibleRoles(apiData.visibleRoles),
  56. brandScope: this.parseBrandScope(apiData.brandScope),
  57. customerBlacklist: this.parseCustomerBlacklist(apiData.customerBlacklist),
  58. remark: apiData.remark || '',
  59. status: apiData.status || 0,
  60. createTime: apiData.createTime,
  61. updateTime: apiData.updateTime
  62. };
  63. },
  64. /**
  65. * 将功能模型转换为API数据
  66. * @param {AnnouncementFormModel} formModel 功能模型数据
  67. * @returns {NoticeFormData} API提交数据
  68. */
  69. formModelToApi(formModel) {
  70. return {
  71. id: formModel.id,
  72. title: formModel.title,
  73. content: formModel.content,
  74. categoryId: formModel.categoryId,
  75. categoryName: formModel.categoryName,
  76. orgId: Number(formModel.orgId),
  77. orgCode: formModel.orgCode,
  78. orgName: formModel.orgName,
  79. /** @type {string} */
  80. visibleRoles: String(this.calculateRolesMask(formModel.visibleRoles)),
  81. brandScope: this.stringifyBrandScope(formModel.brandScope),
  82. customerBlacklist: this.stringifyCustomerBlacklist(formModel.customerBlacklist),
  83. remark: formModel.remark,
  84. /** @type {import('@/api/types/announcement').AnnouncementStatus} */
  85. status: /** @type {import('@/api/types/announcement').AnnouncementStatus} */ (formModel.status)
  86. };
  87. },
  88. /**
  89. * 解析可见角色掩码为角色数组
  90. * @param {string | number} rolesMask 角色掩码值
  91. * @returns {Array<RoleType>} 角色类型数组
  92. */
  93. parseVisibleRoles(rolesMask) {
  94. if (!rolesMask) return [];
  95. const mask = typeof rolesMask === 'string' ? parseInt(rolesMask, 10) : rolesMask;
  96. const roleOptions = parseRolesMask(mask);
  97. // 转换RoleOption[]为RoleType[]
  98. return roleOptions.map(option => option.value);
  99. },
  100. /**
  101. * 计算角色数组的掩码值
  102. * @param {Array<RoleType>} roles 角色类型数组
  103. * @returns {number} 角色掩码值
  104. */
  105. calculateRolesMask(roles) {
  106. if (!Array.isArray(roles) || roles.length === 0) return 0;
  107. return calculateRolesMask(roles);
  108. },
  109. /**
  110. * 解析品牌范围JSON字符串
  111. * @param {string} brandScopeStr 品牌范围JSON字符串
  112. * @returns {Array<BrandScopeItem>} 品牌范围数组
  113. */
  114. parseBrandScope(brandScopeStr) {
  115. if (!brandScopeStr || brandScopeStr === '[]') return [];
  116. try {
  117. const parsed = JSON.parse(brandScopeStr);
  118. return Array.isArray(parsed) ? parsed : [];
  119. } catch (error) {
  120. console.warn('解析品牌范围数据失败:', error);
  121. return [];
  122. }
  123. },
  124. /**
  125. * 序列化品牌范围数组为JSON字符串
  126. * @param {Array<BrandScopeItem>} brandScope 品牌范围数组
  127. * @returns {string} JSON字符串
  128. */
  129. stringifyBrandScope(brandScope) {
  130. if (!Array.isArray(brandScope) || brandScope.length === 0) return '[]';
  131. try {
  132. return JSON.stringify(brandScope);
  133. } catch (error) {
  134. console.warn('序列化品牌范围数据失败:', error);
  135. return '[]';
  136. }
  137. },
  138. /**
  139. * 解析客户黑名单JSON字符串
  140. * @param {string} customerBlacklistStr 客户黑名单JSON字符串
  141. * @returns {Array<CustomerBlacklistItem>} 客户黑名单数组
  142. */
  143. parseCustomerBlacklist(customerBlacklistStr) {
  144. if (!customerBlacklistStr || customerBlacklistStr === '[]') return [];
  145. try {
  146. const parsed = JSON.parse(customerBlacklistStr);
  147. return Array.isArray(parsed) ? parsed : [];
  148. } catch (error) {
  149. console.warn('解析客户黑名单数据失败:', error);
  150. return [];
  151. }
  152. },
  153. /**
  154. * 序列化客户黑名单数组为JSON字符串
  155. * @param {Array<CustomerBlacklistItem>} customerBlacklist 客户黑名单数组
  156. * @returns {string} JSON字符串
  157. */
  158. stringifyCustomerBlacklist(customerBlacklist) {
  159. if (!Array.isArray(customerBlacklist) || customerBlacklist.length === 0) return '[]';
  160. try {
  161. return JSON.stringify(customerBlacklist);
  162. } catch (error) {
  163. console.warn('序列化客户黑名单数据失败:', error);
  164. return '[]';
  165. }
  166. }
  167. };
  168. /**
  169. * 创建初始表单数据
  170. * @returns {AnnouncementFormModel} 初始表单数据
  171. */
  172. export function createInitialFormData() {
  173. return {
  174. title: '',
  175. content: '',
  176. categoryId: '',
  177. categoryName: '',
  178. orgId: 0,
  179. orgCode: '',
  180. orgName: '',
  181. visibleRoles: [],
  182. brandScope: [],
  183. customerBlacklist: [],
  184. remark: '',
  185. status: 0
  186. };
  187. }
  188. /**
  189. * 公告表单混入
  190. */
  191. export default {
  192. props: {
  193. /** 是否显示表单 */
  194. visible: {
  195. type: Boolean,
  196. default: false
  197. },
  198. /** 是否为编辑模式 */
  199. isEdit: {
  200. type: Boolean,
  201. default: false
  202. },
  203. /** 公告ID(编辑时使用) */
  204. announcementId: {
  205. type: String,
  206. default: ''
  207. },
  208. /** 编辑数据(可选,优先级高于announcementId) */
  209. editData: {
  210. type: Object,
  211. default: null
  212. },
  213. /** 表单标题(可选) */
  214. title: {
  215. type: String,
  216. default: ''
  217. }
  218. },
  219. data() {
  220. return {
  221. /** @type {AnnouncementFormModel} 公告表单数据模型 */
  222. formData: createInitialFormData(),
  223. /** @type {boolean} 保存操作加载状态 */
  224. saveLoading: false,
  225. /** @type {boolean} 表单加载状态 */
  226. formLoading: false,
  227. /** @type {Array<{id: string, name: string}>} 分类选项列表 */
  228. categoryOptions: [],
  229. /** @type {boolean} 分类选项加载状态 */
  230. categoryLoading: false,
  231. /** @type {Array<{value: number, label: string}>} 角色选项列表 */
  232. roleOptions: ROLE_OPTIONS,
  233. /** @type {Array<{value: number, label: string, Customer_NAME?: string, Customer_CODE?: string}>} 客户选项列表(用于黑名单选择) */
  234. customerOptions: [],
  235. /** @type {boolean} 客户选项加载状态 */
  236. customerLoading: false,
  237. /** @type {Array<{value: string, label: string, name?: string, code?: string}>} 品牌选项列表 */
  238. brandOptions: [],
  239. /** @type {boolean} 品牌选项加载状态 */
  240. brandLoading: false,
  241. /** @type {Array<number>} 选中的客户ID数组 */
  242. selectedCustomerIds: [],
  243. /** 表单验证规则 */
  244. formRules: {
  245. title: [
  246. { required: true, message: '请输入公告标题', trigger: 'blur' },
  247. { min: 1, max: 200, message: '标题长度在 1 到 200 个字符', trigger: 'blur' }
  248. ],
  249. categoryId: [
  250. { required: true, message: '请选择公告分类', trigger: 'change' }
  251. ],
  252. orgId: [
  253. { required: true, message: '请选择组织', trigger: 'change' }
  254. ],
  255. orgCode: [
  256. { required: true, message: '组织编码不能为空', trigger: 'blur' }
  257. ],
  258. orgName: [
  259. { required: true, message: '组织名称不能为空', trigger: 'blur' }
  260. ],
  261. visibleRoles: [
  262. { required: true, type: 'array', min: 1, message: '请至少选择一个可见角色', trigger: 'change' }
  263. ],
  264. content: [
  265. { required: true, message: '请输入公告内容', trigger: 'blur' }
  266. ]
  267. },
  268. /** 表单配置 */
  269. formOption: {}
  270. };
  271. },
  272. computed: {
  273. /**
  274. * 表单标题
  275. * @returns {string} 表单标题
  276. * @this {AnnouncementFormMixinComponent & Vue}
  277. */
  278. formTitle() {
  279. if (this.title) return this.title;
  280. return this.isEdit ? '编辑公告' : '新增公告';
  281. },
  282. /**
  283. * 是否可编辑
  284. * @returns {boolean} 是否可编辑
  285. * @this {AnnouncementFormMixinComponent & Vue}
  286. */
  287. canEdit() {
  288. return !this.formLoading && !this.saveLoading;
  289. }
  290. },
  291. watch: {
  292. /**
  293. * 监听表单显示状态变化
  294. * @param {boolean} newVal 新值
  295. */
  296. visible: {
  297. /**
  298. * @this {AnnouncementFormMixinComponent & Vue}
  299. * @param {boolean} newVal
  300. */
  301. handler(newVal) {
  302. if (newVal) {
  303. // 初始化表单配置,确保表单列正确渲染
  304. this.initFormOption();
  305. // 初始化表单数据
  306. this.formData = createInitialFormData();
  307. // 加载分类选项
  308. this.loadCategoryOptions();
  309. } else {
  310. // 重置表单
  311. if (this.$refs.form && typeof this.$refs.form.resetFields === 'function') {
  312. this.$refs.form.resetFields();
  313. }
  314. }
  315. },
  316. immediate: true
  317. },
  318. /**
  319. * 监听编辑数据变化
  320. * @param {import('@/api/types/announcement').NoticeRecord|null} newVal 新值
  321. */
  322. editData: {
  323. /**
  324. * @this {AnnouncementFormMixinComponent & Vue}
  325. * @param {import('@/api/types/announcement').NoticeRecord|null} newVal
  326. */
  327. handler(newVal) {
  328. if (newVal && this.visible) {
  329. this.formData = {
  330. id: newVal.id || '',
  331. title: newVal.title || '',
  332. content: newVal.content || '',
  333. categoryId: newVal.categoryId || '',
  334. categoryName: newVal.categoryName,
  335. orgId: Number(newVal.orgId || 0),
  336. orgCode: newVal.orgCode || '',
  337. orgName: newVal.orgName || '',
  338. visibleRoles: dataConverter.parseVisibleRoles(newVal.visibleRoles || 0),
  339. brandScope: dataConverter.parseBrandScope(newVal.brandScope || '[]'),
  340. customerBlacklist: dataConverter.parseCustomerBlacklist(newVal.customerBlacklist || '[]'),
  341. remark: newVal.remark || '',
  342. status: Number(newVal.status || 0),
  343. createTime: newVal.createTime,
  344. updateTime: newVal.updateTime
  345. };
  346. }
  347. },
  348. deep: true,
  349. immediate: true
  350. },
  351. /**
  352. * 监听分类ID变化,自动设置分类名称
  353. */
  354. 'formData.categoryId': {
  355. handler(newVal) {
  356. if (newVal && this.categoryOptions && this.categoryOptions.length > 0) {
  357. const category = this.categoryOptions.find(item => item.value === newVal);
  358. if (category) {
  359. this.formData.categoryName = category.name;
  360. }
  361. } else {
  362. this.formData.categoryName = '';
  363. }
  364. },
  365. immediate: true
  366. },
  367. /**
  368. * 监听客户黑名单变化,同步selectedCustomerIds
  369. */
  370. 'formData.customerBlacklist': {
  371. handler(newVal) {
  372. if (Array.isArray(newVal)) {
  373. this.selectedCustomerIds = newVal.map(item => item.ID || item.id).filter(id => id != null);
  374. }
  375. },
  376. deep: true,
  377. immediate: true
  378. }
  379. },
  380. methods: {
  381. // 数据转换方法
  382. /**
  383. * API数据转换为表单模型
  384. * @param {NoticeRecord} apiData API数据
  385. * @returns {AnnouncementFormModel} 表单模型
  386. * @this {AnnouncementFormMixinComponent & Vue}
  387. */
  388. apiToFormModel: dataConverter.apiToFormModel,
  389. /**
  390. * 表单模型转换为API数据
  391. * @param {AnnouncementFormModel} formModel 表单模型
  392. * @returns {NoticeFormData} API数据
  393. * @this {AnnouncementFormMixinComponent & Vue}
  394. */
  395. formModelToApi: dataConverter.formModelToApi,
  396. /**
  397. * 解析角色掩码
  398. * @param {string | number} rolesMask 角色掩码
  399. * @returns {Array<RoleType>} 角色类型数组
  400. * @this {AnnouncementFormMixinComponent & Vue}
  401. */
  402. parseVisibleRoles: dataConverter.parseVisibleRoles.bind(dataConverter),
  403. /**
  404. * 计算角色掩码
  405. * @param {Array<RoleType>} roles 角色类型数组
  406. * @returns {number} 角色掩码值
  407. * @this {AnnouncementFormMixinComponent & Vue}
  408. */
  409. calculateRolesMask: dataConverter.calculateRolesMask.bind(dataConverter),
  410. /**
  411. * 客户黑名单字符串化
  412. * @param {Array<CustomerBlacklistItem>} customerBlacklist 客户黑名单
  413. * @returns {string} 字符串化结果
  414. * @this {AnnouncementFormMixinComponent & Vue}
  415. */
  416. stringifyCustomerBlacklist: dataConverter.stringifyCustomerBlacklist,
  417. /**
  418. * 品牌范围字符串化
  419. * @param {Array<BrandScopeItem>} brandScope 品牌范围
  420. * @returns {string} 字符串化结果
  421. * @this {AnnouncementFormMixinComponent & Vue}
  422. */
  423. stringifyBrandScope: dataConverter.stringifyBrandScope,
  424. /**
  425. * 创建初始表单数据
  426. * @returns {AnnouncementFormModel} 初始表单数据
  427. * @this {AnnouncementFormMixinComponent & Vue}
  428. */
  429. createInitialFormData,
  430. /**
  431. * 初始化表单数据
  432. * @this {AnnouncementFormMixinComponent & Vue}
  433. * @returns {Promise<void>}
  434. */
  435. async initFormData() {
  436. this.formData = this.createInitialFormData();
  437. if (this.editData) {
  438. // 优先使用传入的编辑数据
  439. this.formData = dataConverter.apiToFormModel(this.editData);
  440. } else if (this.isEdit && this.announcementId) {
  441. // 根据ID加载公告详情
  442. await this.loadAnnouncementDetail();
  443. }
  444. },
  445. /**
  446. * 初始化表单配置
  447. * @this {AnnouncementFormMixinComponent & Vue}
  448. */
  449. initFormOption() {
  450. this.formOption = {
  451. submitBtn: false,
  452. emptyBtn: false,
  453. column: [
  454. {
  455. prop: 'title',
  456. label: '公告标题',
  457. type: 'input',
  458. span: 24,
  459. rules: this.formRules.title
  460. },
  461. {
  462. prop: 'categoryId',
  463. label: '公告分类',
  464. type: 'select',
  465. span: 12,
  466. dicData: this.categoryOptions,
  467. props: {
  468. label: 'name',
  469. value: 'id'
  470. },
  471. rules: this.formRules.categoryId
  472. },
  473. {
  474. prop: 'status',
  475. label: '状态',
  476. type: 'select',
  477. span: 12,
  478. dicData: STATUS_OPTIONS,
  479. props: {
  480. label: 'label',
  481. value: 'value'
  482. }
  483. },
  484. {
  485. prop: 'orgName',
  486. label: '组织名称',
  487. type: 'input',
  488. span: 8,
  489. rules: this.formRules.orgName
  490. },
  491. {
  492. prop: 'orgCode',
  493. label: '组织编码',
  494. type: 'input',
  495. span: 8,
  496. rules: this.formRules.orgCode
  497. },
  498. {
  499. prop: 'orgId',
  500. label: '组织ID',
  501. type: 'number',
  502. span: 8,
  503. rules: this.formRules.orgId
  504. },
  505. {
  506. prop: 'visibleRoles',
  507. label: '可见角色',
  508. type: 'checkbox',
  509. span: 24,
  510. dicData: this.roleOptions,
  511. props: {
  512. label: 'label',
  513. value: 'value'
  514. },
  515. rules: this.formRules.visibleRoles
  516. },
  517. {
  518. prop: 'brandScope',
  519. label: '品牌范围',
  520. type: 'select',
  521. span: 12,
  522. multiple: true,
  523. filterable: true,
  524. remote: true,
  525. remoteMethod: this.remoteSearchBrands,
  526. loading: this.brandLoading,
  527. dicData: this.brandOptions,
  528. props: {
  529. label: 'label',
  530. value: 'value'
  531. }
  532. },
  533. {
  534. prop: 'customerBlacklist',
  535. label: '客户黑名单',
  536. type: 'select',
  537. span: 12,
  538. multiple: true,
  539. filterable: true,
  540. remote: true,
  541. remoteMethod: this.remoteSearchCustomers,
  542. loading: this.customerLoading,
  543. dicData: this.customerOptions,
  544. props: {
  545. label: 'label',
  546. value: 'value'
  547. }
  548. },
  549. {
  550. prop: 'content',
  551. label: '公告内容',
  552. component: 'AvueUeditor',
  553. options: {
  554. action: '/api/blade-resource/oss/endpoint/put-file',
  555. props: {
  556. res: 'data',
  557. url: 'link',
  558. }
  559. },
  560. span: 24,
  561. minRows: 6,
  562. rules: this.formRules.content
  563. },
  564. {
  565. prop: 'remark',
  566. label: '备注',
  567. type: 'textarea',
  568. span: 24,
  569. minRows: 2,
  570. maxRows: 4
  571. }
  572. ]
  573. };
  574. },
  575. /**
  576. * 加载公告详情
  577. * @this {AnnouncementFormMixinComponent & Vue}
  578. * @returns {Promise<void>}
  579. */
  580. async loadAnnouncementDetail() {
  581. if (!this.announcementId) return;
  582. try {
  583. this.formLoading = true;
  584. const response = await getAnnouncement(this.announcementId);
  585. if (response.data && response.data.success) {
  586. /** @type {NoticeRecord} */
  587. const apiData = response.data.data;
  588. this.formData = dataConverter.apiToFormModel(apiData);
  589. this.$emit('loaded');
  590. } else {
  591. this.$message.error(response.data?.message || '加载公告详情失败');
  592. }
  593. } catch (error) {
  594. console.error('加载公告详情失败:', error);
  595. this.$message.error('加载公告详情失败');
  596. } finally {
  597. this.formLoading = false;
  598. }
  599. },
  600. /**
  601. * 加载分类选项
  602. * @this {AnnouncementFormMixinComponent & Vue}
  603. * @returns {Promise<void>}
  604. */
  605. async loadCategoryOptions() {
  606. try {
  607. this.categoryLoading = true;
  608. const response = await getCategoryList();
  609. if (response.data && response.data.success) {
  610. /** @type {Array<{id: string, name: string, label: string, value: string}>} */
  611. this.categoryOptions = (response.data.data || []).map(item => ({
  612. id: String(item.id),
  613. name: String(item.name),
  614. label: String(item.name),
  615. value: String(item.id)
  616. }));
  617. }
  618. } catch (error) {
  619. console.error('加载分类选项失败:', error);
  620. } finally {
  621. this.categoryLoading = false;
  622. }
  623. },
  624. /**
  625. * 处理表单提交
  626. * @this {AnnouncementFormMixinComponent & Vue}
  627. * @returns {Promise<void>}
  628. */
  629. async handleSubmit() {
  630. try {
  631. // 表单验证
  632. const isValid = await this.validateForm();
  633. if (!isValid) return;
  634. this.saveLoading = true;
  635. // 转换数据格式
  636. /** @type {NoticeFormData} */
  637. const submitData = dataConverter.formModelToApi(this.formData);
  638. // 提交数据
  639. const response = await this.submitAnnouncementData(submitData);
  640. if (response.data && response.data.success) {
  641. this.$message.success(this.isEdit ? '更新成功' : '创建成功');
  642. this.$emit('submit-success', this.formData);
  643. this.$emit('save-success', this.formData);
  644. this.handleCancel();
  645. } else {
  646. this.$message.error(response.data?.message || '保存失败');
  647. }
  648. } catch (error) {
  649. console.error('提交表单失败:', error);
  650. this.$message.error('保存失败');
  651. } finally {
  652. this.saveLoading = false;
  653. }
  654. },
  655. /**
  656. * 重置表单
  657. * @this {AnnouncementFormMixinComponent & Vue}
  658. */
  659. handleReset() {
  660. this.formData = this.createInitialFormData();
  661. this.customerOptions = [];
  662. this.selectedCustomerIds = [];
  663. this.brandOptions = [];
  664. this.$emit('reset');
  665. },
  666. /**
  667. * 取消操作
  668. * @this {AnnouncementFormMixinComponent & Vue}
  669. */
  670. handleCancel() {
  671. this.$emit('cancel');
  672. this.$emit('update:visible', false);
  673. },
  674. /**
  675. * 提交公告数据
  676. * @param {NoticeFormData} submitData 提交数据
  677. * @returns {Promise<import('axios').AxiosResponse<any>>} API响应
  678. * @this {AnnouncementFormMixinComponent & Vue}
  679. */
  680. async submitAnnouncementData(submitData) {
  681. if (this.isEdit) {
  682. return await updateAnnouncement({ ...submitData, id: this.announcementId });
  683. } else {
  684. return await addAnnouncement(submitData);
  685. }
  686. },
  687. /**
  688. * 验证表单
  689. * @returns {Promise<boolean>} 验证结果
  690. * @this {AnnouncementFormMixinComponent & Vue}
  691. */
  692. async validateForm() {
  693. try {
  694. const form = this.$refs.form;
  695. if (form && typeof form.validate === 'function') {
  696. await form.validate();
  697. return true;
  698. }
  699. return false;
  700. } catch (error) {
  701. console.warn('表单验证失败:', error);
  702. return false;
  703. }
  704. },
  705. /**
  706. * 远程搜索客户
  707. * @param {string} query 搜索关键词
  708. * @returns {Promise<void>}
  709. * @this {AnnouncementFormMixinComponent & Vue}
  710. */
  711. async remoteSearchCustomers(query) {
  712. if (!query || query.length < 1) {
  713. this.customerOptions = [];
  714. return;
  715. }
  716. try {
  717. this.customerLoading = true;
  718. const response = await getCustomerList(1, 20, {
  719. customerName: query.trim()
  720. });
  721. if (response?.data?.success && response.data.data?.records) {
  722. this.customerOptions = response.data.data.records.map(customer => ({
  723. value: customer.Customer_ID,
  724. label: customer.Customer_NAME,
  725. Customer_NAME: customer.Customer_NAME,
  726. Customer_CODE: customer.Customer_CODE
  727. }));
  728. } else {
  729. this.customerOptions = [];
  730. const errorMsg = response?.data?.msg || '搜索客户失败';
  731. this.$message.warning(errorMsg);
  732. }
  733. } catch (error) {
  734. console.error('搜索客户失败:', error);
  735. this.$message.error('网络错误,搜索客户失败');
  736. this.customerOptions = [];
  737. } finally {
  738. this.customerLoading = false;
  739. }
  740. },
  741. /**
  742. * 远程搜索品牌
  743. * @param {string} query 搜索关键词
  744. * @returns {Promise<void>}
  745. * @this {AnnouncementFormMixinComponent & Vue}
  746. */
  747. async remoteSearchBrands(query) {
  748. if (!query || query.length < 2) {
  749. this.brandOptions = [];
  750. return;
  751. }
  752. try {
  753. this.brandLoading = true;
  754. // TODO: 实现品牌搜索API调用
  755. this.brandOptions = [];
  756. } catch (error) {
  757. console.error('搜索品牌失败:', error);
  758. } finally {
  759. this.brandLoading = false;
  760. }
  761. },
  762. /**
  763. * 处理客户黑名单变化
  764. * @param {Array<number>} selectedCustomerIds 选中的客户ID数组
  765. * @this {AnnouncementFormMixinComponent & Vue}
  766. */
  767. handleCustomerBlacklistChange(selectedCustomerIds) {
  768. this.selectedCustomerIds = selectedCustomerIds;
  769. /** @type {Array<CustomerBlacklistItem>} */
  770. this.formData.customerBlacklist = selectedCustomerIds.map(customerId => {
  771. const customer = this.customerOptions.find(option => option.value === customerId);
  772. return {
  773. ID: customerId,
  774. NAME: customer?.Customer_NAME || '',
  775. CODE: customer?.Customer_CODE || ''
  776. };
  777. });
  778. },
  779. /**
  780. * 处理品牌范围变化
  781. * @param {Array<BrandOption>} selectedBrands 选中的品牌
  782. * @this {AnnouncementFormMixinComponent & Vue}
  783. */
  784. handleBrandScopeChange(selectedBrands) {
  785. /** @type {Array<BrandScopeItem>} */
  786. this.formData.brandScope = selectedBrands.map(brand => {
  787. // 从label中提取品牌代码
  788. const codeMatch = brand.label ? brand.label.match(/\(([^)]+)\)$/) : null;
  789. const code = codeMatch ? codeMatch[1] : brand.code || '';
  790. return {
  791. id: String(brand.value),
  792. name: brand.name || (brand.label ? brand.label.replace(/\([^)]+\)$/, '').trim() : ''),
  793. code: code
  794. };
  795. });
  796. },
  797. /**
  798. * 清空客户选项
  799. * @this {AnnouncementFormMixinComponent & Vue}
  800. */
  801. clearCustomerOptions() {
  802. this.customerOptions = [];
  803. this.selectedCustomerIds = [];
  804. this.formData.customerBlacklist = [];
  805. },
  806. /**
  807. * 清空品牌选项
  808. * @this {AnnouncementFormMixinComponent & Vue}
  809. */
  810. clearBrandOptions() {
  811. this.brandOptions = [];
  812. }
  813. }
  814. };