2026-04-22 07:28:06 +00:00
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
|
|
|
|
import {
|
|
|
|
|
|
App,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Divider,
|
|
|
|
|
|
Empty,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Popconfirm,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Switch,
|
2026-06-30 05:37:33 +00:00
|
|
|
|
Table,
|
2026-04-22 07:28:06 +00:00
|
|
|
|
Tag,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
} from 'antd';
|
2026-06-30 05:37:33 +00:00
|
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
2026-05-09 02:17:46 +00:00
|
|
|
|
import PageContainer from "@/components/shared/PageContainer";
|
2026-06-30 05:37:33 +00:00
|
|
|
|
import DataListPanel from "@/components/shared/DataListPanel";
|
2026-07-01 03:01:38 +00:00
|
|
|
|
import FormDrawer from "@/components/shared/FormDrawer";
|
2026-06-30 05:37:33 +00:00
|
|
|
|
import SectionCard from "@/components/shared/SectionCard";
|
|
|
|
|
|
import { CopyOutlined, DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons';
|
2026-03-02 11:59:47 +00:00
|
|
|
|
import ReactMarkdown from 'react-markdown';
|
2026-04-22 07:28:06 +00:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-03-02 11:59:47 +00:00
|
|
|
|
import { useDict } from '../../hooks/useDict';
|
2026-04-22 07:28:06 +00:00
|
|
|
|
import {
|
|
|
|
|
|
deletePromptTemplate,
|
|
|
|
|
|
getPromptDetail,
|
|
|
|
|
|
getPromptPage,
|
|
|
|
|
|
savePromptTemplate,
|
2026-03-02 11:59:47 +00:00
|
|
|
|
updatePromptStatus,
|
2026-04-22 07:28:06 +00:00
|
|
|
|
updatePromptTemplate,
|
|
|
|
|
|
type PromptTemplateVO,
|
2026-03-02 11:59:47 +00:00
|
|
|
|
} from '../../api/business/prompt';
|
2026-04-22 07:28:06 +00:00
|
|
|
|
import { getHotWordGroupOptions, type HotWordGroupVO } from '../../api/business/hotwordGroup';
|
2026-04-15 09:47:31 +00:00
|
|
|
|
import AppPagination from '../../components/shared/AppPagination';
|
2026-06-30 05:37:33 +00:00
|
|
|
|
import './PromptTemplates.css';
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
|
|
const { Option } = Select;
|
2026-07-01 03:01:38 +00:00
|
|
|
|
const { Text } = Typography;
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
const normalizePromptTags = (tags: unknown) => {
|
|
|
|
|
|
if (Array.isArray(tags)) {
|
|
|
|
|
|
return tags.map((tag) => String(tag).trim()).filter(Boolean);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof tags === 'string') {
|
|
|
|
|
|
return tags.split(',').map((tag) => tag.trim()).filter(Boolean);
|
|
|
|
|
|
}
|
|
|
|
|
|
return [];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
|
const PromptTemplates: React.FC = () => {
|
2026-06-30 05:37:33 +00:00
|
|
|
|
const { message, modal } = App.useApp();
|
2026-03-04 12:49:32 +00:00
|
|
|
|
const { t } = useTranslation();
|
2026-03-02 11:59:47 +00:00
|
|
|
|
const { items: categories, loading: dictLoading } = useDict('biz_prompt_category');
|
|
|
|
|
|
const { items: dictTags } = useDict('biz_prompt_tag');
|
2026-03-04 07:19:40 +00:00
|
|
|
|
const { items: promptLevels } = useDict('biz_prompt_level');
|
2026-04-22 07:28:06 +00:00
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [data, setData] = useState<PromptTemplateVO[]>([]);
|
2026-03-04 12:49:32 +00:00
|
|
|
|
const [total, setTotal] = useState(0);
|
|
|
|
|
|
const [current, setCurrent] = useState(1);
|
2026-06-29 02:17:30 +00:00
|
|
|
|
const [pageSize, setPageSize] = useState(8);
|
2026-06-30 05:37:33 +00:00
|
|
|
|
const [queryDraft, setQueryDraft] = useState<{ name?: string; category?: string }>({});
|
|
|
|
|
|
const [query, setQuery] = useState<{ name?: string; category?: string }>({});
|
2026-04-22 07:28:06 +00:00
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
|
|
|
|
|
const [editingId, setEditingId] = useState<number | null>(null);
|
|
|
|
|
|
const [submitLoading, setSubmitLoading] = useState(false);
|
|
|
|
|
|
const [previewContent, setPreviewContent] = useState('');
|
2026-04-22 07:28:06 +00:00
|
|
|
|
const [groupOptions, setGroupOptions] = useState<HotWordGroupVO[]>([]);
|
2026-06-30 05:37:33 +00:00
|
|
|
|
const [templateLevel, setTemplateLevel] = useState<number | undefined>();
|
|
|
|
|
|
const [selectedHotWordGroupId, setSelectedHotWordGroupId] = useState<number | undefined>();
|
|
|
|
|
|
const [drawerInitialValues, setDrawerInitialValues] = useState<Record<string, any>>({});
|
|
|
|
|
|
const [drawerFormKey, setDrawerFormKey] = useState(0);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
|
|
const userProfile = React.useMemo(() => {
|
|
|
|
|
|
const profileStr = sessionStorage.getItem("userProfile");
|
|
|
|
|
|
return profileStr ? JSON.parse(profileStr) : {};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-04 07:19:40 +00:00
|
|
|
|
const activeTenantId = React.useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
const isPlatformAdmin = userProfile.isPlatformAdmin === true;
|
2026-03-04 07:19:40 +00:00
|
|
|
|
const isTenantAdmin = userProfile.isTenantAdmin === true;
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-22 07:28:06 +00:00
|
|
|
|
void fetchData();
|
2026-06-30 05:37:33 +00:00
|
|
|
|
}, [current, pageSize, query]);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
2026-04-22 07:28:06 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
void loadGroupOptions();
|
|
|
|
|
|
}, [isPlatformAdmin, templateLevel, activeTenantId]);
|
|
|
|
|
|
|
|
|
|
|
|
const loadGroupOptions = async () => {
|
|
|
|
|
|
const targetTenantId = isPlatformAdmin && Number(templateLevel) === 1 ? 0 : undefined;
|
|
|
|
|
|
const res = await getHotWordGroupOptions(targetTenantId ?? (isPlatformAdmin && activeTenantId === 0 ? 0 : undefined));
|
|
|
|
|
|
setGroupOptions(res.data?.data || []);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-04-22 07:28:06 +00:00
|
|
|
|
const res = await getPromptPage({
|
|
|
|
|
|
current,
|
|
|
|
|
|
size: pageSize,
|
2026-06-30 05:37:33 +00:00
|
|
|
|
name: query.name,
|
|
|
|
|
|
category: query.category,
|
2026-03-02 11:59:47 +00:00
|
|
|
|
});
|
2026-04-22 07:28:06 +00:00
|
|
|
|
if (res.data?.data) {
|
2026-06-30 05:37:33 +00:00
|
|
|
|
setData(res.data.data.records || []);
|
|
|
|
|
|
setTotal(res.data.data.total || 0);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleStatusChange = async (id: number, checked: boolean) => {
|
2026-04-22 07:28:06 +00:00
|
|
|
|
await updatePromptStatus(id, checked ? 1 : 0);
|
|
|
|
|
|
message.success(checked ? '模板已启用' : '模板已停用');
|
|
|
|
|
|
await fetchData();
|
2026-03-02 11:59:47 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenDrawer = (record?: PromptTemplateVO, isClone = false) => {
|
|
|
|
|
|
if (record) {
|
|
|
|
|
|
if (isClone) {
|
|
|
|
|
|
setEditingId(null);
|
2026-06-30 05:37:33 +00:00
|
|
|
|
setTemplateLevel(0);
|
|
|
|
|
|
setSelectedHotWordGroupId(record.hotWordGroupId);
|
|
|
|
|
|
setDrawerInitialValues({
|
2026-03-02 11:59:47 +00:00
|
|
|
|
...record,
|
2026-06-30 05:37:33 +00:00
|
|
|
|
tags: normalizePromptTags(record.tags),
|
2026-03-02 11:59:47 +00:00
|
|
|
|
templateName: `${record.templateName} (副本)`,
|
2026-04-22 07:28:06 +00:00
|
|
|
|
isSystem: 0,
|
2026-03-04 07:19:40 +00:00
|
|
|
|
id: undefined,
|
2026-04-22 07:28:06 +00:00
|
|
|
|
tenantId: undefined,
|
2026-03-02 11:59:47 +00:00
|
|
|
|
});
|
|
|
|
|
|
setPreviewContent(record.promptContent);
|
|
|
|
|
|
} else {
|
2026-03-09 06:44:24 +00:00
|
|
|
|
const isPlatformLevel = Number(record.tenantId) === 0 && Number(record.isSystem) === 1;
|
2026-03-04 07:19:40 +00:00
|
|
|
|
const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1;
|
2026-04-22 07:28:06 +00:00
|
|
|
|
|
2026-03-04 07:19:40 +00:00
|
|
|
|
let canEdit = false;
|
2026-03-09 06:44:24 +00:00
|
|
|
|
if (Number(record.isSystem) === 0) {
|
|
|
|
|
|
canEdit = Number(record.creatorId) === currentUserId;
|
|
|
|
|
|
} else if (isPlatformAdmin) {
|
2026-03-04 07:19:40 +00:00
|
|
|
|
canEdit = isPlatformLevel;
|
|
|
|
|
|
} else if (isTenantAdmin) {
|
|
|
|
|
|
canEdit = Number(record.tenantId) === activeTenantId;
|
2026-03-02 11:59:47 +00:00
|
|
|
|
}
|
2026-03-04 07:19:40 +00:00
|
|
|
|
|
|
|
|
|
|
if (!canEdit) {
|
|
|
|
|
|
message.warning('您无权修改此层级的模板');
|
2026-03-02 11:59:47 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-04 07:19:40 +00:00
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
|
setEditingId(record.id);
|
2026-06-30 05:37:33 +00:00
|
|
|
|
setTemplateLevel(Number(record.isSystem));
|
|
|
|
|
|
setSelectedHotWordGroupId(record.hotWordGroupId);
|
|
|
|
|
|
setDrawerInitialValues({
|
|
|
|
|
|
...record,
|
|
|
|
|
|
tags: normalizePromptTags(record.tags),
|
|
|
|
|
|
});
|
2026-03-02 11:59:47 +00:00
|
|
|
|
setPreviewContent(record.promptContent);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-06-30 05:37:33 +00:00
|
|
|
|
const defaultLevel = (isTenantAdmin || isPlatformAdmin) ? 1 : 0;
|
2026-03-02 11:59:47 +00:00
|
|
|
|
setEditingId(null);
|
2026-06-30 05:37:33 +00:00
|
|
|
|
setDrawerInitialValues({
|
2026-04-22 07:28:06 +00:00
|
|
|
|
status: 1,
|
2026-06-30 05:37:33 +00:00
|
|
|
|
isSystem: defaultLevel,
|
2026-03-04 07:19:40 +00:00
|
|
|
|
});
|
2026-06-30 05:37:33 +00:00
|
|
|
|
setTemplateLevel(defaultLevel);
|
|
|
|
|
|
setSelectedHotWordGroupId(undefined);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
setPreviewContent('');
|
|
|
|
|
|
}
|
2026-06-30 05:37:33 +00:00
|
|
|
|
setDrawerFormKey((key) => key + 1);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
setDrawerVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const showDetail = (record: PromptTemplateVO) => {
|
2026-04-22 07:28:06 +00:00
|
|
|
|
void (async () => {
|
|
|
|
|
|
const detailRes = await getPromptDetail(record.id);
|
|
|
|
|
|
const detail = detailRes.data?.data || record;
|
2026-06-30 05:37:33 +00:00
|
|
|
|
modal.info({
|
2026-03-02 11:59:47 +00:00
|
|
|
|
title: record.templateName,
|
|
|
|
|
|
width: 800,
|
|
|
|
|
|
icon: null,
|
|
|
|
|
|
content: (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="prompt-template-detail">
|
2026-04-22 07:28:06 +00:00
|
|
|
|
{detail.description ? (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="prompt-template-detail__description">
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Text type="secondary">{detail.description}</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="prompt-template-detail__section">
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Space wrap>
|
|
|
|
|
|
{detail.hotWordGroupName ? <Tag color="blue">热词组:{detail.hotWordGroupName}</Tag> : <Tag>未绑定热词组</Tag>}
|
2026-06-30 05:37:33 +00:00
|
|
|
|
{normalizePromptTags(detail.tags).map((tag) => {
|
2026-04-22 07:28:06 +00:00
|
|
|
|
const dictItem = dictTags.find((item) => item.itemValue === tag);
|
|
|
|
|
|
return <Tag key={tag}>{dictItem ? dictItem.itemLabel : tag}</Tag>;
|
|
|
|
|
|
})}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{detail.hotWords && detail.hotWords.length > 0 ? (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="prompt-template-detail__section">
|
|
|
|
|
|
<div className="prompt-template-detail__section-title">绑定热词</div>
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Space wrap>
|
|
|
|
|
|
{detail.hotWords.map((word) => <Tag key={word}>{word}</Tag>)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : detail.hotWordGroupId ? (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="prompt-template-detail__section">
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Text type="secondary">该热词组当前没有热词</Text>
|
2026-04-15 09:55:57 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<ReactMarkdown>{detail.promptContent}</ReactMarkdown>
|
2026-03-02 11:59:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
okText: '关闭',
|
2026-04-22 07:28:06 +00:00
|
|
|
|
maskClosable: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
})();
|
2026-03-02 11:59:47 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
const handleSubmit = async (values: any) => {
|
2026-03-02 11:59:47 +00:00
|
|
|
|
try {
|
|
|
|
|
|
setSubmitLoading(true);
|
2026-03-04 07:19:40 +00:00
|
|
|
|
if (!editingId && isPlatformAdmin && values.isSystem === 1) {
|
|
|
|
|
|
values.tenantId = 0;
|
|
|
|
|
|
}
|
2026-03-02 11:59:47 +00:00
|
|
|
|
if (editingId) {
|
|
|
|
|
|
await updatePromptTemplate({ ...values, id: editingId });
|
|
|
|
|
|
message.success('更新成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await savePromptTemplate(values);
|
|
|
|
|
|
message.success('模板已创建');
|
|
|
|
|
|
}
|
|
|
|
|
|
setDrawerVisible(false);
|
2026-04-22 07:28:06 +00:00
|
|
|
|
await fetchData();
|
2026-03-02 11:59:47 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
const getTemplateLevelMeta = (item: PromptTemplateVO) => {
|
2026-03-02 11:59:47 +00:00
|
|
|
|
const isSystem = item.isSystem === 1;
|
2026-03-04 12:49:32 +00:00
|
|
|
|
const isPlatformLevel = Number(item.tenantId) === 0 && isSystem;
|
2026-03-04 07:19:40 +00:00
|
|
|
|
const isTenantLevel = Number(item.tenantId) > 0 && isSystem;
|
2026-06-30 05:37:33 +00:00
|
|
|
|
if (isPlatformLevel) {
|
|
|
|
|
|
return { label: '平台级', color: 'gold' as const };
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isTenantLevel) {
|
|
|
|
|
|
return { label: '租户级', color: 'blue' as const };
|
|
|
|
|
|
}
|
|
|
|
|
|
return { label: '个人级', color: 'cyan' as const };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSearch = () => {
|
|
|
|
|
|
setCurrent(1);
|
|
|
|
|
|
setQuery(queryDraft);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleResetSearch = () => {
|
|
|
|
|
|
setQueryDraft({});
|
|
|
|
|
|
setCurrent(1);
|
|
|
|
|
|
setQuery({});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const canManageTemplate = (item: PromptTemplateVO) => {
|
|
|
|
|
|
const isSystem = item.isSystem === 1;
|
|
|
|
|
|
const isPlatformLevel = Number(item.tenantId) === 0 && isSystem;
|
2026-03-04 12:49:32 +00:00
|
|
|
|
const isPersonalLevel = !isSystem;
|
2026-03-04 07:19:40 +00:00
|
|
|
|
const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1;
|
|
|
|
|
|
|
2026-03-09 06:44:24 +00:00
|
|
|
|
if (isPersonalLevel) {
|
2026-06-30 05:37:33 +00:00
|
|
|
|
return Number(item.creatorId) === currentUserId;
|
2026-03-04 07:19:40 +00:00
|
|
|
|
}
|
2026-06-30 05:37:33 +00:00
|
|
|
|
if (isPlatformAdmin) {
|
|
|
|
|
|
return isPlatformLevel;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isTenantAdmin) {
|
|
|
|
|
|
return Number(item.tenantId) === activeTenantId;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
2026-03-04 07:19:40 +00:00
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
const tableColumns: ColumnsType<PromptTemplateVO> = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '模板名称',
|
|
|
|
|
|
dataIndex: 'templateName',
|
|
|
|
|
|
width: 280,
|
|
|
|
|
|
render: (_: unknown, item: PromptTemplateVO) => (
|
|
|
|
|
|
<div className="prompt-template-name-cell">
|
|
|
|
|
|
<Text strong ellipsis={{ tooltip: item.templateName }}>{item.templateName}</Text>
|
2026-04-15 09:55:57 +00:00
|
|
|
|
{item.description ? (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Text type="secondary" className="prompt-template-description" ellipsis={{ tooltip: item.description }}>
|
2026-04-15 09:55:57 +00:00
|
|
|
|
{item.description}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
) : null}
|
2026-03-09 06:44:24 +00:00
|
|
|
|
</div>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '分类',
|
|
|
|
|
|
dataIndex: 'category',
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (category: string) => categories.find((c) => c.itemValue === category)?.itemLabel || category || '-',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '层级',
|
|
|
|
|
|
dataIndex: 'isSystem',
|
|
|
|
|
|
width: 110,
|
|
|
|
|
|
render: (_: unknown, item: PromptTemplateVO) => {
|
|
|
|
|
|
const level = getTemplateLevelMeta(item);
|
|
|
|
|
|
return <Tag color={level.color} className="prompt-template-level-tag">{level.label}</Tag>;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '热词组',
|
|
|
|
|
|
dataIndex: 'hotWordGroupName',
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
render: (name: string) => name ? <Tag color="blue">{name}</Tag> : <Text type="secondary">未绑定</Text>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '业务标签',
|
|
|
|
|
|
dataIndex: 'tags',
|
|
|
|
|
|
minWidth: 220,
|
|
|
|
|
|
render: (tags: unknown) => {
|
|
|
|
|
|
const tagList = normalizePromptTags(tags);
|
|
|
|
|
|
if (!tagList.length) {
|
|
|
|
|
|
return <Text type="secondary">未配置</Text>;
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Space size={[4, 4]} wrap className="prompt-template-tags-cell">
|
|
|
|
|
|
{tagList.slice(0, 3).map((tag) => {
|
|
|
|
|
|
const dictItem = dictTags.find((dt) => dt.itemValue === tag);
|
|
|
|
|
|
return <Tag key={tag}>{dictItem ? dictItem.itemLabel : tag}</Tag>;
|
|
|
|
|
|
})}
|
|
|
|
|
|
{tagList.length > 3 ? <Tag>+{tagList.length - 3}</Tag> : null}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
|
width: 90,
|
|
|
|
|
|
render: (_: unknown, item: PromptTemplateVO) => (
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
checked={item.status === 1}
|
|
|
|
|
|
onClick={(_, event) => event.stopPropagation()}
|
|
|
|
|
|
onChange={(checked) => void handleStatusChange(item.id, checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'actions',
|
|
|
|
|
|
width: 150,
|
|
|
|
|
|
fixed: 'right' as const,
|
|
|
|
|
|
render: (_: unknown, item: PromptTemplateVO) => {
|
|
|
|
|
|
const canEdit = canManageTemplate(item);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Space size={2} onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
|
<Tooltip title="查看">
|
|
|
|
|
|
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => showDetail(item)} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
{canEdit && (
|
|
|
|
|
|
<Tooltip title="编辑">
|
|
|
|
|
|
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => handleOpenDrawer(item)} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
)}
|
2026-03-02 11:59:47 +00:00
|
|
|
|
<Tooltip title="以此创建">
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => handleOpenDrawer(item, true)} />
|
2026-03-02 11:59:47 +00:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
{canEdit && (
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确定删除?"
|
|
|
|
|
|
onConfirm={() => deletePromptTemplate(item.id).then(() => fetchData())}
|
2026-03-04 12:49:32 +00:00
|
|
|
|
okText={t('common.confirm')}
|
|
|
|
|
|
cancelText={t('common.cancel')}
|
|
|
|
|
|
>
|
2026-03-02 11:59:47 +00:00
|
|
|
|
<Tooltip title="删除">
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
2026-03-02 11:59:47 +00:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<PageContainer title={null} className="prompt-templates-page">
|
|
|
|
|
|
<SectionCard
|
|
|
|
|
|
title="提示词模板"
|
|
|
|
|
|
description="管理 AI 会议总结的提示词模板库。"
|
|
|
|
|
|
>
|
|
|
|
|
|
<DataListPanel
|
|
|
|
|
|
className="prompt-templates-list-panel"
|
|
|
|
|
|
leftActions={
|
|
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenDrawer()}>
|
|
|
|
|
|
新增模板
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
}
|
|
|
|
|
|
rightActions={
|
|
|
|
|
|
<Form layout="inline" onFinish={handleSearch} className="prompt-templates-search">
|
|
|
|
|
|
<Form.Item label="模板名称">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="请输入..."
|
|
|
|
|
|
className="prompt-templates-search__name"
|
|
|
|
|
|
value={queryDraft.name}
|
|
|
|
|
|
onChange={(event) => setQueryDraft((currentDraft) => ({ ...currentDraft, name: event.target.value }))}
|
2026-03-04 12:49:32 +00:00
|
|
|
|
/>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item label="分类">
|
|
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="选择分类"
|
|
|
|
|
|
className="prompt-templates-search__category"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
value={queryDraft.category}
|
|
|
|
|
|
onChange={(value) => setQueryDraft((currentDraft) => ({ ...currentDraft, category: value }))}
|
|
|
|
|
|
>
|
|
|
|
|
|
{categories.map((c) => <Option key={c.itemValue} value={c.itemValue}>{c.itemLabel}</Option>)}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item>
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button type="primary" htmlType="submit">查询</Button>
|
|
|
|
|
|
<Button onClick={handleResetSearch}>重置</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
}
|
|
|
|
|
|
footer={
|
|
|
|
|
|
<AppPagination
|
|
|
|
|
|
current={current}
|
|
|
|
|
|
pageSize={pageSize}
|
|
|
|
|
|
total={total}
|
|
|
|
|
|
onChange={(page, size) => {
|
|
|
|
|
|
setCurrent(page);
|
|
|
|
|
|
setPageSize(size);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Table<PromptTemplateVO>
|
|
|
|
|
|
className="prompt-templates-table"
|
|
|
|
|
|
columns={tableColumns}
|
|
|
|
|
|
dataSource={data}
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
pagination={false}
|
|
|
|
|
|
scroll={{ x: "max-content", y: "100%" }}
|
|
|
|
|
|
onRow={(record) => ({ onClick: () => showDetail(record) })}
|
|
|
|
|
|
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无可用模板" /> }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</DataListPanel>
|
|
|
|
|
|
</SectionCard>
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
2026-07-01 03:01:38 +00:00
|
|
|
|
<FormDrawer
|
|
|
|
|
|
title={editingId ? '编辑模板' : '创建新模板'}
|
|
|
|
|
|
size="md"
|
|
|
|
|
|
bodyDensity="compact"
|
2026-03-02 11:59:47 +00:00
|
|
|
|
onClose={() => setDrawerVisible(false)}
|
|
|
|
|
|
open={drawerVisible}
|
2026-07-01 03:01:38 +00:00
|
|
|
|
okIcon={<SaveOutlined />}
|
|
|
|
|
|
okLoading={submitLoading}
|
|
|
|
|
|
okButtonProps={{ htmlType: "submit", form: "prompt-template-form" }}
|
2026-03-02 11:59:47 +00:00
|
|
|
|
>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Form
|
|
|
|
|
|
id="prompt-template-form"
|
|
|
|
|
|
key={drawerFormKey}
|
|
|
|
|
|
initialValues={drawerInitialValues}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
onFinish={(values) => void handleSubmit(values)}
|
|
|
|
|
|
onValuesChange={(changedValues) => {
|
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(changedValues, 'isSystem')) {
|
|
|
|
|
|
setTemplateLevel(Number(changedValues.isSystem));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(changedValues, 'hotWordGroupId')) {
|
|
|
|
|
|
setSelectedHotWordGroupId(changedValues.hotWordGroupId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-07-01 03:01:38 +00:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={24}>
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Form.Item name="templateName" label="模板名称" rules={[{ required: true }]}>
|
|
|
|
|
|
<Input />
|
|
|
|
|
|
</Form.Item>
|
2026-03-04 07:19:40 +00:00
|
|
|
|
</Col>
|
|
|
|
|
|
{(isPlatformAdmin || isTenantAdmin) && (
|
2026-07-01 03:01:38 +00:00
|
|
|
|
<Col span={24}>
|
2026-03-04 07:19:40 +00:00
|
|
|
|
<Form.Item name="isSystem" label="模板属性" rules={[{ required: true }]}>
|
|
|
|
|
|
<Select placeholder="选择属性">
|
|
|
|
|
|
{promptLevels.length > 0 ? (
|
2026-04-22 07:28:06 +00:00
|
|
|
|
promptLevels.map((i) => <Option key={i.itemValue} value={Number(i.itemValue)}>{i.itemLabel}</Option>)
|
2026-03-04 07:19:40 +00:00
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Option value={1}>{isPlatformAdmin ? '系统预置 (全局)' : '租户预置 (全员)'}</Option>
|
|
|
|
|
|
<Option value={0}>个人模板</Option>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
)}
|
2026-07-01 03:01:38 +00:00
|
|
|
|
<Col span={24}>
|
2026-03-04 07:19:40 +00:00
|
|
|
|
<Form.Item name="category" label="分类" rules={[{ required: true }]}>
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Select loading={dictLoading}>
|
|
|
|
|
|
{categories.map((i) => <Option key={i.itemValue} value={i.itemValue}>{i.itemLabel}</Option>)}
|
|
|
|
|
|
</Select>
|
2026-03-04 07:19:40 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-07-01 03:01:38 +00:00
|
|
|
|
<Col span={24}>
|
2026-03-04 07:19:40 +00:00
|
|
|
|
<Form.Item name="status" label="状态">
|
|
|
|
|
|
<Select>
|
|
|
|
|
|
<Option value={1}>启用</Option>
|
|
|
|
|
|
<Option value={0}>禁用</Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-03-02 11:59:47 +00:00
|
|
|
|
</Row>
|
2026-04-22 07:28:06 +00:00
|
|
|
|
|
2026-04-15 09:55:57 +00:00
|
|
|
|
<Form.Item name="description" label="模板描述">
|
|
|
|
|
|
<Input.TextArea
|
|
|
|
|
|
maxLength={255}
|
|
|
|
|
|
showCount
|
|
|
|
|
|
autoSize={{ minRows: 2, maxRows: 4 }}
|
|
|
|
|
|
placeholder="请输入模板描述"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
2026-04-22 07:28:06 +00:00
|
|
|
|
|
2026-07-01 03:01:38 +00:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={24}>
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Form.Item name="tags" label="业务标签" tooltip="可从现有标签中选择,也可输入新内容按回车保存">
|
|
|
|
|
|
<Select mode="tags" placeholder="选择或输入新标签" allowClear tokenSeparators={[',', ' ', ';']}>
|
|
|
|
|
|
{dictTags.map((item) => <Option key={item.itemValue} value={item.itemValue}>{item.itemLabel}</Option>)}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-07-01 03:01:38 +00:00
|
|
|
|
<Col span={24}>
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="hotWordGroupId"
|
|
|
|
|
|
label="绑定热词组"
|
|
|
|
|
|
tooltip="可选,未绑定则保持兼容"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="选择热词组"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
|
<Divider orientation="left">提示词编辑器 (Markdown 实时预览)</Divider>
|
2026-07-01 03:01:38 +00:00
|
|
|
|
<Row gutter={[0, 16]} className="prompt-template-editor">
|
|
|
|
|
|
<Col span={24} className="prompt-template-editor__col">
|
2026-03-02 11:59:47 +00:00
|
|
|
|
<Form.Item name="promptContent" noStyle rules={[{ required: true }]}>
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Input.TextArea
|
|
|
|
|
|
onChange={(e) => setPreviewContent(e.target.value)}
|
2026-06-30 05:37:33 +00:00
|
|
|
|
className="prompt-template-editor__input"
|
2026-03-02 11:59:47 +00:00
|
|
|
|
placeholder="在此输入 Markdown 指令..."
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-07-01 03:01:38 +00:00
|
|
|
|
<Col span={24} className="prompt-template-editor__preview">
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="prompt-template-editor__preview-meta">
|
|
|
|
|
|
{selectedHotWordGroupId ? (
|
2026-04-22 07:28:06 +00:00
|
|
|
|
<Tag color="blue">
|
|
|
|
|
|
绑定热词组:
|
2026-06-30 05:37:33 +00:00
|
|
|
|
{groupOptions.find((item) => item.id === selectedHotWordGroupId)?.groupName || '已选择'}
|
2026-04-22 07:28:06 +00:00
|
|
|
|
</Tag>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Tag>未绑定热词组</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-03-02 11:59:47 +00:00
|
|
|
|
<div className="markdown-preview"><ReactMarkdown>{previewContent}</ReactMarkdown></div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</Form>
|
2026-07-01 03:01:38 +00:00
|
|
|
|
</FormDrawer>
|
2026-05-09 02:17:46 +00:00
|
|
|
|
</PageContainer>
|
2026-03-02 11:59:47 +00:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default PromptTemplates;
|