Procházet zdrojové kódy

mod: 知识库页面

weieryang před 1 měsícem
rodič
revize
a646f9fad2

binární
apps/web-ele/public/icon/1.png


binární
apps/web-ele/public/icon/2.png


binární
apps/web-ele/public/icon/3.png


binární
apps/web-ele/public/icon/4.png


+ 11 - 0
apps/web-ele/src/router/routes/local.ts

@@ -69,6 +69,17 @@ const localRoutes: RouteRecordStringComponent[] = [
69 69
     name: 'OilstationBaseCreate',
70 70
     path: '/oilstation/base/create',
71 71
   },
72
+  {
73
+    component: '/knowledge/detail/index',
74
+    meta: {
75
+      activePath: '/knowledge/detail',
76
+      icon: 'carbon:data-base',
77
+      title: '知识库详情',
78
+      hideInMenu: true,
79
+    },
80
+    name: 'KnowledgeDetail',
81
+    path: '/knowledge/detail/:classId',
82
+  },
72 83
 ];
73 84
 
74 85
 /**

+ 75 - 0
apps/web-ele/src/views/knowledge/detail/config-data.tsx

@@ -0,0 +1,75 @@
1
+import type { FormSchemaGetter } from '#/adapter/form';
2
+
3
+// 编辑表单配置
4
+export const drawerFormSchema: FormSchemaGetter = () => [
5
+  {
6
+    component: 'Input',
7
+    fieldName: 'title',
8
+    label: '标题',
9
+    componentProps: {
10
+      placeholder: '请输入章节标题',
11
+    },
12
+    rules: 'required',
13
+  },
14
+  {
15
+    component: 'Input',
16
+    fieldName: 'content',
17
+    label: '内容',
18
+    componentProps: {
19
+      placeholder: '请输入章节内容',
20
+      rows: 10,
21
+    },
22
+    // 使用自定义插槽渲染Tinymce编辑器
23
+    slot: 'content',
24
+    rules: 'required',
25
+  },
26
+  {
27
+    component: 'Input',
28
+    fieldName: 'attachments',
29
+    label: '附件',
30
+    componentProps: {
31
+      placeholder: '请上传附件',
32
+      readonly: true,
33
+    },
34
+  },
35
+];
36
+
37
+// 提醒表单配置
38
+export const remindFormSchema: FormSchemaGetter = () => [
39
+  {
40
+    component: 'Input',
41
+    fieldName: 'remindContent',
42
+    label: '提醒内容',
43
+    componentProps: {
44
+      placeholder: '请输入提醒内容',
45
+      rows: 4,
46
+      maxlength: 200,
47
+      type: 'textarea',
48
+      showWordLimit: true,
49
+    },
50
+    // 使用自定义插槽渲染文本域
51
+    slot: 'remindContent',
52
+    rules: 'required',
53
+  },
54
+  {
55
+    // 组件需要在 #/adapter.ts内注册,并加上类型
56
+    component: 'ApiSelect',
57
+    // 对应组件的参数
58
+    componentProps: {
59
+      placeholder: '请选择或搜索提醒人',
60
+      // 菜单接口转options格式
61
+      afterFetch: (data: { name: string; path: string }[]) => {
62
+        return data.map((item: any) => ({
63
+          label: item.name,
64
+          value: item.path,
65
+        }));
66
+      },
67
+      // 菜单接口
68
+      api: () => {},
69
+    },
70
+    // 字段名
71
+    fieldName: 'remindPerson',
72
+    // 界面显示的label
73
+    label: '提醒人',
74
+  },
75
+];

+ 184 - 0
apps/web-ele/src/views/knowledge/detail/edit-drawer.vue

@@ -0,0 +1,184 @@
1
+<script lang="ts" setup>
2
+import { ref } from 'vue';
3
+
4
+import { useVbenDrawer, useVbenForm } from '@vben/common-ui';
5
+
6
+import { Plus } from '@element-plus/icons-vue';
7
+import { ElIcon, ElUpload } from 'element-plus';
8
+
9
+import TinymceEditor from '#/components/tinymce/src/editor.vue';
10
+
11
+import { drawerFormSchema } from './config-data';
12
+
13
+const emit = defineEmits<{
14
+  reload: [];
15
+}>();
16
+
17
+// 附件列表
18
+const attachments = ref<any[]>([]);
19
+const uploadUrlRef = ref<string>('');
20
+
21
+// 表单配置
22
+const [Form, formApi] = useVbenForm({
23
+  showDefaultActions: false,
24
+  schema: drawerFormSchema(),
25
+});
26
+
27
+// 处理附件上传
28
+function handleAttachmentUpload(options: any) {
29
+  // 模拟上传成功处理
30
+  const fileData = {
31
+    uid: options.file.uid,
32
+    name: options.file.name,
33
+    url: URL.createObjectURL(options.file),
34
+    status: 'success',
35
+    raw: options.file,
36
+  };
37
+
38
+  // 多个文件上传时,url以逗号隔开
39
+  uploadUrlRef.value = uploadUrlRef.value
40
+    ? `${uploadUrlRef.value},${fileData.url}`
41
+    : fileData.url;
42
+
43
+  options.onSuccess(fileData);
44
+}
45
+
46
+// 移除附件
47
+function handleAttachmentRemove(file: any, fileList: any[]) {
48
+  attachments.value = fileList;
49
+  // 更新上传url,移除已删除的文件
50
+  uploadUrlRef.value = fileList
51
+    .filter((item: any) => item.status === 'success')
52
+    .map((item: any) => item.url)
53
+    .join(',');
54
+}
55
+
56
+// 预览附件
57
+function handleAttachmentPreview(file: any) {
58
+  console.log('预览附件', file);
59
+  // TODO: 实现附件预览功能
60
+}
61
+
62
+// 确认编辑
63
+async function handleConfirm() {
64
+  try {
65
+    const { valid } = await formApi.validate();
66
+    if (!valid) {
67
+      return;
68
+    }
69
+    const data = await formApi.getValues();
70
+
71
+    // 处理附件url,转换为逗号分隔的字符串
72
+    data.attachments = uploadUrlRef.value;
73
+
74
+    // 模拟提交数据
75
+    console.log('提交数据:', data);
76
+    // TODO: 实现真实的API调用
77
+
78
+    // 提交成功后触发reload事件
79
+    emit('reload');
80
+    drawerApi.close();
81
+  } catch (error) {
82
+    console.error('保存失败:', error);
83
+  }
84
+}
85
+
86
+const isUpdateRef = ref<boolean>(false);
87
+
88
+const [Drawer, drawerApi] = useVbenDrawer({
89
+  async onOpenChange(isOpen) {
90
+    if (!isOpen) {
91
+      return;
92
+    }
93
+    try {
94
+      drawerApi.drawerLoading(true);
95
+      const { chapter, isUpdate } = drawerApi.getData();
96
+      isUpdateRef.value = isUpdate;
97
+      if (isUpdate && chapter) {
98
+        // 处理附件url,转换为文件列表格式
99
+        const attachmentsList = chapter.attachments
100
+          ? chapter.attachments
101
+              .split(',')
102
+              .map((url: string, index: number) => ({
103
+                uid: Date.now() + index,
104
+                name: url.split('/').pop() || `文件${index + 1}`,
105
+                url,
106
+                status: 'success',
107
+              }))
108
+          : [];
109
+
110
+        uploadUrlRef.value = chapter.attachments || '';
111
+        attachments.value = attachmentsList;
112
+
113
+        // 设置表单数据
114
+        await formApi.setValues({
115
+          ...chapter,
116
+          content: chapter.content || '',
117
+          attachments: attachmentsList,
118
+        });
119
+      } else {
120
+        // 新增时重置表单
121
+        await formApi.resetForm();
122
+        attachments.value = [];
123
+        uploadUrlRef.value = '';
124
+      }
125
+    } catch (error) {
126
+      console.error('加载数据失败:', error);
127
+    } finally {
128
+      drawerApi.drawerLoading(false);
129
+    }
130
+  },
131
+  async onConfirm() {
132
+    await handleConfirm();
133
+  },
134
+  onClosed() {
135
+    formApi.resetForm();
136
+    attachments.value = [];
137
+    uploadUrlRef.value = '';
138
+  },
139
+});
140
+</script>
141
+
142
+<template>
143
+  <Drawer :title="isUpdateRef ? '编辑章节' : '新增章节'">
144
+    <Form>
145
+      <!-- 自定义Tinymce编辑器插槽 -->
146
+      <template #content="scope">
147
+        <TinymceEditor v-model="scope.modelValue" :height="400" />
148
+      </template>
149
+
150
+      <!-- 自定义附件上传插槽 -->
151
+      <template #attachments="scope">
152
+        <ElUpload
153
+          v-model:file-list="attachments"
154
+          action="#"
155
+          accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
156
+          list-type="text"
157
+          show-file-list
158
+          :auto-upload="true"
159
+          :drag="false"
160
+          :limit="5"
161
+          :multiple="true"
162
+          :http-request="handleAttachmentUpload"
163
+          :on-remove="handleAttachmentRemove"
164
+          :on-preview="handleAttachmentPreview"
165
+          class="mb-4"
166
+        >
167
+          <div class="flex items-center justify-center">
168
+            <ElIcon class="el-upload__icon">
169
+              <Plus size="18" />
170
+            </ElIcon>
171
+            <div class="el-upload__text">
172
+              <div>上传附件</div>
173
+              <div class="text-xs text-gray-500">最多上传5个附件</div>
174
+            </div>
175
+          </div>
176
+        </ElUpload>
177
+      </template>
178
+    </Form>
179
+  </Drawer>
180
+</template>
181
+
182
+<style scoped lang="scss">
183
+/* 自定义样式 */
184
+</style>

