<script setup lang="ts"> import { ref } from 'vue'; import { NModal, NList, NListItem, NButton, useMessage, NSpace, NInput, NUpload, NVirtualList, type UploadFileInfo, NEmpty } from 'naive-ui'; import { usePromptStore, type IPrompt, type IPromptDownloadConfig } from '@/stores/modules/prompt'; import { storeToRefs } from 'pinia'; import ChatPromptItem from './ChatPromptItem.vue'; const messgae = useMessage(); const promptStore = usePromptStore(); const { promptDownloadConfig, isShowPromptSotre, promptList, keyword, searchPromptList, optPromptConfig } = storeToRefs(promptStore); const isShowDownloadPop = ref(false); const isImporting = ref(false); const isExporting = ref(false); const showAddPromptPop = () => { optPromptConfig.value.isShow = true; optPromptConfig.value.type = 'add'; optPromptConfig.value.title = '添加提示词'; optPromptConfig.value.newPrompt = { act: '', prompt: '', }; }; const savePrompt = () => { const { type, tmpPrompt, newPrompt } = optPromptConfig.value; if (!newPrompt.act) { return messgae.error('提示词标题不能为空'); } if (!newPrompt.prompt) { return messgae.error('提示词描述不能为空'); } if (type === 'add') { promptList.value = [newPrompt, ...promptList.value]; messgae.success('添加提示词成功'); } else if (type === 'edit') { if (newPrompt.act === tmpPrompt?.act && newPrompt.prompt === tmpPrompt?.prompt) { messgae.warning('提示词未变更'); optPromptConfig.value.isShow = false; return; } const rawIndex = promptList.value.findIndex((x) => x.act === tmpPrompt?.act && x.prompt === tmpPrompt?.prompt); if (rawIndex > -1) { promptList.value[rawIndex] = newPrompt; messgae.success('编辑提示词成功'); } else { messgae.error('编辑提示词出错'); } } optPromptConfig.value.isShow = false; }; const readFile = (file: File): Promise<string> => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function (ev) { resolve(ev.target?.result as string); }; reader.onerror = reject; reader.readAsText(file); }); }; const importPrompt = async (options: { file: UploadFileInfo; fileList: Array<UploadFileInfo>; event?: Event }) => { // console.log(options.file); if (options.file.file) { isImporting.value = true; const fileText = await readFile(options.file.file); const promptData = JSON.parse(fileText); const result = promptStore.addPrompt(promptData); if (result.result) { messgae.info(`上传文件含 ${promptData.length} 条数据`); messgae.success(`成功导入 ${result.data?.successCount} 条有效数据`); } else { messgae.error(result.msg || '提示词格式有误'); } isImporting.value = false; } else { messgae.error('上传文件有误'); } }; const exportPrompt = () => { if (promptList.value.length === 0) { return messgae.error('暂无可导出的提示词数据'); } isExporting.value = true; const jsonDataStr = JSON.stringify(promptList.value); const blob = new Blob([jsonDataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'BingAIPrompts.json'; link.click(); URL.revokeObjectURL(url); messgae.success('导出提示词库成功'); isExporting.value = false; }; const clearPrompt = () => { promptList.value = []; messgae.success('清空提示词库成功'); }; const downloadPrompt = async (config: IPromptDownloadConfig) => { if (!config.url) { return messgae.error('请先输入下载链接'); } config.isDownloading = true; let jsonData: Array<IPrompt>; if (config.url.endsWith('.json')) { jsonData = await fetch(config.url).then((res) => res.json()); } else if (config.url.endsWith('.csv')) { const csvData = await fetch(config.url).then((res) => res.text()); console.log(csvData); jsonData = csvData .split('\n') .filter((x) => x) .map((x) => { const arr = x.split('","'); return { act: arr[0].slice(1), prompt: arr[1]?.slice(1), }; }); jsonData.shift(); } else { config.isDownloading = false; return messgae.error('暂不支持下载此后缀的提示词'); } config.isDownloading = false; const result = promptStore.addPrompt(jsonData); if (result.result) { messgae.info(`下载文件含 ${jsonData.length} 条数据`); messgae.success(`成功导入 ${result.data?.successCount} 条有效数据`); } else { messgae.error(result.msg || '提示词格式有误'); } }; </script> <template> <NModal class="w-11/12 xl:w-[900px]" v-model:show="isShowPromptSotre" preset="card" title="提示词库"> <div class="flex justify-start flex-wrap gap-2 px-5 pb-2"> <NInput class="basis-full xl:basis-0 xl:min-w-[300px]" placeholder="搜索提示词" v-model:value="keyword" :clearable="true"></NInput> <NButton secondary type="info" @click="isShowDownloadPop = true">下载</NButton> <NButton secondary type="info" @click="showAddPromptPop">添加</NButton> <NUpload class="w-[56px] xl:w-auto" accept=".json" :default-upload="false" :show-file-list="false" @change="importPrompt"> <NButton secondary type="success" :loading="isImporting">导入</NButton> </NUpload> <!-- <NButton secondary type="success">导入</NButton> --> <NButton secondary type="success" @click="exportPrompt" :loading="isExporting">导出</NButton> <NButton secondary type="error" @click="clearPrompt">清空</NButton> </div> <NVirtualList v-if="searchPromptList.length > 0" class="h-[40vh] xl:h-[60vh] overflow-y-auto" :item-size="131" item-resizable :items="searchPromptList" > <template #default="{ item, index }"> <ChatPromptItem :index="index" :source="item" /> </template> </NVirtualList> <NEmpty v-else class="h-[40vh] xl:h-[60vh] flex justify-center items-center" description="暂无数据"> <template #extra> <NButton secondary type="info" @click="isShowDownloadPop = true">下载提示词</NButton> </template> </NEmpty> </NModal> <NModal class="w-11/12 xl:w-[600px]" v-model:show="optPromptConfig.isShow" preset="card" :title="optPromptConfig.title"> <NSpace vertical> 标题 <NInput placeholder="请输入标题" v-model:value="optPromptConfig.newPrompt.act"></NInput> 描述 <NInput placeholder="请输入描述" type="textarea" v-model:value="optPromptConfig.newPrompt.prompt"></NInput> <NButton block secondary type="info" @click="savePrompt">保存</NButton> </NSpace> </NModal> <NModal class="w-11/12 xl:w-[600px]" v-model:show="isShowDownloadPop" preset="card" title="下载提示词"> <NList class="overflow-y-auto rounded-lg" hoverable clickable> <NListItem v-for="(config, index) in promptDownloadConfig" :key="index"> <a v-if="config.type === 1" class="no-underline text-blue-500" :href="config.url" target="_blank" rel="noopener noreferrer">{{ config.name }}</a> <NInput v-else-if="config.type === 2" placeholder="请输入下载链接,支持 json 及 csv " v-model:value="config.url"></NInput> <template #suffix> <div class="flex justify-center gap-5"> <a class="no-underline" v-if="config.type === 1" :href="config.refer" target="_blank" rel="noopener noreferrer"> <NButton secondary>来源</NButton> </a> <NButton secondary type="info" @click="downloadPrompt(config)" :loading="config.isDownloading">下载</NButton> </div> </template> </NListItem> </NList> </NModal> </template>