CzRger пре 2 месеци
родитељ
комит
022d5bd360

BIN
src/assets/images/knowledge-item-bg.png


src/assets/svg/tips.svg → src/assets/svg/czr_tip.svg


Разлика између датотеке није приказан због своје велике величине
+ 11 - 0
src/assets/svg/tag.svg


+ 1 - 1
src/components/czr-ui/CzrConfirm.vue

@@ -17,7 +17,7 @@
       </div>
       <div class="__czr-confirm-content">
         <div>
-          <SvgIcon name="tips" size="20" :active="true"/>
+          <SvgIcon name="czr_tip" size="20" :active="true"/>
         </div>
         <div v-html="DialogStore.confirmParam.content"/>
       </div>

+ 1 - 1
src/components/czr-ui/CzrTableCard.vue

@@ -5,7 +5,7 @@
         <div class="czr-table-card-content-list" :style="`gap: ${rowMargin} ${colMargin};grid-template-columns: repeat(${col}, calc((100% - ${colMargin} * ${col - 1}) / ${col}));`">
           <template v-for="(item, index) in data">
             <div class="model">
-              <slot name="model" :info="item" :index="index"/>
+              <slot name="model" :row="item" :index="index"/>
             </div>
           </template>
         </div>

+ 2 - 2
src/components/czr-ui/czr-form-link/input.vue

@@ -5,7 +5,7 @@
       :type="type"
       clearable
       :placeholder="((typeof $attrs.disabled !== 'undefined' && $attrs.disabled !== false) || state.formView) ? '' : ($attrs.placeholder ? $attrs.placeholder : `请输入${label}`)"
-      @keyup.enter.native="$emit('emitEnter')"
+      @keyup.enter.native.stop="$emit('emitEnter')"
       :disabled="isValue($attrs.disabled) ? $attrs.disabled : state.formView"
       :title="state.paramVal"
       @blur="() => state.paramVal = state.paramVal ? String(state.paramVal).trim() : ''"
@@ -32,7 +32,7 @@ import {
   inject
 } from 'vue'
 import {isValue} from "@/utils/czr-util";
