| @@ -0,0 +1,216 @@ | |||||
| /* | |||||
| Copyright (C) 2025 QuantumNous | |||||
| This program is free software: you can redistribute it and/or modify | |||||
| it under the terms of the GNU Affero General Public License as | |||||
| published by the Free Software Foundation, either version 3 of the | |||||
| License, or (at your option) any later version. | |||||
| This program is distributed in the hope that it will be useful, | |||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
| GNU Affero General Public License for more details. | |||||
| You should have received a copy of the GNU Affero General Public License | |||||
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |||||
| For commercial licensing, please contact support@quantumnous.com | |||||
| */ | |||||
| import React, { useState, useEffect, useCallback, useRef } from 'react'; | |||||
| import { | |||||
| Button, | |||||
| Table, | |||||
| Modal, | |||||
| Form, | |||||
| Input, | |||||
| Space, | |||||
| Popconfirm, | |||||
| Typography, | |||||
| Tag as SemiTag, | |||||
| } from '@douyinfe/semi-ui'; | |||||
| import { IconPlus, IconEdit, IconDelete } from '@douyinfe/semi-icons'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { pricingTagApi } from '../../../helpers/api'; | |||||
| import { showError, showSuccess } from '../../../helpers'; | |||||
| const PricingTagManager = () => { | |||||
| const { t } = useTranslation(); | |||||
| const [tags, setTags] = useState([]); | |||||
| const [loading, setLoading] = useState(false); | |||||
| const [modalVisible, setModalVisible] = useState(false); | |||||
| const [editingTag, setEditingTag] = useState(null); | |||||
| const formRef = useRef(); | |||||
| const loadTags = useCallback(async () => { | |||||
| setLoading(true); | |||||
| try { | |||||
| const res = await pricingTagApi.getAll(); | |||||
| if (res.data.success) { | |||||
| setTags(res.data.data); | |||||
| } | |||||
| } catch (e) { | |||||
| showError(e); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| loadTags(); | |||||
| }, [loadTags]); | |||||
| const handleCreate = () => { | |||||
| setEditingTag(null); | |||||
| setModalVisible(true); | |||||
| }; | |||||
| const handleEdit = (record) => { | |||||
| setEditingTag(record); | |||||
| setModalVisible(true); | |||||
| }; | |||||
| const handleDelete = async (id) => { | |||||
| try { | |||||
| const res = await pricingTagApi.delete(id); | |||||
| if (res.data.success) { | |||||
| showSuccess(t('删除成功')); | |||||
| loadTags(); | |||||
| } | |||||
| } catch (e) { | |||||
| showError(e); | |||||
| } | |||||
| }; | |||||
| const handleSubmit = async () => { | |||||
| try { | |||||
| const values = await formRef.current.validate(); | |||||
| if (editingTag) { | |||||
| const res = await pricingTagApi.update(editingTag.id, values); | |||||
| if (res.data.success) { | |||||
| showSuccess(t('更新成功')); | |||||
| setModalVisible(false); | |||||
| loadTags(); | |||||
| } | |||||
| } else { | |||||
| const res = await pricingTagApi.create(values); | |||||
| if (res.data.success) { | |||||
| showSuccess(t('创建成功')); | |||||
| setModalVisible(false); | |||||
| loadTags(); | |||||
| } | |||||
| } | |||||
| } catch (e) { | |||||
| showError(e); | |||||
| } | |||||
| }; | |||||
| const columns = [ | |||||
| { | |||||
| title: t('标签名称'), | |||||
| dataIndex: 'name', | |||||
| key: 'name', | |||||
| render: (text, record) => ( | |||||
| <Space> | |||||
| <SemiTag color={record.color}>{text}</SemiTag> | |||||
| </Space> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| title: t('颜色'), | |||||
| dataIndex: 'color', | |||||
| key: 'color', | |||||
| render: (color) => ( | |||||
| <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> | |||||
| <div | |||||
| style={{ | |||||
| width: 20, | |||||
| height: 20, | |||||
| backgroundColor: color, | |||||
| borderRadius: 4, | |||||
| border: '1px solid #ddd', | |||||
| }} | |||||
| /> | |||||
| <Typography.Text>{color}</Typography.Text> | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| title: t('描述'), | |||||
| dataIndex: 'description', | |||||
| key: 'description', | |||||
| }, | |||||
| { | |||||
| title: t('排序'), | |||||
| dataIndex: 'sort_order', | |||||
| key: 'sort_order', | |||||
| width: 80, | |||||
| }, | |||||
| { | |||||
| title: t('操作'), | |||||
| key: 'action', | |||||
| width: 150, | |||||
| render: (_, record) => ( | |||||
| <Space> | |||||
| <Button icon={<IconEdit />} onClick={() => handleEdit(record)} /> | |||||
| <Popconfirm | |||||
| title={t('确定删除此标签?')} | |||||
| onConfirm={() => handleDelete(record.id)} | |||||
| > | |||||
| <Button icon={<IconDelete />} type="danger" /> | |||||
| </Popconfirm> | |||||
| </Space> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <div> | |||||
| <div style={{ marginBottom: 16 }}> | |||||
| <Button icon={<IconPlus />} onClick={handleCreate}> | |||||
| {t('添加标签')} | |||||
| </Button> | |||||
| </div> | |||||
| <Table | |||||
| columns={columns} | |||||
| dataSource={tags} | |||||
| loading={loading} | |||||
| pagination={false} | |||||
| rowKey="id" | |||||
| /> | |||||
| <Modal | |||||
| title={editingTag ? t('编辑标签') : t('添加标签')} | |||||
| visible={modalVisible} | |||||
| onCancel={() => setModalVisible(false)} | |||||
| onOk={handleSubmit} | |||||
| > | |||||
| <Form getFormApi={(api) => (formRef.current = api)} initValues={editingTag}> | |||||
| <Form.Input | |||||
| field="name" | |||||
| label={t('标签名称')} | |||||
| rules={[{ required: true, message: t('请输入标签名称') }]} | |||||
| /> | |||||
| <Form.Input | |||||
| field="color" | |||||
| label={t('颜色')} | |||||
| placeholder="#1890ff" | |||||
| /> | |||||
| <Form.TextArea | |||||
| field="description" | |||||
| label={t('描述')} | |||||
| maxLength={200} | |||||
| /> | |||||
| <Form.InputNumber | |||||
| field="sort_order" | |||||
| label={t('排序')} | |||||
| defaultValue={0} | |||||
| /> | |||||
| </Form> | |||||
| </Modal> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default PricingTagManager; | |||||