Browse Source

公告表单

yz 3 weeks ago
parent
commit
b6d4a32ef2

+ 2 - 2
src/api/types/announcement.d.ts

@@ -144,8 +144,8 @@ export interface NoticeRecord {
   orgCode: string;
   orgName: string;
   visibleRoles: string; // API返回字符串格式的角色掩码
-  brandScope: string; // API返回JSON字符串格式
-  customerBlacklist: string; // API返回JSON字符串格式
+  brandScope: string; // API返回JSON字符串格式 example: "[{\"ID\": 1001, \"CODE\": \"MIC\", \"NAME\": \"米其林\"}, {\"ID\": 1002, \"CODE\": \"GY\", \"NAME\": \"固特异\"}]"
+  customerBlacklist: string; // API返回JSON字符串格式 example: "[{\"ID\": 9001, \"CODE\": \"BLK001\", \"NAME\": \"测试黑店A\"}]"
   remark: string;
 }
 

+ 797 - 0
src/components/announcement/announcement-form-mixin.js

@@ -0,0 +1,797 @@
+// @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 { 
+  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,
+      orgId: Number(formModel.orgId),
+      orgCode: formModel.orgCode,
+      orgName: formModel.orgName,
+      visibleRoles: this.calculateRolesMask(formModel.visibleRoles),
+      brandScope: this.stringifyBrandScope(formModel.brandScope),
+      customerBlacklist: this.stringifyCustomerBlacklist(formModel.customerBlacklist),
+      remark: formModel.remark,
+      status: formModel.status
+    };
+  },
+
+  /**
+   * 解析可见角色掩码为角色数组
+   * @param {string | number} rolesMask 角色掩码值
+   * @returns {Array<RoleType>} 角色类型数组
+   */
+  parseVisibleRoles(rolesMask) {
+    if (!rolesMask) return [];
+    const mask = typeof rolesMask === 'string' ? parseInt(rolesMask, 10) : rolesMask;
+    return parseRolesMask(mask);
+  },
+
+  /**
+   * 计算角色数组的掩码值
+   * @param {Array<RoleType>} roles 角色类型数组
+   * @returns {number} 角色掩码值
+   */
+  calculateRolesMask(roles) {
+    if (!Array.isArray(roles) || roles.length === 0) return 0;
+    return calculateRolesMask(roles);
+  },
+
+  /**
+   * 解析品牌范围JSON字符串
+   * @param {string} brandScopeStr 品牌范围JSON字符串
+   * @returns {Array<BrandScopeItem>} 品牌范围数组
+   */
+  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<BrandScopeItem>} 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<CustomerBlacklistItem>} 客户黑名单数组
+   */
+  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<CustomerBlacklistItem>} 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: '',
+    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,
+      /** 表单验证规则 */
+      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 新值
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    visible: {
+      handler(newVal) {
+        if (newVal) {
+          // 初始化表单数据
+          this.formData = createInitialFormData();
+          // 加载分类选项
+          this.loadCategoryOptions();
+        } else {
+          // 重置表单
+          if (this.$refs.form && typeof this.$refs.form.resetFields === 'function') {
+            this.$refs.form.resetFields();
+          }
+        }
+      },
+      immediate: true
+    },
+
+    /**
+     * 监听编辑数据变化
+     * @param {NoticeRecord|null} newVal 新值
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    editData: {
+      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
+    }
+  },
+
+  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<RoleType>} 角色类型数组
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    parseVisibleRoles: dataConverter.parseVisibleRoles.bind(dataConverter),
+    
+    /**
+     * 计算角色掩码
+     * @param {Array<RoleType>} roles 角色类型数组
+     * @returns {number} 角色掩码值
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    calculateRolesMask: dataConverter.calculateRolesMask.bind(dataConverter),
+    
+    /**
+     * 客户黑名单字符串化
+     * @param {Array<CustomerBlacklistItem>} customerBlacklist 客户黑名单
+     * @returns {string} 字符串化结果
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    stringifyCustomerBlacklist: dataConverter.stringifyCustomerBlacklist,
+    
+    /**
+     * 品牌范围字符串化
+     * @param {Array<BrandScopeItem>} brandScope 品牌范围
+     * @returns {string} 字符串化结果
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    stringifyBrandScope: dataConverter.stringifyBrandScope,
+
+    /**
+     * 创建初始表单数据
+     * @returns {AnnouncementFormModel} 初始表单数据
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    createInitialFormData,
+
+    /**
+     * 初始化表单数据
+     * @this {AnnouncementFormMixinComponent & Vue}
+     * @returns {Promise<void>}
+     */
+    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: '公告内容',
+            type: 'textarea',
+            span: 24,
+            minRows: 4,
+            maxRows: 8,
+            rules: this.formRules.content
+          },
+          {
+            prop: 'remark',
+            label: '备注',
+            type: 'textarea',
+            span: 24,
+            minRows: 2,
+            maxRows: 4
+          }
+        ]
+      };
+    },
+
+    /**
+     * 加载公告详情
+     * @this {AnnouncementFormMixinComponent & Vue}
+     * @returns {Promise<void>}
+     */
+    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<void>}
+     */
+    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<void>}
+     */
+    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.brandOptions = [];
+      this.$emit('reset');
+    },
+
+    /**
+     * 取消操作
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    handleCancel() {
+      this.$emit('cancel');
+      this.$emit('update:visible', false);
+    },
+
+    /**
+     * 提交公告数据
+     * @param {NoticeFormData} submitData 提交数据
+     * @returns {Promise<import('axios').AxiosResponse<any>>} API响应
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    async submitAnnouncementData(submitData) {
+      if (this.isEdit) {
+        return await updateAnnouncement(this.announcementId, submitData);
+      } else {
+        return await addAnnouncement(submitData);
+      }
+    },
+
+    /**
+     * 验证表单
+     * @returns {Promise<boolean>} 验证结果
+     * @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<void>}
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    async remoteSearchCustomers(query) {
+      if (!query || query.length < 2) {
+        this.customerOptions = [];
+        return;
+      }
+
+      try {
+        this.customerLoading = true;
+        // TODO: 实现客户搜索API调用
+        this.customerOptions = [];
+      } catch (error) {
+        console.error('搜索客户失败:', error);
+      } finally {
+        this.customerLoading = false;
+      }
+    },
+
+    /**
+     * 远程搜索品牌
+     * @param {string} query 搜索关键词
+     * @returns {Promise<void>}
+     * @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<CustomerOption>} selectedCustomers 选中的客户
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    handleCustomerBlacklistChange(selectedCustomers) {
+      /** @type {Array<CustomerBlacklistItem>} */
+      this.formData.customerBlacklist = selectedCustomers.map(customer => {
+        // 从label中提取客户代码
+        const codeMatch = customer.label ? customer.label.match(/\(([^)]+)\)$/) : null;
+        const code = codeMatch ? codeMatch[1] : customer.Customer_CODE || '';
+        
+        return {
+          id: typeof customer.value === 'number' ? customer.value : parseInt(String(customer.value), 10),
+          Customer_NAME: customer.Customer_NAME || (customer.label ? customer.label.replace(/\([^)]+\)$/, '').trim() : ''),
+          Customer_CODE: code
+        };
+      });
+    },
+
+    /**
+     * 处理品牌范围变化
+     * @param {Array<BrandOption>} selectedBrands 选中的品牌
+     * @this {AnnouncementFormMixinComponent & Vue}
+     */
+    handleBrandScopeChange(selectedBrands) {
+      /** @type {Array<BrandScopeItem>} */
+      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 {AnnouncementFormMixinComponent & Vue}
+     */
+    clearBrandOptions() {
+      this.brandOptions = [];
+    }
+  }
+};