+ 531 - 0
apps/web-ele/src/views/knowledge/detail/index.vue

@@ -0,0 +1,531 @@
1
+<script lang="ts" setup>
2
+import { onMounted, ref, watch } from 'vue';
3
+
4
+import { ColPage, useVbenDrawer } from '@vben/common-ui';
5
+import { ReadCount } from '@vben/icons';
6
+
7
+import {
8
+  Camera,
9
+  Delete,
10
+  Edit,
11
+  HomeFilled,
12
+  Plus,
13
+  Search,
14
+} from '@element-plus/icons-vue';
15
+import { ElMessage } from 'element-plus';
16
+
17
+// 导入编辑抽屉组件
18
+import EditDrawer from './edit-drawer.vue';
19
+// 导入提醒抽屉组件
20
+import RemindDrawer from './remind-drawer.vue';
21
+
22
+// 模拟章节数据
23
+const chapterDataRef = ref([
24
+  {
25
+    id: 1,
26
+    label: '一级标题',
27
+    children: [
28
+      {
29
+        id: 2,
30
+        label: '二级标题',
31
+        children: [{ id: 3, label: '三级标题' }],
32
+      },
33
+    ],
34
+  },
35
+  {
36
+    id: 4,
37
+    label: '一级标题',
38
+    children: [
39
+      {
40
+        id: 5,
41
+        label: '二级标题',
42
+        children: [{ id: 6, label: '三级标题' }],
43
+      },
44
+      {
45
+        id: 7,
46
+        label: '二级标题',
47
+        children: [
48
+          { id: 8, label: '三级标题' },
49
+          { id: 9, label: '三级标题' },
50
+          { id: 10, label: '三级标题' },
51
+        ],
52
+      },
53
+      {
54
+        id: 11,
55
+        label: '二级标题',
56
+        children: [{ id: 12, label: '三级标题' }],
57
+      },
58
+    ],
59
+  },
60
+]);
61
+
62
+const treeRef = ref();
63
+const searchTextRef = ref<string>('');
64
+const selectedChapterRef = ref<any>({ id: 'home', label: '主页', content: '' });
65
+const contentRef = ref<string>('');
66
+
67
+// 评论相关
68
+const commentContent = ref<string>('');
69
+const commentImages = ref<any[]>([]);
70
+
71
+// 定义编辑抽屉
72
+const [EditDrawerComp, drawerApi] = useVbenDrawer({
73
+  connectedComponent: EditDrawer,
74
+});
75
+
76
+// 定义提醒抽屉
77
+const [RemindDrawerComp, remindDrawerApi] = useVbenDrawer({
78
+  connectedComponent: RemindDrawer,
79
+});
80
+
81
+// 过滤树形图回调
82
+const filterNode = (value: string, data: any) => {
83
+  if (!value) return true;
84
+  return data.label.includes(value);
85
+};
86
+
87
+// 左侧属性搜索关键字监听过滤
88
+watch(searchTextRef, (val) => {
89
+  treeRef.value?.filter(val);
90
+});
91
+
92
+// 点击左侧树形图某一个节点
93
+function nodeClick(currentNode: any) {
94
+  selectedChapterRef.value = currentNode;
95
+}
96
+
97
+// 点击主页
98
+function handleHomeClick() {
99
+  selectedChapterRef.value = { id: 'home', label: '主页', content: '' };
100
+}
101
+
102
+// 添加章节
103
+function addChapter(parentNode?: any) {
104
+  console.log('添加章节', parentNode);
105
+  // TODO: 实现添加章节逻辑
106
+}
107
+
108
+// 编辑章节
109
+function editChapter(node: any) {
110
+  console.log('编辑章节', node);
111
+  // TODO: 实现编辑章节逻辑
112
+}
113
+
114
+// 删除章节
115
+function deleteChapter(node: any) {
116
+  console.log('删除章节', node);
117
+  // TODO: 实现删除章节逻辑
118
+}
119
+
120
+// 章节拖拽结束事件
121
+function handleDragEnd(draggingNode: any, dropNode: any, dropType: string) {
122
+  console.log('拖拽结束', draggingNode, dropNode, dropType);
123
+  // TODO: 实现章节拖拽排序逻辑
124
+}
125
+
126
+// 打开编辑抽屉
127
+function openEditModal() {
128
+  drawerApi
129
+    .setData({ chapter: selectedChapterRef.value, isUpdate: true })
130
+    .open();
131
+}
132
+
133
+// 打开提醒抽屉
134
+function openRemindModal() {
135
+  remindDrawerApi.setData({}).open();
136
+}
137
+
138
+// 保存编辑内容
139
+function handleSaveEdit() {
140
+  console.log('保存编辑内容');
141
+  // TODO: 实现保存编辑内容逻辑
142
+  ElMessage.success('章节编辑成功');
143
+  drawerApi.close();
144
+}
145
+
146
+// 处理评论图片上传
147
+function handleCommentImageUpload(options: any) {
148
+  // 模拟上传成功处理
149
+  const fileData = {
150
+    uid: options.file.uid,
151
+    name: options.file.name,
152
+    url: URL.createObjectURL(options.file),
153
+    status: 'success',
154
+    raw: options.file,
155
+  };
156
+
157
+  // 直接通过v-model:file-list绑定,无需手动添加
158
+  options.onSuccess(fileData);
159
+}
160
+
161
+// 移除评论图片
162
+function handleCommentImageRemove(file: any, fileList: any[]) {
163
+  commentImages.value = fileList;
164
+}
165
+
166
+// 预览评论图片
167
+function handleCommentImagePreview(file: any) {
168
+  console.log('预览图片', file);
169
+  // TODO: 实现图片预览功能
170
+}
171
+
172
+// 提交评论
173
+function submitComment() {
174
+  if (!commentContent.value.trim() && commentImages.value.length === 0) {
175
+    ElMessage.warning('请输入评论内容或上传图片');
176
+    return;
177
+  }
178
+
179
+  // 过滤出已成功上传的图片
180
+  const uploadedImages = commentImages.value.filter(
181
+    (image: any) => image.status === 'success',
182
+  );
183
+  // TODO: 实现提交评论逻辑
184
+
185
+  // 清空评论内容和
186
+  commentContent.value = '';
187
+  commentImages.value = [];
188
+}
189
+function deleteKnowledge() {
190
+  //
191
+}
192
+
193
+onMounted(async () => {
194
+  // TODO: 从接口获取章节数据
195
+  // chapterDataRef.value = await getChapterTree();
196
+});
197
+</script>
198
+
199
+<template>
200
+  <div class="knowledge-detail-container">
201
+    <ColPage :left-width="25" auto-content-height>
202
+      <template #left>
203
+        <div
204
+          :style="{ minWidth: '250px' }"
205
+          class="border-border bg-card mr-2 flex h-full flex-col rounded-[var(--radius)] border p-2"
206
+        >
207
+          <!-- 知识库标题 -->
208
+          <div class="mb-4">
209
+            <div class="mb-2 flex items-center justify-between">
210
+              <h3 class="flex items-center text-lg font-bold">
211
+                <img src="/icon/1.png" alt="知识库" class="mr-2 h-6 w-6" />
212
+                知识库示例
213
+              </h3>
214
+            </div>
215
+            <ElInput
216
+              v-model="searchTextRef"
217
+              placeholder="请输入章节名称"
218
+              :prefix-icon="Search"
219
+            />
220
+          </div>
221
+
222
+          <!-- 单独的主页项 -->
223
+          <div class="mb-2">
224
+            <div
225
+              class="flex cursor-pointer items-center rounded p-2 transition-colors hover:bg-gray-100"
226
+              @click="handleHomeClick"
227
+            >
228
+              <HomeFilled class="mr-2 h-4 w-4" />
229
+              <span>主页</span>
230
+            </div>
231
+          </div>
232
+
233
+          <!-- 单独的目录标题 -->
234
+          <div class="mb-2 flex items-center justify-between p-2 font-medium">
235
+            <span class="text-gray-500">目录</span>
236
+            <ElButton
237
+              type="primary"
238
+              size="small"
239
+              circle
240
+              @click.stop="addChapter()"
241
+              class="!h-6 !w-6 !p-0"
242
+              title="添加一级章节"
243
+            >
244
+              <Plus class="!h-3 !w-3" />
245
+            </ElButton>
246
+          </div>
247
+
248
+          <!-- 章节树 -->
249
+          <div class="flex-1 overflow-auto">
250
+            <ElTree
251
+              ref="treeRef"
252
+              :data="chapterDataRef"
253
+              node-key="id"
254
+              default-expand-all
255
+              highlight-current
256
+              :expand-on-click-node="false"
257
+              :filter-node-method="filterNode"
258
+              @node-click="nodeClick"
259
+              draggable
260
+              :allow-drop="
261
+                (draggingNode, dropNode, type) => {
262
+                  // 只允许在同级或父级下拖拽
263
+                  return type === 'inner' || type === 'prev' || type === 'next';
264
+                }
265
+              "
266
+              @node-drag-end="handleDragEnd"
267
+            >
268
+              <template #default="{ node, data }">
269
+                <div class="flex w-full items-center justify-between">
270
+                  <!-- 节点文本 -->
271
+                  <span>{{ node.label }}</span>
272
+
273
+                  <!-- 操作按钮 -->
274
+                  <div
275
+                    class="flex items-center space-x-1 opacity-0 transition-opacity hover:opacity-100"
276
+                  >
277
+                    <ElButton
278
+                      type="primary"
279
+                      size="small"
280
+                      circle
281
+                      @click.stop="addChapter(data)"
282
+                      class="!h-6 !w-6 !p-0"
283
+                      title="添加子章节"
284
+                    >
285
+                      <Plus class="!h-3 !w-3" />
286
+                    </ElButton>
287
+                    <ElButton
288
+                      type="warning"
289
+                      size="small"
290
+                      circle
291
+                      @click.stop="editChapter(data)"
292
+                      class="!h-6 !w-6 !p-0"
293
+                      title="编辑章节"
294
+                    >
295
+                      <Edit class="!h-3 !w-3" />
296
+                    </ElButton>
297
+                    <ElButton
298
+                      type="danger"
299
+                      size="small"
300
+                      circle
301
+                      @click.stop="deleteChapter(data)"
302
+                      class="!h-6 !w-6 !p-0"
303
+                      title="删除章节"
304
+                    >
305
+                      <Delete class="!h-3 !w-3" />
306
+                    </ElButton>
307
+                  </div>
308
+                </div>
309
+              </template>
310
+            </ElTree>
311
+          </div>
312
+        </div>
313
+      </template>
314
+
315
+      <!-- 右侧内容显示区域 -->
316
+      <div
317
+        class="border-border bg-card mr-2 flex h-full flex-col rounded-[var(--radius)] border p-4"
318
+      >
319
+        <!-- 顶部操作栏 -->
320
+        <div class="mb-4 flex items-center justify-between">
321
+          <div class="flex items-center space-x-3">
322
+            <h3 class="text-xl font-bold">
323
+              {{ selectedChapterRef?.label || '主页' }}
324
+            </h3>
325
+            <div class="flex items-center space-x-1 text-sm text-gray-500">
326
+              2025-07-21 18:01:38
327
+              <ReadCount class="ml-2 size-4 text-blue-500" />
328
+              <span class="text-blue-500">51</span>
329
+            </div>
330
+          </div>
331
+          <div class="flex items-center space-x-2">
332
+            <ElButton type="text" @click="openEditModal"> 编辑 </ElButton>
333
+            <ElButton type="text" @click="openRemindModal"> 提醒 </ElButton>
334
+            <!-- <ElButton type="text"> 历史 </ElButton> -->
335
+            <ElButton type="text" @click="deleteKnowledge"> 删除 </ElButton>
336
+          </div>
337
+        </div>
338
+
339
+        <!-- 内容显示区域 -->
340
+        <div class="mb-4">
341
+          <!-- 主页内容 -->
342
+          <div
343
+            v-if="selectedChapterRef?.id === 'home'"
344
+            class="prose max-w-none"
345
+          >
346
+            <h2>编辑主页</h2>
347
+            <p>
348
+              此页面是知识库的主页,你可以点击右上角「编辑」,开始编辑主页内容,例如在主页中介绍知识库的内容。
349
+            </p>
350
+
351
+            <h2>新建文档</h2>
352
+            <p>点击左侧「目录+」按钮,开始创建你的文档。</p>
353
+          </div>
354
+
355
+          <!-- 章节内容 -->
356
+          <div v-else-if="selectedChapterRef" class="prose max-w-none">
357
+            <p>
358
+              {{
359
+                selectedChapterRef.content || '暂无内容,点击编辑开始添加内容'
360
+              }}
361
+            </p>
362
+          </div>
363
+
364
+          <!-- 默认内容 -->
365
+          <div v-else class="py-10 text-center text-gray-500">
366
+            请选择左侧章节查看内容
367
+          </div>
368
+        </div>
369
+
370
+        <!-- 评论区域 -->
371
+        <div class="border-t pt-4">
372
+          <h4 class="mb-3 font-bold">评论</h4>
373
+
374
+          <!-- 评论列表 -->
375
+          <div class="mb-4">
376
+            <!-- 评论项 -->
377
+            <div class="mb-4 rounded bg-gray-50 p-3">
378
+              <div class="mb-2 flex items-center">
379
+                <div
380
+                  class="mr-2 flex h-8 w-8 items-center justify-center rounded-full bg-gray-200"
381
+                >
382
+                  <Camera class="h-4 w-4" />
383
+                </div>
384
+                <div class="font-medium">用户名</div>
385
+                <div class="ml-2 text-xs text-gray-500">
386
+                  2025-07-21 18:01:38
387
+                </div>
388
+              </div>
389
+              <div class="mb-2">
390
+                这是一条评论内容,用户可以在这里发表自己的看法和意见。
391
+              </div>
392
+              <!-- 评论图片 -->
393
+              <div class="mb-2 flex space-x-2">
394
+                <img
395
+                  src="https://picsum.photos/80/80"
396
+                  alt="评论图片"
397
+                  class="h-20 w-20 rounded object-cover"
398
+                />
399
+                <img
400
+                  src="https://picsum.photos/80/80"
401
+                  alt="评论图片"
402
+                  class="h-20 w-20 rounded object-cover"
403
+                />
404
+              </div>
405
+              <!-- <div class="flex items-center text-sm text-gray-500">
406
+                <span class="mr-4 cursor-pointer hover:text-blue-500"
407
+                  >点赞 (5)</span
408
+                >
409
+                <span class="cursor-pointer hover:text-blue-500">回复</span>
410
+              </div> -->
411
+            </div>
412
+
413
+            <!-- 评论项 -->
414
+            <div class="mb-4 rounded bg-gray-50 p-3">
415
+              <div class="mb-2 flex items-center">
416
+                <div
417
+                  class="mr-2 flex h-8 w-8 items-center justify-center rounded-full bg-gray-200"
418
+                >
419
+                  <Camera class="h-4 w-4" />
420
+                </div>
421
+                <div class="font-medium">用户名</div>
422
+                <div class="ml-2 text-xs text-gray-500">
423
+                  2025-07-21 17:45:22
424
+                </div>
425
+              </div>
426
+              <div class="mb-2">
427
+                这是另一条评论内容,用户可以在这里发表自己的看法和意见。
428
+              </div>
429
+              <!-- <div class="flex items-center text-sm text-gray-500">
430
+                <span class="mr-4 cursor-pointer hover:text-blue-500"
431
+                  >点赞 (3)</span
432
+                >
433
+                <span class="cursor-pointer hover:text-blue-500">回复</span>
434
+              </div> -->
435
+            </div>
436
+          </div>
437
+
438
+          <!-- 评论操作区域 -->
439
+          <div class="flex flex-col space-y-3">
440
+            <div class="flex space-x-3">
441
+              <div
442
+                class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200"
443
+              >
444
+                <Camera class="h-4 w-4" />
445
+              </div>
446
+              <div class="flex-1">
447
+                <ElInput
448
+                  v-model="commentContent"
449
+                  type="textarea"
450
+                  placeholder="写下您的评论..."
451
+                  resize="none"
452
+                  rows="3"
453
+                />
454
+
455
+                <!-- 上传图片区域 -->
456
+                <div class="mt-2">
457
+                  <!-- 使用ElUpload组件实现多图片上传 -->
458
+                  <ElUpload
459
+                    v-model:file-list="commentImages"
460
+                    action="#"
461
+                    accept=".jpg,.jpeg,.png,.gif"
462
+                    list-type="picture-card"
463
+                    show-file-list
464
+                    :auto-upload="true"
465
+                    :drag="false"
466
+                    :limit="10"
467
+                    :multiple="true"
468
+                    :http-request="handleCommentImageUpload"
469
+                    :on-remove="handleCommentImageRemove"
470
+                    :on-preview="handleCommentImagePreview"
471
+                    class="mb-2"
472
+                  >
473
+                    <div class="flex h-full w-full items-center justify-center">
474
+                      <ElIcon class="el-upload__icon">
475
+                        <Plus size="18" />
476
+                      </ElIcon>
477
+                    </div>
478
+                  </ElUpload>
479
+                </div>
480
+
481
+                <!-- 评论按钮 -->
482
+                <div class="mt-2 flex justify-end">
483
+                  <ElButton type="primary" size="small" @click="submitComment">
484
+                    评论
485
+                  </ElButton>
486
+                </div>
487
+              </div>
488
+            </div>
489
+          </div>
490
+        </div>
491
+      </div>
492
+
493
+      <!-- 编辑抽屉组件 -->
494
+      <EditDrawerComp />
495
+      <!-- 提醒抽屉组件 -->
496
+      <RemindDrawerComp />
497
+    </ColPage>
498
+  </div>
499
+</template>
500
+
501
+<style scoped lang="scss">
502
+.knowledge-detail-container {
503
+  display: flex;
504
+  flex-direction: column;
505
+  width: 100%;
506
+  height: 100%;
507
+}
508
+
509
+// 自定义树节点样式
510
+:deep(.el-tree-node__content) {
511
+  padding-right: 8px;
512
+}
513
+
514
+::v-deep .el-upload--picture-card {
515
+  --el-upload-picture-card-size: 148px;
516
+
517
+  box-sizing: border-box;
518
+  display: inline-flex;
519
+  align-items: center;
520
+  justify-content: center;
521
+  width: 60px;
522
+  height: 60px;
523
+  vertical-align: top;
524
+  cursor: pointer;
525
+  background-color: #fafafa;
526
+  background-color: var(--el-fill-color-lighter);
527
+  border: 1px dashed #cdd0d6;
528
+  border: 1px dashed var(--el-border-color-darker);
529
+  border-radius: 6px;
530
+}
531
+</style>

