西格玛许 8 часов назад
Родитель
Сommit
6354135762
4 измененных файлов с 120 добавлено и 148 удалено
  1. 2 0
      package.json
  2. 65 139
      src/components/Editor/index.vue
  3. 27 1
      src/views/system/audit/index.vue
  4. 26 8
      src/views/system/industry/index.vue

+ 2 - 0
package.json

@@ -25,6 +25,8 @@
     "@stomp/stompjs": "^7.3.0",
     "@vueup/vue-quill": "1.2.0",
     "@vueuse/core": "13.9.0",
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "^5.1.12",
     "animate.css": "4.1.1",
     "await-to-js": "3.0.0",
     "axios": "1.13.1",

+ 65 - 139
src/components/Editor/index.vue

@@ -14,26 +14,26 @@
       <i ref="uploadRef"></i>
     </el-upload>
   </div>
-  <div class="editor">
-    <quill-editor
-      ref="quillEditorRef"
-      v-model:content="content"
-      content-type="html"
-      :options="options"
+  <div style="border: 1px solid #ccc">
+    <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" style="border-bottom: 1px solid #ccc" />
+    <WangEditor
+      :defaultConfig="editorConfig"
+      :modelValue="modelValue"
       :style="styles"
-      @text-change="(e: any) => $emit('update:modelValue', content)"
+      @onChange="handleChange"
+      @onCreated="handleCreated"
     />
   </div>
 </template>
 
 <script setup lang="ts">
