对接关于我们 ,正则法规,帮助中心 ,用户提问模块

This commit is contained in:
linxd
2025-06-18 20:14:11 +08:00
parent e8804d6140
commit 8176a9b3d0
32 changed files with 2094 additions and 506 deletions

37
src/pages/help/help.less Normal file
View File

@ -0,0 +1,37 @@
.help-container {
width: 100%;
min-height: calc(100vh - 200px);
padding: 40px;
background-color: #f5f5f5;
.help-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin-bottom: 0;
}
.ask-button {
font-size: 14px;
}
}
.help-table {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
.help-title {
color: #1890ff;
&:hover {
color: #40a9ff;
text-decoration: underline;
}
}
}
}

143
src/pages/help/help.tsx Normal file
View File

@ -0,0 +1,143 @@
import React, { useState, useEffect } from 'react';
import { Table, Typography, Button } from 'antd';
import { history, useIntl } from 'umi';
import { PlusOutlined } from '@ant-design/icons';
import { getHelpCenterList } from '@/servers/api/help';
import { QUESTION_TYPES } from '@/dicts/help';
import './help.less';
const { Title } = Typography;
const HelpPage: React.FC = () => {
const intl = useIntl();
const [helpData, setHelpData] = useState<API.HelpCenterRecord[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
// 获取帮助中心数据
const fetchHelpData = async (current = 1, pageSize = 10) => {
setLoading(true);
try {
const response = await getHelpCenterList({
basePageRequest: {
pageNo: current,
pageSize: pageSize,
},
});
if (response.success) {
setHelpData(response.data.records || []);
setPagination({
current: response.data.current,
pageSize: response.data.size,
total: response.data.total,
});
}
} catch (error) {
console.error('获取帮助中心列表失败:', error);
} finally {
setLoading(false);
}
};
// 初始加载数据
useEffect(() => {
fetchHelpData();
}, []);
// 处理表格分页变化
const handleTableChange = (newPagination: any) => {
fetchHelpData(newPagination.current, newPagination.pageSize);
};
// 处理点击问题标题
const handleHelpClick = (id: string) => {
history.push(`/help/helpInfo?id=${id}`);
};
// 处理点击提问按钮
const handleAskQuestion = () => {
history.push('/help/helpQuestion');
};
// 获取问题分类名称
const getQuestionTypeName = (type: string) => {
const found = QUESTION_TYPES.find(item => item.value === type);
return found ? found.label : type;
};
// 定义表格列
const columns = [
{
title: intl.formatMessage({ id: 'help.column.index' }),
dataIndex: 'index',
key: 'index',
width: 80,
render: (_: any, __: any, index: number) => {
return (pagination.current - 1) * pagination.pageSize + index + 1;
},
},
{
title: intl.formatMessage({ id: 'help.column.title' }),
dataIndex: 'title',
key: 'title',
render: (text: string, record: any) => (
<a onClick={() => handleHelpClick(record.id)} className="help-title">
{text}
</a>
),
},
{
title: intl.formatMessage({ id: 'help.column.type' }),
dataIndex: 'type',
key: 'type',
width: 120,
render: (type: string) => getQuestionTypeName(type),
},
{
title: intl.formatMessage({ id: 'help.column.time' }),
dataIndex: 'createTime',
key: 'createTime',
width: 180,
align: 'center' as 'center',
},
];
return (
<div className="help-container">
<div className="help-header">
<Title level={2}>{intl.formatMessage({ id: 'help.title' })}</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAskQuestion}
className="ask-button"
>
{intl.formatMessage({ id: 'help.ask' })}
</Button>
</div>
<div className="help-table">
<Table
columns={columns}
dataSource={helpData}
rowKey="id"
pagination={{
...pagination,
showTotal: (total) => `${intl.formatMessage({ id: 'help.total' }, { total })}`,
showSizeChanger: true,
showQuickJumper: true,
}}
loading={loading}
onChange={handleTableChange}
bordered
/>
</div>
</div>
);
};
export default HelpPage;

View File