+ 39 - 0
apps/web-ele/src/views/knowledge/detail/remind-drawer.vue

@@ -0,0 +1,39 @@
1
+<script lang="ts" setup>
2
+import { useVbenDrawer } from '@vben/common-ui';
3
+
4
+import { useVbenForm } from '#/adapter/form';
5
+
6
+import { remindFormSchema } from './config-data';
7
+
8
+defineOptions({
9
+  name: 'RemindForm',
10
+});
11
+
12
+const [Form, formApi] = useVbenForm({
13
+  schema: remindFormSchema(),
14
+  showDefaultActions: false,
15
+});
16
+const [Drawer, drawerApi] = useVbenDrawer({
17
+  onCancel() {
18
+    drawerApi.close();
19
+  },
20
+  onConfirm: async () => {
21
+    await formApi.submitForm();
22
+    drawerApi.close();
23
+  },
24
+  onOpenChange(isOpen: boolean) {
25
+    if (isOpen) {
26
+      const { values } = drawerApi.getData<Record<string, any>>();
27
+      if (values) {
28
+        formApi.setValues(values);
29
+      }
30
+    }
31
+  },
32
+  title: '提醒',
33
+});
34
+</script>
35
+<template>
36
+  <Drawer>
37
+    <Form />
38
+  </Drawer>
39
+</template>