+ 427 - 0
src/components/announcement/form-option.js

@@ -0,0 +1,427 @@
+// @ts-check
+
+/**
+ * @typedef {import('./types').AnnouncementFormOption} AnnouncementFormOption
+ * @typedef {import('./types').AnnouncementFormRules} AnnouncementFormRules
+ * @typedef {import('./types').ValidationRule} ValidationRule
+ */
+
+/**
+ * 公告表单AvueJS配置选项
+ * @fileoverview 定义公告表单的字段配置、验证规则和表单选项
+ */
+
+import { ROLE_OPTIONS } from '@/views/announcement/constants';
+
+/**
+ * 公告表单字段配置
+ * @type {import('./types').AnnouncementFormOption}
+ */
+export const announcementFormOption = {
+  // 表单基础配置
+  submitBtn: false, // 不显示默认提交按钮
+  emptyBtn: false,  // 不显示默认清空按钮
+  labelWidth: 120,  // 标签宽度
+  labelPosition: 'right', // 标签位置
+  size: 'medium',   // 表单尺寸
+  menuBtn: false,   // 不显示菜单按钮
+  
+  // 表单字段配置
+  column: [
+    {
+      label: '公告标题',
+      prop: 'title',
+      type: 'input',
+      placeholder: '请输入公告标题',
+      maxlength: 100,
+      showWordLimit: true,
+      rules: [
+        {
+          required: true,
+          message: '请输入公告标题',
+          trigger: 'blur'
+        },
+        {
+          min: 1,
+          max: 100,
+          message: '标题长度在1到100个字符',
+          trigger: 'blur'
+        }
+      ],
+      span: 24
+    },
+    {
+      label: '公告分类',
+      prop: 'categoryId',
+      type: 'select',
+      placeholder: '请选择公告分类',
+      dicUrl: '/api/announcement/categories',
+      dicMethod: 'get',
+      props: {
+        label: 'name',
+        value: 'id'
+      },
+      rules: [
+        {
+          required: true,
+          message: '请选择公告分类',
+          trigger: 'change'
+        }
+      ],
+      span: 12
+    },
+    {
+      label: '发布状态',
+      prop: 'status',
+      type: 'radio',
+      dicData: [
+        {
+          label: '草稿',
+          value: 0
+        },
+        {
+          label: '已发布',
+          value: 1
+        },
+        {
+          label: '已归档',
+          value: 2
+        }
+      ],
+      rules: [
+        {
+          required: true,
+          message: '请选择发布状态',
+          trigger: 'change'
+        }
+      ],
+      span: 12
+    },
+    {
+      label: '组织名称',
+      prop: 'orgName',
+      type: 'input',
+      placeholder: '请输入组织名称',
+      maxlength: 50,
+      showWordLimit: true,
+      rules: [
+        {
+          required: true,
+          message: '请输入组织名称',
+          trigger: 'blur'
+        },
+        {
+          max: 50,
+          message: '组织名称不能超过50个字符',
+          trigger: 'blur'
+        }
+      ],
+      span: 12
+    },
+    {
+      label: '组织ID',
+      prop: 'orgId',
+      type: 'input',
+      placeholder: '请输入组织ID',
+      rules: [
+        {
+          required: true,
+          message: '请输入组织ID',
+          trigger: 'blur'
+        }
+      ],
+      span: 12
+    },
+    {
+      label: '组织编码',
+      prop: 'orgCode',
+      type: 'input',
+      placeholder: '请输入组织编码',
+      maxlength: 20,
+      rules: [
+        {
+          required: true,
+          message: '请输入组织编码',
+          trigger: 'blur'
+        },
+        {
+          pattern: /^[A-Za-z0-9_-]+$/,
+          message: '组织编码只能包含字母、数字、下划线和横线',
+          trigger: 'blur'
+        }
+      ],
+      span: 12
+    },
+    {
+      label: '可见角色',
+      prop: 'visibleRoles',
+      type: 'checkbox',
+      dicData: ROLE_OPTIONS,
+      rules: [
+        {
+          required: true,
+          type: 'array',
+          min: 1,
+          message: '请至少选择一个可见角色',
+          trigger: 'change'
+        }
+      ],
+      span: 24
+    },
+    {
+      label: '品牌范围',
+      prop: 'brandScope',
+      type: 'select',
+      multiple: true,
+      placeholder: '请选择品牌范围',
+      filterable: true,
+      remote: true,
+      slot: true, // 使用自定义插槽
+      span: 24
+    },
+    {
+      label: '客户黑名单',
+      prop: 'customerBlacklist',
+      type: 'select',
+      multiple: true,
+      placeholder: '请选择客户黑名单',
+      filterable: true,
+      remote: true,
+      slot: true, // 使用自定义插槽
+      span: 24
+    },
+    {
+      label: '公告内容',
+      prop: 'content',
+      type: 'textarea',
+      placeholder: '请输入公告内容',
+      minRows: 6,
+      maxRows: 12,
+      maxlength: 5000,
+      showWordLimit: true,
+      slot: true, // 使用自定义插槽
+      rules: [
+        {
+          required: true,
+          message: '请输入公告内容',
+          trigger: 'blur'
+        },
+        {
+          min: 1,
+          max: 5000,
+          message: '内容长度在1到5000个字符',
+          trigger: 'blur'
+        }
+      ],
+      span: 24
+    },
+    {
+      label: '备注',
+      prop: 'remark',
+      type: 'textarea',
+      placeholder: '请输入备注信息(可选)',
+      minRows: 3,
+      maxRows: 6,
+      maxlength: 500,
+      showWordLimit: true,
+      span: 24
+    }
+  ]
+};
+
+/**
+ * 获取表单配置选项
+ * @param {Partial<AnnouncementFormOption>} options 自定义配置选项
+ * @returns {AnnouncementFormOption} 表单配置
+ */
+export function getAnnouncementFormOption(options = {}) {
+  /** @type {AnnouncementFormOption} */
+  const baseOption = announcementFormOption;
+  return {
+    ...baseOption,
+    ...options,
+    column: [
+      ...(baseOption.column || []),
+      ...(options && options.column ? options.column : [])
+    ]
+  };
+}
+
+/**
+ * 获取表单配置
+ * @param {Partial<AnnouncementFormOption>} options 配置选项
+ * @returns {AnnouncementFormOption} 表单配置
+ */
+export function getFormOption(options = {}) {
+  /** @type {AnnouncementFormOption} */
+  const baseOption = {
+    ...announcementFormOption
+  };
+  
+  if (options.column && Array.isArray(options.column)) {
+    baseOption.column = baseOption.column.map(col => {
+      const optionCol = options.column?.find(opt => opt.prop === col.prop);
+      return optionCol ? { ...col, ...optionCol } : col;
+    });
+  }
+  
+  return baseOption;
+}
+
+/**
+ * 表单验证规则
+ */
+export const formValidationRules = {
+  // 标题验证
+  title: [
+    {
+      required: true,
+      message: '请输入公告标题',
+      trigger: 'blur'
+    },
+    {
+      min: 1,
+      max: 100,
+      message: '标题长度在1到100个字符',
+      trigger: 'blur'
+    }
+  ],
+  
+  // 分类验证
+  categoryId: [
+    {
+      required: true,
+      message: '请选择公告分类',
+      trigger: 'change'
+    }
+  ],
+  
+  // 状态验证
+  status: [
+    {
+      required: true,
+      message: '请选择发布状态',
+      trigger: 'change'
+    }
+  ],
+  
+  // 组织名称验证
+  orgName: [
+    {
+      required: true,
+      message: '请输入组织名称',
+      trigger: 'blur'
+    },
+    {
+      max: 50,
+      message: '组织名称不能超过50个字符',
+      trigger: 'blur'
+    }
+  ],
+  
+  // 组织ID验证
+  orgId: [
+    {
+      required: true,
+      message: '请输入组织ID',
+      trigger: 'blur'
+    }
+  ],
+  
+  // 组织编码验证
+  orgCode: [
+    {
+      required: true,
+      message: '请输入组织编码',
+      trigger: 'blur'
+    },
+    {
+      pattern: /^[A-Za-z0-9_-]+$/,
+      message: '组织编码只能包含字母、数字、下划线和横线',
+      trigger: 'blur'
+    }
+  ],
+  
+  // 可见角色验证
+  visibleRoles: [
+    {
+      required: true,
+      type: 'array',
+      min: 1,
+      message: '请至少选择一个可见角色',
+      trigger: 'change'
+    }
+  ],
+  
+  // 内容验证
+  content: [
+    {
+      required: true,
+      message: '请输入公告内容',
+      trigger: 'blur'
+    },
+    {
+      min: 1,
+      max: 5000,
+      message: '内容长度在1到5000个字符',
+      trigger: 'blur'
+    }
+  ]
+};
+
+/**
+ * 字段显示配置
+ */
+export const fieldDisplayConfig = {
+  // 必填字段
+  required: ['title', 'categoryId', 'status', 'orgName', 'orgId', 'orgCode', 'visibleRoles', 'content'],
+  
+  // 可选字段
+  optional: ['brandScope', 'customerBlacklist', 'remark'],
+  
+  // 只读字段(编辑时)
+  readonly: [],
+  
+  // 隐藏字段
+  hidden: []
+};
+
+/**
+ * 根据模式获取字段配置
+ * @param {'add' | 'edit' | 'view'} mode 表单模式
+ * @returns {Object} 字段配置
+ */
+export function getFieldConfigByMode(mode) {
+  const config = {
+    add: {
+      disabled: [],
+      hidden: []
+    },
+    edit: {
+      disabled: [],
+      hidden: []
+    },
+    view: {
+      disabled: fieldDisplayConfig.required.concat(fieldDisplayConfig.optional),
+      hidden: []
+    }
+  };
+  
+  return config[mode] || config.add;
+}
+
+/**
+ * 获取表单验证规则
+ * @returns {AnnouncementFormRules} 验证规则
+ */
+export function getFormRules() {
+  return formValidationRules;
+}
+
+export default {
+  announcementFormOption,
+  getAnnouncementFormOption,
+  formValidationRules,
+  fieldDisplayConfig,
+  getFieldConfigByMode
+};

