SettingsFAQ.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. Button,
  4. Space,
  5. Table,
  6. Form,
  7. Typography,
  8. Empty,
  9. Divider,
  10. Modal
  11. } from '@douyinfe/semi-ui';
  12. import {
  13. IllustrationNoResult,
  14. IllustrationNoResultDark
  15. } from '@douyinfe/semi-illustrations';
  16. import {
  17. Plus,
  18. Edit,
  19. Trash2,
  20. Save,
  21. HelpCircle
  22. } from 'lucide-react';
  23. import { API, showError, showSuccess } from '../../../helpers';
  24. import { useTranslation } from 'react-i18next';
  25. const { Text } = Typography;
  26. const SettingsFAQ = ({ options, refresh }) => {
  27. const { t } = useTranslation();
  28. const [faqList, setFaqList] = useState([]);
  29. const [showFaqModal, setShowFaqModal] = useState(false);
  30. const [showDeleteModal, setShowDeleteModal] = useState(false);
  31. const [deletingFaq, setDeletingFaq] = useState(null);
  32. const [editingFaq, setEditingFaq] = useState(null);
  33. const [modalLoading, setModalLoading] = useState(false);
  34. const [loading, setLoading] = useState(false);
  35. const [hasChanges, setHasChanges] = useState(false);
  36. const [faqForm, setFaqForm] = useState({
  37. title: '',
  38. content: ''
  39. });
  40. const [currentPage, setCurrentPage] = useState(1);
  41. const [pageSize, setPageSize] = useState(10);
  42. const [selectedRowKeys, setSelectedRowKeys] = useState([]);
  43. const columns = [
  44. {
  45. title: t('问题标题'),
  46. dataIndex: 'title',
  47. key: 'title',
  48. render: (text) => (
  49. <div style={{
  50. maxWidth: '300px',
  51. wordBreak: 'break-word',
  52. fontWeight: 'bold'
  53. }}>
  54. {text}
  55. </div>
  56. )
  57. },
  58. {
  59. title: t('回答内容'),
  60. dataIndex: 'content',
  61. key: 'content',
  62. render: (text) => (
  63. <div style={{
  64. maxWidth: '400px',
  65. wordBreak: 'break-word',
  66. whiteSpace: 'pre-wrap',
  67. color: 'var(--semi-color-text-1)'
  68. }}>
  69. {text}
  70. </div>
  71. )
  72. },
  73. {
  74. title: t('操作'),
  75. key: 'action',
  76. fixed: 'right',
  77. width: 150,
  78. render: (text, record) => (
  79. <Space>
  80. <Button
  81. icon={<Edit size={14} />}
  82. theme='light'
  83. type='tertiary'
  84. size='small'
  85. className="!rounded-full"
  86. onClick={() => handleEditFaq(record)}
  87. >
  88. {t('编辑')}
  89. </Button>
  90. <Button
  91. icon={<Trash2 size={14} />}
  92. type='danger'
  93. theme='light'
  94. size='small'
  95. className="!rounded-full"
  96. onClick={() => handleDeleteFaq(record)}
  97. >
  98. {t('删除')}
  99. </Button>
  100. </Space>
  101. )
  102. }
  103. ];
  104. const updateOption = async (key, value) => {
  105. const res = await API.put('/api/option/', {
  106. key,
  107. value,
  108. });
  109. const { success, message } = res.data;
  110. if (success) {
  111. showSuccess('常见问答已更新');
  112. if (refresh) refresh();
  113. } else {
  114. showError(message);
  115. }
  116. };
  117. const submitFAQ = async () => {
  118. try {
  119. setLoading(true);
  120. const faqJson = JSON.stringify(faqList);
  121. await updateOption('FAQ', faqJson);
  122. setHasChanges(false);
  123. } catch (error) {
  124. console.error('常见问答更新失败', error);
  125. showError('常见问答更新失败');
  126. } finally {
  127. setLoading(false);
  128. }
  129. };
  130. const handleAddFaq = () => {
  131. setEditingFaq(null);
  132. setFaqForm({
  133. title: '',
  134. content: ''
  135. });
  136. setShowFaqModal(true);
  137. };
  138. const handleEditFaq = (faq) => {
  139. setEditingFaq(faq);
  140. setFaqForm({
  141. title: faq.title,
  142. content: faq.content
  143. });
  144. setShowFaqModal(true);
  145. };
  146. const handleDeleteFaq = (faq) => {
  147. setDeletingFaq(faq);
  148. setShowDeleteModal(true);
  149. };
  150. const confirmDeleteFaq = () => {
  151. if (deletingFaq) {
  152. const newList = faqList.filter(item => item.id !== deletingFaq.id);
  153. setFaqList(newList);
  154. setHasChanges(true);
  155. showSuccess('问答已删除,请及时点击“保存设置”进行保存');
  156. }
  157. setShowDeleteModal(false);
  158. setDeletingFaq(null);
  159. };
  160. const handleSaveFaq = async () => {
  161. if (!faqForm.title || !faqForm.content) {
  162. showError('请填写完整的问答信息');
  163. return;
  164. }
  165. try {
  166. setModalLoading(true);
  167. let newList;
  168. if (editingFaq) {
  169. newList = faqList.map(item =>
  170. item.id === editingFaq.id
  171. ? { ...item, ...faqForm }
  172. : item
  173. );
  174. } else {
  175. const newId = Math.max(...faqList.map(item => item.id), 0) + 1;
  176. const newFaq = {
  177. id: newId,
  178. ...faqForm
  179. };
  180. newList = [...faqList, newFaq];
  181. }
  182. setFaqList(newList);
  183. setHasChanges(true);
  184. setShowFaqModal(false);
  185. showSuccess(editingFaq ? '问答已更新,请及时点击“保存设置”进行保存' : '问答已添加,请及时点击“保存设置”进行保存');
  186. } catch (error) {
  187. showError('操作失败: ' + error.message);
  188. } finally {
  189. setModalLoading(false);
  190. }
  191. };
  192. const parseFAQ = (faqStr) => {
  193. if (!faqStr) {
  194. setFaqList([]);
  195. return;
  196. }
  197. try {
  198. const parsed = JSON.parse(faqStr);
  199. const list = Array.isArray(parsed) ? parsed : [];
  200. // 确保每个项目都有id
  201. const listWithIds = list.map((item, index) => ({
  202. ...item,
  203. id: item.id || index + 1
  204. }));
  205. setFaqList(listWithIds);
  206. } catch (error) {
  207. console.error('解析常见问答失败:', error);
  208. setFaqList([]);
  209. }
  210. };
  211. useEffect(() => {
  212. if (options.FAQ !== undefined) {
  213. parseFAQ(options.FAQ);
  214. }
  215. }, [options.FAQ]);
  216. const handleBatchDelete = () => {
  217. if (selectedRowKeys.length === 0) {
  218. showError('请先选择要删除的常见问答');
  219. return;
  220. }
  221. const newList = faqList.filter(item => !selectedRowKeys.includes(item.id));
  222. setFaqList(newList);
  223. setSelectedRowKeys([]);
  224. setHasChanges(true);
  225. showSuccess(`已删除 ${selectedRowKeys.length} 个常见问答,请及时点击“保存设置”进行保存`);
  226. };
  227. const renderHeader = () => (
  228. <div className="flex flex-col w-full">
  229. <div className="mb-2">
  230. <div className="flex items-center text-blue-500">
  231. <HelpCircle size={16} className="mr-2" />
  232. <Text>{t('常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)')}</Text>
  233. </div>
  234. </div>
  235. <Divider margin="12px" />
  236. <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
  237. <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
  238. <Button
  239. theme='light'
  240. type='primary'
  241. icon={<Plus size={14} />}
  242. className="!rounded-full w-full md:w-auto"
  243. onClick={handleAddFaq}
  244. >
  245. {t('添加问答')}
  246. </Button>
  247. <Button
  248. icon={<Trash2 size={14} />}
  249. type='danger'
  250. theme='light'
  251. onClick={handleBatchDelete}
  252. disabled={selectedRowKeys.length === 0}
  253. className="!rounded-full w-full md:w-auto"
  254. >
  255. {t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
  256. </Button>
  257. <Button
  258. icon={<Save size={14} />}
  259. onClick={submitFAQ}
  260. loading={loading}
  261. disabled={!hasChanges}
  262. type='secondary'
  263. className="!rounded-full w-full md:w-auto"
  264. >
  265. {t('保存设置')}
  266. </Button>
  267. </div>
  268. </div>
  269. </div>
  270. );
  271. // 计算当前页显示的数据
  272. const getCurrentPageData = () => {
  273. const startIndex = (currentPage - 1) * pageSize;
  274. const endIndex = startIndex + pageSize;
  275. return faqList.slice(startIndex, endIndex);
  276. };
  277. const rowSelection = {
  278. selectedRowKeys,
  279. onChange: (selectedRowKeys, selectedRows) => {
  280. setSelectedRowKeys(selectedRowKeys);
  281. },
  282. onSelect: (record, selected, selectedRows) => {
  283. console.log(`选择行: ${selected}`, record);
  284. },
  285. onSelectAll: (selected, selectedRows) => {
  286. console.log(`全选: ${selected}`, selectedRows);
  287. },
  288. getCheckboxProps: (record) => ({
  289. disabled: false,
  290. name: record.id,
  291. }),
  292. };
  293. return (
  294. <>
  295. <Form.Section text={renderHeader()}>
  296. <Table
  297. columns={columns}
  298. dataSource={getCurrentPageData()}
  299. rowSelection={rowSelection}
  300. rowKey="id"
  301. scroll={{ x: 'max-content' }}
  302. pagination={{
  303. currentPage: currentPage,
  304. pageSize: pageSize,
  305. total: faqList.length,
  306. showSizeChanger: true,
  307. showQuickJumper: true,
  308. formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  309. start: page.currentStart,
  310. end: page.currentEnd,
  311. total: faqList.length,
  312. }),
  313. pageSizeOptions: ['5', '10', '20', '50'],
  314. onChange: (page, size) => {
  315. setCurrentPage(page);
  316. setPageSize(size);
  317. },
  318. onShowSizeChange: (current, size) => {
  319. setCurrentPage(1);
  320. setPageSize(size);
  321. }
  322. }}
  323. size='middle'
  324. loading={loading}
  325. empty={
  326. <Empty
  327. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  328. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  329. description={t('暂无常见问答')}
  330. style={{ padding: 30 }}
  331. />
  332. }
  333. className="rounded-xl overflow-hidden"
  334. />
  335. </Form.Section>
  336. <Modal
  337. title={editingFaq ? t('编辑问答') : t('添加问答')}
  338. visible={showFaqModal}
  339. onOk={handleSaveFaq}
  340. onCancel={() => setShowFaqModal(false)}
  341. okText={t('保存')}
  342. cancelText={t('取消')}
  343. className="rounded-xl"
  344. confirmLoading={modalLoading}
  345. width={800}
  346. >
  347. <Form layout='vertical' initValues={faqForm} key={editingFaq ? editingFaq.id : 'new'}>
  348. <Form.Input
  349. field='title'
  350. label={t('问题标题')}
  351. placeholder={t('请输入问题标题')}
  352. maxLength={200}
  353. rules={[{ required: true, message: t('请输入问题标题') }]}
  354. onChange={(value) => setFaqForm({ ...faqForm, title: value })}
  355. />
  356. <Form.TextArea
  357. field='content'
  358. label={t('回答内容')}
  359. placeholder={t('请输入回答内容')}
  360. maxCount={1000}
  361. rows={6}
  362. rules={[{ required: true, message: t('请输入回答内容') }]}
  363. onChange={(value) => setFaqForm({ ...faqForm, content: value })}
  364. />
  365. </Form>
  366. </Modal>
  367. <Modal
  368. title={t('确认删除')}
  369. visible={showDeleteModal}
  370. onOk={confirmDeleteFaq}
  371. onCancel={() => {
  372. setShowDeleteModal(false);
  373. setDeletingFaq(null);
  374. }}
  375. okText={t('确认删除')}
  376. cancelText={t('取消')}
  377. type="warning"
  378. className="rounded-xl"
  379. okButtonProps={{
  380. type: 'danger',
  381. theme: 'solid'
  382. }}
  383. >
  384. <Text>{t('确定要删除此问答吗?')}</Text>
  385. </Modal>
  386. </>
  387. );
  388. };
  389. export default SettingsFAQ;