| <script setup lang='ts'> |
| import type { DataTableColumns } from 'naive-ui' |
| import { computed, h, ref, watch } from 'vue' |
| import { NButton, NCard, NDataTable, NDivider, NInput, NList, NListItem, NModal, NPopconfirm, NSpace, NTabPane, NTabs, NThing, useMessage } from 'naive-ui' |
| import PromptRecommend from '../../../assets/recommend.json' |
| import { SvgIcon } from '..' |
| import { usePromptStore } from '@/store' |
| import { useBasicLayout } from '@/hooks/useBasicLayout' |
| import { t } from '@/locales' |
| |
| interface DataProps { |
| renderKey: string |
| renderValue: string |
| key: string |
| value: string |
| } |
| |
| interface Props { |
| visible: boolean |
| } |
| |
| interface Emit { |
| (e: 'update:visible', visible: boolean): void |
| } |
| |
| const props = defineProps<Props>() |
| |
| const emit = defineEmits<Emit>() |
| |
| const message = useMessage() |
| |
| const show = computed({ |
| get: () => props.visible, |
| set: (visible: boolean) => emit('update:visible', visible), |
| }) |
| |
| const showModal = ref(false) |
| |
| const importLoading = ref(false) |
| const exportLoading = ref(false) |
| |
| const searchValue = ref<string>('') |
| |
| |
| const { isMobile } = useBasicLayout() |
| |
| const promptStore = usePromptStore() |
| |
| |
| const promptRecommendList = PromptRecommend |
| const promptList = ref<any>(promptStore.promptList) |
| |
| |
| const tempPromptKey = ref('') |
| const tempPromptValue = ref('') |
| |
| |
| const modalMode = ref('') |
| |
| |
| const tempModifiedItem = ref<any>({}) |
| |
| |
| const changeShowModal = (mode: 'add' | 'modify' | 'local_import', selected = { key: '', value: '' }) => { |
| if (mode === 'add') { |
| tempPromptKey.value = '' |
| tempPromptValue.value = '' |
| } |
| else if (mode === 'modify') { |
| tempModifiedItem.value = { ...selected } |
| tempPromptKey.value = selected.key |
| tempPromptValue.value = selected.value |
| } |
| else if (mode === 'local_import') { |
| tempPromptKey.value = 'local_import' |
| tempPromptValue.value = '' |
| } |
| showModal.value = !showModal.value |
| modalMode.value = mode |
| } |
| |
| |
| const downloadURL = ref('') |
| const downloadDisabled = computed(() => downloadURL.value.trim().length < 1) |
| const setDownloadURL = (url: string) => { |
| downloadURL.value = url |
| } |
| |
| |
| const inputStatus = computed (() => tempPromptKey.value.trim().length < 1 || tempPromptValue.value.trim().length < 1) |
| |
| |
| const addPromptTemplate = () => { |
| for (const i of promptList.value) { |
| if (i.key === tempPromptKey.value) { |
| message.error(t('store.addRepeatTitleTips')) |
| return |
| } |
| if (i.value === tempPromptValue.value) { |
| message.error(t('store.addRepeatContentTips', { msg: tempPromptKey.value })) |
| return |
| } |
| } |
| promptList.value.unshift({ key: tempPromptKey.value, value: tempPromptValue.value } as never) |
| message.success(t('common.addSuccess')) |
| changeShowModal('add') |
| } |
| |
| const modifyPromptTemplate = () => { |
| let index = 0 |
| |
| // 通过临时索引把待修改项摘出来 |
| for (const i of promptList.value) { |
| if (i.key === tempModifiedItem.value.key && i.value === tempModifiedItem.value.value) |
| break |
| index = index + 1 |
| } |
| |
| const tempList = promptList.value.filter((_: any, i: number) => i !== index) |
| |
| |
| for (const i of tempList) { |
| if (i.key === tempPromptKey.value) { |
| message.error(t('store.editRepeatTitleTips')) |
| return |
| } |
| if (i.value === tempPromptValue.value) { |
| message.error(t('store.editRepeatContentTips', { msg: i.key })) |
| return |
| } |
| } |
| |
| promptList.value = [{ key: tempPromptKey.value, value: tempPromptValue.value }, ...tempList] as never |
| message.success(t('common.editSuccess')) |
| changeShowModal('modify') |
| } |
| |
| const deletePromptTemplate = (row: { key: string; value: string }) => { |
| promptList.value = [ |
| ...promptList.value.filter((item: { key: string; value: string }) => item.key !== row.key), |
| ] as never |
| message.success(t('common.deleteSuccess')) |
| } |
| |
| const clearPromptTemplate = () => { |
| promptList.value = [] |
| message.success(t('common.clearSuccess')) |
| } |
| |
| const importPromptTemplate = (from = 'online') => { |
| try { |
| const jsonData = JSON.parse(tempPromptValue.value) |
| let key = '' |
| let value = '' |
| // 可以扩展加入更多模板字典的key |
| if ('key' in jsonData[0]) { |
| key = 'key' |
| value = 'value' |
| } |
| else if ('act' in jsonData[0]) { |
| key = 'act' |
| value = 'prompt' |
| } |
| else { |
| // 不支持的字典的key防止导入 以免破坏prompt商店打开 |
| message.warning('prompt key not supported.') |
| throw new Error('prompt key not supported.') |
| } |
| |
| for (const i of jsonData) { |
| if (!(key in i) || !(value in i)) |
| throw new Error(t('store.importError')) |
| let safe = true |
| for (const j of promptList.value) { |
| if (j.key === i[key]) { |
| message.warning(t('store.importRepeatTitle', { msg: i[key] })) |
| safe = false |
| break |
| } |
| if (j.value === i[value]) { |
| message.warning(t('store.importRepeatContent', { msg: i[key] })) |
| safe = false |
| break |
| } |
| } |
| if (safe) |
| promptList.value.unshift({ key: i[key], value: i[value] } as never) |
| } |
| message.success(t('common.importSuccess')) |
| } |
| catch { |
| message.error('JSON 格式错误,请检查 JSON 格式') |
| } |
| if (from === 'local') |
| showModal.value = !showModal.value |
| } |
| |
| |
| const exportPromptTemplate = () => { |
| exportLoading.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 = 'ChatGPTPromptTemplate.json' |
| link.click() |
| URL.revokeObjectURL(url) |
| exportLoading.value = false |
| } |
| |
| |
| const downloadPromptTemplate = async () => { |
| try { |
| importLoading.value = true |
| const response = await fetch(downloadURL.value) |
| const jsonData = await response.json() |
| if ('key' in jsonData[0] && 'value' in jsonData[0]) |
| tempPromptValue.value = JSON.stringify(jsonData) |
| if ('act' in jsonData[0] && 'prompt' in jsonData[0]) { |
| const newJsonData = jsonData.map((item: { act: string; prompt: string }) => { |
| return { |
| key: item.act, |
| value: item.prompt, |
| } |
| }) |
| tempPromptValue.value = JSON.stringify(newJsonData) |
| } |
| importPromptTemplate() |
| downloadURL.value = '' |
| } |
| catch { |
| message.error(t('store.downloadError')) |
| downloadURL.value = '' |
| } |
| finally { |
| importLoading.value = false |
| } |
| } |
| |
| |
| const renderTemplate = () => { |
| const [keyLimit, valueLimit] = isMobile.value ? [10, 30] : [15, 50] |
| |
| return promptList.value.map((item: { key: string; value: string }) => { |
| return { |
| renderKey: item.key.length <= keyLimit ? item.key : `${item.key.substring(0, keyLimit)}...`, |
| renderValue: item.value.length <= valueLimit ? item.value : `${item.value.substring(0, valueLimit)}...`, |
| key: item.key, |
| value: item.value, |
| } |
| }) |
| } |
| |
| const pagination = computed(() => { |
| const [pageSize, pageSlot] = isMobile.value ? [6, 5] : [7, 15] |
| return { |
| pageSize, pageSlot, |
| } |
| }) |
| |
| |
| const createColumns = (): DataTableColumns<DataProps> => { |
| return [ |
| { |
| title: t('store.title'), |
| key: 'renderKey', |
| }, |
| { |
| title: t('store.description'), |
| key: 'renderValue', |
| }, |
| { |
| title: t('common.action'), |
| key: 'actions', |
| width: 100, |
| align: 'center', |
| render(row) { |
| return h('div', { class: 'flex items-center flex-col gap-2' }, { |
| default: () => [h( |
| NButton, |
| { |
| tertiary: true, |
| size: 'small', |
| type: 'info', |
| onClick: () => changeShowModal('modify', row), |
| }, |
| { default: () => t('common.edit') }, |
| ), |
| h( |
| NButton, |
| { |
| tertiary: true, |
| size: 'small', |
| type: 'error', |
| onClick: () => deletePromptTemplate(row), |
| }, |
| { default: () => t('common.delete') }, |
| ), |
| ], |
| }) |
| }, |
| }, |
| ] |
| } |
| |
| const columns = createColumns() |
| |
| watch( |
| () => promptList, |
| () => { |
| promptStore.updatePromptList(promptList.value) |
| }, |
| { deep: true }, |
| ) |
| |
| const dataSource = computed(() => { |
| const data = renderTemplate() |
| const value = searchValue.value |
| if (value && value !== '') { |
| return data.filter((item: DataProps) => { |
| return item.renderKey.includes(value) || item.renderValue.includes(value) |
| }) |
| } |
| return data |
| }) |
| </script> |
| |
| <template> |
| <NModal v-model:show="show" style="width: 90%; max-width: 900px;" preset="card"> |
| <div class="space-y-4"> |
| <NTabs type="segment"> |
| <NTabPane name="local" :tab="$t('store.local')"> |
| <div |
| class="flex gap-3 mb-4" |
| :class="[isMobile ? 'flex-col' : 'flex-row justify-between']" |
| > |
| <div class="flex items-center space-x-4"> |
| <NButton |
| type="primary" |
| size="small" |
| @click="changeShowModal('add')" |
| > |
| {{ $t('common.add') }} |
| </NButton> |
| <NButton |
| size="small" |
| @click="changeShowModal('local_import')" |
| > |
| {{ $t('common.import') }} |
| </NButton> |
| <NButton |
| size="small" |
| :loading="exportLoading" |
| @click="exportPromptTemplate()" |
| > |
| {{ $t('common.export') }} |
| </NButton> |
| <NPopconfirm @positive-click="clearPromptTemplate"> |
| <template #trigger> |
| <NButton size="small"> |
| {{ $t('common.clear') }} |
| </NButton> |
| </template> |
| {{ $t('store.clearStoreConfirm') }} |
| </NPopconfirm> |
| </div> |
| <div class="flex items-center"> |
| <NInput v-model:value="searchValue" style="width: 100%" /> |
| </div> |
| </div> |
| <NDataTable |
| v-if="!isMobile" |
| :max-height="400" |
| :columns="columns" |
| :data="dataSource" |
| :pagination="pagination" |
| :bordered="false" |
| /> |
| <NList v-if="isMobile" style="max-height: 400px; overflow-y: auto;"> |
| <NListItem v-for="(item, index) of dataSource" :key="index"> |
| <NThing :title="item.renderKey" :description="item.renderValue" /> |
| <template #suffix> |
| <div class="flex flex-col items-center gap-2"> |
| <NButton tertiary size="small" type="info" @click="changeShowModal('modify', item)"> |
| {{ t('common.edit') }} |
| </NButton> |
| <NButton tertiary size="small" type="error" @click="deletePromptTemplate(item)"> |
| {{ t('common.delete') }} |
| </NButton> |
| </div> |
| </template> |
| </NListItem> |
| </NList> |
| </NTabPane> |
| <NTabPane name="download" :tab="$t('store.online')"> |
| <p class="mb-4"> |
| {{ $t('store.onlineImportWarning') }} |
| </p> |
| <div class="flex items-center gap-4"> |
| <NInput v-model:value="downloadURL" placeholder="" /> |
| <NButton |
| strong |
| secondary |
| :disabled="downloadDisabled" |
| :loading="importLoading" |
| @click="downloadPromptTemplate()" |
| > |
| {{ $t('common.download') }} |
| </NButton> |
| </div> |
| <NDivider /> |
| <div class="max-h-[360px] overflow-y-auto space-y-4"> |
| <NCard |
| v-for="info in promptRecommendList" |
| :key="info.key" :title="info.key" |
| :bordered="true" |
| embedded |
| > |
| <p |
| class="overflow-hidden text-ellipsis whitespace-nowrap" |
| :title="info.desc" |
| > |
| {{ info.desc }} |
| </p> |
| <template #footer> |
| <div class="flex items-center justify-end space-x-4"> |
| <NButton text> |
| <a |
| :href="info.url" |
| target="_blank" |
| > |
| <SvgIcon class="text-xl" icon="ri:link" /> |
| </a> |
| </NButton> |
| <NButton text @click="setDownloadURL(info.downloadUrl) "> |
| <SvgIcon class="text-xl" icon="ri:add-fill" /> |
| </NButton> |
| </div> |
| </template> |
| </NCard> |
| </div> |
| </NTabPane> |
| </NTabs> |
| </div> |
| </NModal> |
| |
| <NModal v-model:show="showModal" style="width: 90%; max-width: 600px;" preset="card"> |
| <NSpace v-if="modalMode === 'add' || modalMode === 'modify'" vertical> |
| {{ t('store.title') }} |
| <NInput v-model:value="tempPromptKey" /> |
| {{ t('store.description') }} |
| <NInput v-model:value="tempPromptValue" type="textarea" /> |
| <NButton |
| block |
| type="primary" |
| :disabled="inputStatus" |
| @click="() => { modalMode === 'add' ? addPromptTemplate() : modifyPromptTemplate() }" |
| > |
| {{ t('common.confirm') }} |
| </NButton> |
| </NSpace> |
| <NSpace v-if="modalMode === 'local_import'" vertical> |
| <NInput |
| v-model:value="tempPromptValue" |
| :placeholder="t('store.importPlaceholder')" |
| :autosize="{ minRows: 3, maxRows: 15 }" |
| type="textarea" |
| /> |
| <NButton |
| block |
| type="primary" |
| :disabled="inputStatus" |
| @click="() => { importPromptTemplate('local') }" |
| > |
| {{ t('common.import') }} |
| </NButton> |
| </NSpace> |
| </NModal> |
| </template> |
| |