+ 81 - 0
src/components/announcement/index.js

@@ -0,0 +1,81 @@
+// @ts-check
+
+import AnnouncementFormMixin from './announcement-form-mixin';
+import { getFormOption, getFormRules, announcementFormOption, formValidationRules } from './form-option';
+
+/**
+ * 数据转换器(临时实现)
+ */
+const DataConverter = {
+  /**
+   * 将API数据转换为表单数据
+   * @param {import('@/api/types/announcement').NoticeRecord} apiData API数据
+   * @returns {import('./types').AnnouncementFormModel} 表单数据
+   */
+  apiToForm(apiData) {
+     return {
+       id: apiData.id || '',
+       title: apiData.title || '',
+       content: apiData.content || '',
+       categoryId: apiData.categoryId || '',
+       orgId: apiData.orgId || 0,
+       orgCode: apiData.orgCode || '',
+       orgName: apiData.orgName || '',
+       visibleRoles: [],
+       brandScope: [],
+       customerBlacklist: [],
+       remark: apiData.remark || '',
+       status: apiData.status || 0
+     };
+   },
+
+  /**
+   * 将表单数据转换为API数据
+   * @param {import('./types').AnnouncementFormModel} formData 表单数据
+   * @returns {import('@/api/types/announcement').NoticeFormData} API数据
+   */
+  formToApi(formData) {
+     return {
+       title: formData.title,
+       content: formData.content,
+       categoryId: formData.categoryId,
+       orgId: Number(formData.orgId) || 0,
+       orgCode: formData.orgCode,
+       orgName: formData.orgName,
+       visibleRoles: 0, // 需要根据实际逻辑计算
+       brandScope: '',
+       customerBlacklist: '',
+       remark: formData.remark,
+       status: Number(formData.status) || 0
+     };
+   }
+};
+
+/**
+ * 公告表单组件安装函数
+ * @param {any} Vue Vue构造函数
+ */
+function install(Vue) {
+  if (Vue && typeof Vue.mixin === 'function') {
+    Vue.mixin(AnnouncementFormMixin);
+  }
+}
+
+export default {
+  install,
+  AnnouncementFormMixin,
+  getFormOption,
+  getFormRules,
+  DataConverter,
+  announcementFormOption,
+  formValidationRules
+};
+
+export {
+  AnnouncementFormMixin,
+  getFormOption,
+  getFormRules,
+  DataConverter,
+  announcementFormOption,
+  formValidationRules
+};