-const emit = defineEmits(['emitParam'])
+const emit = defineEmits(['emitParam', 'emitEnter'])
 const props = defineProps({
   param: {},
   label: {},

+ 150 - 0
src/directives/title.ts

@@ -0,0 +1,150 @@
+import type { Directive, DirectiveBinding } from 'vue';
+
+interface AutoTitleOptions {
+  always?: boolean;       // 是否总是显示title
+  content?: string;      // 自定义title内容
+  lines?: number;        // 最大行数(支持多行省略)
+}
+
+const autoTitle: Directive<HTMLElement, boolean | AutoTitleOptions | string> = {
+  mounted(el, binding) {
+    observeElement(el, binding);
+  },
+  updated(el, binding) {
+    observeElement(el, binding);
+  },
+  unmounted(el) {
+    const observer = (el as any)._autoTitleObserver;
+    if (observer) {
+      observer.disconnect();
+      delete (el as any)._autoTitleObserver;
+    }
+  }
+};
+
+function observeElement(el: HTMLElement, binding: DirectiveBinding) {
+  // 先移除旧的观察者
+  const oldObserver = (el as any)._autoTitleObserver;
+  if (oldObserver) {
+    oldObserver.disconnect();
+  }
+
+  const options = parseBinding(binding);
+  const observer = new ResizeObserver(() => {
+    updateTitle(el, options);
+  });
+
+  observer.observe(el);
+  (el as any)._autoTitleObserver = observer;
+
+  // 初始更新
+  updateTitle(el, options);
+}
+
+function updateTitle(el: HTMLElement, options: AutoTitleOptions) {
+  const isOverflow = options.lines
+    ? checkMultilineOverflow(el, options.lines)
+    : checkSinglelineOverflow(el);
+
+  if (options.always || isOverflow) {
+    el.setAttribute('title', options.content || el.textContent?.trim() || '');
+  } else {
+    el.removeAttribute('title');
+  }
+}
+
+function checkSinglelineOverflow(el: HTMLElement): boolean {
+  return el.scrollWidth > el.clientWidth;
+}
+
+function checkMultilineOverflow(el: HTMLElement, lines: number): boolean {
+  const lineHeight = parseFloat(getComputedStyle(el).lineHeight);
+  const maxHeight = lineHeight * lines;
+  return el.scrollHeight > maxHeight + 1; // 加1是为了避免舍入误差
+}
+
+function parseBinding(binding: DirectiveBinding): AutoTitleOptions {
+  if (typeof binding.value === 'boolean') {
+    return { always: binding.value };
+  }
+  if (typeof binding.value === 'string') {
+    return { content: binding.value };
+  }
+  if (typeof binding.value === 'object') {
+    return binding.value;
+  }
+  return { always: false };
+}
+
+export default autoTitle;
+// v-title="{ lines: 3, content: '自定义标题内容' }"
+// /* 单行省略 */
+// .truncate {
+//   white-space: nowrap;
+//   overflow: hidden;
+//   text-overflow: ellipsis;
+// }
+//
+// /* 多行省略 */
+// .line-clamp-1 { -webkit-line-clamp: 1; }
+// .line-clamp-2 { -webkit-line-clamp: 2; }
+// .line-clamp-3 { -webkit-line-clamp: 3; }
+// .line-clamp-4 { -webkit-line-clamp: 4; }
+// .line-clamp-5 { -webkit-line-clamp: 5; }
+//
+// .line-clamp {
+//   display: -webkit-box;
+//   -webkit-box-orient: vertical;
+//   overflow: hidden;
+// }
+
+
+// import type { Directive, DirectiveBinding } from 'vue';
+//
+// interface TitleOptions {
+//   // 是否总是显示title,即使没有文本溢出
+//   always?: boolean;
+//   // 自定义title内容,如果不提供则使用元素文本
+//   content?: string;
+// }
+//
+// const title: Directive<HTMLElement, boolean | TitleOptions | string> = {
+//   mounted(el, binding) {
+//     updateTitle(el, binding);
+//   },
+//   updated(el, binding) {
+//     updateTitle(el, binding);
+//   }
+// };
+//
+// function updateTitle(el: HTMLElement, binding: DirectiveBinding) {
+//   const options = parseBinding(binding);
+//
+//   if (options.always || el.scrollWidth > el.clientWidth) {
+//     el.setAttribute('title', options.content || el.textContent?.trim() || '');
+//   } else {
+//     el.removeAttribute('title');
+//   }
+// }
+//
+// function parseBinding(binding: DirectiveBinding): TitleOptions {
+//   // 如果绑定值是布尔值
+//   if (typeof binding.value === 'boolean') {
+//     return { always: binding.value };
+//   }
+//
+//   // 如果绑定值是字符串
+//   if (typeof binding.value === 'string') {
+//     return { content: binding.value };
+//   }
+//
+//   // 如果绑定值是对象
+//   if (typeof binding.value === 'object') {
+//     return binding.value;
+//   }
+//
+//   // 默认情况
+//   return { always: false };
+// }
+//
+// export default title;

+ 2 - 0
src/main.ts

@@ -5,6 +5,7 @@ import './style/tailwindcss.css'
 import 'virtual:svg-icons-register'    // 【svg-icons相关】
 import initComponent from '@/plugins/initComponent'
 import initProperties from '@/plugins/initProperties'
+import initDirectives from '@/plugins/initDirectives';
 import 'default-passive-events'
 import './browerPatch'
 import { createPinia } from 'pinia'
@@ -17,6 +18,7 @@ import './style/index.scss'
 
 
 const app = createApp(App)
+app.use(initDirectives)
 app.use(createPinia())
 await initProperties(app)
 initComponent(app)

+ 8 - 0
src/plugins/initDirectives.ts

@@ -0,0 +1,8 @@
+import type { App } from 'vue';
+import title from '@/directives/title';
+
+export default {
+    install(app: App) {
+        app.directive('title', title);
+    }
+};

+ 1 - 0
src/stores/index.ts

@@ -1,3 +1,4 @@
 export * from './modules/dialog'
 export * from './modules/menu'
 export * from './modules/workflow'
+export * from './modules/dictionary'

+ 39 - 0
src/stores/modules/dictionary.ts

@@ -0,0 +1,39 @@
+import { defineStore } from 'pinia'
+
+const listToMap = (list, labelKey = 'label', valueKey = 'value') => {
+	const map = new Map()
+	list.forEach(v => {
+		map.set(v[valueKey], v[labelKey])
+	})
+	return map
+}
+export const useDictionaryStore = defineStore('dictionary', {
+	state: () => ({
+		knowledgeTags: {
+			waiting: false,
+			list: [],
+			map: new Map()
+		}
+	}),
+	getters: {
+		// dictStateMap() {
+		// 	return listToMap(this.dictStateList)
+		// },
+	},
+	actions: {
+		initKnowledgeTags() {
+			if (!this.knowledgeTags.waiting) {
+				this.knowledgeTags.waiting = true
+				setTimeout(() => {
+					const arr: any = []
+					for (let i = 0; i < 100; i++) {
+						arr.push({name: '标签' + i, id: i + '', total: i})
+					}
+					this.knowledgeTags.list = arr
+					this.knowledgeTags.map = listToMap(arr, 'name', 'id')
+					this.knowledgeTags.waiting = false
+				}, 1000)
+			}
+		}
+	},
+})

+ 18 - 13
src/style/czr.scss

@@ -2,6 +2,8 @@
   --czr-main-color: rgba(47, 130, 255, 1);
   --czr-main-color-rgb: 47, 130, 255;
   --czr-gap: 0.63rem;
+  --czr-border-color: #10182814;
+  --czr-border: 1px solid #10182814;
 }
 
 .__disabled {
@@ -142,9 +144,12 @@
   }
   .el-overlay-dialog {
     .el-dialog {
+      background-image: url("@/assets/images/dialog-bg.png");
+      background-size: 100% 100%;
+      background-repeat: no-repeat;
       padding: 0;
       height: var(--czr-dialog_height);
-      $borderRadius: 8px;
+      $borderRadius: 0.5rem;
       border-radius: $borderRadius;
       display: flex;
       flex-direction: column;
@@ -155,19 +160,18 @@
         padding: 0;
         margin: 0;
         ._czr-dialog-head {
-          height: 50px;
           width: 100%;
-          background-color: #F5F7FA;
+          height: 3.13rem;
+          background-color: rgba(255,255,255,0.8);
           display: flex;
           align-items: center;
-          border-radius: $borderRadius $borderRadius 0 0 ;
-          font-size: 18px;
-          font-family: PingFang SC-Medium, PingFang SC;
-          font-weight: 500;
-          color: #303133;
-          padding: 0 20px 0 16px;
-          box-sizing: border-box;
-          .__cdh-title {}
+          padding: 0 1rem;
+          border-radius: $borderRadius $borderRadius 0 0;
+          .__cdh-title {
+            font-weight: bold;
+            font-size: 1.13rem;
+            color: #303133;
+          }
           .__cdh-slot {}
           .__cdh-close {
             margin-left: auto;
@@ -176,7 +180,7 @@
       }
       .el-dialog__body {
         padding: 0;
-        height: calc(100% - 50px);
+        height: calc(100% - 3.13rem);
         width: 100%;
         display: flex;
         overflow-y: hidden;
@@ -188,13 +192,14 @@
           .__czr-dialog-content {
             flex: 1;
             overflow-y: auto;
+            padding: 1rem 1.5rem;
           }
           .__czr-dialog-foot {
             width: 100%;
             display: flex;
             align-items: center;
             box-sizing: border-box;
-            gap: 10px;
+            gap: 1rem;
           }
           &.isFull {
             overflow-y: auto;

+ 1 - 0
src/style/index.scss

@@ -9,6 +9,7 @@
   -webkit-user-drag: none;
   box-sizing: border-box;
   font-family: "PingFang SC";
+  line-height: 1;
 }
 
 html, body, #app {

+ 29 - 24
src/views/manage/knowledge/index.vue

@@ -21,7 +21,7 @@
               {label: '分组三', value: 2},
               {label: '分组二', value: 3},
             ]"
-            placeholder="全部分"
+            placeholder="全部分"
           />
           <CzrFormColumn
             class="__czr-table-form-column w-[6.88rem]"
@@ -29,11 +29,9 @@
             :label-width="0"
             v-model:param="state.query.form.tag"
             link="select"
-            :options="[
-                {label: '标签一', value: 1},
-                {label: '标签三', value: 2},
-                {label: '标签二', value: 3},
-              ]"
+            :options="DictionaryStore.knowledgeTags.list"
+            labelKey="name"
+            valueKey="id"
             placeholder="全部标签"
           />
           <CzrFormColumn
@@ -55,12 +53,10 @@
         :total="state.query.result.total"
         :data="state.query.result.data"
         @handlePage="onPage"
-        col-margin="0px"
-        row-margin="0px"
         :col="4"
       >
-        <template #model="{ info }">
-          <template v-if="info.empty">
+        <template #model="{ row }">
+          <template v-if="row.empty">
             <div class="knowledge flex justify-center items-center __hover" @click="onAdd">
               <img src="@/assets/images/knowledge-item-add.png"/>
               <span class="text-[1.25rem] ml-[var(--czr-gap)] text-[var(--czr-main-color)]">创建知识库</span>
@@ -70,26 +66,29 @@
             <div class="knowledge flex flex-col">
               <div class="flex">
                 <img src="@/assets/images/knowledge-item-icon.png" class="mr-[var(--czr-gap)]"/>
-                <div class="flex flex flex-col justify-around">
-                  <div class="text-[1.25rem] text-[#2E3238] font-bold">{{info.p1}}</div>
-                  <div class="text-[0.75rem] text-[#6F7889]">创建者:{{info.p2}}</div>
+                <div class="flex flex-1 flex-col justify-around overflow-hidden">
+                  <div class="__text-ellipsis text-[1.25rem] text-[#2E3238] font-bold" v-title>{{row.p1}}</div>
+                  <div class="text-[0.75rem] text-[#6F7889]">创建者:{{row.p2}}</div>
+                </div>
+                <div class="ml-auto mt-[0.25rem]">
+                  <tagsSelect :value="row.tags" @onChange="tags => onChangeTag(row, tags)"/>
                 </div>
               </div>
-              <div class="text-[0.88rem] text-[#606266] mb-auto mt-[var(--czr-gap)]">{{info.p3}}</div>
+              <div class="text-[0.88rem] text-[#606266] mb-auto mt-[var(--czr-gap)]">{{row.p3}}</div>
               <div class="flex items-center text-[0.75rem] text-[#6F7889] gap-[var(--czr-gap)]">
-                <div>文档数:{{info.p4}}</div>
+                <div>文档数:{{row.p4}}</div>
                 <div>|</div>
-                <div>文档数:{{info.p5}}</div>
+                <div>字符:{{row.p5}}</div>
                 <div>|</div>
-                <div>文档数:{{info.p6}}</div>
+                <div>关联应用:{{row.p6}}</div>
                 <el-tooltip content="编辑" effect="light" placement="top">
                   <SvgIcon name="czr_edit" size="14" class="__hover ml-auto"/>
                 </el-tooltip>
                 <el-tooltip content="删除" effect="light" placement="top">
-                  <SvgIcon name="czr_del" size="16" class="__hover" @click="onDel(info)"/>
+                  <SvgIcon name="czr_del" size="16" class="__hover" @click="onDel(row)"/>
                 </el-tooltip>
-                <el-tooltip :content="info.p7 ? '取消收藏' : '收藏'" effect="light" placement="top">
-                  <SvgIcon name="star" size="15" class="__hover" :active="!!info.p7"/>
+                <el-tooltip :content="row.p7 ? '取消收藏' : '收藏'" effect="light" placement="top">
+                  <SvgIcon name="star" size="15" class="__hover" :active="!!row.p7"/>
                 </el-tooltip>
               </div>
             </div>
@@ -104,10 +103,12 @@
 import {getCurrentInstance, onMounted, reactive, ref, watch} from "vue";
 import { Search } from '@element-plus/icons-vue'
 import {debounce} from "lodash";
-import {useDialogStore} from "@/stores";
+import {useDialogStore, useDictionaryStore} from "@/stores";
 import {ElMessage} from "element-plus";
+import tagsSelect from './tags-select.vue'
 
 const DialogStore = useDialogStore();
+const DictionaryStore = useDictionaryStore();
 const emits = defineEmits([])
 const props = defineProps({})
 const {proxy}: any = getCurrentInstance()
@@ -162,13 +163,14 @@ const onPage = (pageNum, pageSize) => {
     for (let i = 1; i <= params.pageSize; i++) {
       const n = (params.pageNum - 1) * params.pageSize + i
       arr.push({
-        p1: '部门知识库-' + n,
+        p1: '部门知识库-部门知识库-部门知识库-' + n,
         p2: '王一鸣',
         p3: '只是一个政务服务事项办事指南',
         p4: n,
         p5: '2980k',
         p6: n % 4,
-        p7: n % 2
+        p7: n % 2,
+        tags: n % 2 ? '1,2,3,41,2,3,41,2,3,41,2,3,41,2,3,4' : ''
       })
     }
     state.query.result.data = arr
@@ -221,7 +223,10 @@ const onDel = (row: any) => {
     }
   })
 }
-
+const onChangeTag = (row, tags) => {
+  row.tags = tags
+  // onSearch()
+}
 const initDictionary = () => {
 }
 onMounted(() => {

+ 192 - 0
src/views/manage/knowledge/tags-select.vue

@@ -0,0 +1,192 @@
+<template>
+  <div>
+    <el-popover
+      :visible="state.show"
+      placement="bottom"
+      trigger="click"
+      :popper-style="{
+        padding: 0,
+        minWidth: 0,
+        width: popoverWidth ? `${popoverWidth}px` : 'fit-content'
+      }"
+    >
+      <template #reference>
+        <div @click="state.show = !state.show" class="flex items-center text-[0.88rem] text-[#98a2b2] rounded py-[0.3rem] px-[0.5rem] cursor-pointer hover:bg-[#c8ceda33]" :class="{
+          'bg-[#c8ceda33]': state.show
+        }">
+          <SvgIcon name="tag" size="14" class="mr-1"/>
+          <span class="__text-ellipsis max-w-[5rem]" v-title>{{value ? value.split(',').map(v => DictionaryStore.knowledgeTags.map.get(v) || v).join(',') : '添加标签'}}</span>
+        </div>
+      </template>
+      <div class="w-[15rem] max-h-[20rem] bg-[#c8ceda24]" :select-popover="true">
+        <div class="p-[0.5rem] border-b-[1px] border-[var(--czr-border-color)]">
+          <CzrFormColumn
+            class="__czr-table-form-column"
+            :span="24"
+            :label-width="0"
+            v-model:param="state.text"
+            placeholder="搜索或者创建"
+            :prefix-icon="Search"
+          />
+        </div>
+        <div class="p-[0.3rem]">
+          <template v-if="state.text && !DictionaryStore.knowledgeTags.list.some(v => v.name.includes(state.text))">
+            <div class="__hover-bg p-[0.5rem] flex items-center text-[0.88rem] text-[#354052] rounded" @click="onAdd">
+              <SvgIcon name="czr_add" size="10" class="mr-2"/>
+              创建“{{state.text}}”
+            </div>
+          </template>
+          <template v-else>
+            <template v-if="DictionaryStore.knowledgeTags.list.length > 0">
+              <div class="max-h-[200px] overflow-y-auto">
+                <template v-for="item in DictionaryStore.knowledgeTags.list">
+                  <div class="__hover-bg p-[0.5rem] flex items-center text-[0.88rem] text-[#354052] rounded" @click="state.valueMap.has(item.id) ? state.valueMap.delete(item.id) : state.valueMap.set(item.id, item.id)">
+                    <div class="__check scale-[75%] mr-1" :class="{active: state.valueMap.has(item.id)}"/>
+                    <span class="__text-ellipsis flex-1" v-title>{{item.name}}</span>
+                  </div>
+                </template>
+              </div>
+            </template>
+            <template v-else>
+              <div class="flex flex flex-col justify-center items-center text-[0.75rem] text-[#676f83] py-5">
+                <SvgIcon name="tag" size="24" class="mb-1"/>
+                没有标签
+              </div>
+            </template>
+          </template>
+        </div>
+        <div class="p-[0.3rem] border-t-[1px] border-[var(--czr-border-color)]">
+          <div class="__hover-bg p-[0.5rem] flex items-center text-[0.88rem] text-[#354052] rounded" @click="state.show = false, state.showDialog = true">
+            <SvgIcon name="tag" size="14" class="mr-1"/> 管理标签
+          </div>
+        </div>
+      </div>
+    </el-popover>
+  </div>
+  <CzrDialog
+    :show="state.showDialog"
+    title="管理标签"
+    @onClose="state.showDialog = false"
+    width="40rem"
+    height="auto"
+    maxHeight="80%"
+    :loading="state.loading"
+    :show-submit="false"
+    :show-close="false"
+  >
+    <div class="flex flex flex-wrap gap-2 bg-[#ffffff] p-[1rem] rounded">
+      <template v-for="item in DictionaryStore.knowledgeTags.list">
+        <template v-if="item.__edit">
+          <div class="w-[17rem] border border-[var(--czr-border-color)] rounded">
+            <CzrFormColumn
+              :label-width="0"
+              :span="24"
+              v-model:param="item.__value"
+              :transparent="true"
+              :clearable="false"
+              @blur="onEdit(item)"
+              @keyup.enter.stop="onEdit(item)"
+            />
+          </div>
+        </template>
+        <template v-else>
+          <div class="max-w-[17rem] border border-[var(--czr-border-color)] px-[0.5rem] h-[2.25rem] rounded flex items-center gap-2">
+            <span class="__text-ellipsis" v-title>
+              {{item.name}}
+            </span>
+            <span class="opacity-75">{{item.total}}</span>
+            <SvgIcon name="czr_edit" size="12" class="__hover" @click="item.__value = item.name + '', item.__edit = true"/>
+            <SvgIcon name="czr_del" size="14" class="__hover" @click="onDel(item)"/>
+          </div>
+        </template>
+      </template>
+    </div>
+  </CzrDialog>
+</template>
+
+<script setup lang="ts">
+import {computed, getCurrentInstance, onBeforeMount, reactive, ref, watch} from "vue";
+import {domRootHasAttr} from "@/utils/czr-util";
+import {useDialogStore, useDictionaryStore} from "@/stores";
+import {Search} from "@element-plus/icons-vue";
+import {ElMessage} from "element-plus";
+
+const DictionaryStore = useDictionaryStore()
+const DialogStore = useDialogStore()
+const emits = defineEmits(['onChange'])
+const props = defineProps({
+  options: {default: <any>[]},
+  value: {default: ''},
+  popoverWidth: {default: 0},
+})
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({
+  show: false,
+  text: '',
+  valueMap: new Map(),
+  showDialog: false,
+})
+const onAdd = () => {
+  state.text = ''
+  ElMessage.success('创建标签成功!')
+  DictionaryStore.initKnowledgeTags()
+}
+const onDel = (row) => {
+  const delHandle = () => {
+    ElMessage.success('删除标签成功!')
+    DictionaryStore.initKnowledgeTags()
+  }
+  if (row.total > 0) {
+    DialogStore.confirm({
+      title: `删除标签“${row.name}”`,
+      content: `标签正在使用中,是否删除?`,
+      onSubmit: () => {
+        delHandle()
+      }
+    })
+  } else {
+    delHandle()
+  }
+}
+const onEdit = (row) => {
+  if (!row.__value) {
+    row.__edit = false
+  } else {
+    ElMessage.success('修改标签成功!')
+    DictionaryStore.initKnowledgeTags()
+  }
+}
+
+const onMouseDown = (e) => {
+  if (!domRootHasAttr(e.target, 'select-popover')) {
+    state.show = false
+  }
+}
+watch(() => state.show, (n) => {
+  if (n) {
+    state.text = ''
+    document.addEventListener('mousedown', onMouseDown)
+  }  else {
+    document.removeEventListener('mousedown', onMouseDown)
+    emits('onChange', Array.from(state.valueMap.keys()).join(','))
+  }
+}, {immediate: true})
+watch(() => props.value, (n) => {
+  const map = new Map()
+  if (n) {
+    n.split(',').forEach(v => {
+      map.set(v, v)
+    })
+  }
+  state.valueMap = map
+}, {immediate: true})
+onBeforeMount(() => {
+  DictionaryStore.initKnowledgeTags()
+})
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-input__wrapper) {
+  background-color: rgba(200, 206, 218, 0.25);
+}
+</style>