作者 lixiang

问题库以及自定义问答

此 diff 太大无法显示。
@@ -23,7 +23,7 @@ export const columns: BasicColumn[] = [ @@ -23,7 +23,7 @@ export const columns: BasicColumn[] = [
23 }, 23 },
24 }, 24 },
25 { 25 {
26 - title: '按钮值', 26 + title: '发送内容',
27 align: 'center', 27 align: 'center',
28 dataIndex: 'buttonValues', 28 dataIndex: 'buttonValues',
29 }, 29 },
@@ -64,7 +64,7 @@ export const searchFormSchema: FormSchema[] = [ @@ -64,7 +64,7 @@ export const searchFormSchema: FormSchema[] = [
64 component: 'JInput', 64 component: 'JInput',
65 }, 65 },
66 { 66 {
67 - label: '按钮值', 67 + label: '发送内容',
68 field: 'buttonValues', 68 field: 'buttonValues',
69 component: 'JInput', 69 component: 'JInput',
70 }, 70 },
@@ -85,7 +85,7 @@ export const formSchema: FormSchema[] = [ @@ -85,7 +85,7 @@ export const formSchema: FormSchema[] = [
85 componentProps: {}, 85 componentProps: {},
86 }, 86 },
87 { 87 {
88 - label: '按钮值', 88 + label: '发送内容',
89 field: 'buttonValues', 89 field: 'buttonValues',
90 component: 'Input', 90 component: 'Input',
91 required: true, 91 required: true,
@@ -103,7 +103,7 @@ export const formSchema: FormSchema[] = [ @@ -103,7 +103,7 @@ export const formSchema: FormSchema[] = [
103 export const superQuerySchema = { 103 export const superQuerySchema = {
104 buttonName: { title: '按钮名称', order: 0, view: 'text', type: 'string' }, 104 buttonName: { title: '按钮名称', order: 0, view: 'text', type: 'string' },
105 buttonSwitch: { title: '按钮开关', order: 1, view: 'switch', type: 'string' }, 105 buttonSwitch: { title: '按钮开关', order: 1, view: 'switch', type: 'string' },
106 - buttonValues: { title: '按钮值', order: 2, view: 'text', type: 'string' }, 106 + buttonValues: { title: '发送内容', order: 2, view: 'text', type: 'string' },
107 }; 107 };
108 108
109 /** 109 /**
@@ -11,38 +11,52 @@ enum Api { @@ -11,38 +11,52 @@ enum Api {
11 deleteBatch = '/question/embedding/deleteBatch', 11 deleteBatch = '/question/embedding/deleteBatch',
12 importExcel = '/question/embedding/importZip', 12 importExcel = '/question/embedding/importZip',
13 uploadZip = '/question/embedding/uploadZip', 13 uploadZip = '/question/embedding/uploadZip',
  14 + listKnowledge = '/embeddings/embeddings/listknowledge',
14 } 15 }
15 16
16 export const getExportUrl = Api.importExcel; 17 export const getExportUrl = Api.importExcel;
17 export const getImportZipUrl = Api.uploadZip; 18 export const getImportZipUrl = Api.uploadZip;
18 19
  20 +// 新增知识库列表查询方法
  21 +export const listKnowledge = async () => {
  22 + try {
  23 + const res = await defHttp.get({ url: Api.listKnowledge });
  24 + return res;
  25 + } catch (error) {
  26 + console.error("Error fetching knowledge list:", error);
  27 + return [];
  28 + }
  29 +}
  30 +
19 export const list = async (params) => { 31 export const list = async (params) => {
20 try { 32 try {
21 const res = await defHttp.get({ 33 const res = await defHttp.get({
22 url: Api.list, 34 url: Api.list,
23 - params: { ...params, size: 1000 }, 35 + params: { ...params, size: 1000 }
24 }); 36 });
25 37
26 if (res?.records && Array.isArray(res.records)) { 38 if (res?.records && Array.isArray(res.records)) {
27 - res.records = res.records.map((item) => ({ 39 + res.records = res.records.map(item => ({
28 ...item, 40 ...item,
29 ...item.metadata, 41 ...item.metadata,
30 question: item.question || '', 42 question: item.question || '',
31 answer: item.answer || '', 43 answer: item.answer || '',
  44 + knowledgeName: item.knowledgeName || '',
  45 + knowledgeId: item.knowledgeId || ''
32 })); 46 }));
33 } 47 }
34 return res; 48 return res;
35 } catch (error) { 49 } catch (error) {
36 - console.error('Error fetching question embeddings:', error); 50 + console.error("Error fetching question embeddings:", error);
37 return { 51 return {
38 records: [], 52 records: [],
39 total: 0, 53 total: 0,
40 size: 10, 54 size: 10,
41 current: 1, 55 current: 1,
42 - pages: 0, 56 + pages: 0
43 }; 57 };
44 } 58 }
45 -}; 59 +}
46 60
47 export const deleteOne = (params, handleSuccess) => { 61 export const deleteOne = (params, handleSuccess) => {
48 return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => { 62 return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
1 import { BasicColumn } from '/@/components/Table'; 1 import { BasicColumn } from '/@/components/Table';
2 import { FormSchema } from '/@/components/Table'; 2 import { FormSchema } from '/@/components/Table';
  3 +import { listKnowledge } from '@/views/super/airag/questionembedding/QuestionEmbedding.api';
3 4
4 export const columns: BasicColumn[] = [ 5 export const columns: BasicColumn[] = [
5 { 6 {
@@ -9,45 +10,49 @@ export const columns: BasicColumn[] = [ @@ -9,45 +10,49 @@ export const columns: BasicColumn[] = [
9 width: 200, 10 width: 200,
10 }, 11 },
11 { 12 {
  13 + title: '知识库',
  14 + align: 'center',
  15 + dataIndex: 'knowledgeName',
  16 + width: 150
  17 + },
  18 + {
12 title: '问题', 19 title: '问题',
13 align: 'center', 20 align: 'center',
14 dataIndex: 'question', 21 dataIndex: 'question',
15 - width: 250, 22 + width: 250
16 }, 23 },
17 { 24 {
18 title: '回答', 25 title: '回答',
19 align: 'center', 26 align: 'center',
20 dataIndex: 'answer', 27 dataIndex: 'answer',
21 - width: 300,  
22 - },  
23 - {  
24 - title: '原文',  
25 - align: 'center',  
26 - dataIndex: 'text',  
27 - width: 300,  
28 - },  
29 - {  
30 - title: '元数据',  
31 - align: 'center',  
32 - dataIndex: 'metadata',  
33 - width: 200,  
34 - customRender: ({ text }) => JSON.stringify(text || {}), 28 + width: 300
35 }, 29 },
36 ]; 30 ];
37 31
38 export const searchFormSchema: FormSchema[] = [ 32 export const searchFormSchema: FormSchema[] = [
39 { 33 {
  34 + field: 'knowledgeId',
  35 + label: '知识库',
  36 + component: 'ApiSelect',
  37 + componentProps: {
  38 + api: listKnowledge,
  39 + labelField: 'name',
  40 + valueField: 'id',
  41 + },
  42 + colProps: { span: 8 }
  43 + },
  44 + {
40 field: 'question', 45 field: 'question',
41 label: '问题', 46 label: '问题',
42 component: 'Input', 47 component: 'Input',
43 - colProps: { span: 8 }, 48 + colProps: { span: 8 }
44 }, 49 },
45 { 50 {
46 field: 'answer', 51 field: 'answer',
47 label: '回答', 52 label: '回答',
48 component: 'Input', 53 component: 'Input',
49 colProps: { span: 8 }, 54 colProps: { span: 8 },
50 - }, 55 + }
51 ]; 56 ];
52 57
53 export const formSchema: FormSchema[] = [ 58 export const formSchema: FormSchema[] = [
@@ -58,36 +63,43 @@ export const formSchema: FormSchema[] = [ @@ -58,36 +63,43 @@ export const formSchema: FormSchema[] = [
58 show: false, 63 show: false,
59 }, 64 },
60 { 65 {
  66 + field: 'knowledgeId',
  67 + label: '知识库',
  68 + component: 'ApiSelect',
  69 + required: true,
  70 + componentProps: {
  71 + api: listKnowledge,
  72 + labelField: 'name',
  73 + valueField: 'id',
  74 + }
  75 + },
  76 + {
61 field: 'question', 77 field: 'question',
62 label: '问题', 78 label: '问题',
63 - component: 'InputTextArea', 79 + component: 'Input',
64 required: true, 80 required: true,
65 - colProps: { span: 24 }, 81 + colProps: { span: 24 }
66 }, 82 },
67 { 83 {
68 field: 'answer', 84 field: 'answer',
69 label: '回答', 85 label: '回答',
70 component: 'InputTextArea', 86 component: 'InputTextArea',
71 required: true, 87 required: true,
72 - colProps: { span: 24 },  
73 - },  
74 - {  
75 - field: 'text',  
76 - label: '原文',  
77 - component: 'InputTextArea',  
78 - colProps: { span: 24 },  
79 - },  
80 - {  
81 - field: 'metadata',  
82 - label: '元数据',  
83 - component: 'InputTextArea',  
84 - colProps: { span: 24 },  
85 - }, 88 + componentProps: {
  89 + allowClear: true,
  90 + showCount: true,
  91 + autoSize: {
  92 + minRows: 5,
  93 + maxRows: 10
  94 + }
  95 + }
  96 + }
86 ]; 97 ];
87 98
88 export const superQuerySchema = { 99 export const superQuerySchema = {
89 - question: { title: '问题', order: 0, view: 'text', type: 'string' },  
90 - answer: { title: '回答', order: 1, view: 'text', type: 'string' }, 100 + knowledgeId: { title: '知识库', order: 0, view: 'text', type: 'string' },
  101 + question: { title: '问题', order: 1, view: 'text', type: 'string' },
  102 + answer: { title: '回答', order: 2, view: 'text', type: 'string' }
91 }; 103 };
92 104
93 export function getBpmFormSchema(_formData): FormSchema[] { 105 export function getBpmFormSchema(_formData): FormSchema[] {
@@ -4,38 +4,117 @@ @@ -4,38 +4,117 @@
4 <template #tableTitle> 4 <template #tableTitle>
5 <a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined">新增</a-button> 5 <a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined">新增</a-button>
6 <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button> 6 <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
7 - <j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button> 7 + <a-button
  8 + type="primary"
  9 + preIcon="ant-design:import-outlined"
  10 + @click="handleOpenImportModal"
  11 + >导入ZIP</a-button>
8 <a-dropdown v-if="selectedRowKeys.length > 0"> 12 <a-dropdown v-if="selectedRowKeys.length > 0">
9 <template #overlay> 13 <template #overlay>
10 <a-menu> 14 <a-menu>
11 - <a-menu-item key="1" @click="batchHandleDelete"> <Icon icon="ant-design:delete-outlined" />删除 </a-menu-item> 15 + <a-menu-item key="1" @click="batchHandleDelete">
  16 + <Icon icon="ant-design:delete-outlined" />删除
  17 + </a-menu-item>
12 </a-menu> 18 </a-menu>
13 </template> 19 </template>
14 <a-button>批量操作<Icon icon="mdi:chevron-down" /></a-button> 20 <a-button>批量操作<Icon icon="mdi:chevron-down" /></a-button>
15 </a-dropdown> 21 </a-dropdown>
16 - <super-query :config="superQueryConfig" @search="handleSuperQuery" />  
17 </template> 22 </template>
18 <template #action="{ record }"> 23 <template #action="{ record }">
19 <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" /> 24 <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
20 </template> 25 </template>
21 </BasicTable> 26 </BasicTable>
22 <QuestionEmbeddingModal @register="registerModal" @success="handleSuccess" /> 27 <QuestionEmbeddingModal @register="registerModal" @success="handleSuccess" />
  28 +
  29 + <!-- 导入ZIP模态框 -->
  30 + <BasicModal
  31 + v-model:visible="importModalVisible"
  32 + title="导入ZIP文件"
  33 + @ok="handleImportSubmit"
  34 + @cancel="handleImportCancel"
  35 + width="600px"
  36 + :okButtonProps="{ disabled: !selectedKnowledgeId || !currentFile }"
  37 + >
  38 + <BasicForm @register="registerImportForm" :showActionButtonGroup="false" />
  39 + <div style="margin-top: 20px;margin-left: 5%">
  40 + <input
  41 + type="file"
  42 + ref="fileInput"
  43 + accept=".zip"
  44 + @change="handleFileChange"
  45 + style="display: none"
  46 + >
  47 + <a-button
  48 + type="primary"
  49 + @click="triggerFileInput"
  50 + >
  51 + <UploadOutlined /> 选择ZIP文件
  52 + </a-button>
  53 + <div v-if="currentFile" style="margin-top: 8px;">
  54 + 已选择文件: {{ currentFile.name }} ({{ formatFileSize(currentFile.size) }})
  55 + </div>
  56 + <div v-if="uploadProgress > 0" style="margin-top: 8px;">
  57 + 上传进度: {{ uploadProgress }}%
  58 + <a-progress
  59 + :percent="uploadProgress"
  60 + :stroke-color="progressColor"
  61 + :status="uploadStatus"
  62 + />
  63 + </div>
  64 + </div>
  65 + </BasicModal>
23 </div> 66 </div>
24 </template> 67 </template>
25 68
26 <script lang="ts" setup> 69 <script lang="ts" setup>
27 - import { reactive } from 'vue';  
28 - import { BasicTable, TableAction } from '/@/components/Table';  
29 - import { useModal } from '/@/components/Modal';  
30 - import { useListPage } from '/@/hooks/system/useListPage';  
31 - import QuestionEmbeddingModal from './components/QuestionEmbeddingModal.vue';  
32 - import { columns, searchFormSchema, superQuerySchema } from './QuestionEmbedding.data';  
33 - import { list, deleteOne, batchDelete, getImportZipUrl, getExportUrl } from './QuestionEmbedding.api';  
34 -  
35 - const queryParam = reactive<any>({});  
36 - const [registerModal, { openModal }] = useModal();  
37 -  
38 - const { tableContext, onExportXls, onImportXls } = useListPage({ 70 +import { ref, reactive } from 'vue';
  71 +import { UploadOutlined } from '@ant-design/icons-vue';
  72 +import { BasicTable, useTable, TableAction } from '/@/components/Table';
  73 +import { BasicModal } from '/@/components/Modal';
  74 +import { BasicForm, useForm } from '/@/components/Form';
  75 +import { useModal } from '/@/components/Modal';
  76 +import { useListPage } from '/@/hooks/system/useListPage';
  77 +import { message } from 'ant-design-vue';
  78 +import QuestionEmbeddingModal from './components/QuestionEmbeddingModal.vue';
  79 +import { columns, searchFormSchema } from './QuestionEmbedding.data';
  80 +import { list, deleteOne, batchDelete, getImportZipUrl, getExportUrl, listKnowledge } from './QuestionEmbedding.api';
  81 +import { useUserStore } from '/@/store/modules/user';
  82 +import { getToken } from '/@/utils/auth';
  83 +
  84 +const queryParam = reactive<any>({});
  85 +const [registerModal, { openModal }] = useModal();
  86 +const importModalVisible = ref(false);
  87 +const selectedKnowledgeId = ref('');
  88 +const fileInput = ref<HTMLInputElement | null>(null);
  89 +const currentFile = ref<File | null>(null);
  90 +const uploadProgress = ref(0);
  91 +const progressColor = ref('#1890ff');
  92 +const uploadStatus = ref<'normal' | 'exception' | 'active' | 'success'>('normal');
  93 +
  94 +// 知识库选择表单
  95 +const [registerImportForm] = useForm({
  96 + labelWidth: 100,
  97 + showActionButtonGroup: false,
  98 + schemas: [
  99 + {
  100 + field: 'knowledgeId',
  101 + label: '知识库',
  102 + component: 'ApiSelect',
  103 + required: true,
  104 + componentProps: {
  105 + placeholder: '请选择知识库',
  106 + api: listKnowledge,
  107 + labelField: 'name',
  108 + valueField: 'id',
  109 + onChange: (value: string) => {
  110 + selectedKnowledgeId.value = value;
  111 + }
  112 + },
  113 + },
  114 + ],
  115 +});
  116 +
  117 +const { tableContext, onExportXls } = useListPage({
39 tableProps: { 118 tableProps: {
40 title: '问答向量库', 119 title: '问答向量库',
41 api: list, 120 api: list,
@@ -43,71 +122,184 @@ @@ -43,71 +122,184 @@
43 formConfig: { 122 formConfig: {
44 schemas: searchFormSchema, 123 schemas: searchFormSchema,
45 autoSubmitOnEnter: true, 124 autoSubmitOnEnter: true,
46 - showAdvancedButton: true, 125 + showAdvancedButton: true
47 }, 126 },
48 actionColumn: { 127 actionColumn: {
49 width: 120, 128 width: 120,
50 - fixed: 'right', 129 + fixed: 'right'
51 }, 130 },
52 - beforeFetch: (params) => Object.assign(params, queryParam), 131 + beforeFetch: (params) => Object.assign(params, queryParam)
53 }, 132 },
54 exportConfig: { 133 exportConfig: {
55 name: '问答向量库', 134 name: '问答向量库',
56 - url: getExportUrl,  
57 - },  
58 - importConfig: {  
59 - url: getImportZipUrl,  
60 - success: handleSuccess, 135 + url: getExportUrl
  136 + }
  137 +});
  138 +
  139 +const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
  140 +
  141 +function handleOpenImportModal() {
  142 + importModalVisible.value = true;
  143 + selectedKnowledgeId.value = '';
  144 + resetFileInput();
  145 +}
  146 +
  147 +function handleImportCancel() {
  148 + importModalVisible.value = false;
  149 + resetFileInput();
  150 +}
  151 +
  152 +function triggerFileInput() {
  153 + fileInput.value?.click();
  154 +}
  155 +
  156 +function handleFileChange(e: Event) {
  157 + const input = e.target as HTMLInputElement;
  158 + if (input.files && input.files.length > 0) {
  159 + currentFile.value = input.files[0];
  160 + uploadProgress.value = 0;
  161 + uploadStatus.value = 'normal';
  162 + console.log('文件选择成功:', {
  163 + name: currentFile.value.name,
  164 + size: currentFile.value.size,
  165 + type: currentFile.value.type
  166 + });
  167 + } else {
  168 + currentFile.value = null;
  169 + }
  170 +}
  171 +
  172 +function resetFileInput() {
  173 + if (fileInput.value) {
  174 + fileInput.value.value = '';
  175 + }
  176 + currentFile.value = null;
  177 + uploadProgress.value = 0;
  178 + uploadStatus.value = 'normal';
  179 +}
  180 +
  181 +function formatFileSize(bytes: number): string {
  182 + if (bytes === 0) return '0 Bytes';
  183 + const k = 1024;
  184 + const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  185 + const i = Math.floor(Math.log(bytes) / Math.log(k));
  186 + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  187 +}
  188 +
  189 +// 获取用户token
  190 +const userStore = useUserStore();
  191 +const token = getToken();
  192 +
  193 +async function handleImportSubmit() {
  194 + if (!selectedKnowledgeId.value || !currentFile.value) {
  195 + message.warning('请完整填写上传信息');
  196 + return;
  197 + }
  198 +
  199 + const formData = new FormData();
  200 + formData.append('file', currentFile.value, currentFile.value.name);
  201 + formData.append('knowledgeId', selectedKnowledgeId.value);
  202 +
  203 + try {
  204 + uploadProgress.value = 0;
  205 + uploadStatus.value = 'active';
  206 +
  207 + // 使用fetch API并添加认证头
  208 + const response = await fetch("/jeecgboot" + getImportZipUrl, {
  209 + method: 'POST',
  210 + body: formData,
  211 + headers: {
  212 + 'X-Access-Token': token, // JEECG标准认证头
  213 + 'Authorization': `Bearer ${token}` // 备用认证头
61 }, 214 },
  215 + credentials: 'include' // 携带cookie
62 }); 216 });
63 217
64 - const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;  
65 - const superQueryConfig = reactive(superQuerySchema); 218 + if (response.status === 401) {
  219 + message.error('登录已过期,请重新登录');
  220 + userStore.logout(true);
  221 + return;
  222 + }
66 223
67 - function handleSuperQuery(params) { 224 + if (!response.ok) {
  225 + throw new Error(`上传失败: ${response.statusText}`);
  226 + }
  227 +
  228 + const result = await response.json();
  229 + if (result.success) {
  230 + message.success('上传成功');
  231 + importModalVisible.value = false;
  232 + handleSuccess();
  233 + } else {
  234 + throw new Error(result.message || '上传处理失败');
  235 + }
  236 + } catch (error) {
  237 + uploadStatus.value = 'exception';
  238 + message.error(error.message || '上传失败');
  239 + console.error('上传错误:', error);
  240 + }
  241 +}
  242 +
  243 +function handleSuperQuery(params: any) {
68 Object.assign(queryParam, params); 244 Object.assign(queryParam, params);
69 reload(); 245 reload();
70 - } 246 +}
71 247
72 - function handleAdd() { 248 +function handleAdd() {
73 openModal(true, { isUpdate: false, showFooter: true }); 249 openModal(true, { isUpdate: false, showFooter: true });
74 - } 250 +}
75 251
76 - function handleEdit(record) { 252 +function handleEdit(record: any) {
77 openModal(true, { record, isUpdate: true, showFooter: true }); 253 openModal(true, { record, isUpdate: true, showFooter: true });
78 - } 254 +}
79 255
80 - function handleDetail(record) { 256 +function handleDetail(record: any) {
81 openModal(true, { record, isUpdate: true, showFooter: false }); 257 openModal(true, { record, isUpdate: true, showFooter: false });
82 - } 258 +}
83 259
84 - async function handleDelete(record) { 260 +async function handleDelete(record: any) {
85 await deleteOne({ id: record.id }, handleSuccess); 261 await deleteOne({ id: record.id }, handleSuccess);
86 - } 262 +}
87 263
88 - async function batchHandleDelete() { 264 +async function batchHandleDelete() {
89 await batchDelete({ ids: selectedRowKeys.value }, handleSuccess); 265 await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
90 - } 266 +}
91 267
92 - function handleSuccess() { 268 +function handleSuccess() {
93 selectedRowKeys.value = []; 269 selectedRowKeys.value = [];
94 reload(); 270 reload();
95 - } 271 +}
96 272
97 - function getTableAction(record) {  
98 - return [{ label: '编辑', onClick: handleEdit.bind(null, record) }];  
99 - } 273 +function getTableAction(record: any) {
  274 + return [
  275 + { label: '编辑', onClick: handleEdit.bind(null, record) }
  276 + ];
  277 +}
100 278
101 - function getDropDownAction(record) { 279 +function getDropDownAction(record: any) {
102 return [ 280 return [
103 { label: '详情', onClick: handleDetail.bind(null, record) }, 281 { label: '详情', onClick: handleDetail.bind(null, record) },
104 { 282 {
105 label: '删除', 283 label: '删除',
106 popConfirm: { 284 popConfirm: {
107 title: '确认删除此问答?', 285 title: '确认删除此问答?',
108 - confirm: handleDelete.bind(null, record),  
109 - },  
110 - },  
111 - ]; 286 + confirm: handleDelete.bind(null, record)
112 } 287 }
  288 + }
  289 + ];
  290 +}
113 </script> 291 </script>
  292 +
  293 +<style scoped>
  294 +.upload-area {
  295 + margin-top: 20px;
  296 + padding: 20px;
  297 + border: 1px dashed #d9d9d9;
  298 + border-radius: 4px;
  299 + text-align: center;
  300 + cursor: pointer;
  301 +}
  302 +.upload-area:hover {
  303 + border-color: #1890ff;
  304 +}
  305 +</style>
@@ -5,6 +5,7 @@ const { createMessage } = useMessage(); @@ -5,6 +5,7 @@ const { createMessage } = useMessage();
5 5
6 enum Api { 6 enum Api {
7 send = '/airag/zdyRag/send', 7 send = '/airag/zdyRag/send',
  8 + buttonList = '/airagbutton/airagButton/buttonList',
8 } 9 }
9 10
10 /** 11 /**
@@ -35,3 +36,16 @@ export const sendMessage = async (params: { questionText: string }) => { @@ -35,3 +36,16 @@ export const sendMessage = async (params: { questionText: string }) => {
35 return null; 36 return null;
36 } 37 }
37 }; 38 };
  39 +
  40 +
  41 +export const getButtonList = async () =>{
  42 + const res = await defHttp.get({
  43 + url: Api.buttonList,
  44 + });
  45 + console.log("res",res);
  46 + if (res){
  47 + return res;
  48 + }
  49 + return null;
  50 +
  51 +}
1 <template> 1 <template>
2 <div class="chat-container"> 2 <div class="chat-container">
3 - <div class="chat-layout"> 3 + <!-- 主内容区域 -->
  4 + <div class="main-content">
4 <!-- 聊天消息区域 --> 5 <!-- 聊天消息区域 -->
5 <div class="chat-messages" ref="messagesContainer"> 6 <div class="chat-messages" ref="messagesContainer">
6 <div v-for="(message, index) in messages" :key="index" :class="['message', message.type]"> 7 <div v-for="(message, index) in messages" :key="index" :class="['message', message.type]">
@@ -8,7 +9,10 @@ @@ -8,7 +9,10 @@
8 <div class="message-text" v-html="message.text"></div> 9 <div class="message-text" v-html="message.text"></div>
9 <div v-if="message.type === 'assistant' && message.similarity" class="message-meta"> 10 <div v-if="message.type === 'assistant' && message.similarity" class="message-meta">
10 相似度: {{ (message.similarity * 100).toFixed(2) }}% 11 相似度: {{ (message.similarity * 100).toFixed(2) }}%
11 - <a v-if="message.fileBase64" @click="showPreview(message)">预览文件</a> 12 + <div v-if="message.fileBase64">
  13 + 相关资料:
  14 + <a @click="showPreview(message)">{{message.fileName}}</a>
  15 + </div>
12 </div> 16 </div>
13 </div> 17 </div>
14 </div> 18 </div>
@@ -19,29 +23,65 @@ @@ -19,29 +23,65 @@
19 </div> 23 </div>
20 </div> 24 </div>
21 25
22 - <!-- 文件预览区域 - 右侧 --> 26 + <!-- 文件预览区域 -->
23 <div v-if="previewContent" class="file-preview"> 27 <div v-if="previewContent" class="file-preview">
  28 +
24 <div class="preview-header"> 29 <div class="preview-header">
25 - <span>文件预览</span>  
26 - <a-button type="text" @click="closePreview">  
27 - <template #icon><CloseOutlined /></template> 30 + <span class="preview-title">文件预览</span>
  31 + <div class="preview-actions">
  32 + <a-button type="link" @click="downloadFile" class="download-btn">
  33 + <DownloadOutlined />
  34 + <span class="btn-text">下载</span>
  35 + </a-button>
  36 + <a-button
  37 + type="text"
  38 + @click="closePreview"
  39 + class="close-btn"
  40 + title="关闭预览"
  41 + >
  42 + <CloseOutlined />
28 </a-button> 43 </a-button>
29 </div> 44 </div>
  45 + </div>
30 <div class="preview-content"> 46 <div class="preview-content">
31 <div v-if="previewType === 'txt' || previewType === 'md'" class="text-preview"> 47 <div v-if="previewType === 'txt' || previewType === 'md'" class="text-preview">
32 <pre>{{ previewContent }}</pre> 48 <pre>{{ previewContent }}</pre>
33 </div> 49 </div>
34 - <div v-else-if="previewType === 'doc' || previewType === 'docx'" class="doc-preview">  
35 - <!-- 使用Blob URL直接预览 -->  
36 - <iframe  
37 - :src="previewUrl"  
38 - frameborder="0"  
39 - style="width:100%; height:100%;"  
40 - ></iframe> 50 + <div v-else-if="previewType === 'pdf'" class="pdf-preview" ref="pdfPreview"></div>
  51 + <div v-else-if="previewType === 'docx'" class="docx-preview" ref="docxPreview"></div>
  52 + <div v-else-if="previewType === 'doc'" class="unsupported-preview">
  53 + 不支持预览 DOC 格式的文件,请下载后使用本地软件查看
41 </div> 54 </div>
  55 + <div v-else class="unsupported-preview">
  56 + 不支持预览 {{ previewType.toUpperCase() }} 格式的文件
42 </div> 57 </div>
43 </div> 58 </div>
44 </div> 59 </div>
  60 + </div>
  61 + <div>
  62 + <a-button
  63 + style="margin-left: 1%;
  64 + background-color: white;
  65 + color: #666;
  66 + border: 1px solid rgba(0, 0, 0, 0.1);
  67 + border-radius: 16px;
  68 + cursor: pointer;
  69 + font-size: 14px;
  70 + padding: 2px 10px;
  71 + width: max-content;
  72 + margin-right: 6px;
  73 + white-space: nowrap;
  74 + transition: all 300ms ease;
  75 +"
  76 + v-for="(btn, index) in buttonList"
  77 + :key="index"
  78 + type="primary"
  79 + class="action-button"
  80 + @click="sendButtonMessage(btn.buttonValues)"
  81 + >
  82 + {{ btn.buttonName }}
  83 + </a-button>
  84 + </div>
45 85
46 <!-- 输入区域 --> 86 <!-- 输入区域 -->
47 <div class="chat-input"> 87 <div class="chat-input">
@@ -64,12 +104,31 @@ @@ -64,12 +104,31 @@
64 </template> 104 </template>
65 105
66 <script lang="ts" name="zdy-rag-chat" setup> 106 <script lang="ts" name="zdy-rag-chat" setup>
67 -import { ref, reactive, nextTick } from 'vue';  
68 -import { sendMessage as apiSendMessage } from './ZdyRag.api'; 107 +import { ref, reactive, nextTick, onMounted, onBeforeUnmount } from 'vue';
  108 +import { sendMessage as apiSendMessage , getButtonList} from './ZdyRag.api';
69 import { useMessage } from '/@/hooks/web/useMessage'; 109 import { useMessage } from '/@/hooks/web/useMessage';
70 -import { CloseOutlined } from '@ant-design/icons-vue'; 110 +import { CloseOutlined, DownloadOutlined } from '@ant-design/icons-vue';
  111 +import * as pdfjsLib from 'pdfjs-dist';
  112 +import * as mammoth from 'mammoth';
  113 +import { renderAsync } from 'docx-preview';
71 114
72 -const { createMessage } = useMessage(); 115 +// 设置PDF.js worker路径
  116 +pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.min.js';
  117 +
  118 +const buttonList = reactive<any[]>([]);
  119 +onMounted(async () => {
  120 + try {
  121 + const res = await getButtonList();
  122 + if (res && Array.isArray(res)) {
  123 + buttonList.push(...res);
  124 + } else {
  125 + // createMessage.warning('未获取到按钮列表数据');
  126 + }
  127 + } catch (error) {
  128 + console.error('获取按钮列表失败:', error);
  129 + // createMessage.error('获取按钮列表失败');
  130 + }
  131 +});
73 132
74 // 聊天消息数据 133 // 聊天消息数据
75 const messages = reactive([ 134 const messages = reactive([
@@ -77,7 +136,8 @@ const messages = reactive([ @@ -77,7 +136,8 @@ const messages = reactive([
77 type: 'assistant', 136 type: 'assistant',
78 text: '您好!我是智能助手,请问有什么可以帮您?', 137 text: '您好!我是智能助手,请问有什么可以帮您?',
79 similarity: null, 138 similarity: null,
80 - fileBase64: null 139 + fileBase64: null,
  140 + fileName: null
81 } 141 }
82 ]); 142 ]);
83 143
@@ -87,6 +147,9 @@ const messagesContainer = ref<HTMLElement>(); @@ -87,6 +147,9 @@ const messagesContainer = ref<HTMLElement>();
87 const previewContent = ref(''); 147 const previewContent = ref('');
88 const previewType = ref(''); 148 const previewType = ref('');
89 const previewUrl = ref(''); 149 const previewUrl = ref('');
  150 +const currentFile = ref<any>(null);
  151 +const pdfPreview = ref<HTMLElement>();
  152 +const docxPreview = ref<HTMLElement>();
90 153
91 // 发送消息 154 // 发送消息
92 const sendMessage = async () => { 155 const sendMessage = async () => {
@@ -98,7 +161,8 @@ const sendMessage = async () => { @@ -98,7 +161,8 @@ const sendMessage = async () => {
98 type: 'user', 161 type: 'user',
99 text: text, 162 text: text,
100 similarity: null, 163 similarity: null,
101 - fileBase64: null 164 + fileBase64: null,
  165 + fileName: null
102 }); 166 });
103 167
104 inputMessage.value = ''; 168 inputMessage.value = '';
@@ -119,61 +183,212 @@ const sendMessage = async () => { @@ -119,61 +183,212 @@ const sendMessage = async () => {
119 fileName: res.fileName || '' 183 fileName: res.fileName || ''
120 }; 184 };
121 messages.push(newMessage); 185 messages.push(newMessage);
122 -  
123 - // 如果有文件内容,自动显示预览  
124 - if (res.fileBase64 && res.fileName) {  
125 - showPreview(newMessage);  
126 - }  
127 } else { 186 } else {
128 - createMessage.error('获取回答失败: 返回数据为空'); 187 + // createMessage.error('获取回答失败: 返回数据为空');
129 } 188 }
130 - } catch (error) { 189 + } catch (error: any) {
131 console.error('发送消息失败:', error); 190 console.error('发送消息失败:', error);
132 - createMessage.error('发送消息失败: ' + (error as Error).message); 191 + // createMessage.error('发送消息失败: ' + error.message);
133 } finally { 192 } finally {
134 loading.value = false; 193 loading.value = false;
135 scrollToBottom(); 194 scrollToBottom();
136 } 195 }
137 }; 196 };
138 197
139 -// 显示文件预览  
140 -const showPreview = (message) => {  
141 - if (!message.fileBase64 || !message.fileName) {  
142 - console.warn('无法预览: 缺少fileBase64或fileName');  
143 - return; 198 +const sendButtonMessage = async (buttonValue: string) => {
  199 + if (loading.value) return;
  200 +
  201 + // 添加用户消息
  202 + messages.push({
  203 + type: 'user',
  204 + text: buttonValue,
  205 + similarity: null,
  206 + fileBase64: null,
  207 + fileName: null
  208 + });
  209 +
  210 + loading.value = true;
  211 + closePreview();
  212 +
  213 + try {
  214 + // 调用API发送按钮值
  215 + const res = await apiSendMessage({ questionText: buttonValue });
  216 +
  217 + if (res) {
  218 + const newMessage = {
  219 + type: 'assistant',
  220 + text: formatAnswer(res.answer),
  221 + similarity: res.similarity || null,
  222 + fileBase64: res.fileBase64 || null,
  223 + fileName: res.fileName || ''
  224 + };
  225 + messages.push(newMessage);
  226 + } else {
  227 + // createMessage.error('获取回答失败: 返回数据为空');
144 } 228 }
  229 + } catch (error: any) {
  230 + console.error('发送按钮消息失败:', error);
  231 + // createMessage.error('发送按钮消息失败: ' + error.message);
  232 + } finally {
  233 + loading.value = false;
  234 + scrollToBottom();
  235 + }
  236 +};
145 237
  238 +// 增强版showPreview函数
  239 +const showPreview = async (message: any) => {
146 try { 240 try {
147 - const fileExt = message.fileName.split('.').pop()?.toLowerCase() || ''; 241 + // 验证基本数据
  242 + if (!message?.fileBase64 || !message?.fileName) {
  243 + throw new Error('文件数据不完整');
  244 + }
  245 +
  246 + // 关闭之前的预览
  247 + closePreview();
  248 +
  249 + // 设置当前文件
  250 + currentFile.value = message;
  251 + const fileExt = getFileExtension(message.fileName);
148 previewType.value = fileExt; 252 previewType.value = fileExt;
149 253
150 - if (fileExt === 'txt' || fileExt === 'md') {  
151 - // 对于文本文件,直接解码显示  
152 - previewContent.value = atob(message.fileBase64);  
153 - } else if (fileExt === 'doc' || fileExt === 'docx') {  
154 - // 对于Word文档,创建Blob对象并生成URL  
155 - const byteCharacters = atob(message.fileBase64);  
156 - const byteNumbers = new Array(byteCharacters.length);  
157 - for (let i = 0; i < byteCharacters.length; i++) {  
158 - byteNumbers[i] = byteCharacters.charCodeAt(i); 254 + // 根据文件类型处理
  255 + if (['txt', 'md'].includes(fileExt)) {
  256 + // 文本文件处理
  257 + previewContent.value = decodeBase64Content(message.fileBase64);
  258 + } else if (fileExt === 'pdf') {
  259 + // PDF文件处理
  260 + previewContent.value = 'pdf-preview';
  261 + await nextTick(); // 等待DOM更新
  262 + await renderPdf(message.fileBase64);
  263 + } else if (fileExt === 'docx') {
  264 + // DOCX文件处理
  265 + previewContent.value = 'docx-preview';
  266 + await nextTick(); // 等待DOM更新
  267 + await renderDocx(message.fileBase64);
  268 + } else if (fileExt === 'doc') {
  269 + // 不支持DOC格式预览
  270 + previewContent.value = 'unsupported';
  271 + throw new Error('不支持DOC格式预览,请下载后查看');
  272 + } else {
  273 + throw new Error(`不支持的文件类型: ${fileExt}`);
  274 + }
  275 + } catch (error) {
  276 + console.error('预览失败:', error);
  277 + previewContent.value = 'error';
  278 + // createMessage.error(`预览失败: ${error.message}`);
  279 + }
  280 +};
  281 +
  282 +// 渲染PDF
  283 +const renderPdf = async (base64: string) => {
  284 + if (!pdfPreview.value) return;
  285 +
  286 + // 清空容器
  287 + pdfPreview.value.innerHTML = '';
  288 +
  289 + // 将Base64转换为Uint8Array
  290 + const binaryString = atob(base64);
  291 + const bytes = new Uint8Array(binaryString.length);
  292 + for (let i = 0; i < binaryString.length; i++) {
  293 + bytes[i] = binaryString.charCodeAt(i);
  294 + }
  295 +
  296 + // 加载PDF文档
  297 + const pdfDoc = await pdfjsLib.getDocument({ data: bytes }).promise;
  298 +
  299 + // 创建Canvas容器
  300 + const container = document.createElement('div');
  301 + container.className = 'pdf-pages';
  302 + pdfPreview.value.appendChild(container);
  303 +
  304 + // 渲染每一页
  305 + for (let i = 1; i <= pdfDoc.numPages; i++) {
  306 + const page = await pdfDoc.getPage(i);
  307 + const viewport = page.getViewport({ scale: 1.5 });
  308 +
  309 + const canvas = document.createElement('canvas');
  310 + const context = canvas.getContext('2d');
  311 + canvas.height = viewport.height;
  312 + canvas.width = viewport.width;
  313 +
  314 + const renderContext = {
  315 + canvasContext: context,
  316 + viewport: viewport
  317 + };
  318 +
  319 + await page.render(renderContext).promise;
  320 +
  321 + const pageDiv = document.createElement('div');
  322 + pageDiv.className = 'pdf-page';
  323 + pageDiv.appendChild(canvas);
  324 + container.appendChild(pageDiv);
  325 + }
  326 +};
  327 +
  328 +// 渲染DOCX
  329 +const renderDocx = async (base64: string) => {
  330 + if (!docxPreview.value) return;
  331 +
  332 + // 清空容器
  333 + docxPreview.value.innerHTML = '';
  334 +
  335 + // 将Base64转换为Blob
  336 + const byteString = atob(base64);
  337 + const byteNumbers = new Array(byteString.length);
  338 + for (let i = 0; i < byteString.length; i++) {
  339 + byteNumbers[i] = byteString.charCodeAt(i);
159 } 340 }
160 const byteArray = new Uint8Array(byteNumbers); 341 const byteArray = new Uint8Array(byteNumbers);
161 const blob = new Blob([byteArray], { 342 const blob = new Blob([byteArray], {
162 - type: fileExt === 'docx' ?  
163 - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' :  
164 - 'application/msword' 343 + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
165 }); 344 });
166 345
167 - // 直接使用Blob URL  
168 - previewUrl.value = URL.createObjectURL(blob);  
169 - previewContent.value = 'doc-preview';  
170 - } else {  
171 - console.warn('不支持的文件类型:', fileExt);  
172 - createMessage.warning(`不支持预览 ${fileExt} 格式的文件`); 346 + // 使用docx-preview渲染
  347 + await renderAsync(blob, docxPreview.value);
  348 +};
  349 +
  350 +// 获取文件扩展名
  351 +const getFileExtension = (filename: string): string => {
  352 + const match = filename.match(/\.([0-9a-z]+)(?:[\?#]|$)/i);
  353 + return match ? match[1].toLowerCase() : 'unknown';
  354 +};
  355 +
  356 +// Base64解码
  357 +const decodeBase64Content = (base64: string): string => {
  358 + try {
  359 + return decodeURIComponent(escape(atob(base64)));
  360 + } catch {
  361 + return atob(base64);
173 } 362 }
174 - } catch (error) {  
175 - console.error('文件预览失败:', error);  
176 - createMessage.error('文件预览失败'); 363 +};
  364 +
  365 +// 下载文件
  366 +const downloadFile = () => {
  367 + if (!currentFile.value || !currentFile.value.fileBase64 || !currentFile.value.fileName) {
  368 + // createMessage.warning('无法下载文件');
  369 + return;
  370 + }
  371 +
  372 + try {
  373 + const byteCharacters = atob(currentFile.value.fileBase64);
  374 + const byteNumbers = new Array(byteCharacters.length);
  375 + for (let i = 0; i < byteCharacters.length; i++) {
  376 + byteNumbers[i] = byteCharacters.charCodeAt(i);
  377 + }
  378 + const byteArray = new Uint8Array(byteNumbers);
  379 + const blob = new Blob([byteArray], { type: 'application/octet-stream' });
  380 +
  381 + const link = document.createElement('a');
  382 + link.href = URL.createObjectURL(blob);
  383 + link.download = currentFile.value.fileName;
  384 + document.body.appendChild(link);
  385 + link.click();
  386 + document.body.removeChild(link);
  387 +
  388 + // createMessage.success('文件下载开始');
  389 + } catch (error: any) {
  390 + console.error('文件下载失败:', error);
  391 + // createMessage.error('文件下载失败: ' + error.message);
177 } 392 }
178 }; 393 };
179 394
@@ -185,6 +400,11 @@ const closePreview = () => { @@ -185,6 +400,11 @@ const closePreview = () => {
185 URL.revokeObjectURL(previewUrl.value); 400 URL.revokeObjectURL(previewUrl.value);
186 previewUrl.value = ''; 401 previewUrl.value = '';
187 } 402 }
  403 + currentFile.value = null;
  404 +
  405 + // 清空预览容器
  406 + if (pdfPreview.value) pdfPreview.value.innerHTML = '';
  407 + if (docxPreview.value) docxPreview.value.innerHTML = '';
188 }; 408 };
189 409
190 // 格式化回答内容 410 // 格式化回答内容
@@ -201,46 +421,125 @@ const scrollToBottom = () => { @@ -201,46 +421,125 @@ const scrollToBottom = () => {
201 } 421 }
202 }); 422 });
203 }; 423 };
  424 +
  425 +// 组件卸载时清理资源
  426 +onBeforeUnmount(() => {
  427 + closePreview();
  428 +});
204 </script> 429 </script>
205 430
206 <style lang="less" scoped> 431 <style lang="less" scoped>
  432 +/* 样式保持不变 */
207 .chat-container { 433 .chat-container {
208 display: flex; 434 display: flex;
209 flex-direction: column; 435 flex-direction: column;
210 height: calc(100vh - 120px); 436 height: calc(100vh - 120px);
211 - max-width: 1200px; /* 增加宽度以容纳两个区域 */ 437 + max-width: 1200px;
212 margin: 0 auto; 438 margin: 0 auto;
213 padding: 20px; 439 padding: 20px;
214 background-color: #f5f5f5; 440 background-color: #f5f5f5;
215 border-radius: 8px; 441 border-radius: 8px;
216 } 442 }
217 443
218 -.chat-layout { 444 +.main-content {
219 display: flex; 445 display: flex;
220 flex: 1; 446 flex: 1;
221 gap: 20px; 447 gap: 20px;
222 - overflow: hidden;  
223 margin-bottom: 20px; 448 margin-bottom: 20px;
  449 + overflow: hidden;
224 } 450 }
225 451
226 .chat-messages { 452 .chat-messages {
227 - flex: 2; 453 + flex: 1;
228 overflow-y: auto; 454 overflow-y: auto;
229 - padding: 10px; 455 + padding: 15px;
230 background-color: white; 456 background-color: white;
231 border-radius: 8px; 457 border-radius: 8px;
232 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 458 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  459 + min-height: 300px;
233 } 460 }
234 461
  462 +//.file-preview {
  463 +// width: 55%;
  464 +// min-width: 300px;
  465 +// display: flex;
  466 +// flex-direction: column;
  467 +// background-color: white;
  468 +// border-radius: 8px;
  469 +// box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  470 +// overflow: hidden;
  471 +//
  472 +// .preview-header {
  473 +// display: flex;
  474 +// justify-content: space-between;
  475 +// align-items: center;
  476 +// padding: 10px 15px;
  477 +// background-color: #f0f0f0;
  478 +// border-bottom: 1px solid #e8e8e8;
  479 +//
  480 +// .preview-actions {
  481 +// display: flex;
  482 +// align-items: center;
  483 +// gap: 8px;
  484 +// }
  485 +// }
  486 +//
  487 +// .preview-content {
  488 +// flex: 1;
  489 +// overflow: auto;
  490 +// padding: 10px;
  491 +//
  492 +// .text-preview {
  493 +// padding: 15px;
  494 +// white-space: pre-wrap;
  495 +// font-family: monospace;
  496 +// line-height: 1.6;
  497 +// }
  498 +//
  499 +// .pdf-preview {
  500 +// height: 100%;
  501 +// overflow-y: auto;
  502 +//
  503 +// .pdf-pages {
  504 +// display: flex;
  505 +// flex-direction: column;
  506 +// gap: 20px;
  507 +// padding: 10px;
  508 +//
  509 +// .pdf-page {
  510 +// border: 1px solid #e8e8e8;
  511 +// box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  512 +// }
  513 +// }
  514 +// }
  515 +//
  516 +// .docx-preview {
  517 +// height: 100%;
  518 +// overflow-y: auto;
  519 +// padding: 20px;
  520 +// background-color: #fff;
  521 +// }
  522 +//
  523 +// .unsupported-preview {
  524 +// height: 100%;
  525 +// padding: 20px;
  526 +// display: flex;
  527 +// align-items: center;
  528 +// justify-content: center;
  529 +// color: #ff4d4f;
  530 +// font-weight: 500;
  531 +// }
  532 +// }
  533 +//}
235 .file-preview { 534 .file-preview {
236 - flex: 1;  
237 - min-width: 350px; /* 确保预览区域有最小宽度 */ 535 + width: 55%;
  536 + min-width: 300px;
  537 + display: flex;
  538 + flex-direction: column;
238 background-color: white; 539 background-color: white;
239 border-radius: 8px; 540 border-radius: 8px;
240 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 541 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
241 overflow: hidden; 542 overflow: hidden;
242 - display: flex;  
243 - flex-direction: column;  
244 543
245 .preview-header { 544 .preview-header {
246 display: flex; 545 display: flex;
@@ -249,30 +548,123 @@ const scrollToBottom = () => { @@ -249,30 +548,123 @@ const scrollToBottom = () => {
249 padding: 10px 15px; 548 padding: 10px 15px;
250 background-color: #f0f0f0; 549 background-color: #f0f0f0;
251 border-bottom: 1px solid #e8e8e8; 550 border-bottom: 1px solid #e8e8e8;
  551 + font-weight: 500;
  552 + font-size: 14px;
  553 +
  554 + .preview-actions {
  555 + display: flex;
  556 + align-items: center;
  557 + gap: 10px;
  558 +
  559 + .ant-btn {
  560 + padding: 0;
  561 + height: auto;
  562 + color: #666;
  563 + display: flex;
  564 + align-items: center;
  565 + transition: color 0.3s;
  566 +
  567 + &:hover {
  568 + color: #1890ff;
  569 + background: transparent;
  570 + }
  571 +
  572 + .anticon {
  573 + font-size: 16px;
  574 + }
  575 + }
  576 +
  577 + .ant-btn-link {
  578 + padding: 0 8px;
  579 + }
  580 + }
252 } 581 }
253 582
254 .preview-content { 583 .preview-content {
255 flex: 1; 584 flex: 1;
256 overflow: auto; 585 overflow: auto;
  586 + padding: 10px;
257 587
258 .text-preview { 588 .text-preview {
259 padding: 15px; 589 padding: 15px;
260 white-space: pre-wrap; 590 white-space: pre-wrap;
261 font-family: monospace; 591 font-family: monospace;
  592 + line-height: 1.6;
  593 + background-color: #fafafa;
  594 + border-radius: 4px;
262 } 595 }
263 596
264 - .doc-preview { 597 + .pdf-preview {
265 height: 100%; 598 height: 100%;
  599 + overflow-y: auto;
266 600
267 - iframe {  
268 - width: 100%; 601 + .pdf-pages {
  602 + display: flex;
  603 + flex-direction: column;
  604 + gap: 20px;
  605 + padding: 10px;
  606 +
  607 + .pdf-page {
  608 + border: 1px solid #e8e8e8;
  609 + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  610 + background-color: white;
  611 + }
  612 + }
  613 + }
  614 +
  615 + .docx-preview {
269 height: 100%; 616 height: 100%;
270 - border: none; 617 + overflow-y: auto;
  618 + padding: 20px;
  619 + background-color: #fff;
  620 + border: 1px solid #f0f0f0;
  621 + border-radius: 4px;
271 } 622 }
  623 +
  624 + .unsupported-preview {
  625 + height: 100%;
  626 + padding: 20px;
  627 + display: flex;
  628 + align-items: center;
  629 + justify-content: center;
  630 + color: #ff4d4f;
  631 + font-weight: 500;
  632 + background-color: #fff2f0;
  633 + border-radius: 4px;
  634 + text-align: center;
272 } 635 }
273 } 636 }
274 } 637 }
275 638
  639 +/* 响应式设计 */
  640 +@media (max-width: 768px) {
  641 + .file-preview {
  642 + width: 100%;
  643 + min-width: auto;
  644 + max-height: 300px;
  645 +
  646 + .preview-header {
  647 + padding: 8px 12px;
  648 +
  649 + .preview-actions {
  650 + gap: 6px;
  651 +
  652 + .ant-btn-link {
  653 + padding: 0 4px;
  654 + }
  655 + }
  656 + }
  657 +
  658 + .preview-content {
  659 + padding: 8px;
  660 +
  661 + .text-preview,
  662 + .docx-preview {
  663 + padding: 10px;
  664 + }
  665 + }
  666 + }
  667 +}
276 .message { 668 .message {
277 margin-bottom: 15px; 669 margin-bottom: 15px;
278 display: flex; 670 display: flex;
@@ -299,15 +691,16 @@ const scrollToBottom = () => { @@ -299,15 +691,16 @@ const scrollToBottom = () => {
299 } 691 }
300 692
301 .message-content { 693 .message-content {
302 - max-width: 70%;  
303 - padding: 10px 15px; 694 + max-width: 80%;
  695 + padding: 12px 16px;
304 word-wrap: break-word; 696 word-wrap: break-word;
  697 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
305 } 698 }
306 699
307 .message-meta { 700 .message-meta {
308 font-size: 12px; 701 font-size: 12px;
309 color: #666; 702 color: #666;
310 - margin-top: 5px; 703 + margin-top: 8px;
311 text-align: right; 704 text-align: right;
312 705
313 a { 706 a {
@@ -323,17 +716,47 @@ const scrollToBottom = () => { @@ -323,17 +716,47 @@ const scrollToBottom = () => {
323 716
324 .chat-input { 717 .chat-input {
325 display: flex; 718 display: flex;
326 - gap: 10px; 719 + gap: 12px;
  720 + padding: 10px 0;
327 721
328 :deep(.ant-input) { 722 :deep(.ant-input) {
329 flex: 1; 723 flex: 1;
330 border-radius: 20px; 724 border-radius: 20px;
331 - padding: 10px 15px; 725 + padding: 12px 18px;
  726 + border: 1px solid #d9d9d9;
  727 + transition: all 0.3s;
  728 +
  729 + &:hover {
  730 + border-color: #40a9ff;
  731 + }
  732 +
  733 + &:focus {
  734 + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
  735 + }
332 } 736 }
333 737
334 .ant-btn { 738 .ant-btn {
335 border-radius: 20px; 739 border-radius: 20px;
336 - padding: 0 20px; 740 + padding: 0 24px;
  741 + height: 40px;
  742 + font-weight: 500;
  743 + }
  744 +}
  745 +
  746 +/* 响应式设计 */
  747 +@media (max-width: 768px) {
  748 + .main-content {
  749 + flex-direction: column;
  750 + }
  751 +
  752 + .file-preview {
  753 + width: 100%;
  754 + min-width: auto;
  755 + max-height: 300px;
  756 + }
  757 +
  758 + .message-content {
  759 + max-width: 90%;
337 } 760 }
338 } 761 }
339 </style> 762 </style>