{
+ e.preventDefault();
+ if (!dragging) setDragging(true);
+ }}
+ onDragLeave={(e) => {
+ if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false);
+ }}
+ onDrop={handleDrop}
+ >
+ {dragging && (
+
+ {t('松开鼠标上传图片')}
+
+ )}
{images.length > 0 && (
{images.map((b64, idx) => (
diff --git a/web/classic/src/components/feedback/feedbackHelpers.js b/web/classic/src/components/feedback/feedbackHelpers.js
index cd2a02d46a8..414307a9103 100644
--- a/web/classic/src/components/feedback/feedbackHelpers.js
+++ b/web/classic/src/components/feedback/feedbackHelpers.js
@@ -32,6 +32,26 @@ export const FEEDBACK_CATEGORY_OPTIONS = Object.entries(FEEDBACK_CATEGORY).map(
([value, { label }]) => ({ value: Number(value), label }),
);
+// 把「选择」或「拖拽」进来的文件统一处理成 base64[]:自动过滤非图片、按剩余
+// 配额(FEEDBACK_MAX_IMAGES - currentCount)裁剪、逐张压缩。点击上传与拖拽上传共用。
+// 返回 { encoded, error };error 为待 t() 翻译的文案 key(无错误则 undefined)。
+export async function encodeFeedbackImageFiles(fileList, currentCount) {
+ const files = Array.from(fileList || []).filter((f) =>
+ f.type ? f.type.startsWith('image/') : true,
+ );
+ if (files.length === 0) return { encoded: [] };
+ const room = FEEDBACK_MAX_IMAGES - currentCount;
+ if (room <= 0) return { encoded: [], error: '最多上传 3 张图片' };
+ try {
+ const encoded = await Promise.all(
+ files.slice(0, room).map((f) => compressImageToBase64(f)),
+ );
+ return { encoded };
+ } catch {
+ return { encoded: [], error: '图片处理失败' };
+ }
+}
+
// 将图片 File 压缩为纯 base64(无 data: 前缀)。与 KYC/企业认证同一套:
// 缩放到最长边 2400px、JPEG 0.88,超 1.5MB 再降一档质量重试一次。
export async function compressImageToBase64(
diff --git a/web/classic/src/components/layout/SiderBar.jsx b/web/classic/src/components/layout/SiderBar.jsx
index 80383fc29af..cfccc7b742b 100644
--- a/web/classic/src/components/layout/SiderBar.jsx
+++ b/web/classic/src/components/layout/SiderBar.jsx
@@ -54,6 +54,7 @@ const routerMap = {
kyc: '/console/kyc',
enterprise: '/console/enterprise',
feedback: '/console/feedback',
+ myfeedback: '/console/myfeedback',
};
const SiderBar = ({ onNavigate = () => {} }) => {
@@ -161,10 +162,15 @@ const SiderBar = ({ onNavigate = () => {} }) => {
to: '/topup',
},
{
- text: withUnreadBadge(t('个人设置'), userUnread),
+ text: t('个人设置'),
itemKey: 'personal',
to: '/personal',
},
+ {
+ text: withUnreadBadge(t('我的工单'), userUnread),
+ itemKey: 'myfeedback',
+ to: '/console/myfeedback',
+ },
];
// 根据配置过滤项目
@@ -220,6 +226,12 @@ const SiderBar = ({ onNavigate = () => {} }) => {
to: '/user',
className: isAdmin() ? '' : 'tableHiddle',
},
+ {
+ text: withUnreadBadge(t('工单管理'), adminUnread),
+ itemKey: 'feedback',
+ to: '/console/feedback',
+ className: isAdmin() ? '' : 'tableHiddle',
+ },
{
text: t('实名认证'),
itemKey: 'kyc',
@@ -232,12 +244,6 @@ const SiderBar = ({ onNavigate = () => {} }) => {
to: '/enterprise',
className: isAdmin() ? '' : 'tableHiddle',
},
- {
- text: withUnreadBadge(t('工单管理'), adminUnread),
- itemKey: 'feedback',
- to: '/console/feedback',
- className: isAdmin() ? '' : 'tableHiddle',
- },
{
text: t('系统设置'),
itemKey: 'setting',
diff --git a/web/classic/src/components/settings/PersonalSetting.jsx b/web/classic/src/components/settings/PersonalSetting.jsx
index e231664764e..0cc6cd9ebea 100644
--- a/web/classic/src/components/settings/PersonalSetting.jsx
+++ b/web/classic/src/components/settings/PersonalSetting.jsx
@@ -43,7 +43,6 @@ import PreferencesSettings from './personal/cards/PreferencesSettings';
import CheckinCalendar from './personal/cards/CheckinCalendar';
import KYCSetting from './personal/cards/KYCSetting';
import EnterpriseSetting from './personal/cards/EnterpriseSetting';
-import FeedbackConsult from './personal/cards/FeedbackConsult';
import EmailBindModal from './personal/modals/EmailBindModal';
import WeChatBindModal from './personal/modals/WeChatBindModal';
import AccountDeleteModal from './personal/modals/AccountDeleteModal';
@@ -615,9 +614,6 @@ const PersonalSetting = () => {
{/* 偏好设置(语言等) */}
-
- {/* 我的工单(建议及咨询) */}
-
{/* 右侧:通知设置 + 实名认证 */}
diff --git a/web/classic/src/components/settings/personal/cards/FeedbackConsult.jsx b/web/classic/src/components/settings/personal/cards/FeedbackConsult.jsx
deleted file mode 100644
index 258bfab80e4..00000000000
--- a/web/classic/src/components/settings/personal/cards/FeedbackConsult.jsx
+++ /dev/null
@@ -1,390 +0,0 @@
-import React, { useEffect, useRef, useState } from 'react';
-import {
- Avatar,
- Badge,
- Button,
- Card,
- Image,
- Input,
- Select,
- Spin,
- Tag,
- TextArea,
- Typography,
-} from '@douyinfe/semi-ui';
-import {
- IconArrowLeft,
- IconImage,
- IconClose,
-} from '@douyinfe/semi-icons';
-import { Ticket } from 'lucide-react';
-import { API, showError, showSuccess } from '../../../../helpers';
-import { useTranslation } from 'react-i18next';
-import FeedbackThread from '../../../feedback/FeedbackThread';
-import {
- USER_FEEDBACK_BASE,
- FEEDBACK_ROLE_USER,
- FEEDBACK_STATUS,
- FEEDBACK_CATEGORY,
- FEEDBACK_CATEGORY_OPTIONS,
- FEEDBACK_MAX_IMAGES,
- compressImageToBase64,
-} from '../../../feedback/feedbackHelpers';
-
-const { Text, Title } = Typography;
-
-export default function FeedbackConsult() {
- const { t } = useTranslation();
- const [topics, setTopics] = useState([]);
- const [loadingList, setLoadingList] = useState(false);
- const [detail, setDetail] = useState(null); // { topic, messages }
- const [loadingDetail, setLoadingDetail] = useState(false);
- const [sending, setSending] = useState(false);
-
- // 新建表单
- const [showCreate, setShowCreate] = useState(false);
- const [form, setForm] = useState({ category: 2, title: '', content: '' });
- const [createImages, setCreateImages] = useState([]);
- const [creating, setCreating] = useState(false);
- const createFileRef = useRef(null);
-
- const loadTopics = async () => {
- setLoadingList(true);
- try {
- const res = await API.get(
- `${USER_FEEDBACK_BASE}/topics?page=1&page_size=50`,
- );
- if (res.data.success) setTopics(res.data.data || []);
- } finally {
- setLoadingList(false);
- }
- };
-
- useEffect(() => {
- loadTopics();
- }, []);
-
- const openTopic = async (id) => {
- setLoadingDetail(true);
- try {
- const res = await API.get(
- `${USER_FEEDBACK_BASE}/topics/${id}?page=1&page_size=200`,
- );
- if (res.data.success) {
- setDetail(res.data.data);
- loadTopics(); // 刷新卡片内未读红点
- // 通知侧边栏角标即时刷新(新建首单后恢复轮询 + 打开工单清未读)
- window.dispatchEvent(new Event('feedback:changed'));
- } else {
- showError(res.data.message);
- }
- } finally {
- setLoadingDetail(false);
- }
- };
-
- const handleReply = async (content, images) => {
- setSending(true);
- try {
- const res = await API.post(
- `${USER_FEEDBACK_BASE}/topics/${detail.topic.id}/messages`,
- { content, images },
- );
- if (res.data.success) {
- await openTopic(detail.topic.id);
- return true;
- }
- showError(res.data.message);
- return false;
- } catch (e) {
- showError(t('发送失败'));
- return false;
- } finally {
- setSending(false);
- }
- };
-
- const handleClose = async () => {
- try {
- const res = await API.put(
- `${USER_FEEDBACK_BASE}/topics/${detail.topic.id}/close`,
- );
- if (res.data.success) {
- showSuccess(t('工单已关闭'));
- await openTopic(detail.topic.id);
- loadTopics();
- } else {
- showError(res.data.message);
- }
- } catch {
- showError(t('操作失败'));
- }
- };
-
- const handleCreateFiles = async (e) => {
- const files = Array.from(e.target.files || []);
- e.target.value = '';
- const room = FEEDBACK_MAX_IMAGES - createImages.length;
- if (room <= 0) {
- showError(t('最多上传 3 张图片'));
- return;
- }
- try {
- const encoded = await Promise.all(
- files.slice(0, room).map((f) => compressImageToBase64(f)),
- );
- setCreateImages((prev) => [...prev, ...encoded]);
- } catch {
- showError(t('图片处理失败'));
- }
- };
-
- const submitCreate = async () => {
- if (!form.title.trim()) {
- showError(t('请填写标题'));
- return;
- }
- if (!form.content.trim() && createImages.length === 0) {
- showError(t('请填写内容或上传图片'));
- return;
- }
- setCreating(true);
- try {
- const res = await API.post(`${USER_FEEDBACK_BASE}/topics`, {
- category: form.category,
- title: form.title.trim(),
- content: form.content.trim(),
- images: createImages,
- });
- if (res.data.success) {
- showSuccess(t('工单已创建'));
- setForm({ category: 2, title: '', content: '' });
- setCreateImages([]);
- setShowCreate(false);
- await loadTopics();
- openTopic(res.data.data.id);
- } else {
- showError(res.data.message);
- }
- } finally {
- setCreating(false);
- }
- };
-
- // ─── 渲染 ───────────────────────────────────────────────────────────────────
-
- const renderTopicRow = (tp) => {
- const st = FEEDBACK_STATUS[tp.status] || {};
- const cat = FEEDBACK_CATEGORY[tp.category] || {};
- return (
-
openTopic(tp.id)}
- >
-
- {tp.user_unread && }
-
- {tp.title}
-
-
- {t(cat.label)}
-
-
-
-
- {t(st.label)}
-
-
- {new Date(tp.last_reply_at).toLocaleDateString()}
-
-
-
- );
- };
-
- const header = (
-
-
-
-
-
-
-
{t('我的工单')}
-
- {t('遇到问题或有建议,随时向我们反馈')}
-
-
-
- {!detail && !showCreate && (
-
- )}
-
- );
-
- // 详情视图
- if (detail) {
- const st = FEEDBACK_STATUS[detail.topic.status] || {};
- const closed = detail.topic.status === 4;
- return (
-
-
-
}
- theme='borderless'
- onClick={() => {
- setDetail(null);
- loadTopics();
- }}
- >
- {t('返回列表')}
-
-
- {t(st.label)}
- {!closed && (
-
- )}
-
-
-
- {detail.topic.title}
-
-
-
- );
- }
-
- // 新建视图
- if (showCreate) {
- return (
-
-
-
- {t('分类')}
-
-
- {t('标题')}
- setForm({ ...form, title: v })}
- maxLength={128}
- placeholder={t('一句话描述你的问题或建议')}
- className='mt-1'
- />
-
-
- {t('内容')}
-
-
-
-
}
- onClick={() => createFileRef.current?.click()}
- disabled={createImages.length >= FEEDBACK_MAX_IMAGES}
- >
- {t('添加图片')}({createImages.length}/{FEEDBACK_MAX_IMAGES})
-
- {createImages.length > 0 && (
-
- {createImages.map((b64, idx) => (
-
-
-
- setCreateImages((prev) =>
- prev.filter((_, i) => i !== idx),
- )
- }
- />
-
- ))}
-
- )}
-
-
-
-
-
-
-
- );
- }
-
- // 列表视图
- return (
-
- {loadingList ? (
-
-
-
- ) : topics.length === 0 ? (
-
-
- {t('还没有工单,点击「新建工单」向我们反馈')}
-
-
- ) : (
- {topics.map(renderTopicRow)}
- )}
-
- );
-}
diff --git a/web/classic/src/helpers/render.jsx b/web/classic/src/helpers/render.jsx
index f7e5025efa5..e0ae28cbf69 100644
--- a/web/classic/src/helpers/render.jsx
+++ b/web/classic/src/helpers/render.jsx
@@ -161,6 +161,7 @@ export function getLucideIcon(key, selected = false) {
case 'enterprise':
return
;
case 'feedback':
+ case 'myfeedback':
return
;
case 'setting':
return
;
diff --git a/web/classic/src/hooks/common/useSidebar.js b/web/classic/src/hooks/common/useSidebar.js
index 484fe5f6c12..532c93ac065 100644
--- a/web/classic/src/hooks/common/useSidebar.js
+++ b/web/classic/src/hooks/common/useSidebar.js
@@ -43,6 +43,7 @@ export const DEFAULT_ADMIN_CONFIG = {
enabled: true,
topup: true,
personal: true,
+ myfeedback: true,
},
admin: {
enabled: true,
diff --git a/web/classic/src/i18n/locales/en.json b/web/classic/src/i18n/locales/en.json
index 4e609edc433..08724adde58 100644
--- a/web/classic/src/i18n/locales/en.json
+++ b/web/classic/src/i18n/locales/en.json
@@ -3996,6 +3996,11 @@
"最后回复": "Last reply",
"用户ID": "User ID",
"我的工单": "My Tickets",
+ "松开鼠标上传图片": "Release to upload images",
+ "或将图片拖拽到此处": "or drag and drop images here",
+ "查看与回复所有用户的工单": "View and reply to all users' tickets",
+ "用户查看与提交自己的工单": "Users view and submit their own tickets",
+ "查看与提交自己的工单": "View and submit your own tickets",
"工单管理": "Ticket Management",
"新建工单": "New Ticket",
"工单详情": "Ticket Details",
diff --git a/web/classic/src/pages/Feedback/MyFeedback.jsx b/web/classic/src/pages/Feedback/MyFeedback.jsx
new file mode 100644
index 00000000000..f42a59f486f
--- /dev/null
+++ b/web/classic/src/pages/Feedback/MyFeedback.jsx
@@ -0,0 +1,541 @@
+import React, { useEffect, useRef, useState } from 'react';
+import {
+ Badge,
+ Button,
+ Image,
+ Input,
+ Select,
+ SideSheet,
+ Space,
+ Table,
+ Tag,
+ TextArea,
+ Typography,
+} from '@douyinfe/semi-ui';
+import { IconComment, IconImage, IconClose } from '@douyinfe/semi-icons';
+import { API, showError, showSuccess } from '../../helpers';
+import { createCardProPagination } from '../../helpers/utils';
+import { useTranslation } from 'react-i18next';
+import { useIsMobile } from '../../hooks/common/useIsMobile';
+import CardPro from '../../components/common/ui/CardPro';
+import FeedbackThread from '../../components/feedback/FeedbackThread';
+import {
+ USER_FEEDBACK_BASE,
+ FEEDBACK_ROLE_USER,
+ FEEDBACK_STATUS,
+ FEEDBACK_CATEGORY,
+ FEEDBACK_CATEGORY_OPTIONS,
+ FEEDBACK_MAX_IMAGES,
+ encodeFeedbackImageFiles,
+} from '../../components/feedback/feedbackHelpers';
+
+const { Text } = Typography;
+
+const STATUS_OPTIONS = [
+ { value: 0, label: '全部状态' },
+ { value: 1, label: '待处理' },
+ { value: 2, label: '处理中' },
+ { value: 3, label: '已回复' },
+ { value: 4, label: '已关闭' },
+];
+
+// 通知侧边栏未读角标即时刷新(新建首单后恢复轮询 + 打开/关闭工单清未读)。
+const notifyChanged = () => window.dispatchEvent(new Event('feedback:changed'));
+
+export default function MyFeedbackPage() {
+ const { t } = useTranslation();
+ const isMobile = useIsMobile();
+ const [list, setList] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+ const [loading, setLoading] = useState(false);
+
+ // 筛选(用户只有自己的工单,无「按用户筛选」)
+ const [filters, setFilters] = useState({
+ status: 0,
+ category: 0,
+ keyword: '',
+ });
+
+ // 详情抽屉
+ const [detail, setDetail] = useState(null);
+ const [sending, setSending] = useState(false);
+
+ // 新建抽屉
+ const [showCreate, setShowCreate] = useState(false);
+ const [form, setForm] = useState({ category: 2, title: '', content: '' });
+ const [createImages, setCreateImages] = useState([]);
+ const [creating, setCreating] = useState(false);
+ const [createDragging, setCreateDragging] = useState(false);
+ const createFileRef = useRef(null);
+
+ const buildQuery = (p, ps, f) => {
+ const params = new URLSearchParams();
+ params.set('page', p);
+ params.set('page_size', ps);
+ if (f.status) params.set('status', f.status);
+ if (f.category) params.set('category', f.category);
+ if (f.keyword) params.set('keyword', f.keyword);
+ return params.toString();
+ };
+
+ // f 默认取当前 filters;重置等场景显式传入清空后的对象,避免闭包读到旧值。
+ const loadList = async (p = page, ps = pageSize, f = filters) => {
+ setLoading(true);
+ try {
+ const res = await API.get(
+ `${USER_FEEDBACK_BASE}/topics?${buildQuery(p, ps, f)}`,
+ );
+ if (res.data.success) {
+ setList(res.data.data || []);
+ setTotal(res.data.total || 0);
+ } else {
+ showError(res.data.message);
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadList(page, pageSize);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [page, pageSize]);
+
+ const handleSearch = () => {
+ setPage(1);
+ loadList(1, pageSize);
+ };
+
+ const handleReset = () => {
+ const cleared = { status: 0, category: 0, keyword: '' };
+ setFilters(cleared);
+ setPage(1);
+ loadList(1, pageSize, cleared);
+ };
+
+ const openDetail = async (id) => {
+ try {
+ const res = await API.get(
+ `${USER_FEEDBACK_BASE}/topics/${id}?page=1&page_size=200`,
+ );
+ if (res.data.success) {
+ setDetail(res.data.data);
+ loadList(); // 刷新未读
+ notifyChanged(); // 打开工单清未读 → 角标即时刷新
+ } else {
+ showError(res.data.message);
+ }
+ } catch {
+ showError(t('查询失败'));
+ }
+ };
+
+ const handleReply = async (content, images) => {
+ setSending(true);
+ try {
+ const res = await API.post(
+ `${USER_FEEDBACK_BASE}/topics/${detail.topic.id}/messages`,
+ { content, images },
+ );
+ if (res.data.success) {
+ await openDetail(detail.topic.id);
+ return true;
+ }
+ showError(res.data.message);
+ return false;
+ } catch {
+ showError(t('发送失败'));
+ return false;
+ } finally {
+ setSending(false);
+ }
+ };
+
+ const handleClose = async () => {
+ try {
+ const res = await API.put(
+ `${USER_FEEDBACK_BASE}/topics/${detail.topic.id}/close`,
+ );
+ if (res.data.success) {
+ showSuccess(t('工单已关闭'));
+ await openDetail(detail.topic.id);
+ loadList();
+ } else {
+ showError(res.data.message);
+ }
+ } catch {
+ showError(t('操作失败'));
+ }
+ };
+
+ // ─── 新建工单 ───────────────────────────────────────────────────────────────
+
+ const resetCreate = () => {
+ setForm({ category: 2, title: '', content: '' });
+ setCreateImages([]);
+ };
+
+ // 点击选择与拖拽共用:处理一批文件 → 追加到 createImages。
+ const addCreateFiles = async (fileList) => {
+ const { encoded, error } = await encodeFeedbackImageFiles(
+ fileList,
+ createImages.length,
+ );
+ if (error) showError(t(error));
+ // 函数式封顶:即使并发拖拽/选择读到的是旧 count,也保证不超过上限。
+ if (encoded.length)
+ setCreateImages((prev) =>
+ [...prev, ...encoded].slice(0, FEEDBACK_MAX_IMAGES),
+ );
+ };
+
+ const handleCreateFiles = async (e) => {
+ const fileList = e.target.files;
+ e.target.value = '';
+ await addCreateFiles(fileList);
+ };
+
+ const handleCreateDrop = async (e) => {
+ e.preventDefault();
+ setCreateDragging(false);
+ if (createImages.length >= FEEDBACK_MAX_IMAGES) {
+ showError(t('最多上传 3 张图片'));
+ return;
+ }
+ await addCreateFiles(e.dataTransfer.files);
+ };
+
+ const submitCreate = async () => {
+ if (!form.title.trim()) {
+ showError(t('请填写标题'));
+ return;
+ }
+ if (!form.content.trim() && createImages.length === 0) {
+ showError(t('请填写内容或上传图片'));
+ return;
+ }
+ setCreating(true);
+ try {
+ const res = await API.post(`${USER_FEEDBACK_BASE}/topics`, {
+ category: form.category,
+ title: form.title.trim(),
+ content: form.content.trim(),
+ images: createImages,
+ });
+ if (res.data.success) {
+ showSuccess(t('工单已创建'));
+ resetCreate();
+ setShowCreate(false);
+ await loadList(1, pageSize);
+ setPage(1);
+ notifyChanged();
+ openDetail(res.data.data.id);
+ } else {
+ showError(res.data.message);
+ }
+ } finally {
+ setCreating(false);
+ }
+ };
+
+ const columns = [
+ { title: 'ID', dataIndex: 'id', width: 70 },
+ {
+ title: t('标题'),
+ dataIndex: 'title',
+ render: (v, r) => (
+
+ {r.user_unread && }
+
+ {v}
+
+
+ ),
+ },
+ {
+ title: t('分类'),
+ dataIndex: 'category',
+ width: 110,
+ render: (v) => (
+
{t((FEEDBACK_CATEGORY[v] || {}).label)}
+ ),
+ },
+ {
+ title: t('状态'),
+ dataIndex: 'status',
+ width: 100,
+ render: (v) => {
+ const st = FEEDBACK_STATUS[v] || {};
+ return
{t(st.label)};
+ },
+ },
+ { title: t('消息数'), dataIndex: 'message_count', width: 80 },
+ {
+ title: t('创建时间'),
+ dataIndex: 'created_at',
+ width: 170,
+ render: (v) => new Date(v).toLocaleString(),
+ },
+ {
+ title: t('最后回复'),
+ dataIndex: 'last_reply_at',
+ width: 170,
+ render: (v) => new Date(v).toLocaleString(),
+ },
+ {
+ title: t('操作'),
+ width: 90,
+ render: (_, r) => (
+
+ ),
+ },
+ ];
+
+ const descriptionArea = (
+
+
+ {t('我的工单')}
+
+ );
+
+ const actionsArea = (
+
+
+ );
+
+ return (
+
+
setPage(p),
+ onPageSizeChange: (ps) => {
+ setPageSize(ps);
+ setPage(1);
+ },
+ isMobile,
+ t,
+ })}
+ t={t}
+ >
+
+
+
+ {/* 详情抽屉 */}
+
{
+ setDetail(null);
+ loadList();
+ }}
+ width={560}
+ >
+ {detail && (
+
+
+
+
+ {t((FEEDBACK_CATEGORY[detail.topic.category] || {}).label)}
+
+
+ {t((FEEDBACK_STATUS[detail.topic.status] || {}).label)}
+
+
+ {detail.topic.status !== 4 && (
+
+ )}
+
+
+
+ )}
+
+
+ {/* 新建抽屉 */}
+
setShowCreate(false)}
+ width={480}
+ >
+
+
+ {t('分类')}
+ setForm({ ...form, category: v })}
+ className='w-full mt-1'
+ optionList={FEEDBACK_CATEGORY_OPTIONS.map((o) => ({
+ value: o.value,
+ label: t(o.label),
+ }))}
+ />
+
+
+ {t('标题')}
+ setForm({ ...form, title: v })}
+ maxLength={128}
+ placeholder={t('一句话描述你的问题或建议')}
+ className='mt-1'
+ />
+
+
+ {t('内容')}
+
+
{
+ e.preventDefault();
+ if (!createDragging) setCreateDragging(true);
+ }}
+ onDragLeave={(e) => {
+ if (!e.currentTarget.contains(e.relatedTarget))
+ setCreateDragging(false);
+ }}
+ onDrop={handleCreateDrop}
+ >
+
+
+ }
+ onClick={() => createFileRef.current?.click()}
+ disabled={createImages.length >= FEEDBACK_MAX_IMAGES}
+ >
+ {t('添加图片')}({createImages.length}/{FEEDBACK_MAX_IMAGES})
+
+
+ {createDragging
+ ? t('松开鼠标上传图片')
+ : t('或将图片拖拽到此处')}
+
+
+ {createImages.length > 0 && (
+
+ {createImages.map((b64, idx) => (
+
+
+
+ setCreateImages((prev) =>
+ prev.filter((_, i) => i !== idx),
+ )
+ }
+ />
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/classic/src/pages/Feedback/index.jsx b/web/classic/src/pages/Feedback/index.jsx
index 91d7ef3dac1..163cd0d49f7 100644
--- a/web/classic/src/pages/Feedback/index.jsx
+++ b/web/classic/src/pages/Feedback/index.jsx
@@ -205,6 +205,12 @@ export default function FeedbackPage() {
},
},
{ title: t('消息数'), dataIndex: 'message_count', width: 80 },
+ {
+ title: t('创建时间'),
+ dataIndex: 'created_at',
+ width: 170,
+ render: (v) => new Date(v).toLocaleString(),
+ },
{
title: t('最后回复'),
dataIndex: 'last_reply_at',
diff --git a/web/classic/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx b/web/classic/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx
index 8c13cac010e..6755455e524 100644
--- a/web/classic/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx
+++ b/web/classic/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx
@@ -30,6 +30,7 @@ import {
} from '@douyinfe/semi-ui';
import { API, showSuccess, showError } from '../../../helpers';
import { StatusContext } from '../../../context/Status';
+import { mergeAdminConfig } from '../../../hooks/common/useSidebar';
const { Text } = Typography;
@@ -57,6 +58,7 @@ export default function SettingsSidebarModulesAdmin(props) {
enabled: true,
topup: true,
personal: true,
+ myfeedback: true,
},
admin: {
enabled: true,
@@ -65,6 +67,7 @@ export default function SettingsSidebarModulesAdmin(props) {
deployment: true,
redemption: true,
user: true,
+ feedback: true,
kyc: true,
enterprise: true,
subscription: true,
@@ -121,6 +124,7 @@ export default function SettingsSidebarModulesAdmin(props) {
enabled: true,
topup: true,
personal: true,
+ myfeedback: true,
},
admin: {
enabled: true,
@@ -129,6 +133,7 @@ export default function SettingsSidebarModulesAdmin(props) {
deployment: true,
redemption: true,
user: true,
+ feedback: true,
kyc: true,
enterprise: true,
subscription: true,
@@ -179,7 +184,11 @@ export default function SettingsSidebarModulesAdmin(props) {
// 从 props.options 中获取配置
if (props.options && props.options.SidebarModulesAdmin) {
try {
- const modules = JSON.parse(props.options.SidebarModulesAdmin);
+ // 与运行时(useSidebar)同一套 merge:已存配置缺少新模块键时用默认补齐,
+ // 避免新增模块(如 myfeedback)在开关上显示为关、却实际可见,甚至误存盘隐藏。
+ const modules = mergeAdminConfig(
+ JSON.parse(props.options.SidebarModulesAdmin),
+ );
setSidebarModulesAdmin(modules);
} catch (error) {
// 使用默认配置
@@ -193,7 +202,12 @@ export default function SettingsSidebarModulesAdmin(props) {
midjourney: true,
task: true,
},
- personal: { enabled: true, topup: true, personal: true },
+ personal: {
+ enabled: true,
+ topup: true,
+ personal: true,
+ myfeedback: true,
+ },
admin: {
enabled: true,
channel: true,
@@ -201,6 +215,7 @@ export default function SettingsSidebarModulesAdmin(props) {
deployment: true,
redemption: true,
user: true,
+ feedback: true,
kyc: true,
enterprise: true,
subscription: true,
@@ -255,6 +270,11 @@ export default function SettingsSidebarModulesAdmin(props) {
title: t('个人设置'),
description: t('个人信息设置'),
},
+ {
+ key: 'myfeedback',
+ title: t('我的工单'),
+ description: t('用户查看与提交自己的工单'),
+ },
],
},
{
@@ -285,8 +305,21 @@ export default function SettingsSidebarModulesAdmin(props) {
description: t('兑换码生成管理'),
},
{ key: 'user', title: t('用户管理'), description: t('用户账户管理') },
- { key: 'kyc', title: t('实名认证'), description: t('实名认证审核管理') },
- { key: 'enterprise', title: t('企业认证'), description: t('企业认证审核管理') },
+ {
+ key: 'feedback',
+ title: t('工单管理'),
+ description: t('查看与回复所有用户的工单'),
+ },
+ {
+ key: 'kyc',
+ title: t('实名认证'),
+ description: t('实名认证审核管理'),
+ },
+ {
+ key: 'enterprise',
+ title: t('企业认证'),
+ description: t('企业认证审核管理'),
+ },
{
key: 'setting',
title: t('系统设置'),
diff --git a/web/classic/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx b/web/classic/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
index eb70724fc1e..4e8c078d119 100644
--- a/web/classic/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
+++ b/web/classic/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
@@ -30,7 +30,6 @@ import {
} from '@douyinfe/semi-ui';
import { API, showSuccess, showError } from '../../../helpers';
import { StatusContext } from '../../../context/Status';
-import { UserContext } from '../../../context/User';
import { useUserPermissions } from '../../../hooks/common/useUserPermissions';
import { mergeAdminConfig, useSidebar } from '../../../hooks/common/useSidebar';
import { Settings } from 'lucide-react';
@@ -95,6 +94,7 @@ export default function SettingsSidebarModulesUser() {
enabled: true,
topup: isSidebarModuleAllowed('personal', 'topup'),
personal: isSidebarModuleAllowed('personal', 'personal'),
+ myfeedback: isSidebarModuleAllowed('personal', 'myfeedback'),
};
}
@@ -330,6 +330,11 @@ export default function SettingsSidebarModulesUser() {
title: t('个人设置'),
description: t('个人信息设置'),
},
+ {
+ key: 'myfeedback',
+ title: t('我的工单'),
+ description: t('查看与提交自己的工单'),
+ },
],
},
{