+ 194 - 0
apps/web-ele/src/views/knowledge/type/config-data.tsx

@@ -0,0 +1,194 @@
1
+import type { FormSchemaGetter } from '#/adapter/form';
2
+import type { VxeGridProps } from '#/adapter/vxe-table';
3
+
4
+// 知识库类别数据模型
5
+export interface KnowledgeType {
6
+  id: string;
7
+  name: string;
8
+  icon: string;
9
+  description: string;
10
+  admin: string;
11
+  applicableEnd: string;
12
+  applicablePost: string;
13
+  createTime: string;
14
+  createUser: string;
15
+}
16
+
17
+// 查询表单配置
18
+export const queryFormSchema: FormSchemaGetter = () => [
19
+  {
20
+    component: 'Input',
21
+    fieldName: 'name',
22
+    label: '名称',
23
+    componentProps: {
24
+      placeholder: '请输入知识库类别名称',
25
+      allowClear: true,
26
+    },
27
+  },
28
+  {
29
+    component: 'Input',
30
+    fieldName: 'description',
31
+    label: '描述',
32
+    componentProps: {
33
+      placeholder: '请输入知识库类别描述',
34
+      allowClear: true,
35
+    },
36
+  },
37
+];
38
+
39
+// 抽屉表单配置
40
+export const drawerFormSchema: FormSchemaGetter = () => [
41
+  {
42
+    component: 'Input',
43
+    dependencies: {
44
+      show: () => false,
45
+      triggerFields: [''],
46
+    },
47
+    fieldName: 'id',
48
+  },
49
+  {
50
+    component: 'Input',
51
+    fieldName: 'icon',
52
+    label: '知识图标',
53
+    // 使用插槽自定义上传组件
54
+    rules: 'required',
55
+  },
56
+  {
57
+    component: 'Input',
58
+    fieldName: 'name',
59
+    label: '知识库名称',
60
+    componentProps: {
61
+      placeholder: '请输入知识库名称',
62
+      maxlength: 100,
63
+    },
64
+    rules: 'required',
65
+  },
66
+  {
67
+    component: 'Input',
68
+    fieldName: 'description',
69
+    label: '知识库描述',
70
+    componentProps: {
71
+      placeholder: '请输入知识库描述',
72
+      maxlength: 200,
73
+      type: 'textarea',
74
+      showWordLimit: true,
75
+    },
76
+    rules: 'required',
77
+  },
78
+
79
+  {
80
+    component: 'Input',
81
+    fieldName: 'admin',
82
+    label: '知识库管理员',
83
+    componentProps: {
84
+      placeholder: '请输入知识库管理员',
85
+      maxlength: 50,
86
+    },
87
+    rules: 'required',
88
+  },
89
+  {
90
+    component: 'Select',
91
+    componentProps: {
92
+      placeholder: '请选择适用端',
93
+      options: [
94
+        { label: '钉钉', value: 'dingtalk' },
95
+        { label: 'PC', value: 'pc' },
96
+      ],
97
+    },
98
+    fieldName: 'applicableEnd',
99
+    label: '适用端',
100
+    rules: 'required',
101
+  },
102
+  {
103
+    component: 'Select',
104
+    componentProps: {
105
+      placeholder: '请选择适用岗位',
106
+      options: [
107
+        { label: '管理员', value: 'admin' },
108
+        { label: '普通用户', value: 'user' },
109
+        { label: '技术人员', value: 'tech' },
110
+      ],
111
+    },
112
+    fieldName: 'applicablePost',
113
+    label: '适用岗位',
114
+    rules: 'required',
115
+  },
116
+];
117
+
118
+// 表格列配置
119
+export const tableColumns: VxeGridProps['columns'] = [
120
+  {
121
+    type: 'checkbox',
122
+    width: 60,
123
+  },
124
+  {
125
+    field: 'id',
126
+    title: '类别ID',
127
+    width: 80,
128
+  },
129
+  {
130
+    field: 'name',
131
+    title: '类别名称',
132
+    minWidth: 120,
133
+  },
134
+  {
135
+    field: 'icon',
136
+    slots: { default: 'icon' },
137
+    title: '类别图标',
138
+    minWidth: 100,
139
+  },
140
+  {
141
+    field: 'description',
142
+    title: '类别描述',
143
+    minWidth: 150,
144
+  },
145
+  {
146
+    field: 'admin',
147
+    title: '管理员',
148
+    minWidth: 100,
149
+  },
150
+  {
151
+    field: 'applicableEnd',
152
+    title: '适用端',
153
+    minWidth: 100,
154
+    formatter: (params: { cellValue: string }) => {
155
+      const options = [
156
+        { label: '钉钉', value: 'dingtalk' },
157
+        { label: 'PC', value: 'pc' },
158
+      ];
159
+      const option = options.find((item) => item.value === params.cellValue);
160
+      return option ? option.label : params.cellValue;
161
+    },
162
+  },
163
+  {
164
+    field: 'applicablePost',
165
+    title: '适用岗位',
166
+    minWidth: 100,
167
+    formatter: (params: { cellValue: string }) => {
168
+      const options = [
169
+        { label: '管理员', value: 'admin' },
170
+        { label: '普通用户', value: 'user' },
171
+        { label: '技术人员', value: 'tech' },
172
+      ];
173
+      const option = options.find((item) => item.value === params.cellValue);
174
+      return option ? option.label : params.cellValue;
175
+    },
176
+  },
177
+  {
178
+    field: 'createUser',
179
+    title: '创建人',
180
+    minWidth: 100,
181
+  },
182
+  {
183
+    field: 'createTime',
184
+    title: '创建时间',
185
+    minWidth: 150,
186
+  },
187
+  {
188
+    field: 'action',
189
+    fixed: 'right',
190
+    slots: { default: 'action' },
191
+    title: '操作',
192
+    width: 180,
193
+  },
194
+];