+ 484 - 0
src/components/announcement/index.scss

@@ -0,0 +1,484 @@
+/**
+ * 公告表单组件样式
+ * @fileoverview 公告表单组件的样式定义,包括响应式设计和主题适配
+ */
+
+// 变量定义
+$primary-color: #409eff;
+$success-color: #67c23a;
+$warning-color: #e6a23c;
+$danger-color: #f56c6c;
+$info-color: #909399;
+
+$border-color-base: #dcdfe6;
+$border-color-light: #e4e7ed;
+$border-color-lighter: #ebeef5;
+
+$text-color-primary: #303133;
+$text-color-regular: #606266;
+$text-color-secondary: #909399;
+$text-color-placeholder: #c0c4cc;
+
+$background-color-base: #f5f7fa;
+$background-color-light: #fafafa;
+
+// 公告表单主容器
+.announcement-form {
+  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
+  
+  // 对话框样式
+  .announcement-form-dialog {
+    .el-dialog {
+      border-radius: 8px;
+      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
+      
+      .el-dialog__header {
+        background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+        border-radius: 8px 8px 0 0;
+        padding: 20px 24px;
+        border-bottom: 1px solid $border-color-light;
+        
+        .el-dialog__title {
+          font-size: 18px;
+          font-weight: 600;
+          color: $text-color-primary;
+          line-height: 1.4;
+        }
+        
+        .el-dialog__headerbtn {
+          top: 20px;
+          right: 20px;
+          
+          .el-dialog__close {
+            color: $text-color-secondary;
+            font-size: 18px;
+            
+            &:hover {
+              color: $primary-color;
+            }
+          }
+        }
+      }
+      
+      .el-dialog__body {
+        padding: 24px;
+        background-color: #fff;
+      }
+      
+      .el-dialog__footer {
+        padding: 16px 24px 24px;
+        background-color: $background-color-light;
+        border-top: 1px solid $border-color-light;
+        border-radius: 0 0 8px 8px;
+      }
+    }
+    
+    // 表单容器
+    .form-container {
+      max-height: 70vh;
+      overflow-y: auto;
+      
+      // 自定义滚动条
+      &::-webkit-scrollbar {
+        width: 6px;
+      }
+      
+      &::-webkit-scrollbar-track {
+        background: $background-color-base;
+        border-radius: 3px;
+      }
+      
+      &::-webkit-scrollbar-thumb {
+        background: $border-color-base;
+        border-radius: 3px;
+        
+        &:hover {
+          background: $text-color-placeholder;
+        }
+      }
+    }
+    
+    // 富文本编辑器容器
+    .rich-editor-container {
+      width: 100%;
+      
+      .el-textarea {
+        .el-textarea__inner {
+          border-radius: 6px;
+          border: 1px solid $border-color-base;
+          transition: border-color 0.2s ease;
+          
+          &:focus {
+            border-color: $primary-color;
+            box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+          }
+        }
+      }
+    }
+    
+    // 对话框底部按钮
+    .dialog-footer {
+      text-align: right;
+      
+      .el-button {
+        margin-left: 12px;
+        padding: 10px 20px;
+        border-radius: 6px;
+        font-weight: 500;
+        transition: all 0.2s ease;
+        
+        &:first-child {
+          margin-left: 0;
+        }
+        
+        &.el-button--primary {
+          background: linear-gradient(135deg, $primary-color 0%, #66b1ff 100%);
+          border: none;
+          
+          &:hover {
+            background: linear-gradient(135deg, #66b1ff 0%, $primary-color 100%);
+            transform: translateY(-1px);
+            box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+          }
+          
+          &:active {
+            transform: translateY(0);
+          }
+        }
+        
+        &.el-button--default {
+          &:hover {
+            color: $primary-color;
+            border-color: $primary-color;
+            background-color: rgba(64, 158, 255, 0.05);
+          }
+        }
+      }
+    }
+  }
+}
+
+// AvueJS 表单样式增强
+.avue-form {
+  .el-form-item {
+    margin-bottom: 24px;
+    
+    .el-form-item__label {
+      font-weight: 500;
+      color: $text-color-primary;
+      line-height: 1.6;
+      padding-bottom: 8px;
+      
+      &::before {
+        margin-right: 4px;
+      }
+    }
+    
+    .el-form-item__content {
+      line-height: 1.6;
+    }
+  }
+  
+  // 输入框样式
+  .el-input {
+    .el-input__inner {
+      border-radius: 6px;
+      border: 1px solid $border-color-base;
+      transition: all 0.2s ease;
+      
+      &:focus {
+        border-color: $primary-color;
+        box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+      }
+      
+      &:hover {
+        border-color: $text-color-placeholder;
+      }
+    }
+  }
+  
+  // 选择器样式
+  .el-select {
+    width: 100%;
+    
+    .el-input__inner {
+      border-radius: 6px;
+    }
+    
+    .el-select__tags {
+      max-height: 120px;
+      overflow-y: auto;
+      
+      .el-tag {
+        margin: 2px 4px 2px 0;
+        border-radius: 4px;
+        background-color: rgba(64, 158, 255, 0.1);
+        border-color: rgba(64, 158, 255, 0.2);
+        color: $primary-color;
+        
+        .el-tag__close {
+          color: $primary-color;
+          
+          &:hover {
+            background-color: $primary-color;
+            color: #fff;
+          }
+        }
+      }
+    }
+  }
+  
+  // 多选框组样式
+  .el-checkbox-group {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+    
+    .el-checkbox {
+      margin-right: 0;
+      margin-bottom: 8px;
+      
+      .el-checkbox__label {
+        font-weight: 400;
+        color: $text-color-regular;
+      }
+      
+      &.is-checked {
+        .el-checkbox__label {
+          color: $primary-color;
+          font-weight: 500;
+        }
+      }
+    }
+  }
+  
+  // 文本域样式
+  .el-textarea {
+    .el-textarea__inner {
+      border-radius: 6px;
+      border: 1px solid $border-color-base;
+      transition: all 0.2s ease;
+      font-family: inherit;
+      
+      &:focus {
+        border-color: $primary-color;
+        box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+      }
+      
+      &:hover {
+        border-color: $text-color-placeholder;
+      }
+    }
+  }
+  
+  // 数字输入框样式
+  .el-input-number {
+    width: 100%;
+    
+    .el-input__inner {
+      text-align: left;
+    }
+  }
+}
+
+// 下拉选项样式
+.el-select-dropdown {
+  border-radius: 6px;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+  
+  .el-select-dropdown__item {
+    padding: 8px 16px;
+    
+    &:hover {
+      background-color: rgba(64, 158, 255, 0.05);
+    }
+    
+    &.selected {
+      background-color: rgba(64, 158, 255, 0.1);
+      color: $primary-color;
+      font-weight: 500;
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 1200px) {
+  .announcement-form {
+    .announcement-form-dialog {
+      .el-dialog {
+        width: 90% !important;
+        margin: 0 auto;
+      }
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .announcement-form {
+    .announcement-form-dialog {
+      .el-dialog {
+        width: 95% !important;
+        margin: 0 auto;
+        
+        .el-dialog__header {
+          padding: 16px 20px;
+          
+          .el-dialog__title {
+            font-size: 16px;
+          }
+        }
+        
+        .el-dialog__body {
+          padding: 16px 20px;
+        }
+        
+        .el-dialog__footer {
+          padding: 12px 20px 20px;
+        }
+      }
+      
+      .form-container {
+        max-height: 60vh;
+      }
+      
+      .dialog-footer {
+        .el-button {
+          margin-left: 8px;
+          padding: 8px 16px;
+          font-size: 14px;
+        }
+      }
+    }
+    
+    .avue-form {
+      .el-form-item {
+        margin-bottom: 20px;
+        
+        .el-form-item__label {
+          font-size: 14px;
+          padding-bottom: 6px;
+        }
+      }
+      
+      .el-checkbox-group {
+        gap: 8px;
+        
+        .el-checkbox {
+          margin-bottom: 6px;
+          
+          .el-checkbox__label {
+            font-size: 14px;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media (max-width: 480px) {
+  .announcement-form {
+    .announcement-form-dialog {
+      .el-dialog {
+        width: 98% !important;
+        
+        .el-dialog__header {
+          padding: 12px 16px;
+          
+          .el-dialog__title {
+            font-size: 15px;
+          }
+        }
+        
+        .el-dialog__body {
+          padding: 12px 16px;
+        }
+        
+        .el-dialog__footer {
+          padding: 8px 16px 16px;
+        }
+      }
+      
+      .form-container {
+        max-height: 55vh;
+      }
+      
+      .dialog-footer {
+        text-align: center;
+        
+        .el-button {
+          margin: 4px;
+          width: calc(50% - 8px);
+          
+          &:first-child {
+            margin-left: 4px;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 加载状态样式
+.el-loading-mask {
+  border-radius: 6px;
+  
+  .el-loading-spinner {
+    .el-loading-text {
+      color: $text-color-regular;
+      font-weight: 400;
+    }
+  }
+}
+
+// 错误状态样式
+.el-form-item.is-error {
+  .el-input__inner,
+  .el-textarea__inner {
+    border-color: $danger-color;
+    
+    &:focus {
+      box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.2);
+    }
+  }
+}
+
+// 成功状态样式
+.el-form-item.is-success {
+  .el-input__inner,
+  .el-textarea__inner {
+    border-color: $success-color;
+    
+    &:focus {
+      box-shadow: 0 0 0 2px rgba(103, 194, 58, 0.2);
+    }
+  }
+}
+
+// 暗色主题适配
+@media (prefers-color-scheme: dark) {
+  .announcement-form {
+    .announcement-form-dialog {
+      .el-dialog {
+        background-color: #2d3748;
+        
+        .el-dialog__header {
+          background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%);
+          border-bottom-color: #4a5568;
+          
+          .el-dialog__title {
+            color: #e2e8f0;
+          }
+        }
+        
+        .el-dialog__body {
+          background-color: #2d3748;
+        }
+        
+        .el-dialog__footer {
+          background-color: #1a202c;
+          border-top-color: #4a5568;
+        }
+      }
+    }
+  }
+}

+ 365 - 0
src/components/announcement/index.vue

@@ -0,0 +1,365 @@
+<template>
+  <div class="announcement-form">
+    <!-- 表单对话框 -->
+    <el-dialog
+      :title="formTitle"
+      :visible.sync="dialogVisible"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      width="80%"
+      top="5vh"
+      class="announcement-form-dialog"
+      @close="handleCancel"
+    >
+      <!-- 表单内容 -->
+      <div v-loading="formLoading" class="form-container">
+        <avue-form
+          ref="form"
+          v-model="formData"
+          :option="formOption"
+          :rules="formRules"
+          @submit="handleSubmit"
+          @reset-change="handleReset"
+        >
+          <!-- 自定义插槽:品牌范围 -->
+          <template #brandScope="{ value, column }">
+            <el-select
+              v-model="formData.brandScope"
+              multiple
+              filterable
+              remote
+              reserve-keyword
+              placeholder="请搜索并选择品牌"
+              :remote-method="remoteSearchBrands"
+              :loading="brandLoading"
+              style="width: 100%"
+              @change="handleBrandScopeChange"
+              @clear="clearBrandOptions"
+            >
+              <el-option
+                v-for="item in brandOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </template>
+
+          <!-- 自定义插槽:客户黑名单 -->
+          <template #customerBlacklist="{ value, column }">
+            <el-select
+              v-model="formData.customerBlacklist"
+              multiple
+              filterable
+              remote
+              reserve-keyword
+              placeholder="请搜索并选择客户"
+              :remote-method="remoteSearchCustomers"
+              :loading="customerLoading"
+              style="width: 100%"
+              @change="handleCustomerBlacklistChange"
+              @clear="clearCustomerOptions"
+            >
+              <el-option
+                v-for="item in customerOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </template>
+
+          <!-- 自定义插槽:富文本编辑器 -->
+          <template #content="{ value, column }">
+            <div class="rich-editor-container">
+              <el-input
+                v-model="formData.content"
+                type="textarea"
+                :rows="6"
+                placeholder="请输入公告内容"
+                maxlength="5000"
+                show-word-limit
+              />
+            </div>
+          </template>
+        </avue-form>
+      </div>
+
+      <!-- 对话框底部按钮 -->
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="handleCancel">
+          取消
+        </el-button>
+        <el-button @click="handleReset">
+          重置
+        </el-button>
+        <el-button
+          type="primary"
+          :loading="saveLoading"
+          :disabled="!canEdit"
+          @click="handleSubmit"
+        >
+          {{ isEdit ? '更新' : '保存' }}
+        </el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import announcementFormMixin from './announcement-form-mixin';
+
+/**
+ * 公告表单组件
+ * @description 基于AvueJS的公告表单组件,支持新增和编辑功能
+ */
+export default {
+  name: 'AnnouncementForm',
+  
+  mixins: [announcementFormMixin],
+
+  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 {
+      /** 对话框显示状态(内部控制) */
+      dialogVisible: false
+    };
+  },
+
+  watch: {
+    visible: {
+      handler(newVal) {
+        this.dialogVisible = newVal;
+      },
+      immediate: true
+    },
+
+    dialogVisible(newVal) {
+      if (!newVal) {
+        this.$emit('update:visible', false);
+      }
+    }
+  },
+
+  methods: {
+    /**
+     * 处理品牌范围变化
+     * @param {Array} selectedValues 选中的品牌ID数组
+     */
+    handleBrandScopeChange(selectedValues) {
+      // 根据选中的值更新formData中的brandScope
+      const selectedBrands = this.brandOptions.filter(option => 
+        selectedValues.includes(option.value)
+      );
+      
+      this.formData.brandScope = selectedBrands.map(brand => ({
+        id: brand.value,
+        name: brand.name || brand.label.split(' (')[0],
+        code: brand.code || brand.label.split('(')[1]?.replace(')', '') || ''
+      }));
+    },
+
+    /**
+     * 处理客户黑名单变化
+     * @param {Array} selectedValues 选中的客户ID数组
+     */
+    handleCustomerBlacklistChange(selectedValues) {
+      // 根据选中的值更新formData中的customerBlacklist
+      const selectedCustomers = this.customerOptions.filter(option => 
+        selectedValues.includes(option.value)
+      );
+      
+      this.formData.customerBlacklist = selectedCustomers.map(customer => ({
+        id: customer.value,
+        Customer_NAME: customer.Customer_NAME || customer.label.split(' (')[0],
+        Customer_CODE: customer.Customer_CODE || customer.label.split('(')[1]?.replace(')', '') || ''
+      }));
+    },
+
+    /**
+     * 处理表单提交
+     */
+    async handleSubmit() {
+      try {
+        // 调用混入中的提交方法
+        await this.$parent.handleSubmit.call(this);
+      } catch (error) {
+        console.error('表单提交失败:', error);
+      }
+    },
+
+    /**
+     * 处理表单重置
+     */
+    handleReset() {
+      // 调用混入中的重置方法
+      this.$parent.handleReset.call(this);
+      
+      // 重置表单引用
+      if (this.$refs.form) {
+        this.$refs.form.resetFields();
+      }
+    },
+
+    /**
+     * 处理取消操作
+     */
+    handleCancel() {
+      // 调用混入中的取消方法
+      this.$parent.handleCancel.call(this);
+      
+      // 关闭对话框
+      this.dialogVisible = false;
+    },
+
+    /**
+     * 验证表单
+     * @returns {Promise<boolean>} 验证结果
+     */
+    async validateForm() {
+      try {
+        if (this.$refs.form) {
+          await this.$refs.form.validate();
+          return true;
+        }
+        return false;
+      } catch (error) {
+        console.warn('表单验证失败:', error);
+        return false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.announcement-form {
+  .announcement-form-dialog {
+    .form-container {
+      max-height: 70vh;
+      overflow-y: auto;
+      padding: 0 20px;
+    }
+
+    .rich-editor-container {
+      width: 100%;
+      
+      .el-textarea {
+        width: 100%;
+      }
+    }
+
+    .dialog-footer {
+      text-align: right;
+      padding: 20px 0 0;
+      border-top: 1px solid #e4e7ed;
+      
+      .el-button {
+        margin-left: 10px;
+      }
+    }
+  }
+
+  // 表单样式优化
+  :deep(.avue-form) {
+    .el-form-item {
+      margin-bottom: 20px;
+    }
+
+    .el-form-item__label {
+      font-weight: 500;
+      color: #303133;
+    }
+
+    .el-input,
+    .el-select,
+    .el-textarea {
+      width: 100%;
+    }
+
+    // 多选框组样式
+    .el-checkbox-group {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      
+      .el-checkbox {
+        margin-right: 0;
+        margin-bottom: 8px;
+      }
+    }
+
+    // 远程搜索选择器样式
+    .el-select {
+      .el-select__tags {
+        max-height: 100px;
+        overflow-y: auto;
+      }
+    }
+  }
+
+  // 响应式设计
+  @media (max-width: 768px) {
+    .announcement-form-dialog {
+      width: 95% !important;
+      margin: 0 auto;
+      
+      .form-container {
+        padding: 0 10px;
+        max-height: 60vh;
+      }
+    }
+  }
+}
+
+// 全局样式(不使用scoped)
+.announcement-form-dialog {
+  .el-dialog__header {
+    background-color: #f5f7fa;
+    border-bottom: 1px solid #e4e7ed;
+    
+    .el-dialog__title {
+      font-size: 18px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+
+  .el-dialog__body {
+    padding: 20px;
+  }
+
+  .el-dialog__footer {
+    padding: 10px 20px 20px;
+    background-color: #fafafa;
+    border-top: 1px solid #e4e7ed;
+  }
+}
+</style>

+ 381 - 0
src/components/announcement/types.d.ts

@@ -0,0 +1,381 @@
+// @ts-check
+/**
+ * 公告表单组件类型定义
+ * @fileoverview 定义公告表单组件的所有类型,包括功能模型、API数据模型转换等
+ */
+
+/**
+ * 导入基础类型
+ */
+import type { AxiosResponse } from 'axios';
+import type { Vue } from 'vue/types/vue';
+import type { FormValidateCallback } from 'element-ui/types/form';
+
+/**
+ * 角色类型定义
+ */
+export type RoleType = 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
+
+/**
+ * 角色选项类型定义
+ */
+export interface RoleOption {
+  label: string;
+  value: RoleType;
+}
+
+/**
+ * 客户选项类型定义(用于远程搜索)
+ */
+export interface CustomerOption {
+  value: number;
+  label: string;
+  Customer_NAME?: string;
+  Customer_CODE?: string;
+}
+
+/**
+ * 品牌选项类型定义(用于远程搜索)
+ */
+export interface BrandOption {
+  value: string;
+  label: string;
+  name?: string;
+  code?: string;
+}
+
+/**
+ * 分类选项类型定义
+ */
+export interface CategoryOption {
+  id: string;
+  name: string;
+  code?: string;
+  label: string;
+  value: string;
+}
+
+/**
+ * 品牌范围项类型定义
+ */
+export interface BrandScopeItem {
+  id: string;
+  name: string;
+  code: string;
+}
+
+/**
+ * 客户黑名单项类型定义
+ */
+export interface CustomerBlacklistItem {
+  id: number;
+  Customer_NAME: string;
+  Customer_CODE: string;
+}
+
+/**
+ * 公告表单功能模型
+ * @description 表单内部使用的数据模型,便于表单操作和验证
+ */
+export interface AnnouncementFormModel {
+  /** 公告ID(编辑时存在) */
+  id?: string;
+  /** 公告标题 */
+  title: string;
+  /** 公告内容(富文本) */
+  content: string;
+  /** 分类ID */
+  categoryId: string;
+  /** 分类名称(显示用) */
+  categoryName?: string;
+  /** 组织ID */
+  orgId: number;
+  /** 组织编码 */
+  orgCode: string;
+  /** 组织名称 */
+  orgName: string;
+  /** 可见角色数组(功能模型,便于多选操作) */
+  visibleRoles: Array<RoleType>;
+  /** 品牌范围数组(功能模型) */
+  brandScope: Array<BrandScopeItem>;
+  /** 客户黑名单数组(功能模型) */
+  customerBlacklist: Array<CustomerBlacklistItem>;
+  /** 备注 */
+  remark: string;
+  /** 状态 */
+  status: number;
+  /** 创建时间 */
+  createTime?: string;
+  /** 更新时间 */
+  updateTime?: string;
+}
+
+/**
+ * 表单验证规则项类型
+ */
+export interface ValidationRule {
+  required?: boolean;
+  message: string;
+  trigger: 'blur' | 'change' | 'submit';
+  min?: number;
+  max?: number;
+  type?: 'string' | 'number' | 'boolean' | 'object' | 'method' | 'regexp' | 'integer' | 'float' | 'array' | 'enum' | 'date' | 'url' | 'hex' | 'email';
+  pattern?: RegExp;
+}
+
+/**
+ * 公告表单验证规则类型
+ */
+export interface AnnouncementFormRules {
+  title: Array<ValidationRule>;
+  content: Array<ValidationRule>;
+  categoryId: Array<ValidationRule>;
+  orgId: Array<ValidationRule>;
+  orgCode: Array<ValidationRule>;
+  orgName: Array<ValidationRule>;
+  visibleRoles: Array<ValidationRule>;
+  status?: Array<ValidationRule>;
+}
+
+
+
+/**
+ * 公告表单混入数据类型
+ */
+export interface AnnouncementFormMixinData {
+  /** 公告表单数据模型 */
+  formData: AnnouncementFormModel;
+  /** 保存操作加载状态 */
+  saveLoading: boolean;
+  /** 表单加载状态 */
+  formLoading: boolean;
+  /** 分类选项列表 */
+  categoryOptions: Array<CategoryOption>;
+  /** 分类选项加载状态 */
+  categoryLoading: boolean;
+  /** 角色选项列表 */
+  roleOptions: Array<RoleOption>;
+  /** 客户选项列表(用于黑名单选择) */
+  customerOptions: Array<CustomerOption>;
+  /** 客户选项加载状态 */
+  customerLoading: boolean;
+  /** 品牌选项列表 */
+  brandOptions: Array<BrandOption>;
+  /** 品牌选项加载状态 */
+  brandLoading: boolean;
+  /** 表单验证规则 */
+  formRules: AnnouncementFormRules;
+  /** 表单配置 */
+  formOption: AnnouncementFormOption;
+}
+
+/**
+ * 数据转换工具类型
+ */
+export interface DataConverter {
+  /**
+   * 将API数据转换为功能模型
+   * @param apiData API返回的公告数据
+   * @returns 功能模型数据
+   */
+  apiToFormModel(apiData: NoticeRecord): AnnouncementFormModel;
+  
+  /**
+   * 将功能模型转换为API数据
+   * @param formModel 功能模型数据
+   * @returns API提交数据
+   */
+  formModelToApi(formModel: AnnouncementFormModel): NoticeFormData;
+  
+  /**
+   * 解析可见角色掩码为角色数组
+   * @param rolesMask 角色掩码值
+   * @returns 角色类型数组
+   */
+  parseVisibleRoles(rolesMask: string | number): Array<RoleType>;
+  
+  /**
+   * 计算角色数组的掩码值
+   * @param roles 角色类型数组
+   * @returns 角色掩码值
+   */
+  calculateRolesMask(roles: Array<RoleType>): number;
+  
+  /**
+   * 解析品牌范围JSON字符串
+   * @param brandScopeStr 品牌范围JSON字符串
+   * @returns 品牌范围数组
+   */
+  parseBrandScope(brandScopeStr: string): Array<BrandScopeItem>;
+  
+  /**
+   * 序列化品牌范围数组为JSON字符串
+   * @param brandScope 品牌范围数组
+   * @returns JSON字符串
+   */
+  stringifyBrandScope(brandScope: Array<BrandScopeItem>): string;
+  
+  /**
+   * 解析客户黑名单JSON字符串
+   * @param customerBlacklistStr 客户黑名单JSON字符串
+   * @returns 客户黑名单数组
+   */
+  parseCustomerBlacklist(customerBlacklistStr: string): Array<CustomerBlacklistItem>;
+  
+  /**
+   * 序列化客户黑名单数组为JSON字符串
+   * @param customerBlacklist 客户黑名单数组
+   * @returns JSON字符串
+   */
+  stringifyCustomerBlacklist(customerBlacklist: Array<CustomerBlacklistItem>): string;
+}
+
+/**
+ * 公告表单事件类型
+ */
+export interface AnnouncementFormEvents {
+  /** 表单提交成功事件 */
+  'submit-success': (data: AnnouncementFormModel) => void;
+  /** 表单取消事件 */
+  'cancel': () => void;
+  /** 表单加载完成事件 */
+  'loaded': () => void;
+  /** 表单重置事件 */
+  'reset': () => void;
+  /** 表单提交事件 */
+  'submit': (data: AnnouncementFormModel) => void;
+  /** 更新可见性事件 */
+  'update:visible': (visible: boolean) => void;
+  /** 返回事件 */
+  'back': () => void;
+  /** 保存成功事件 */
+  'save-success': (data: AnnouncementFormModel) => void;
+}
+
+/**
+ * 公告表单混入组件类型
+ */
+export interface AnnouncementFormMixinComponent {
+  // Props
+  visible: boolean;
+  isEdit: boolean;
+  announcementId?: string;
+  editData?: import('@/api/types/announcement').NoticeRecord | null;
+  title?: string;
+  
+  // Data
+  formData: AnnouncementFormModel;
+  saveLoading: boolean;
+  formLoading: boolean;
+  categoryOptions: Array<CategoryOption>;
+  categoryLoading: boolean;
+  roleOptions: Array<RoleOption>;
+  customerOptions: Array<CustomerOption>;
+  customerLoading: boolean;
+  brandOptions: Array<BrandOption>;
+  brandLoading: boolean;
+  formRules: AnnouncementFormRules;
+  formOption: AvueFormOption;
+  
+  // Computed
+  formTitle: string;
+  canEdit: boolean;
+  
+  // Methods
+  createInitialFormData(): AnnouncementFormModel;
+  initFormData(): void;
+  initFormOption(): void;
+  loadAnnouncementDetail(): Promise<void>;
+  loadCategoryOptions(): Promise<void>;
+  handleSubmit(): Promise<void>;
+  handleReset(): void;
+  handleCancel(): void;
+  submitAnnouncementData(submitData: import('@/api/types/announcement').NoticeFormData): Promise<AxiosResponse<ApiResponse<any>>>;
+  validateForm(): Promise<boolean>;
+  remoteSearchCustomers(query: string): Promise<void>;
+  remoteSearchBrands(query: string): Promise<void>;
+  handleCustomerBlacklistChange(selectedCustomers: Array<CustomerOption>): void;
+  handleBrandScopeChange(selectedBrands: Array<BrandOption>): void;
+  clearCustomerOptions(): void;
+  clearBrandOptions(): void;
+}
+
+
+
+/**
+ * AvueJS表单配置类型
+ */
+export interface AnnouncementFormOption {
+  submitBtn: boolean;
+  emptyBtn: boolean;
+  labelWidth?: number;
+  labelPosition?: string;
+  size?: string;
+  menuBtn?: boolean;
+  column: Array<{
+    prop: string;
+    label: string;
+    type: string;
+    span?: number;
+    placeholder?: string;
+    maxlength?: number;
+    showWordLimit?: boolean;
+    rules?: Array<ValidationRule>;
+    dicData?: Array<any>;
+    props?: { label: string; value: string };
+    multiple?: boolean;
+    filterable?: boolean;
+    remote?: boolean;
+    remoteMethod?: (query: string) => void;
+    loading?: boolean;
+    minRows?: number;
+    maxRows?: number;
+    slot?: boolean;
+    dicUrl?: string;
+    dicMethod?: string;
+  }>;
+}
+
+/**
+ * 完整组件类型(用于this类型注解)
+ */
+export interface AnnouncementFormMixinComponent extends 
+  AnnouncementFormMixinData {
+  // Props
+  visible: boolean;
+  isEdit: boolean;
+  announcementId: string;
+  editData: import('@/api/types/announcement').NoticeRecord | null;
+  title: string;
+  
+  // Computed
+  formTitle: string;
+  canEdit: boolean;
+  
+  // Methods
+  createInitialFormData(): AnnouncementFormModel;
+  initFormData(): Promise<void>;
+  initFormOption(): void;
+  loadAnnouncementDetail(): Promise<void>;
+  loadCategoryOptions(): Promise<void>;
+  handleSubmit(): Promise<void>;
+  handleReset(): void;
+  handleCancel(): void;
+  submitAnnouncementData(submitData: import('@/api/types/announcement').NoticeFormData): Promise<AxiosResponse<ApiResponse<any>>>;
+  validateForm(): Promise<boolean>;
+  remoteSearchCustomers(query: string): Promise<void>;
+  remoteSearchBrands(query: string): Promise<void>;
+  handleCustomerBlacklistChange(selectedCustomers: Array<CustomerOption>): void;
+  handleBrandScopeChange(selectedBrands: Array<BrandOption>): void;
+  clearCustomerOptions(): void;
+  clearBrandOptions(): void;
+  
+  // Vue refs
+  $refs: {
+    form: {
+      validate: (callback?: FormValidateCallback) => Promise<boolean>;
+      resetFields: () => void;
+      clearValidate: () => void;
+    };
+  };
+}

+ 6 - 1
src/views/announcement/constants.js

@@ -5,10 +5,15 @@
 
 /**
  * 角色类型枚举
- * @typedef {1|2|4} RoleType
+ * @typedef {1|2|4|8|16|32|64|128} RoleType
  * - 1: 工厂
  * - 2: 经销商
  * - 4: 零售商
+ * - 8: 其他角色1
+ * - 16: 其他角色2
+ * - 32: 其他角色3
+ * - 64: 其他角色4
+ * - 128: 其他角色5
  */
 
 /**