-import '@vueup/vue-quill/dist/vue-quill.snow.css';
-
-import { QuillEditor, Quill } from '@vueup/vue-quill';
+import '@wangeditor/editor/dist/css/style.css';
+import { Editor as WangEditor, Toolbar } from '@wangeditor/editor-for-vue';
+import { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor';
 import { propTypes } from '@/utils/propTypes';
 import { globalHeaders } from '@/utils/request';
 
-defineEmits(['update:modelValue']);
+const emit = defineEmits(['update:modelValue']);
 
 const props = defineProps({
   /* 编辑器的内容 */
@@ -52,47 +52,38 @@ const props = defineProps({
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
+const editorRef = shallowRef<IDomEditor>();
+const uploadRef = ref<HTMLDivElement>();
+
 const upload = reactive<UploadOption>({
   headers: globalHeaders(),
   url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
 });
-const quillEditorRef = ref();
-const uploadRef = ref<HTMLDivElement>();
 
-const options = ref<any>({
-  theme: 'snow',
-  bounds: document.body,
-  debug: 'warn',
-  modules: {
-    // 工具栏配置
-    toolbar: {
-      container: [
-        ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
-        ['blockquote', 'code-block'], // 引用  代码块
-        [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
-        [{ indent: '-1' }, { indent: '+1' }], // 缩进
-        [{ size: ['small', false, 'large', 'huge'] }], // 字体大小
-        [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
-        [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
-        [{ align: [] }], // 对齐方式
-        ['clean'], // 清除文本格式
-        ['link', 'image', 'video'] // 链接、图片、视频
-      ],
-      handlers: {
-        image: (value: boolean) => {
-          if (value) {
-            // 调用element图片上传
-            uploadRef.value.click();
-          } else {
-            Quill.format('image', true);
-          }
-        }
-      }
-    }
+const toolbarConfig: Partial<IToolbarConfig> = {};
+
+const editorConfig: Partial<IEditorConfig> = {
+  placeholder: '请输入内容...',
+  readOnly: props.readOnly,
+  MENU_CONF: {}
+};
+
+// 图片上传配置
+editorConfig.MENU_CONF!['uploadImage'] = {
+  server: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload',
+  fieldName: 'file',
+  headers: {
+    Authorization: globalHeaders().Authorization,
+    clientid: import.meta.env.VITE_APP_CLIENT_ID
   },
-  placeholder: '请输入内容',
-  readOnly: props.readOnly
-});
+  customInsert(res: any, insertFn: Function) {
+    if (res.code === 200) {
+      insertFn(res.data.url, res.data.fileName, res.data.url);
+    } else {
+      proxy?.$modal.msgError('图片插入失败');
+    }
+  }
+};
 
 const styles = computed(() => {
   const style: any = {};
@@ -102,32 +93,44 @@ const styles = computed(() => {
   if (props.height) {
     style.height = `${props.height}px`;
   }
+  style.overflowY = 'hidden';
   return style;
 });
 
-const content = ref('');
+const handleCreated = (editor: IDomEditor) => {
+  editorRef.value = editor;
+};
+
+// 监听 modelValue 变化,手动更新编辑器内容
 watch(
   () => props.modelValue,
-  (v: string) => {
-    if (v !== content.value) {
-      content.value = v || '<p></p>';
+  (val) => {
+    if (!editorRef.value) return;
+    // 避免重复设置相同内容导致光标重置
+    if (editorRef.value.getHtml() !== val) {
+      editorRef.value.setHtml(val || '');
     }
-  },
-  { immediate: true }
+  }
 );
 
-// 图片上传成功返回图片地址
+const handleChange = (editor: IDomEditor) => {
+  emit('update:modelValue', editor.getHtml());
+};
+
+// 组件销毁时,也及时销毁编辑器
+onBeforeUnmount(() => {
+  const editor = editorRef.value;
+  if (editor == null) return;
+  editor.destroy();
+});
+
+// 图片上传成功返回图片地址(备用:el-upload 方式)
 const handleUploadSuccess = (res: any) => {
-  // 如果上传成功
   if (res.code === 200) {
-    // 获取富文本实例
-    const quill = toRaw(quillEditorRef.value).getQuill();
-    // 获取光标位置
-    const length = quill.selection.savedRange.index;
-    // 插入图片,res为服务器返回的图片链接地址
-    quill.insertEmbed(length, 'image', res.data.url);
-    // 调整光标到最后
-    quill.setSelection(length + 1);
+    const editor = editorRef.value;
+    if (editor) {
+      editor.insertNode({ type: 'image', src: res.data.url, children: [] });
+    }
     proxy?.$modal.closeLoading();
   } else {
     proxy?.$modal.msgError('图片插入失败');
@@ -139,12 +142,10 @@ const handleUploadSuccess = (res: any) => {
 const handleBeforeUpload = (file: any) => {
   const type = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg'];
   const isJPG = type.includes(file.type);
-  //检验文件格式
   if (!isJPG) {
     proxy?.$modal.msgError(`图片格式错误!`);
     return false;
   }
-  // 校检文件大小
   if (props.fileSize) {
     const isLt = file.size / 1024 / 1024 < props.fileSize;
     if (!isLt) {
@@ -166,79 +167,4 @@ const handleUploadError = (err: any) => {
 .editor-img-uploader {
   display: none;
 }
-.editor,
-.ql-toolbar {
-  white-space: pre-wrap !important;
-  line-height: normal !important;
-}
-.quill-img {
-  display: none;
-}
-.ql-snow .ql-tooltip[data-mode='link']::before {
-  content: '请输入链接地址:';
-}
-.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
-  border-right: 0;
-  content: '保存';
-  padding-right: 0;
-}
-.ql-snow .ql-tooltip[data-mode='video']::before {
-  content: '请输入视频地址:';
-}
-.ql-snow .ql-picker.ql-size .ql-picker-label::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item::before {
-  content: '14px';
-}
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
-  content: '10px';
-}
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
-  content: '18px';
-}
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
-  content: '32px';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item::before {
-  content: '文本';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
-  content: '标题1';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
-  content: '标题2';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
-  content: '标题3';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
-  content: '标题4';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
-  content: '标题5';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
-  content: '标题6';
-}
-.ql-snow .ql-picker.ql-font .ql-picker-label::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item::before {
-  content: '标准字体';
-}
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
-  content: '衬线字体';
-}
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
-  content: '等宽字体';
-}
 </style>

+ 27 - 1
src/views/system/audit/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="audit-container">
-    <el-container class="h-full">
+    <el-container class="audit-layout h-full">
       <!-- 左侧分类侧边栏 -->
       <el-aside width="250px" class="mr-4 h-full sidebar modern-card">
         <div class="sidebar-title">审核类型</div>
@@ -346,10 +346,25 @@ onMounted(() => {
   padding: 20px;
   background-color: #f0f2f5;
   min-height: calc(100vh - 84px);
+  height: calc(100vh - 84px);
+  box-sizing: border-box;
+}
+
+.audit-layout {
+  height: 100%;
+  align-items: stretch;
 }
 
 .sidebar {
   border-radius: 4px;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  :deep(.el-menu) {
+    flex: 1;
+  }
+
   .sidebar-title {
     padding: 20px;
     font-size: 16px;
@@ -364,6 +379,9 @@ onMounted(() => {
 
 .main-content {
   border-radius: 4px;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
 }
 
 .header-section {
@@ -408,6 +426,14 @@ onMounted(() => {
 
 .table-section {
   padding-top: 15px;
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.table-section :deep(.el-table) {
+  flex: 1;
 }
 
 :deep(.custom-header th) {

+ 26 - 8
src/views/system/industry/index.vue

@@ -135,11 +135,19 @@ import {
   addIndustrySkill,
   updateIndustrySkill,
   delIndustrySkill
-} from '@/api/system/industry';
-import { IndustryForm, IndustryQuery, IndustryVO, IndustrySkillVO } from '@/api/system/industry/types';
+} from '@/api/system/industry/index';
+import type { IndustryForm, IndustryQuery } from '@/api/system/industry/types';
 import { ComponentInternalInstance, getCurrentInstance, reactive, ref, toRefs, onMounted } from 'vue';
 import { handleTree } from '@/utils/ruoyi';
 
+interface IndustryPageQuery extends IndustryQuery {
+  parentId?: string | number;
+}
+
+interface IndustryPageForm extends IndustryForm {
+  remark?: string;
+}
+
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
 
@@ -167,7 +175,7 @@ const initFormData: IndustryForm = {
   type: 'industry'
 };
 
-const data = reactive<PageData<IndustryForm, IndustryQuery>>({
+const data = reactive<PageData<IndustryPageForm, IndustryPageQuery>>({
   form: { ...initFormData },
   queryParams: {
     industryName: undefined,
@@ -181,6 +189,16 @@ const data = reactive<PageData<IndustryForm, IndustryQuery>>({
 
 const { queryParams, form, rules } = toRefs(data);
 
+const getListData = <T = any>(res: any): T[] => {
+  if (Array.isArray(res?.data)) {
+    return res.data;
+  }
+  if (Array.isArray(res?.rows)) {
+    return res.rows;
+  }
+  return [];
+};
+
 // 存储懒加载的 resolve 函数,用于局部刷新
 const refreshMap = new Map<any, { row: any, treeNode: any, resolve: (data: any[]) => void }>();
 
@@ -190,7 +208,7 @@ const getChildrenList = async (row: any, treeNode: unknown, resolve: (data: any[
   // 如果是二级节点,则查询三级职位表数据
   if (row.parentId !== 0 && row.type === 'industry') {
     const res = await listIndustrySkill({ industryId: row.industryId });
-    const skills = res.data.map(s => ({
+    const skills = getListData(res).map((s: any) => ({
       ...s,
       uniqueId: `skill_${s.skillId}`,
       industryName: s.skillName,
@@ -201,7 +219,7 @@ const getChildrenList = async (row: any, treeNode: unknown, resolve: (data: any[
   } else {
     // 处理一级节点的二级分类
     const res = await listIndustry({ parentId: row.industryId });
-    const children = res.data.map(c => ({
+    const children = getListData(res).map((c: any) => ({
       ...c,
       uniqueId: `industry_${c.industryId}`,
       type: 'industry',
@@ -236,9 +254,9 @@ const getList = async () => {
     // 始终开启懒加载模式,搜索时只过滤一级节点
     const params = { ...queryParams.value, parentId: 0 };
     const res = await listIndustry(params);
-    const rawData = res.data || [];
+    const rawData = getListData(res);
     
-    industryList.value = rawData.map(item => ({
+    industryList.value = rawData.map((item: any) => ({
       ...item,
       uniqueId: `industry_${item.industryId}`,
       type: 'industry',
@@ -286,7 +304,7 @@ const toggleRowExpansionAll = (data: any[], isExpansion: boolean) => {
 /** 查询行业下拉树结构 */
 const getTreeselect = async () => {
   const res = await listIndustry(); // 获取所有行业数据用于树选择
-  const rawData = res.data || [];
+  const rawData = getListData(res);
   const industry: any = { industryId: 0, industryName: '主类目', children: [] };
   industry.children = handleTree(rawData, 'industryId');
   industryOptions.value = [industry];