+ 208 - 0
apps/web-ele/src/views/knowledge/type/index.vue

@@ -0,0 +1,208 @@
1
+<script setup lang="ts">
2
+import type { VbenFormProps } from '@vben/common-ui';
3
+
4
+import type { KnowledgeType } from './config-data';
5
+
6
+import type { VxeGridProps } from '#/adapter/vxe-table';
7
+
8
+import { ref } from 'vue';
9
+import { useRouter } from 'vue-router';
10
+
11
+import { Page } from '@vben/common-ui';
12
+
13
+import { ElImage, ElMessageBox } from 'element-plus';
14
+
15
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
16
+
17
+import { queryFormSchema, tableColumns } from './config-data';
18
+import TypeDrawer from './type-drawer.vue';
19
+
20
+const router = useRouter();
21
+
22
+// 模拟API调用,实际项目中需要替换为真实API
23
+const getKnowledgeTypeList = async (params: any) => {
24
+  // 模拟数据
25
+  const mockData = {
26
+    rows: [
27
+      {
28
+        id: '1',
29
+        name: '产品知识',
30
+        icon: '/icon/1.png',
31
+        description: '产品相关知识',
32
+        admin: 'admin',
33
+        applicableEnd: 'pc',
34
+        applicablePost: 'admin',
35
+        createTime: '2024-01-01 10:00:00',
36
+        createUser: 'admin',
37
+      },
38
+      {
39
+        id: '2',
40
+        name: '技术文档',
41
+        icon: '/icon/3.png',
42
+        description: '技术相关文档',
43
+        admin: 'admin',
44
+        applicableEnd: 'dingtalk',
45
+        applicablePost: 'tech',
46
+        createTime: '2024-01-02 11:00:00',
47
+        createUser: 'admin',
48
+      },
49
+      {
50
+        id: '3',
51
+        name: '常见问题',
52
+        icon: '/icon/4.png',
53
+        description: '常见问题解答',
54
+        admin: 'admin',
55
+        applicableEnd: 'pc',
56
+        applicablePost: 'user',
57
+        createTime: '2024-01-03 14:00:00',
58
+        createUser: 'admin',
59
+      },
60
+      {
61
+        id: '4',
62
+        name: '用户指南',
63
+        icon: 'carbon:book-open',
64
+        description: '用户使用指南',
65
+        admin: 'admin',
66
+        applicableEnd: 'dingtalk',
67
+        applicablePost: 'user',
68
+        createTime: '2024-01-04 16:00:00',
69
+        createUser: 'admin',
70
+      },
71
+    ],
72
+    total: 4,
73
+  };
74
+  return mockData;
75
+};
76
+
77
+const deleteKnowledgeType = async (ids: string[]) => {
78
+  // 模拟删除操作
79
+  // console.log('删除知识库类别', ids);
80
+  return { success: true };
81
+};
82
+
83
+// 查询表单配置
84
+const formOptions: VbenFormProps = {
85
+  commonConfig: {
86
+    labelWidth: 80,
87
+    componentProps: {
88
+      allowClear: true,
89
+    },
90
+  },
91
+  schema: queryFormSchema(),
92
+  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
93
+};
94
+
95
+// 列表配置
96
+const gridOptions: VxeGridProps = {
97
+  checkboxConfig: {
98
+    // 高亮
99
+    highlight: true,
100
+    // 翻页时保留选中状态
101
+    reserve: true,
102
+    // 点击行选中
103
+    trigger: 'default',
104
+  },
105
+  columns: tableColumns,
106
+  size: 'medium',
107
+  height: 'auto',
108
+  proxyConfig: {
109
+    ajax: {
110
+      query: async ({ page }, formValues = {}) => {
111
+        const resp = await getKnowledgeTypeList({
112
+          ...formValues,
113
+          pageNum: page.currentPage,
114
+          pageSize: page.pageSize,
115
+        });
116
+        return { items: resp.rows, total: resp.total };
117
+      },
118
+    },
119
+  },
120
+  rowConfig: {
121
+    keyField: 'id',
122
+  },
123
+  toolbarConfig: {
124
+    custom: true,
125
+    refresh: true,
126
+    zoom: true,
127
+  },
128
+  id: 'knowledge-type-index',
129
+};
130
+
131
+// 创建列表实例
132
+const [BasicTable, BasicTableApi] = useVbenVxeGrid({
133
+  formOptions,
134
+  gridOptions,
135
+});
136
+
137
+// 抽屉组件引用
138
+const typeDrawerRef = ref();
139
+
140
+// 新增知识库类别
141
+const handleAdd = () => {
142
+  // 调用抽屉组件的open方法
143
+  const drawerApi = typeDrawerRef.value?.getDrawerApi?.();
144
+  if (drawerApi) {
145
+    drawerApi.setData({ isUpdate: false }).open();
146
+  }
147
+};
148
+
149
+// 编辑知识库类别
150
+const handleEdit = (row: KnowledgeType) => {
151
+  // 调用抽屉组件的open方法
152
+  const drawerApi = typeDrawerRef.value?.getDrawerApi?.();
153
+  if (drawerApi) {
154
+    drawerApi.setData({ id: row.id, isUpdate: true }).open();
155
+  }
156
+};
157
+
158
+// 查看知识库类别详情
159
+const handleView = (row: KnowledgeType) => {
160
+  // 实际项目中可以跳转到详情页面
161
+
162
+  router.push(`/knowledge/detail/${row.id}`);
163
+};
164
+
165
+// 删除知识库类别
166
+const handleDelete = async (row: KnowledgeType) => {
167
+  try {
168
+    await ElMessageBox.confirm(`确认删除知识库类别"${row.name}"吗?`, '提示', {
169
+      confirmButtonText: '确定',
170
+      cancelButtonText: '取消',
171
+      type: 'warning',
172
+    });
173
+    await deleteKnowledgeType([row.id]);
174
+    await BasicTableApi.reload();
175
+  } catch {
176
+    // 取消删除操作
177
+  }
178
+};
179
+</script>
180
+
181
+<template>
182
+  <Page :auto-content-height="true">
183
+    <BasicTable table-title="知识库类别列表">
184
+      <!-- 自定义图标列 -->
185
+      <template #icon="{ row }">
186
+        <ElImage style="width: 30px; height: 30px" :src="row.icon" />
187
+      </template>
188
+
189
+      <!-- 自定义操作列 -->
190
+      <template #action="{ row }">
191
+        <el-button size="small" type="info" plain @click="handleView(row)">
192
+          查看
193
+        </el-button>
194
+        <el-button size="small" type="primary" plain @click="handleEdit(row)">
195
+          编辑
196
+        </el-button>
197
+      </template>
198
+
199
+      <!-- 自定义工具栏 -->
200
+      <template #toolbar-tools>
201
+        <el-button type="primary" @click="handleAdd">新增</el-button>
202
+      </template>
203
+    </BasicTable>
204
+
205
+    <!-- 抽屉组件 -->
206
+    <TypeDrawer @reload="BasicTableApi.reload()" />
207
+  </Page>
208
+</template>

