+
{error}
) : null}
{submitted ? (
-
+
反馈已提交
) : null}
{notice ? (
-
+
{notice}
) : null}
@@ -293,7 +332,7 @@ export function PlatformFeedbackView({
type="button"
onClick={submitFeedback}
disabled={isSubmitting}
- className="mt-2 flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-[#2f7cf6] text-base font-semibold text-white shadow-[0_10px_22px_rgba(47,124,246,0.26)] transition hover:bg-[#1f6bea] disabled:cursor-not-allowed disabled:opacity-60"
+ className="platform-button platform-button--primary mt-2 h-12 w-full justify-center text-base disabled:cursor-not-allowed disabled:opacity-60"
>
{isSubmitting ? '提交中' : '提交'}
@@ -302,19 +341,10 @@ export function PlatformFeedbackView({
-
-
diff --git a/src/services/rpg-entry/rpgProfileClient.test.ts b/src/services/rpg-entry/rpgProfileClient.test.ts
index 79d97338..d36f965a 100644
--- a/src/services/rpg-entry/rpgProfileClient.test.ts
+++ b/src/services/rpg-entry/rpgProfileClient.test.ts
@@ -9,6 +9,7 @@ import {
listRpgProfileBrowseHistory,
listRpgProfileSaveArchives,
resumeRpgProfileSaveArchive,
+ submitRpgProfileFeedback,
syncRpgProfileBrowseHistory,
upsertRpgProfileBrowseHistory,
} from './rpgProfileClient';
@@ -156,3 +157,59 @@ describe('rpgProfileClient save archive routes', () => {
);
});
});
+
+describe('rpgProfileClient feedback routes', () => {
+ beforeEach(() => {
+ requestJsonMock.mockReset();
+ requestJsonMock.mockResolvedValue({
+ feedback: {
+ feedbackId: 'feedback:user-1:1',
+ status: 'open',
+ createdAt: '2026-05-08T10:00:00Z',
+ evidenceItems: [],
+ },
+ });
+ });
+
+ it('submits profile feedback through the profile route', async () => {
+ await submitRpgProfileFeedback({
+ description: '图片上传后没有展示预览',
+ contactPhone: null,
+ evidenceItems: [
+ {
+ fileName: 'preview.png',
+ contentType: 'image/png',
+ sizeBytes: 128,
+ dataUrl: 'data:image/png;base64,ZmVlZGJhY2s=',
+ },
+ ],
+ });
+
+ expect(requestJsonMock).toHaveBeenCalledWith(
+ '/api/profile/feedback',
+ expect.objectContaining({
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ }),
+ '提交反馈失败',
+ expect.objectContaining({
+ retry: expect.objectContaining({
+ maxRetries: 1,
+ retryUnsafeMethods: true,
+ }),
+ }),
+ );
+ expect(JSON.parse(requestJsonMock.mock.calls[0][1].body)).toEqual({
+ description: '图片上传后没有展示预览',
+ contactPhone: null,
+ evidenceItems: [
+ {
+ fileName: 'preview.png',
+ contentType: 'image/png',
+ sizeBytes: 128,
+ dataUrl: 'data:image/png;base64,ZmVlZGJhY2s=',
+ },
+ ],
+ });
+ });
+});
diff --git a/src/services/rpg-entry/rpgProfileClient.ts b/src/services/rpg-entry/rpgProfileClient.ts
index 564f9ee6..5da1b211 100644
--- a/src/services/rpg-entry/rpgProfileClient.ts
+++ b/src/services/rpg-entry/rpgProfileClient.ts
@@ -15,6 +15,8 @@ import type {
RedeemProfileReferralInviteCodeResponse,
RedeemProfileRewardCodeResponse,
RuntimeSettings,
+ SubmitProfileFeedbackRequest,
+ SubmitProfileFeedbackResponse,
} from '../../../packages/shared/src/contracts/runtime';
import { rehydrateSavedSnapshot } from '../../persistence/runtimeSnapshot';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
@@ -101,6 +103,22 @@ export function createRpgProfileRechargeOrder(
);
}
+export function submitRpgProfileFeedback(
+ payload: SubmitProfileFeedbackRequest,
+ options: RuntimeRequestOptions = {},
+) {
+ return requestRpgRuntimeJson
(
+ '/profile/feedback',
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ },
+ '提交反馈失败',
+ options,
+ );
+}
+
export function getRpgProfileReferralInviteCenter(
options: RuntimeRequestOptions = {},
) {
@@ -276,6 +294,7 @@ export const rpgProfileClient = {
getWalletLedger: getRpgProfileWalletLedger,
getRechargeCenter: getRpgProfileRechargeCenter,
createRechargeOrder: createRpgProfileRechargeOrder,
+ submitFeedback: submitRpgProfileFeedback,
getReferralInviteCenter: getRpgProfileReferralInviteCenter,
redeemReferralInviteCode: redeemRpgProfileReferralInviteCode,
getTasks: getRpgProfileTasks,