@ -0,0 +1,88 @@
@import '~antd/es/style/themes/default.less';
.helpInfoContainer {
padding: 24px;
background-color: #fff;
min-height: calc(100vh - 200px);
}
.helpInfoHeader {
margin-bottom: 24px;
}
.backButton {
color: rgb(0, 79, 142);
padding: 0;
&:hover {
color: lighten(rgb(0, 79, 142), 10%);
}
}
.helpInfoContent {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
.titleContainer {
text-align: center;
margin-bottom: 24px;
}
.title {
color: rgb(0, 79, 142);
font-weight: 500;
}
.metaInfo {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
color: #999;
.metaLeft, .metaRight {
flex: 1;
}
.metaRight {
text-align: right;
}
}
.divider {
margin: 16px 0;
border-top: 1px solid #e8e8e8;
}
.contentBody {
font-size: 16px;
line-height: 1.8;
p {
margin-bottom: 16px;
}
h3 {
font-weight: 500;
color: rgb(0, 79, 142);
margin-top: 24px;
margin-bottom: 16px;
}
}
.loadingContainer {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.notFoundContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 400px;
gap: 24px;
}

121
src/pages/help/helpInfo.tsx Normal file
View File

@ -0,0 +1,121 @@
import React, { useState, useEffect } from 'react';
import { Typography, Button, Divider, Spin, message } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { history, useIntl } from 'umi';
import { getHelpCenterDetail } from '@/servers/api/help';
import { QUESTION_TYPES } from '@/dicts/help';
import styles from './helpInfo.less';
const { Title, Text } = Typography;
const HelpInfoPage: React.FC = () => {
const intl = useIntl();
const [helpDetail, setHelpDetail] = useState<API.HelpCenterDetailResponse | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
// 获取URL中的id参数
const query = new URLSearchParams(window.location.search);
const id = query.get('id');
if (!id) {
message.error(intl.formatMessage({ id: 'help.message.idNotFound' }));
setLoading(false);
return;
}
// 请求详情数据
const fetchHelpDetail = async () => {
try {
const response = await getHelpCenterDetail({ id });
if (response.success) {
setHelpDetail(response.data);
} else {
message.error(response.message || intl.formatMessage({ id: 'help.message.loadFailed' }));
}
} catch (error) {
console.error('获取帮助中心详情失败:', error);
message.error(intl.formatMessage({ id: 'help.message.loadFailed' }));
} finally {
setLoading(false);
}
};
fetchHelpDetail();
}, [intl]);
// 返回列表页
const handleBack = () => {
history.push('/help');
};
// 获取问题分类名称
const getQuestionTypeName = (type: string) => {
const found = QUESTION_TYPES.find(item => item.value === type);
return found ? found.label : type;
};
if (loading) {
return (
<div className={styles.loadingContainer}>
<Spin size="large" />
</div>
);
}
if (!helpDetail) {
return (
<div className={styles.notFoundContainer}>
<Title level={4}>{intl.formatMessage({ id: 'help.info.notFound' })}</Title>
<Button type="primary" onClick={handleBack}>
{intl.formatMessage({ id: 'help.back' })}
</Button>
</div>
);
}
return (
<div className={styles.helpInfoContainer}>
<div className={styles.helpInfoHeader}>
<Button
type="link"
icon={<ArrowLeftOutlined />}
onClick={handleBack}
className={styles.backButton}
>
{intl.formatMessage({ id: 'help.back' })}
</Button>
</div>
<div className={styles.helpInfoContent}>
<div className={styles.titleContainer}>
<Title level={2} className={styles.title}>
{helpDetail.title}
</Title>
</div>
<div className={styles.metaInfo}>
<div className={styles.metaLeft}>
<Text type="secondary">
{intl.formatMessage({ id: 'help.info.publishTime' })}: {helpDetail.createTime}
</Text>
</div>
<div className={styles.metaRight}>
<Text type="secondary">
{intl.formatMessage({ id: 'help.info.questionType' })}: {getQuestionTypeName(helpDetail.type)} |
{intl.formatMessage({ id: 'help.info.publisher' })}: {helpDetail.createBy || '系统管理员'}
</Text>
</div>
</div>
<Divider className={styles.divider} />
<div className={styles.contentBody}>
<div dangerouslySetInnerHTML={{ __html: helpDetail.answerContent || helpDetail.content || '暂无内容' }} />
</div>
</div>
</div>
);
};
export default HelpInfoPage;

