// @ts-check /** * 公告表单混入组件 * @fileoverview 提供公告表单的核心业务逻辑,包括数据转换、表单验证、API调用等 */ /** * @typedef {import('./types').AnnouncementFormMixinComponent} AnnouncementFormMixinComponent * @typedef {import('./types').AnnouncementFormModel} AnnouncementFormModel * @typedef {import('./types').RoleType} RoleType * @typedef {import('./types').CustomerOption} CustomerOption * @typedef {import('./types').BrandOption} BrandOption * @typedef {import('./types').CategoryOption} CategoryOption * @typedef {import('./types').BrandScopeItem} BrandScopeItem * @typedef {import('./types').CustomerBlacklistItem} CustomerBlacklistItem * @typedef {import('@/api/types/announcement').NoticeRecord} NoticeRecord * @typedef {import('@/api/types/announcement').NoticeFormData} NoticeFormData */ import { getAnnouncement, add as addAnnouncement, update as updateAnnouncement } from '@/api/announcement'; import { getCategoryList } from '@/api/announcement/category'; import { getCustomerList } from '@/api/common/index'; import { ROLE_OPTIONS, STATUS_OPTIONS, calculateRolesMask, parseRolesMask } from '@/views/announcement/constants'; /** * @typedef {import('./types').AnnouncementFormMixinData} AnnouncementFormMixinData * @typedef {import('./types').DataConverter} DataConverter */ /** * 数据转换工具类 * @type {DataConverter} */ const dataConverter = { /** * 将API数据转换为功能模型 * @param {NoticeRecord} apiData API返回的公告数据 * @returns {AnnouncementFormModel} 功能模型数据 */ apiToFormModel(apiData) { return { id: apiData.id, title: apiData.title || '', content: apiData.content || '', categoryId: apiData.categoryId || '', categoryName: apiData.categoryName, orgId: apiData.orgId || 0, orgCode: apiData.orgCode || '', orgName: apiData.orgName || '', visibleRoles: this.parseVisibleRoles(apiData.visibleRoles), brandScope: this.parseBrandScope(apiData.brandScope), customerBlacklist: this.parseCustomerBlacklist(apiData.customerBlacklist), remark: apiData.remark || '', status: apiData.status || 0, createTime: apiData.createTime, updateTime: apiData.updateTime }; }, /** * 将功能模型转换为API数据 * @param {AnnouncementFormModel} formModel 功能模型数据 * @returns {NoticeFormData} API提交数据 */ formModelToApi(formModel) { return { id: formModel.id, title: formModel.title, content: formModel.content, categoryId: formModel.categoryId, categoryName: formModel.categoryName, orgId: Number(formModel.orgId), orgCode: formModel.orgCode, orgName: formModel.orgName, /** @type {string} */ visibleRoles: String(this.calculateRolesMask(formModel.visibleRoles)), brandScope: this.stringifyBrandScope(formModel.brandScope), customerBlacklist: this.stringifyCustomerBlacklist(formModel.customerBlacklist), remark: formModel.remark, /** @type {import('@/api/types/announcement').AnnouncementStatus} */ status: /** @type {import('@/api/types/announcement').AnnouncementStatus} */ (formModel.status) }; }, /** * 解析可见角色掩码为角色数组 * @param {string | number} rolesMask 角色掩码值 * @returns {Array} 角色类型数组 */ parseVisibleRoles(rolesMask) { if (!rolesMask) return []; const mask = typeof rolesMask === 'string' ? parseInt(rolesMask, 10) : rolesMask; const roleOptions = parseRolesMask(mask); // 转换RoleOption[]为RoleType[] return roleOptions.map(option => option.value); }, /** * 计算角色数组的掩码值 * @param {Array} roles 角色类型数组 * @returns {number} 角色掩码值 */ calculateRolesMask(roles) { if (!Array.isArray(roles) || roles.length === 0) return 0; return calculateRolesMask(roles); }, /** * 解析品牌范围JSON字符串 * @param {string} brandScopeStr 品牌范围JSON字符串 * @returns {Array} 品牌范围数组 */ parseBrandScope(brandScopeStr) { if (!brandScopeStr || brandScopeStr === '[]') return []; try { const parsed = JSON.parse(brandScopeStr); return Array.isArray(parsed) ? parsed : []; } catch (error) { console.warn('解析品牌范围数据失败:', error); return []; } }, /** * 序列化品牌范围数组为JSON字符串 * @param {Array} brandScope 品牌范围数组 * @returns {string} JSON字符串 */ stringifyBrandScope(brandScope) { if (!Array.isArray(brandScope) || brandScope.length === 0) return '[]'; try { return JSON.stringify(brandScope); } catch (error) { console.warn('序列化品牌范围数据失败:', error); return '[]'; } }, /** * 解析客户黑名单JSON字符串 * @param {string} customerBlacklistStr 客户黑名单JSON字符串 * @returns {Array} 客户黑名单数组 */ parseCustomerBlacklist(customerBlacklistStr) { if (!customerBlacklistStr || customerBlacklistStr === '[]') return []; try { const parsed = JSON.parse(customerBlacklistStr); return Array.isArray(parsed) ? parsed : []; } catch (error) { console.warn('解析客户黑名单数据失败:', error); return []; } }, /** * 序列化客户黑名单数组为JSON字符串 * @param {Array} customerBlacklist 客户黑名单数组 * @returns {string} JSON字符串 */ stringifyCustomerBlacklist(customerBlacklist) { if (!Array.isArray(customerBlacklist) || customerBlacklist.length === 0) return '[]'; try { return JSON.stringify(customerBlacklist); } catch (error) { console.warn('序列化客户黑名单数据失败:', error); return '[]'; } } }; /** * 创建初始表单数据 * @returns {AnnouncementFormModel} 初始表单数据 */ export function createInitialFormData() { return { title: '', content: '', categoryId: '', categoryName: '', orgId: 0, orgCode: '', orgName: '', visibleRoles: [], brandScope: [], customerBlacklist: [], remark: '', status: 0 }; } /** * 公告表单混入 */ export default { props: { /** 是否显示表单 */ visible: { type: Boolean, default: false }, /** 是否为编辑模式 */ isEdit: { type: Boolean, default: false }, /** 公告ID(编辑时使用) */ announcementId: { type: String, default: '' }, /** 编辑数据(可选,优先级高于announcementId) */ editData: { type: Object, default: null }, /** 表单标题(可选) */ title: { type: String, default: '' } }, data() { return { /** @type {AnnouncementFormModel} 公告表单数据模型 */ formData: createInitialFormData(), /** @type {boolean} 保存操作加载状态 */ saveLoading: false, /** @type {boolean} 表单加载状态 */ formLoading: false, /** @type {Array<{id: string, name: string}>} 分类选项列表 */ categoryOptions: [], /** @type {boolean} 分类选项加载状态 */ categoryLoading: false, /** @type {Array<{value: number, label: string}>} 角色选项列表 */ roleOptions: ROLE_OPTIONS, /** @type {Array<{value: number, label: string, Customer_NAME?: string, Customer_CODE?: string}>} 客户选项列表(用于黑名单选择) */ customerOptions: [], /** @type {boolean} 客户选项加载状态 */ customerLoading: false, /** @type {Array<{value: string, label: string, name?: string, code?: string}>} 品牌选项列表 */ brandOptions: [], /** @type {boolean} 品牌选项加载状态 */ brandLoading: false, /** @type {Array} 选中的客户ID数组 */ selectedCustomerIds: [], /** 表单验证规则 */ formRules: { title: [ { required: true, message: '请输入公告标题', trigger: 'blur' }, { min: 1, max: 200, message: '标题长度在 1 到 200 个字符', trigger: 'blur' } ], categoryId: [ { required: true, message: '请选择公告分类', trigger: 'change' } ], orgId: [ { required: true, message: '请选择组织', trigger: 'change' } ], orgCode: [ { required: true, message: '组织编码不能为空', trigger: 'blur' } ], orgName: [ { required: true, message: '组织名称不能为空', trigger: 'blur' } ], visibleRoles: [ { required: true, type: 'array', min: 1, message: '请至少选择一个可见角色', trigger: 'change' } ], content: [ { required: true, message: '请输入公告内容', trigger: 'blur' } ] }, /** 表单配置 */ formOption: {} }; }, computed: { /** * 表单标题 * @returns {string} 表单标题 * @this {AnnouncementFormMixinComponent & Vue} */ formTitle() { if (this.title) return this.title; return this.isEdit ? '编辑公告' : '新增公告'; }, /** * 是否可编辑 * @returns {boolean} 是否可编辑 * @this {AnnouncementFormMixinComponent & Vue} */ canEdit() { return !this.formLoading && !this.saveLoading; } }, watch: { /** * 监听表单显示状态变化 * @param {boolean} newVal 新值 */ visible: { /** * @this {AnnouncementFormMixinComponent & Vue} * @param {boolean} newVal */ handler(newVal) { if (newVal) { // 初始化表单配置,确保表单列正确渲染 this.initFormOption(); // 初始化表单数据 this.formData = createInitialFormData(); // 加载分类选项 this.loadCategoryOptions(); } else { // 重置表单 if (this.$refs.form && typeof this.$refs.form.resetFields === 'function') { this.$refs.form.resetFields(); } } }, immediate: true }, /** * 监听编辑数据变化 * @param {import('@/api/types/announcement').NoticeRecord|null} newVal 新值 */ editData: { /** * @this {AnnouncementFormMixinComponent & Vue} * @param {import('@/api/types/announcement').NoticeRecord|null} newVal */ handler(newVal) { if (newVal && this.visible) { this.formData = { id: newVal.id || '', title: newVal.title || '', content: newVal.content || '', categoryId: newVal.categoryId || '', categoryName: newVal.categoryName, orgId: Number(newVal.orgId || 0), orgCode: newVal.orgCode || '', orgName: newVal.orgName || '', visibleRoles: dataConverter.parseVisibleRoles(newVal.visibleRoles || 0), brandScope: dataConverter.parseBrandScope(newVal.brandScope || '[]'), customerBlacklist: dataConverter.parseCustomerBlacklist(newVal.customerBlacklist || '[]'), remark: newVal.remark || '', status: Number(newVal.status || 0), createTime: newVal.createTime, updateTime: newVal.updateTime }; } }, deep: true, immediate: true }, /** * 监听分类ID变化,自动设置分类名称 */ 'formData.categoryId': { handler(newVal) { if (newVal && this.categoryOptions && this.categoryOptions.length > 0) { const category = this.categoryOptions.find(item => item.value === newVal); if (category) { this.formData.categoryName = category.name; } } else { this.formData.categoryName = ''; } }, immediate: true }, /** * 监听客户黑名单变化,同步selectedCustomerIds */ 'formData.customerBlacklist': { handler(newVal) { if (Array.isArray(newVal)) { this.selectedCustomerIds = newVal.map(item => item.ID || item.id).filter(id => id != null); } }, deep: true, immediate: true } }, methods: { // 数据转换方法 /** * API数据转换为表单模型 * @param {NoticeRecord} apiData API数据 * @returns {AnnouncementFormModel} 表单模型 * @this {AnnouncementFormMixinComponent & Vue} */ apiToFormModel: dataConverter.apiToFormModel, /** * 表单模型转换为API数据 * @param {AnnouncementFormModel} formModel 表单模型 * @returns {NoticeFormData} API数据 * @this {AnnouncementFormMixinComponent & Vue} */ formModelToApi: dataConverter.formModelToApi, /** * 解析角色掩码 * @param {string | number} rolesMask 角色掩码 * @returns {Array} 角色类型数组 * @this {AnnouncementFormMixinComponent & Vue} */ parseVisibleRoles: dataConverter.parseVisibleRoles.bind(dataConverter), /** * 计算角色掩码 * @param {Array} roles 角色类型数组 * @returns {number} 角色掩码值 * @this {AnnouncementFormMixinComponent & Vue} */ calculateRolesMask: dataConverter.calculateRolesMask.bind(dataConverter), /** * 客户黑名单字符串化 * @param {Array} customerBlacklist 客户黑名单 * @returns {string} 字符串化结果 * @this {AnnouncementFormMixinComponent & Vue} */ stringifyCustomerBlacklist: dataConverter.stringifyCustomerBlacklist, /** * 品牌范围字符串化 * @param {Array} brandScope 品牌范围 * @returns {string} 字符串化结果 * @this {AnnouncementFormMixinComponent & Vue} */ stringifyBrandScope: dataConverter.stringifyBrandScope, /** * 创建初始表单数据 * @returns {AnnouncementFormModel} 初始表单数据 * @this {AnnouncementFormMixinComponent & Vue} */ createInitialFormData, /** * 初始化表单数据 * @this {AnnouncementFormMixinComponent & Vue} * @returns {Promise} */ async initFormData() { this.formData = this.createInitialFormData(); if (this.editData) { // 优先使用传入的编辑数据 this.formData = dataConverter.apiToFormModel(this.editData); } else if (this.isEdit && this.announcementId) { // 根据ID加载公告详情 await this.loadAnnouncementDetail(); } }, /** * 初始化表单配置 * @this {AnnouncementFormMixinComponent & Vue} */ initFormOption() { this.formOption = { submitBtn: false, emptyBtn: false, column: [ { prop: 'title', label: '公告标题', type: 'input', span: 24, rules: this.formRules.title }, { prop: 'categoryId', label: '公告分类', type: 'select', span: 12, dicData: this.categoryOptions, props: { label: 'name', value: 'id' }, rules: this.formRules.categoryId }, { prop: 'status', label: '状态', type: 'select', span: 12, dicData: STATUS_OPTIONS, props: { label: 'label', value: 'value' } }, { prop: 'orgName', label: '组织名称', type: 'input', span: 8, rules: this.formRules.orgName }, { prop: 'orgCode', label: '组织编码', type: 'input', span: 8, rules: this.formRules.orgCode }, { prop: 'orgId', label: '组织ID', type: 'number', span: 8, rules: this.formRules.orgId }, { prop: 'visibleRoles', label: '可见角色', type: 'checkbox', span: 24, dicData: this.roleOptions, props: { label: 'label', value: 'value' }, rules: this.formRules.visibleRoles }, { prop: 'brandScope', label: '品牌范围', type: 'select', span: 12, multiple: true, filterable: true, remote: true, remoteMethod: this.remoteSearchBrands, loading: this.brandLoading, dicData: this.brandOptions, props: { label: 'label', value: 'value' } }, { prop: 'customerBlacklist', label: '客户黑名单', type: 'select', span: 12, multiple: true, filterable: true, remote: true, remoteMethod: this.remoteSearchCustomers, loading: this.customerLoading, dicData: this.customerOptions, props: { label: 'label', value: 'value' } }, { prop: 'content', label: '公告内容', component: 'AvueUeditor', options: { action: '/api/blade-resource/oss/endpoint/put-file', props: { res: 'data', url: 'link', } }, span: 24, minRows: 6, rules: this.formRules.content }, { prop: 'remark', label: '备注', type: 'textarea', span: 24, minRows: 2, maxRows: 4 } ] }; }, /** * 加载公告详情 * @this {AnnouncementFormMixinComponent & Vue} * @returns {Promise} */ async loadAnnouncementDetail() { if (!this.announcementId) return; try { this.formLoading = true; const response = await getAnnouncement(this.announcementId); if (response.data && response.data.success) { /** @type {NoticeRecord} */ const apiData = response.data.data; this.formData = dataConverter.apiToFormModel(apiData); this.$emit('loaded'); } else { this.$message.error(response.data?.message || '加载公告详情失败'); } } catch (error) { console.error('加载公告详情失败:', error); this.$message.error('加载公告详情失败'); } finally { this.formLoading = false; } }, /** * 加载分类选项 * @this {AnnouncementFormMixinComponent & Vue} * @returns {Promise} */ async loadCategoryOptions() { try { this.categoryLoading = true; const response = await getCategoryList(); if (response.data && response.data.success) { /** @type {Array<{id: string, name: string, label: string, value: string}>} */ this.categoryOptions = (response.data.data || []).map(item => ({ id: String(item.id), name: String(item.name), label: String(item.name), value: String(item.id) })); } } catch (error) { console.error('加载分类选项失败:', error); } finally { this.categoryLoading = false; } }, /** * 处理表单提交 * @this {AnnouncementFormMixinComponent & Vue} * @returns {Promise} */ async handleSubmit() { try { // 表单验证 const isValid = await this.validateForm(); if (!isValid) return; this.saveLoading = true; // 转换数据格式 /** @type {NoticeFormData} */ const submitData = dataConverter.formModelToApi(this.formData); // 提交数据 const response = await this.submitAnnouncementData(submitData); if (response.data && response.data.success) { this.$message.success(this.isEdit ? '更新成功' : '创建成功'); this.$emit('submit-success', this.formData); this.$emit('save-success', this.formData); this.handleCancel(); } else { this.$message.error(response.data?.message || '保存失败'); } } catch (error) { console.error('提交表单失败:', error); this.$message.error('保存失败'); } finally { this.saveLoading = false; } }, /** * 重置表单 * @this {AnnouncementFormMixinComponent & Vue} */ handleReset() { this.formData = this.createInitialFormData(); this.customerOptions = []; this.selectedCustomerIds = []; this.brandOptions = []; this.$emit('reset'); }, /** * 取消操作 * @this {AnnouncementFormMixinComponent & Vue} */ handleCancel() { this.$emit('cancel'); this.$emit('update:visible', false); }, /** * 提交公告数据 * @param {NoticeFormData} submitData 提交数据 * @returns {Promise>} API响应 * @this {AnnouncementFormMixinComponent & Vue} */ async submitAnnouncementData(submitData) { if (this.isEdit) { return await updateAnnouncement({ ...submitData, id: this.announcementId }); } else { return await addAnnouncement(submitData); } }, /** * 验证表单 * @returns {Promise} 验证结果 * @this {AnnouncementFormMixinComponent & Vue} */ async validateForm() { try { const form = this.$refs.form; if (form && typeof form.validate === 'function') { await form.validate(); return true; } return false; } catch (error) { console.warn('表单验证失败:', error); return false; } }, /** * 远程搜索客户 * @param {string} query 搜索关键词 * @returns {Promise} * @this {AnnouncementFormMixinComponent & Vue} */ async remoteSearchCustomers(query) { if (!query || query.length < 1) { this.customerOptions = []; return; } try { this.customerLoading = true; const response = await getCustomerList(1, 20, { customerName: query.trim() }); if (response?.data?.success && response.data.data?.records) { this.customerOptions = response.data.data.records.map(customer => ({ value: customer.Customer_ID, label: customer.Customer_NAME, Customer_NAME: customer.Customer_NAME, Customer_CODE: customer.Customer_CODE })); } else { this.customerOptions = []; const errorMsg = response?.data?.msg || '搜索客户失败'; this.$message.warning(errorMsg); } } catch (error) { console.error('搜索客户失败:', error); this.$message.error('网络错误,搜索客户失败'); this.customerOptions = []; } finally { this.customerLoading = false; } }, /** * 远程搜索品牌 * @param {string} query 搜索关键词 * @returns {Promise} * @this {AnnouncementFormMixinComponent & Vue} */ async remoteSearchBrands(query) { if (!query || query.length < 2) { this.brandOptions = []; return; } try { this.brandLoading = true; // TODO: 实现品牌搜索API调用 this.brandOptions = []; } catch (error) { console.error('搜索品牌失败:', error); } finally { this.brandLoading = false; } }, /** * 处理客户黑名单变化 * @param {Array} selectedCustomerIds 选中的客户ID数组 * @this {AnnouncementFormMixinComponent & Vue} */ handleCustomerBlacklistChange(selectedCustomerIds) { this.selectedCustomerIds = selectedCustomerIds; /** @type {Array} */ this.formData.customerBlacklist = selectedCustomerIds.map(customerId => { const customer = this.customerOptions.find(option => option.value === customerId); return { ID: customerId, NAME: customer?.Customer_NAME || '', CODE: customer?.Customer_CODE || '' }; }); }, /** * 处理品牌范围变化 * @param {Array} selectedBrands 选中的品牌 * @this {AnnouncementFormMixinComponent & Vue} */ handleBrandScopeChange(selectedBrands) { /** @type {Array} */ this.formData.brandScope = selectedBrands.map(brand => { // 从label中提取品牌代码 const codeMatch = brand.label ? brand.label.match(/\(([^)]+)\)$/) : null; const code = codeMatch ? codeMatch[1] : brand.code || ''; return { id: String(brand.value), name: brand.name || (brand.label ? brand.label.replace(/\([^)]+\)$/, '').trim() : ''), code: code }; }); }, /** * 清空客户选项 * @this {AnnouncementFormMixinComponent & Vue} */ clearCustomerOptions() { this.customerOptions = []; this.selectedCustomerIds = []; this.formData.customerBlacklist = []; }, /** * 清空品牌选项 * @this {AnnouncementFormMixinComponent & Vue} */ clearBrandOptions() { this.brandOptions = []; } } };