+ 263 - 0
apps/web-ele/src/views/knowledge/type/type-drawer.vue

@@ -0,0 +1,263 @@
1
+<script setup lang="ts">
2
+import { ref } from 'vue';
3
+import { useVbenDrawer, useVbenForm } from '@vben/common-ui';
4
+import { UploadFilled, Plus } from '@element-plus/icons-vue';
5
+import { ElUpload } from 'element-plus';
6
+import { VbenIcon } from '@vben-core/shadcn-ui';
7
+import { drawerFormSchema } from './config-data';
8
+
9
+const emit = defineEmits<{
10
+  reload: [];
11
+}>();
12
+
13
+// 模拟API调用,实际项目中需要替换为真实API
14
+const addKnowledgeType = async (data: any) => {
15
+  console.log('添加知识库类型', data);
16
+  return { success: true };
17
+};
18
+
19
+const updateKnowledgeType = async (data: any) => {
20
+  console.log('更新知识库类型', data);
21
+  return { success: true };
22
+};
23
+
24
+const getKnowledgeTypeDetail = async (id: string) => {
25
+  console.log('获取知识库类型详情', id);
26
+  // 模拟数据
27
+  return {
28
+    id,
29
+    name: '测试类型',
30
+    description: '测试描述',
31
+    icon: 'carbon:test',
32
+    admin: 'admin',
33
+    applicableEnd: 'pc',
34
+    applicablePost: 'admin',
35
+  };
36
+};
37
+
38
+// 默认图标列表
39
+const defaultIcons = [
40
+  { label: '产品知识', value: 'carbon:product' },
41
+  { label: '技术文档', value: 'carbon:code' },
42
+  { label: '常见问题', value: 'carbon:question' },
43
+];
44
+
45
+// 图标文件列表
46
+const iconFileList = ref([]);
47
+
48
+// 可选图标列表,使用public/icon下的本地图片
49
+const optionalIcons = [
50
+  { label: '文档', value: '/icon/1.png' },
51
+  { label: '代码', value: '/icon/2.png' },
52
+  { label: '问题', value: '/icon/3.png' },
53
+  { label: '书籍', value: '/icon/4.png' }
54
+];
55
+
56
+// 上传请求处理
57
+const handleUpload = (options) => {
58
+  console.log('上传图标', options);
59
+  // 实际项目中需要调用上传API
60
+  // 这里模拟上传成功
61
+  const mockResponse = {
62
+    data: {
63
+      url: URL.createObjectURL(options.file),
64
+      filename: options.file.name,
65
+    },
66
+  };
67
+  // 更新文件列表
68
+  scope.icon = [
69
+    {
70
+      name: options.file.name,
71
+      url: mockResponse.data.url,
72
+      status: 'success',
73
+      uid: options.file.uid,
74
+    },
75
+  ];
76
+  // 更新表单值
77
+  formApi.setValues({ icon: mockResponse.data.url });
78
+  options.onSuccess(mockResponse);
79
+};
80
+
81
+// 移除文件处理
82
+const handleRemove = (file, fileList) => {
83
+  console.log('移除图标', file, fileList);
84
+  scope.icon = fileList;
85
+  if (fileList.length === 0) {
86
+    formApi.setValues({ icon: '' });
87
+  }
88
+};
89
+
90
+// 预览文件处理
91
+const handlePreview = (file) => {
92
+  console.log('预览图标', file);
93
+  // 实际项目中可以实现预览功能
94
+};
95
+
96
+// 选择可选图标
97
+const selectOptionalIcon = (iconValue, currentScope) => {
98
+  console.log('选择可选图标', iconValue, currentScope);
99
+  // 生成一个模拟的文件对象
100
+  const mockFile = {
101
+    name: `${iconValue.replace('carbon:', '')}.svg`,
102
+    url: iconValue,
103
+    status: 'success',
104
+    uid: Date.now(),
105
+    isOptional: true,
106
+  };
107
+  // 更新文件列表
108
+  currentScope.icon = [mockFile];
109
+  // 更新表单值
110
+  formApi.setValues({ icon: iconValue });
111
+};
112
+
113
+const [Form, formApi] = useVbenForm({
114
+  showDefaultActions: false,
115
+  schema: drawerFormSchema(),
116
+});
117
+
118
+const isUpdateRef = ref<boolean>(false);
119
+
120
+const [Drawer, drawerApi] = useVbenDrawer({
121
+  async onOpenChange(isOpen) {
122
+    if (!isOpen) {
123
+      return;
124
+    }
125
+    try {
126
+      drawerApi.drawerLoading(true);
127
+      const { id, isUpdate } = drawerApi.getData();
128
+      isUpdateRef.value = isUpdate;
129
+      if (isUpdate && id) {
130
+        const res = await getKnowledgeTypeDetail(id);
131
+        console.log('获取知识库类型详情数据:', res);
132
+        // 确保从正确的响应结构中获取数据
133
+        const detailData = res?.data || res;
134
+        await formApi.setValues(detailData);
135
+      } else {
136
+        // 新增时重置表单
137
+        await formApi.resetForm();
138
+      }
139
+    } catch (error) {
140
+      console.error('获取知识库类型详情失败:', error);
141
+    } finally {
142
+      drawerApi.drawerLoading(false);
143
+    }
144
+  },
145
+
146
+  async onConfirm() {
147
+    try {
148
+      this.confirmLoading = true;
149
+      const { valid } = await formApi.validate();
150
+      if (!valid) {
151
+        return;
152
+      }
153
+      const data = await formApi.getValues();
154
+
155
+      console.log('提交数据:', data);
156
+      await (isUpdateRef.value
157
+        ? updateKnowledgeType(data)
158
+        : addKnowledgeType(data));
159
+      emit('reload');
160
+      drawerApi.close();
161
+    } catch (error) {
162
+      console.error('保存知识库类型失败:', error);
163
+    } finally {
164
+      this.confirmLoading = false;
165
+    }
166
+  },
167
+});
168
+
169
+// 暴露drawerApi给父组件
170
+defineExpose({
171
+  getDrawerApi: () => drawerApi,
172
+});
173
+</script>
174
+
175
+<template>
176
+  <Drawer :title="isUpdateRef ? '编辑知识库分类' : '新增知识库分类'">
177
+    <Form>
178
+      <!-- 自定义Upload组件插槽 -->
179
+      <template #icon="scope">
180
+        <div class="flex items-start gap-6">
181
+          <!-- 上传组件 -->
182
+          <ElUpload
183
+            v-model:file-list="scope.icon"
184
+            action="#"
185
+            accept=".jpg,.jpeg,.png"
186
+            list-type="picture-card"
187
+            show-file-list
188
+            :auto-upload="true"
189
+            :drag="false"
190
+            :http-request="handleUpload"
191
+            :on-remove="handleRemove"
192
+            :on-preview="handlePreview"
193
+            class="w-[60px] h-[60px]"
194
+            :style="{ width: '60px', height: '60px' }"
195
+          >
196
+            <div class="w-full h-full flex items-center justify-center">
197
+              <ElIcon class="el-upload__icon">
198
+                <Plus size="18" />
199
+              </ElIcon>
200
+            </div>
201
+          </ElUpload>
202
+          
203
+          <!-- 可选图标列表 -->
204
+          <div class="flex flex-col gap-2">
205
+            <div class="text-xs text-gray-500">可选图标:</div>
206
+            <div class="flex gap-3 flex-wrap">
207
+              <div 
208
+                v-for="icon in optionalIcons" 
209
+                :key="icon.value"
210
+                class="optional-icon-item"
211
+                @click="selectOptionalIcon(icon.value, scope)"
212
+              >
213
+                <img :src="icon.value" :alt="icon.label" class="w-10 h-10" />
214
+              </div>
215
+            </div>
216
+          </div>
217
+        </div>
218
+      </template>
219
+    </Form>
220
+  </Drawer>
221
+</template>
222
+
223
+<style scoped lang="scss">
224
+.optional-icon-item {
225
+  width: 40px;
226
+  height: 40px;
227
+  display: flex;
228
+  align-items: center;
229
+  justify-content: center;
230
+  border: 2px solid transparent;
231
+  border-radius: 6px;
232
+  cursor: pointer;
233
+  transition: all 0.3s ease;
234
+  background-color: #f5f7fa;
235
+  
236
+  &:hover {
237
+    border-color: #409eff;
238
+    background-color: #ecf5ff;
239
+    transform: scale(1.1);
240
+  }
241
+  
242
+  &:active {
243
+    transform: scale(0.9);
244
+  }
245
+}
246
+
247
+::v-deep .el-upload--picture-card {
248
+    --el-upload-picture-card-size: 148px;
249
+    align-items: center;
250
+    background-color: #fafafa;
251
+    background-color: var(--el-fill-color-lighter);
252
+    border: 1px dashed #cdd0d6;
253
+    border: 1px dashed var(--el-border-color-darker);
254
+    border-radius: 6px;
255
+    box-sizing: border-box;
256
+    cursor: pointer;
257
+    display: inline-flex;
258
+    height: 60px;
259
+    justify-content: center;
260
+    vertical-align: top;
261
+    width: 60px;
262
+}
263
+</style>

+ 3 - 0
packages/icons/src/iconify/index.ts

@@ -31,3 +31,6 @@ export const Refresh = createIconifyIcon('material-symbols-light:refresh');
31 31
 export const Clear = createIconifyIcon('material-symbols-light:delete-outline');
32 32
 export const Copy = createIconifyIcon('mingcute:copy-line');
33 33
 export const UserCenter = createIconifyIcon('la:user-cog');
34
+
35
+// 阅读量
36
+export const ReadCount = createIconifyIcon('la:eye');