View File

@ -0,0 +1,92 @@
@import '~antd/es/style/themes/default.less';
.helpQuestionContainer {
padding: 24px;
background-color: #fff;
min-height: calc(100vh - 200px);
}
.helpQuestionHeader {
margin-bottom: 24px;
}
.backButton {
color: rgb(0, 79, 142);
padding: 0;
&:hover {
color: lighten(rgb(0, 79, 142), 10%);
}
}
.helpQuestionContent {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.titleContainer {
text-align: center;
margin-bottom: 16px;
.title {
color: rgb(0, 79, 142);
font-weight: 500;
}
.titleDesc {
display: block;
margin-top: 8px;
}
}
.divider {
margin: 16px 0 24px;
border-top: 1px solid #e8e8e8;
}
.questionForm {
.ant-form-item-label > label {
font-weight: 500;
color: #333;
}
.ant-form-item {
margin-bottom: 24px;
}
.ant-input-affix-wrapper,
.ant-input,
.ant-select-selector {
border-radius: 4px;
}
.ant-input:focus,
.ant-input-focused,
.ant-select-focused .ant-select-selector {
border-color: rgb(0, 79, 142);
box-shadow: 0 0 0 2px rgba(0, 79, 142, 0.2);
}
}
.formButtons {
text-align: center;
margin-top: 40px;
.cancelButton {
margin-right: 16px;
min-width: 100px;
}
.submitButton {
background-color: rgb(0, 79, 142);
border-color: rgb(0, 79, 142);
min-width: 100px;
&:hover,
&:focus {
background-color: lighten(rgb(0, 79, 142), 10%);
border-color: lighten(rgb(0, 79, 142), 10%);
}
}
}

View File

@ -0,0 +1,257 @@
import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Typography, Select, Row, Col, Divider, message } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { history, useIntl } from 'umi';
import WangEditor from 'wangeditor';
import { QUESTION_TYPES } from '@/dicts/help';
import { addHelpQuestion } from '@/servers/api/help';
import styles from './helpQuestion.less';
const { Title } = Typography;
const { Option } = Select;
// 表单字段接口定义
interface FormValues {
title: string;
content: string;
type: string;
name: string;
company: string;
account: string;
email: string;
phone?: string; // 可选字段
}
const HelpQuestionPage: React.FC = () => {
const intl = useIntl();
const [form] = Form.useForm();
const [editor, setEditor] = useState<WangEditor | null>(null);
const [content, setContent] = useState<string>('');
const [submitting, setSubmitting] = useState<boolean>(false);
// 初始化编辑器
useEffect(() => {
const editorInstance = new WangEditor('#editor');
// 配置编辑器
editorInstance.config.height = 300;
editorInstance.config.placeholder = intl.formatMessage({ id: 'help.form.content.placeholder' });
editorInstance.config.zIndex = 10;
// 上传图片相关配置
editorInstance.config.uploadImgShowBase64 = true;
// 设置编辑器内容变化回调
editorInstance.config.onchange = (html: string) => {
setContent(html);
form.setFieldsValue({ content: html });
};
// 创建编辑器
editorInstance.create();
setEditor(editorInstance);
// 组件销毁时销毁编辑器实例
return () => {
if (editorInstance) {
editorInstance.destroy();
}
};
}, [form, intl]);
// 表单提交处理
const handleSubmit = async (values: FormValues) => {
// 确保内容非空
if (!content.trim()) {
message.error(intl.formatMessage({ id: 'help.form.content.required' }));
return;
}
setSubmitting(true);
try {
const response = await addHelpQuestion({
...values,
content: content,
});
if (response.success) {
message.success(intl.formatMessage({ id: 'help.message.submitSuccess' }));
// 提交成功后返回帮助中心列表页
setTimeout(() => {
history.push('/help');
}, 1500);
} else {
message.error(response.message || intl.formatMessage({ id: 'help.message.submitFailed' }));
}
} catch (error) {
console.error('提交问题失败:', error);
message.error(intl.formatMessage({ id: 'help.message.submitFailed' }));
} finally {
setSubmitting(false);
}
};
// 处理取消按钮点击
const handleCancel = () => {
history.push('/help');
};
// 处理返回列表按钮点击
const handleBack = () => {
history.push('/help');
};
// 邮箱验证规则
const emailValidator = (rule: any, value: string) => {
if (!value || /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value)) {
return Promise.resolve();
}
return Promise.reject(intl.formatMessage({ id: 'help.form.email.invalid' }));
};
// 手机号验证规则 (可选字段)
const phoneValidator = (rule: any, value: string) => {
if (!value || /^1[3-9]\d{9}$/.test(value)) {
return Promise.resolve();
}
return Promise.reject(intl.formatMessage({ id: 'help.form.phone.invalid' }));
};
return (
<div className={styles.helpQuestionContainer}>
<div className={styles.header}>
<Button
type="link"
icon={<ArrowLeftOutlined />}
onClick={handleBack}
className={styles.backButton}
>
{intl.formatMessage({ id: 'help.back' })}
</Button>
</div>
<div className={styles.contentWrapper}>
<div className={styles.titleContainer}>
<Title level={2}>{intl.formatMessage({ id: 'help.question.title' })}</Title>
<p className={styles.subtitle}>
{intl.formatMessage({ id: 'help.question.subtitle' })}
</p>
</div>
<Divider className={styles.divider} />
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{ type: QUESTION_TYPES[0]?.value }}
className={styles.questionForm}
>
<Row gutter={24}>
<Col span={24} md={12}>
<Form.Item
name="type"
label={intl.formatMessage({ id: 'help.form.type' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'help.form.type.required' }) }]}
>
<Select placeholder={intl.formatMessage({ id: 'help.form.type.placeholder' })}>
{QUESTION_TYPES.map(type => (
<Option key={type.value} value={type.value}>
{type.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={24} md={12}>
<Form.Item
name="title"
label={intl.formatMessage({ id: 'help.form.title' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'help.form.title.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'help.form.title.placeholder' })} />
</Form.Item>
</Col>
</Row>
<Form.Item
name="content"
label={intl.formatMessage({ id: 'help.form.content' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'help.form.content.required' }) }]}
>
<div id="editor" className={styles.editor}></div>
</Form.Item>
<Row gutter={24}>
<Col span={24} md={12}>
<Form.Item
name="name"
label={intl.formatMessage({ id: 'help.form.name' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'help.form.name.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'help.form.name.placeholder' })} />
</Form.Item>
</Col>
<Col span={24} md={12}>
<Form.Item
name="company"
label={intl.formatMessage({ id: 'help.form.company' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'help.form.company.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'help.form.company.placeholder' })} />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={24} md={12}>
<Form.Item
name="account"
label={intl.formatMessage({ id: 'help.form.account' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'help.form.account.required' }) }]}
>
<Input placeholder={intl.formatMessage({ id: 'help.form.account.placeholder' })} />
</Form.Item>
</Col>
<Col span={24} md={12}>
<Form.Item
name="email"
label={intl.formatMessage({ id: 'help.form.email' })}
rules={[
{ required: true, message: intl.formatMessage({ id: 'help.form.email.required' }) },
{ validator: emailValidator }
]}
>
<Input placeholder={intl.formatMessage({ id: 'help.form.email.placeholder' })} />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={24} md={12}>
<Form.Item
name="phone"
label={intl.formatMessage({ id: 'help.form.phone' })}
rules={[{ validator: phoneValidator }]}
>
<Input placeholder={intl.formatMessage({ id: 'help.form.phone.placeholder' })} />
</Form.Item>
</Col>
</Row>
<Form.Item className={styles.buttonGroup}>
<Button type="primary" htmlType="submit" loading={submitting} className={styles.submitButton}>
{intl.formatMessage({ id: 'help.submit' })}
</Button>
<Button onClick={handleCancel} className={styles.cancelButton}>
{intl.formatMessage({ id: 'help.cancel' })}
</Button>
</Form.Item>
</Form>
</div>
</div>
);
};
export default HelpQuestionPage;