Merge remote-tracking branch 'origin/master' into feat/agc_add_on
# Conflicts: # docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md
This commit is contained in:
@@ -74,9 +74,9 @@ test('后台账号创建和更新同时携带 Tab 与独立操作权限', async
|
||||
|
||||
test('账号配置一次提交初始和每日免费泥点', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({configId: 'profile_wallet'}), {
|
||||
new Response(JSON.stringify({ configId: 'profile_wallet' }), {
|
||||
status: 200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
@@ -90,7 +90,7 @@ test('账号配置一次提交初始和每日免费泥点', async () => {
|
||||
'/admin/api/profile/wallet-config',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({Authorization: 'Bearer owner-token'}),
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer owner-token' }),
|
||||
body: JSON.stringify({
|
||||
initialMudPoints: 100,
|
||||
dailyFreePointsPerDay: 35,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {render, screen, waitFor} from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {beforeEach, expect, test, vi} from 'vitest';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getAdminUserDetail,
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
AdminProfileWalletPayload,
|
||||
AdminUserDetailResponse,
|
||||
} from '../api/adminApiTypes';
|
||||
import {AdminUserReferenceButton} from './AdminUserReferenceButton';
|
||||
import { AdminUserReferenceButton } from './AdminUserReferenceButton';
|
||||
|
||||
vi.mock('../api/adminApiClient', () => ({
|
||||
formatAdminApiError: vi.fn((error: unknown) =>
|
||||
@@ -93,7 +93,7 @@ beforeEach(() => {
|
||||
changed: true,
|
||||
reconciledAtMicros: 1_720_000_000_000_000,
|
||||
});
|
||||
vi.mocked(updateAdminWalletRestriction).mockResolvedValue({wallet});
|
||||
vi.mocked(updateAdminWalletRestriction).mockResolvedValue({ wallet });
|
||||
});
|
||||
|
||||
test('用户查看按钮按内部 ID 查询并展示脱敏资料、余额与退款限制', async () => {
|
||||
@@ -109,10 +109,10 @@ test('用户查看按钮按内部 ID 查询并展示脱敏资料、余额与退
|
||||
</div>,
|
||||
);
|
||||
|
||||
const trigger = screen.getByRole('button', {name: '查看用户信息'});
|
||||
const trigger = screen.getByRole('button', { name: '查看用户信息' });
|
||||
await user.click(trigger);
|
||||
expect(parentClick).not.toHaveBeenCalled();
|
||||
expect(await screen.findByRole('dialog', {name: '用户详情'})).toBeTruthy();
|
||||
expect(await screen.findByRole('dialog', { name: '用户详情' })).toBeTruthy();
|
||||
expect(getAdminUserDetail).toHaveBeenCalledWith('admin-token', {
|
||||
userId: 'user-1',
|
||||
publicUserCode: undefined,
|
||||
@@ -121,13 +121,15 @@ test('用户查看按钮按内部 ID 查询并展示脱敏资料、余额与退
|
||||
expect(screen.getAllByText('TN1001').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('138****5678')).toBeTruthy();
|
||||
expect(screen.getByText('退款欠账限制')).toBeTruthy();
|
||||
expect(screen.getByText('25', {selector: 'strong'})).toBeTruthy();
|
||||
expect(screen.getByText('25', { selector: 'strong' })).toBeTruthy();
|
||||
expect(screen.getByText('历史花费')).toBeTruthy();
|
||||
expect(screen.getByText('1234', {selector: 'strong'})).toBeTruthy();
|
||||
expect(screen.getByText('1234', { selector: 'strong' })).toBeTruthy();
|
||||
expect(screen.getByText('order-1')).toBeTruthy();
|
||||
|
||||
await user.keyboard('{Escape}');
|
||||
await waitFor(() => expect(screen.queryByRole('dialog', {name: '用户详情'})).toBeNull());
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog', { name: '用户详情' })).toBeNull(),
|
||||
);
|
||||
await waitFor(() => expect(document.activeElement).toBe(trigger));
|
||||
});
|
||||
|
||||
@@ -141,7 +143,7 @@ test('只有陶泥号时按 publicUserCode 查询用户', async () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
||||
await user.click(screen.getByRole('button', { name: '查看用户信息' }));
|
||||
await screen.findByText('陶泥用户');
|
||||
expect(getAdminUserDetail).toHaveBeenCalledWith('admin-token', {
|
||||
userId: undefined,
|
||||
@@ -159,10 +161,10 @@ test('历史花费支持手动对账并用权威结果校准展示', async () =>
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
||||
await user.click(screen.getByRole('button', { name: '查看用户信息' }));
|
||||
await screen.findByText('陶泥用户');
|
||||
await user.click(screen.getByRole('button', {name: '手动对账历史花费'}));
|
||||
await user.click(screen.getByRole('button', {name: '确认'}));
|
||||
await user.click(screen.getByRole('button', { name: '手动对账历史花费' }));
|
||||
await user.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(reconcileAdminUserConsumption).toHaveBeenCalledWith('admin-token', {
|
||||
@@ -170,7 +172,7 @@ test('历史花费支持手动对账并用权威结果校准展示', async () =>
|
||||
});
|
||||
});
|
||||
expect(await screen.findByText('对账完成,历史花费已校准')).toBeTruthy();
|
||||
expect(screen.getByText('1300', {selector: 'strong'})).toBeTruthy();
|
||||
expect(screen.getByText('1300', { selector: 'strong' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('没有独立操作权限时不显示历史花费对账按钮', async () => {
|
||||
@@ -187,10 +189,10 @@ test('没有独立操作权限时不显示历史花费对账按钮', async () =>
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
||||
await user.click(screen.getByRole('button', { name: '查看用户信息' }));
|
||||
await screen.findByText('陶泥用户');
|
||||
|
||||
expect(screen.queryByRole('button', {name: '手动对账历史花费'})).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '手动对账历史花费' })).toBeNull();
|
||||
});
|
||||
|
||||
test('人工冻结和解除人工冻结分别提交原因且不解除退款欠账限制', async () => {
|
||||
@@ -211,8 +213,8 @@ test('人工冻结和解除人工冻结分别提交原因且不解除退款欠
|
||||
},
|
||||
};
|
||||
vi.mocked(updateAdminWalletRestriction)
|
||||
.mockResolvedValueOnce({wallet: manuallyFrozenWallet})
|
||||
.mockResolvedValueOnce({wallet: {...wallet, manualFrozen: false}});
|
||||
.mockResolvedValueOnce({ wallet: manuallyFrozenWallet })
|
||||
.mockResolvedValueOnce({ wallet: { ...wallet, manualFrozen: false } });
|
||||
render(
|
||||
<AdminUserReferenceButton
|
||||
token="admin-token"
|
||||
@@ -220,32 +222,48 @@ test('人工冻结和解除人工冻结分别提交原因且不解除退款欠
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
||||
await user.click(screen.getByRole('button', { name: '查看用户信息' }));
|
||||
await screen.findByText('陶泥用户');
|
||||
|
||||
await user.type(screen.getByRole('textbox', {name: '人工冻结操作原因'}), '异常登录');
|
||||
await user.click(screen.getByRole('button', {name: '人工冻结钱包'}));
|
||||
await user.click(screen.getByRole('button', {name: '确认'}));
|
||||
await user.type(
|
||||
screen.getByRole('textbox', { name: '人工冻结操作原因' }),
|
||||
'异常登录',
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: '人工冻结钱包' }));
|
||||
await user.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(updateAdminWalletRestriction).toHaveBeenNthCalledWith(1, 'admin-token', {
|
||||
userId: 'user-1',
|
||||
frozen: true,
|
||||
reason: '异常登录',
|
||||
});
|
||||
expect(updateAdminWalletRestriction).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'admin-token',
|
||||
{
|
||||
userId: 'user-1',
|
||||
frozen: true,
|
||||
reason: '异常登录',
|
||||
},
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText(/后台负责人/)).toBeTruthy();
|
||||
expect(screen.queryByText(/admin:root/)).toBeNull();
|
||||
|
||||
expect(await screen.findByText('解除人工冻结后,退款欠账限制仍会保留。')).toBeTruthy();
|
||||
await user.type(screen.getByRole('textbox', {name: '人工冻结操作原因'}), '核查完成');
|
||||
await user.click(screen.getByRole('button', {name: '解除人工冻结'}));
|
||||
await user.click(screen.getByRole('button', {name: '确认'}));
|
||||
expect(
|
||||
await screen.findByText('解除人工冻结后,退款欠账限制仍会保留。'),
|
||||
).toBeTruthy();
|
||||
await user.type(
|
||||
screen.getByRole('textbox', { name: '人工冻结操作原因' }),
|
||||
'核查完成',
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: '解除人工冻结' }));
|
||||
await user.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(updateAdminWalletRestriction).toHaveBeenNthCalledWith(2, 'admin-token', {
|
||||
userId: 'user-1',
|
||||
frozen: false,
|
||||
reason: '核查完成',
|
||||
});
|
||||
expect(updateAdminWalletRestriction).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'admin-token',
|
||||
{
|
||||
userId: 'user-1',
|
||||
frozen: false,
|
||||
reason: '核查完成',
|
||||
},
|
||||
);
|
||||
});
|
||||
expect(screen.getByText('退款欠账限制')).toBeTruthy();
|
||||
});
|
||||
@@ -263,9 +281,9 @@ test('用户详情读取失败后可以重试', async () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
||||
await user.click(screen.getByRole('button', { name: '查看用户信息' }));
|
||||
expect(await screen.findByText('读取失败')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', {name: '重试'}));
|
||||
await user.click(screen.getByRole('button', { name: '重试' }));
|
||||
expect(await screen.findByText('陶泥用户')).toBeTruthy();
|
||||
expect(getAdminUserDetail).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {RefreshCcw, ShieldAlert, UserRound, X} from 'lucide-react';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
import { RefreshCcw, ShieldAlert, UserRound, X } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import {
|
||||
formatAdminApiError,
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
AdminProfileWalletPayload,
|
||||
AdminUserDetailResponse,
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from './useAdminWriteConfirm';
|
||||
import { useAdminWriteConfirm } from './useAdminWriteConfirm';
|
||||
|
||||
interface AdminUserDetailDialogProps {
|
||||
token: string;
|
||||
@@ -35,11 +35,12 @@ export function AdminUserDetailDialog({
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [restrictionReason, setRestrictionReason] = useState('');
|
||||
const [isSavingRestriction, setIsSavingRestriction] = useState(false);
|
||||
const [isReconcilingConsumption, setIsReconcilingConsumption] = useState(false);
|
||||
const [isReconcilingConsumption, setIsReconcilingConsumption] =
|
||||
useState(false);
|
||||
const [reconcileMessage, setReconcileMessage] = useState('');
|
||||
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const requestVersionRef = useRef(0);
|
||||
const {confirmWrite, confirmDialog, isConfirming} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog, isConfirming } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void loadDetail();
|
||||
@@ -139,7 +140,7 @@ export function AdminUserDetailDialog({
|
||||
reason,
|
||||
});
|
||||
setDetail((current) =>
|
||||
current ? {...current, wallet: response.wallet} : current,
|
||||
current ? { ...current, wallet: response.wallet } : current,
|
||||
);
|
||||
setRestrictionReason('');
|
||||
} catch (error: unknown) {
|
||||
@@ -219,7 +220,9 @@ export function AdminUserDetailDialog({
|
||||
<div className="admin-panel-heading">
|
||||
<div>
|
||||
<h3 id="admin-user-detail-title">用户详情</h3>
|
||||
<span>{detail?.publicUserCode || publicUserCode || userId || '-'}</span>
|
||||
<span>
|
||||
{detail?.publicUserCode || publicUserCode || userId || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-detail-actions">
|
||||
<button
|
||||
@@ -292,9 +295,14 @@ export function AdminUserDetailDialog({
|
||||
</div>
|
||||
{detail.wallet.manualRestriction ? (
|
||||
<div className="admin-user-restriction-record">
|
||||
<span>{detail.wallet.manualRestriction.reason || '未填写原因'}</span>
|
||||
<span>
|
||||
{detail.wallet.manualRestriction.reason || '未填写原因'}
|
||||
</span>
|
||||
<small>
|
||||
{formatMicros(detail.wallet.manualRestriction.updatedAtMicros)} /{' '}
|
||||
{formatMicros(
|
||||
detail.wallet.manualRestriction.updatedAtMicros,
|
||||
)}{' '}
|
||||
/{' '}
|
||||
{detail.wallet.manualRestriction.updatedByAdminDisplayName}
|
||||
</small>
|
||||
</div>
|
||||
@@ -312,7 +320,9 @@ export function AdminUserDetailDialog({
|
||||
aria-label="人工冻结操作原因"
|
||||
disabled={isSavingRestriction || isReconcilingConsumption}
|
||||
value={restrictionReason}
|
||||
onChange={(event) => setRestrictionReason(event.target.value)}
|
||||
onChange={(event) =>
|
||||
setRestrictionReason(event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
@@ -362,7 +372,9 @@ export function AdminUserDetailDialog({
|
||||
{detail.rechargeOrders.map((order) => (
|
||||
<tr key={order.orderId}>
|
||||
<td>
|
||||
<span className="admin-mono-value">{order.orderId}</span>
|
||||
<span className="admin-mono-value">
|
||||
{order.orderId}
|
||||
</span>
|
||||
<small>{formatMicros(order.createdAtMicros)}</small>
|
||||
</td>
|
||||
<td>
|
||||
@@ -393,12 +405,15 @@ export function AdminUserDetailDialog({
|
||||
);
|
||||
}
|
||||
|
||||
function UserIdentityHeader({detail}: {detail: AdminUserDetailResponse}) {
|
||||
function UserIdentityHeader({ detail }: { detail: AdminUserDetailResponse }) {
|
||||
return (
|
||||
<section className="admin-user-identity">
|
||||
<div className="admin-user-avatar">
|
||||
{detail.avatarUrl ? (
|
||||
<img alt={`${detail.displayName || detail.publicUserCode}头像`} src={detail.avatarUrl} />
|
||||
<img
|
||||
alt={`${detail.displayName || detail.publicUserCode}头像`}
|
||||
src={detail.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<UserRound size={30} aria-hidden="true" />
|
||||
)}
|
||||
@@ -423,7 +438,8 @@ function UserIdentityHeader({detail}: {detail: AdminUserDetailResponse}) {
|
||||
<div>
|
||||
<dt>绑定状态</dt>
|
||||
<dd>
|
||||
{detail.bindingStatus || '-'} / 手机{detail.phoneBound ? '已绑定' : '未绑定'} / 微信
|
||||
{detail.bindingStatus || '-'} / 手机
|
||||
{detail.phoneBound ? '已绑定' : '未绑定'} / 微信
|
||||
{detail.wechatBound ? '已绑定' : '未绑定'}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -477,7 +493,9 @@ function WalletSection({
|
||||
</button>
|
||||
) : null}
|
||||
<div className="admin-tag-list">
|
||||
{wallet.manualFrozen ? <span className="admin-tag">人工冻结</span> : null}
|
||||
{wallet.manualFrozen ? (
|
||||
<span className="admin-tag">人工冻结</span>
|
||||
) : null}
|
||||
{wallet.refundDebtFrozen ? (
|
||||
<span className="admin-tag">退款欠账限制</span>
|
||||
) : null}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {UserRoundSearch} from 'lucide-react';
|
||||
import {MouseEvent, useRef, useState} from 'react';
|
||||
import { UserRoundSearch } from 'lucide-react';
|
||||
import { MouseEvent, useRef, useState } from 'react';
|
||||
|
||||
import {AdminUserDetailDialog} from './AdminUserDetailDialog';
|
||||
import { AdminUserDetailDialog } from './AdminUserDetailDialog';
|
||||
|
||||
interface AdminUserReferenceButtonProps {
|
||||
token: string;
|
||||
@@ -21,9 +21,9 @@ export function AdminUserReferenceButton({
|
||||
const normalizedUserId = normalizeUserReference(userId);
|
||||
const normalizedPublicUserCode = normalizeUserReference(publicUserCode);
|
||||
const lookup = normalizedUserId
|
||||
? {userId: normalizedUserId}
|
||||
? { userId: normalizedUserId }
|
||||
: normalizedPublicUserCode
|
||||
? {publicUserCode: normalizedPublicUserCode}
|
||||
? { publicUserCode: normalizedPublicUserCode }
|
||||
: null;
|
||||
|
||||
if (!lookup) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface AdminWriteConfirmOptions {
|
||||
action: string;
|
||||
@@ -10,7 +10,9 @@ interface PendingConfirm extends AdminWriteConfirmOptions {
|
||||
}
|
||||
|
||||
export function useAdminWriteConfirm() {
|
||||
const [pendingConfirm, setPendingConfirm] = useState<PendingConfirm | null>(null);
|
||||
const [pendingConfirm, setPendingConfirm] = useState<PendingConfirm | null>(
|
||||
null,
|
||||
);
|
||||
const cancelButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const confirmWrite = useCallback((options: AdminWriteConfirmOptions) => {
|
||||
@@ -19,7 +21,7 @@ export function useAdminWriteConfirm() {
|
||||
if (current) {
|
||||
current.resolve(false);
|
||||
}
|
||||
return {...options, resolve};
|
||||
return { ...options, resolve };
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import './styles/admin.css';
|
||||
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import {AdminApp} from './app/AdminApp';
|
||||
import { AdminApp } from './app/AdminApp';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {Plus, RefreshCcw, Save} from 'lucide-react';
|
||||
import {type FormEvent, useEffect, useState} from 'react';
|
||||
import { Plus, RefreshCcw, Save } from 'lucide-react';
|
||||
import { type FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
createAdminAccount,
|
||||
listAdminAccounts,
|
||||
updateAdminAccount,
|
||||
} from '../api/adminApiClient';
|
||||
import type {AdminAccountPayload} from '../api/adminApiTypes';
|
||||
import {adminRoutes} from '../app/adminRoutes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import type { AdminAccountPayload } from '../api/adminApiTypes';
|
||||
import { adminRoutes } from '../app/adminRoutes';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminAccountsPageProps {
|
||||
token: string;
|
||||
@@ -34,7 +34,7 @@ export function AdminAccountsPage({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshAccounts();
|
||||
@@ -87,8 +87,8 @@ export function AdminAccountsPage({
|
||||
checked
|
||||
? assignableRoutes
|
||||
.map((route) => route.id)
|
||||
.filter((routeId) =>
|
||||
routeId === permission || current.includes(routeId),
|
||||
.filter(
|
||||
(routeId) => routeId === permission || current.includes(routeId),
|
||||
)
|
||||
: current.filter((item) => item !== permission),
|
||||
);
|
||||
@@ -129,7 +129,7 @@ export function AdminAccountsPage({
|
||||
const response = selectedAccountId
|
||||
? await updateAdminAccount(token, selectedAccountId, {
|
||||
displayName: normalizedDisplayName,
|
||||
...(password ? {password} : {}),
|
||||
...(password ? { password } : {}),
|
||||
tabPermissions,
|
||||
actionPermissions,
|
||||
enabled,
|
||||
@@ -205,7 +205,9 @@ export function AdminAccountsPage({
|
||||
data-active={account.accountId === selectedAccountId}
|
||||
disabled={account.accountRole === 'owner'}
|
||||
key={account.accountId}
|
||||
title={account.accountRole === 'owner' ? 'owner' : account.username}
|
||||
title={
|
||||
account.accountRole === 'owner' ? 'owner' : account.username
|
||||
}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (account.accountRole === 'member') {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {Plus, Send, Trash2} from 'lucide-react';
|
||||
import {FormEvent, useMemo, useState} from 'react';
|
||||
import { Plus, Send, Trash2 } from 'lucide-react';
|
||||
import { FormEvent, useMemo, useState } from 'react';
|
||||
|
||||
import {debugAdminHttp} from '../api/adminApiClient';
|
||||
import { debugAdminHttp } from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminDebugHeaderInput,
|
||||
AdminDebugHttpMethod,
|
||||
AdminDebugHttpResponse,
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {formatUnknownJson, handlePageError} from './pageUtils';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { formatUnknownJson, handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminDebugHttpPageProps {
|
||||
token: string;
|
||||
@@ -34,7 +34,7 @@ export function AdminDebugHttpPage({
|
||||
const [result, setResult] = useState<AdminDebugHttpResponse | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
const jsonPreview = useMemo(
|
||||
() => formatUnknownJson(result?.bodyJson),
|
||||
@@ -117,10 +117,7 @@ export function AdminDebugHttpPage({
|
||||
className="admin-ghost-button"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setHeaders((current) => [
|
||||
...current,
|
||||
{name: '', value: ''},
|
||||
])
|
||||
setHeaders((current) => [...current, { name: '', value: '' }])
|
||||
}
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
@@ -135,7 +132,7 @@ export function AdminDebugHttpPage({
|
||||
setHeaders((current) =>
|
||||
current.map((item, itemIndex) =>
|
||||
itemIndex === index
|
||||
? {...item, name: event.target.value}
|
||||
? { ...item, name: event.target.value }
|
||||
: item,
|
||||
),
|
||||
)
|
||||
@@ -147,7 +144,7 @@ export function AdminDebugHttpPage({
|
||||
setHeaders((current) =>
|
||||
current.map((item, itemIndex) =>
|
||||
itemIndex === index
|
||||
? {...item, value: event.target.value}
|
||||
? { ...item, value: event.target.value }
|
||||
: item,
|
||||
),
|
||||
)
|
||||
@@ -198,7 +195,9 @@ export function AdminDebugHttpPage({
|
||||
<section className="admin-panel admin-result-panel">
|
||||
<div className="admin-panel-heading">
|
||||
<h3>结果</h3>
|
||||
<span>{result ? `${result.status} ${result.statusText}` : '-'}</span>
|
||||
<span>
|
||||
{result ? `${result.status} ${result.statusText}` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
{result ? (
|
||||
<>
|
||||
|
||||
@@ -642,9 +642,7 @@ test('后台素材查询将中间产物按真实类型逐行收纳在最终产
|
||||
expect(within(sourceRow!).getByText('角色生图')).toBeTruthy();
|
||||
expect(within(sourceRow!).getByText('12 泥点')).toBeTruthy();
|
||||
expect(within(postprocessRow!).getByText('角色抠图')).toBeTruthy();
|
||||
expect(
|
||||
within(postprocessRow!).getByText('本阶段不额外扣费'),
|
||||
).toBeTruthy();
|
||||
expect(within(postprocessRow!).getByText('本阶段不额外扣费')).toBeTruthy();
|
||||
|
||||
fireEvent.click(within(sourceRow!).getByRole('button', { name: '详情' }));
|
||||
const detail = await screen.findByRole('dialog', { name: '素材详情' });
|
||||
@@ -1189,9 +1187,7 @@ test('后台素材查询在现有预览弹窗播放完整角色动作序列', as
|
||||
});
|
||||
});
|
||||
const callsBeforeRetry = vi.mocked(getAdminAssetReadUrl).mock.calls.length;
|
||||
await user.click(
|
||||
within(dialog).getByRole('button', { name: '重试失败帧' }),
|
||||
);
|
||||
await user.click(within(dialog).getByRole('button', { name: '重试失败帧' }));
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(getAdminAssetReadUrl).mock.calls.length).toBeGreaterThan(
|
||||
callsBeforeRetry,
|
||||
@@ -1472,7 +1468,9 @@ test('后台素材详情分别展示真实操作和归组 Task ID', async () =>
|
||||
fireEvent.click(await screen.findByRole('button', { name: '详情' }));
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '素材详情' });
|
||||
expect(within(dialog).getByText('editor-atlas-split-operation-1')).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByText('editor-atlas-split-operation-1'),
|
||||
).toBeTruthy();
|
||||
expect(within(dialog).getByText('vector-engine-source-task-1')).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {beforeEach, expect, test, vi} from 'vitest';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getAdminEditorGenerationPricing,
|
||||
upsertAdminEditorGenerationPricing,
|
||||
} from '../api/adminApiClient';
|
||||
import type {EditorGenerationPricingConfigPayload} from '../api/adminApiTypes';
|
||||
import {AdminEditorGenerationPricingPage} from './AdminEditorGenerationPricingPage';
|
||||
import type { EditorGenerationPricingConfigPayload } from '../api/adminApiTypes';
|
||||
import { AdminEditorGenerationPricingPage } from './AdminEditorGenerationPricingPage';
|
||||
|
||||
vi.mock('../api/adminApiClient', () => ({
|
||||
formatAdminApiError: vi.fn((error: unknown) =>
|
||||
@@ -24,22 +24,22 @@ const pricing: EditorGenerationPricingConfigPayload = {
|
||||
models: {
|
||||
'gemini-3.1-flash-image-preview': {
|
||||
unit: 'perGeneration',
|
||||
prices: {'0.5K': 8, '1K': 12, '2K': 24},
|
||||
prices: { '0.5K': 8, '1K': 12, '2K': 24 },
|
||||
},
|
||||
'gpt-image-2': {
|
||||
unit: 'perGeneration',
|
||||
prices: {'1K': 3, '2K': 5},
|
||||
prices: { '1K': 3, '2K': 5 },
|
||||
},
|
||||
'seedance2.0-fast': {
|
||||
unit: 'perSecond',
|
||||
prices: {'480p': 10, '720p': 20, '1080p': 40},
|
||||
prices: { '480p': 10, '720p': 20, '1080p': 40 },
|
||||
},
|
||||
'seedance2.0': {
|
||||
unit: 'perSecond',
|
||||
prices: {'480p': 12, '720p': 24, '1080p': 48},
|
||||
prices: { '480p': 12, '720p': 24, '1080p': 48 },
|
||||
},
|
||||
'audio1.0': {unit: 'perGeneration', price: 5},
|
||||
'chirp-v5': {unit: 'perGeneration', price: 12},
|
||||
'audio1.0': { unit: 'perGeneration', price: 5 },
|
||||
'chirp-v5': { unit: 'perGeneration', price: 12 },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ beforeEach(() => {
|
||||
...pricing.models,
|
||||
'gpt-image-2': {
|
||||
unit: 'perGeneration',
|
||||
prices: {'1K': 20, '2K': 58},
|
||||
prices: { '1K': 20, '2K': 58 },
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -70,9 +70,9 @@ test('模型定价后台按模型展示单位并保存尺寸定价', async () =>
|
||||
expect((await screen.findAllByText('按次')).length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('按秒').length).toBeGreaterThan(0);
|
||||
const gptImage2kInput = screen.getByLabelText('gpt-image-2 2K');
|
||||
fireEvent.change(gptImage2kInput, {target: {value: '58'}});
|
||||
await user.click(screen.getByRole('button', {name: '保存定价'}));
|
||||
await user.click(screen.getByRole('button', {name: '确认'}));
|
||||
fireEvent.change(gptImage2kInput, { target: { value: '58' } });
|
||||
await user.click(screen.getByRole('button', { name: '保存定价' }));
|
||||
await user.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(upsertAdminEditorGenerationPricing).toHaveBeenCalledWith(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {RefreshCcw, Save} from 'lucide-react';
|
||||
import {FormEvent, useEffect, useState} from 'react';
|
||||
import { RefreshCcw, Save } from 'lucide-react';
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
getAdminEditorGenerationPricing,
|
||||
@@ -10,8 +10,8 @@ import type {
|
||||
EditorGenerationPricingConfigPayload,
|
||||
EditorGenerationPricingUnitPayload,
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminEditorGenerationPricingPageProps {
|
||||
token: string;
|
||||
@@ -36,7 +36,7 @@ export function AdminEditorGenerationPricingPage({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshPricing();
|
||||
@@ -156,7 +156,11 @@ export function AdminEditorGenerationPricingPage({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button className="admin-primary-button" disabled={isSaving} type="submit">
|
||||
<button
|
||||
className="admin-primary-button"
|
||||
disabled={isSaving}
|
||||
type="submit"
|
||||
>
|
||||
<Save size={17} aria-hidden="true" />
|
||||
<span>{isSaving ? '保存中' : '保存定价'}</span>
|
||||
</button>
|
||||
@@ -196,7 +200,9 @@ function renderModelPricingCard({
|
||||
step={1}
|
||||
type="number"
|
||||
value={price}
|
||||
onChange={(event) => onTierChange(model, tier, event.target.value)}
|
||||
onChange={(event) =>
|
||||
onTierChange(model, tier, event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {render, screen} from '@testing-library/react';
|
||||
import {beforeEach, expect, test, vi} from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {listProfileInviteCodes} from '../api/adminApiClient';
|
||||
import {AdminInviteCodePage} from './AdminInviteCodePage';
|
||||
import { listProfileInviteCodes } from '../api/adminApiClient';
|
||||
import { AdminInviteCodePage } from './AdminInviteCodePage';
|
||||
|
||||
vi.mock('../api/adminApiClient', () => ({
|
||||
formatAdminApiError: vi.fn((error: unknown) =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {RefreshCcw, Save} from 'lucide-react';
|
||||
import {FormEvent, useEffect, useState} from 'react';
|
||||
import { RefreshCcw, Save } from 'lucide-react';
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
listProfileInviteCodes,
|
||||
@@ -10,8 +10,8 @@ import type {
|
||||
ProfileCodeOperationAdminResponse,
|
||||
ProfileInviteCodeAdminResponse,
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminInviteCodePageProps {
|
||||
token: string;
|
||||
@@ -30,10 +30,12 @@ export function AdminInviteCodePage({
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [listErrorMessage, setListErrorMessage] = useState('');
|
||||
const [entries, setEntries] = useState<ProfileInviteCodeAdminResponse[]>([]);
|
||||
const [operations, setOperations] = useState<ProfileCodeOperationAdminResponse[]>([]);
|
||||
const [operations, setOperations] = useState<
|
||||
ProfileCodeOperationAdminResponse[]
|
||||
>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshInviteCodes();
|
||||
@@ -239,7 +241,9 @@ export function AdminInviteCodePage({
|
||||
<TagList tags={metadataUserTags(entry.metadata)} />
|
||||
</td>
|
||||
<td>
|
||||
<span className={`admin-status ${inviteValidityClass(entry)}`}>
|
||||
<span
|
||||
className={`admin-status ${inviteValidityClass(entry)}`}
|
||||
>
|
||||
{inviteValidityLabel(entry)}
|
||||
</span>
|
||||
<small>{formatValidityWindow(entry)}</small>
|
||||
@@ -296,7 +300,7 @@ export function AdminInviteCodePage({
|
||||
);
|
||||
}
|
||||
|
||||
function TagList({tags}: {tags: string[]}) {
|
||||
function TagList({ tags }: { tags: string[] }) {
|
||||
if (!tags.length) {
|
||||
return <span className="admin-muted-text">-</span>;
|
||||
}
|
||||
@@ -318,14 +322,18 @@ function metadataUserTags(metadata: Record<string, unknown>) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parseUserTags(raw.filter((value): value is string => typeof value === 'string').join('、'));
|
||||
return parseUserTags(
|
||||
raw
|
||||
.filter((value): value is string => typeof value === 'string')
|
||||
.join('、'),
|
||||
);
|
||||
}
|
||||
|
||||
function withMetadataUserTags(
|
||||
metadata: Record<string, unknown>,
|
||||
tags: string[],
|
||||
): Record<string, unknown> {
|
||||
const next = {...metadata};
|
||||
const next = { ...metadata };
|
||||
delete next.user_tags;
|
||||
if (tags.length) {
|
||||
next.userTags = tags;
|
||||
@@ -443,7 +451,7 @@ function formatDateTime(value: string) {
|
||||
if (!Number.isFinite(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
return date.toLocaleString('zh-CN', {hour12: false});
|
||||
return date.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
function operationActionLabel(action: string) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {LockKeyhole, ShieldCheck} from 'lucide-react';
|
||||
import {FormEvent, useState} from 'react';
|
||||
import { LockKeyhole, ShieldCheck } from 'lucide-react';
|
||||
import { FormEvent, useState } from 'react';
|
||||
|
||||
import {formatAdminApiError} from '../api/adminApiClient';
|
||||
import { formatAdminApiError } from '../api/adminApiClient';
|
||||
|
||||
interface AdminLoginPageProps {
|
||||
notice: string;
|
||||
onLogin: (username: string, password: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function AdminLoginPage({notice, onLogin}: AdminLoginPageProps) {
|
||||
export function AdminLoginPage({ notice, onLogin }: AdminLoginPageProps) {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {RefreshCw} from 'lucide-react';
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {getAdminOverview} from '../api/adminApiClient';
|
||||
import { getAdminOverview } from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminDatabaseTableStatPayload,
|
||||
AdminOverviewResponse,
|
||||
} from '../api/adminApiTypes';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminOverviewPageProps {
|
||||
token: string;
|
||||
@@ -66,7 +66,12 @@ export function AdminOverviewPage({
|
||||
<InfoPanel
|
||||
title="服务"
|
||||
rows={[
|
||||
['监听', overview ? `${overview.service.bindHost}:${overview.service.bindPort}` : '-'],
|
||||
[
|
||||
'监听',
|
||||
overview
|
||||
? `${overview.service.bindHost}:${overview.service.bindPort}`
|
||||
: '-',
|
||||
],
|
||||
['JWT issuer', overview?.service.jwtIssuer ?? '-'],
|
||||
['后台', overview?.service.adminEnabled ? '已启用' : '未启用'],
|
||||
]}
|
||||
@@ -152,7 +157,7 @@ function InfoPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function TableStatRow({stat}: {stat: AdminDatabaseTableStatPayload}) {
|
||||
function TableStatRow({ stat }: { stat: AdminDatabaseTableStatPayload }) {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
@@ -1,36 +1,60 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {beforeEach, expect, test, vi} from 'vitest';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {getProfileWalletConfig, upsertProfileWalletConfig} from '../api/adminApiClient';
|
||||
import type {ProfileWalletConfigAdminResponse} from '../api/adminApiTypes';
|
||||
import {AdminProfileWalletConfigPage} from './AdminProfileWalletConfigPage';
|
||||
import {
|
||||
getProfileWalletConfig,
|
||||
upsertProfileWalletConfig,
|
||||
} from '../api/adminApiClient';
|
||||
import type { ProfileWalletConfigAdminResponse } from '../api/adminApiTypes';
|
||||
import { AdminProfileWalletConfigPage } from './AdminProfileWalletConfigPage';
|
||||
|
||||
vi.mock('../api/adminApiClient', () => ({
|
||||
formatAdminApiError: vi.fn((error: unknown) => error instanceof Error ? error.message : '请求失败'),
|
||||
formatAdminApiError: vi.fn((error: unknown) =>
|
||||
error instanceof Error ? error.message : '请求失败',
|
||||
),
|
||||
getProfileWalletConfig: vi.fn(),
|
||||
isAdminApiError: vi.fn(() => false),
|
||||
upsertProfileWalletConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
const configResponse: ProfileWalletConfigAdminResponse = {
|
||||
configId: 'profile_wallet', initialMudPoints: 100, dailyFreePointsPerDay: 20,
|
||||
createdBy: 'owner-1', createdByDisplayName: '管理员',
|
||||
createdAt: '2026-07-31T01:00:00Z', updatedBy: 'owner-1',
|
||||
updatedByDisplayName: '管理员', updatedAt: '2026-07-31T01:00:00Z',
|
||||
configId: 'profile_wallet',
|
||||
initialMudPoints: 100,
|
||||
dailyFreePointsPerDay: 20,
|
||||
createdBy: 'owner-1',
|
||||
createdByDisplayName: '管理员',
|
||||
createdAt: '2026-07-31T01:00:00Z',
|
||||
updatedBy: 'owner-1',
|
||||
updatedByDisplayName: '管理员',
|
||||
updatedAt: '2026-07-31T01:00:00Z',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getProfileWalletConfig).mockResolvedValue(configResponse);
|
||||
vi.mocked(upsertProfileWalletConfig).mockResolvedValue({...configResponse, initialMudPoints: 120, dailyFreePointsPerDay: 35});
|
||||
vi.mocked(upsertProfileWalletConfig).mockResolvedValue({
|
||||
...configResponse,
|
||||
initialMudPoints: 120,
|
||||
dailyFreePointsPerDay: 35,
|
||||
});
|
||||
});
|
||||
|
||||
test('账号配置页加载并展示每日免费泥点', async () => {
|
||||
render(<AdminProfileWalletConfigPage token="admin-token" result={configResponse} onUnauthorized={vi.fn()} onResultChange={vi.fn()} />);
|
||||
expect((await screen.findByLabelText('每日免费泥点数') as HTMLInputElement).value).toBe('20');
|
||||
render(
|
||||
<AdminProfileWalletConfigPage
|
||||
token="admin-token"
|
||||
result={configResponse}
|
||||
onUnauthorized={vi.fn()}
|
||||
onResultChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
((await screen.findByLabelText('每日免费泥点数')) as HTMLInputElement)
|
||||
.value,
|
||||
).toBe('20');
|
||||
expect(getProfileWalletConfig).toHaveBeenCalledWith('admin-token');
|
||||
expect(screen.getByText('每日免费泥点')).toBeTruthy();
|
||||
});
|
||||
@@ -38,21 +62,52 @@ test('账号配置页加载并展示每日免费泥点', async () => {
|
||||
test('账号配置页一次保存初始和每日免费泥点', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onResultChange = vi.fn();
|
||||
render(<AdminProfileWalletConfigPage token="admin-token" result={configResponse} onUnauthorized={vi.fn()} onResultChange={onResultChange} />);
|
||||
render(
|
||||
<AdminProfileWalletConfigPage
|
||||
token="admin-token"
|
||||
result={configResponse}
|
||||
onUnauthorized={vi.fn()}
|
||||
onResultChange={onResultChange}
|
||||
/>,
|
||||
);
|
||||
await screen.findByLabelText('每日免费泥点数');
|
||||
fireEvent.change(screen.getByLabelText('账号初始泥点数'), {target: {value: '120'}});
|
||||
fireEvent.change(screen.getByLabelText('每日免费泥点数'), {target: {value: '35'}});
|
||||
await user.click(screen.getByRole('button', {name: '保存'}));
|
||||
fireEvent.change(screen.getByLabelText('账号初始泥点数'), {
|
||||
target: { value: '120' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('每日免费泥点数'), {
|
||||
target: { value: '35' },
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: '保存' }));
|
||||
expect(screen.getByText('初始 120 泥点,每日免费 35 泥点')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', {name: '确认'}));
|
||||
await waitFor(() => expect(upsertProfileWalletConfig).toHaveBeenCalledWith('admin-token', {initialMudPoints: 120, dailyFreePointsPerDay: 35}));
|
||||
expect(onResultChange).toHaveBeenLastCalledWith(expect.objectContaining({initialMudPoints: 120, dailyFreePointsPerDay: 35}));
|
||||
await user.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() =>
|
||||
expect(upsertProfileWalletConfig).toHaveBeenCalledWith('admin-token', {
|
||||
initialMudPoints: 120,
|
||||
dailyFreePointsPerDay: 35,
|
||||
}),
|
||||
);
|
||||
expect(onResultChange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
initialMudPoints: 120,
|
||||
dailyFreePointsPerDay: 35,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('账号配置页拒绝非正整数每日免费额度', async () => {
|
||||
render(<AdminProfileWalletConfigPage token="admin-token" result={configResponse} onUnauthorized={vi.fn()} onResultChange={vi.fn()} />);
|
||||
render(
|
||||
<AdminProfileWalletConfigPage
|
||||
token="admin-token"
|
||||
result={configResponse}
|
||||
onUnauthorized={vi.fn()}
|
||||
onResultChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const input = await screen.findByLabelText('每日免费泥点数');
|
||||
fireEvent.change(input, {target: {value: '1.5'}});
|
||||
expect((screen.getByRole('button', {name: '保存'}) as HTMLButtonElement).disabled).toBe(true);
|
||||
fireEvent.change(input, { target: { value: '1.5' } });
|
||||
expect(
|
||||
(screen.getByRole('button', { name: '保存' }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
expect(upsertProfileWalletConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {RefreshCcw, Save} from 'lucide-react';
|
||||
import {FormEvent, useEffect, useState} from 'react';
|
||||
import { RefreshCcw, Save } from 'lucide-react';
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
getProfileWalletConfig,
|
||||
upsertProfileWalletConfig,
|
||||
} from '../api/adminApiClient';
|
||||
import type {ProfileWalletConfigAdminResponse} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import type { ProfileWalletConfigAdminResponse } from '../api/adminApiTypes';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminProfileWalletConfigPageProps {
|
||||
token: string;
|
||||
@@ -28,7 +28,7 @@ export function AdminProfileWalletConfigPage({
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [loadErrorMessage, setLoadErrorMessage] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshConfig();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {RefreshCcw, Save} from 'lucide-react';
|
||||
import {FormEvent, useEffect, useState} from 'react';
|
||||
import { RefreshCcw, Save } from 'lucide-react';
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
listProfileRechargeProducts,
|
||||
@@ -10,8 +10,8 @@ import type {
|
||||
ProfileRechargeProductConfigAdminResponse,
|
||||
ProfileRechargeProductKind,
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminRechargeProductPageProps {
|
||||
token: string;
|
||||
@@ -20,20 +20,24 @@ interface AdminRechargeProductPageProps {
|
||||
onResultChange: (result: ProfileRechargeProductConfigAdminResponse) => void;
|
||||
}
|
||||
|
||||
const productKinds: Array<{value: ProfileRechargeProductKind; label: string}> = [
|
||||
{value: 'points', label: '泥点'},
|
||||
{value: 'membership', label: '会员'},
|
||||
const productKinds: Array<{
|
||||
value: ProfileRechargeProductKind;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: 'points', label: '泥点' },
|
||||
{ value: 'membership', label: '会员' },
|
||||
];
|
||||
|
||||
const membershipTiers: Array<{value: ProfileMembershipTier; label: string}> = [
|
||||
{value: 'starter', label: 'Starter'},
|
||||
{value: 'basic', label: 'Basic'},
|
||||
{value: 'pro', label: 'Pro'},
|
||||
{value: 'ultimate', label: 'Ultimate'},
|
||||
{value: 'month', label: '月卡'},
|
||||
{value: 'season', label: '季卡'},
|
||||
{value: 'year', label: '年卡'},
|
||||
];
|
||||
const membershipTiers: Array<{ value: ProfileMembershipTier; label: string }> =
|
||||
[
|
||||
{ value: 'starter', label: 'Starter' },
|
||||
{ value: 'basic', label: 'Basic' },
|
||||
{ value: 'pro', label: 'Pro' },
|
||||
{ value: 'ultimate', label: 'Ultimate' },
|
||||
{ value: 'month', label: '月卡' },
|
||||
{ value: 'season', label: '季卡' },
|
||||
{ value: 'year', label: '年卡' },
|
||||
];
|
||||
|
||||
export function AdminRechargeProductPage({
|
||||
token,
|
||||
@@ -64,7 +68,7 @@ export function AdminRechargeProductPage({
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [listErrorMessage, setListErrorMessage] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshProducts();
|
||||
@@ -111,21 +115,31 @@ export function AdminRechargeProductPage({
|
||||
title: title.trim(),
|
||||
priceCents: parsePositiveInteger(priceCents),
|
||||
kind,
|
||||
pointsAmount: kind === 'points' ? parsePositiveInteger(pointsAmount) : 0,
|
||||
bonusPoints: kind === 'points' ? parseNonNegativeInteger(bonusPoints) : 0,
|
||||
pointsAmount:
|
||||
kind === 'points' ? parsePositiveInteger(pointsAmount) : 0,
|
||||
bonusPoints:
|
||||
kind === 'points' ? parseNonNegativeInteger(bonusPoints) : 0,
|
||||
durationDays:
|
||||
kind === 'membership' ? parsePositiveInteger(durationDays) : 0,
|
||||
badgeLabel: kind === 'points' ? badgeLabel.trim() : '',
|
||||
description: description.trim(),
|
||||
tier: kind === 'membership' ? tier : 'normal',
|
||||
membershipPeriodPoints:
|
||||
kind === 'membership' ? parsePositiveInteger(membershipPeriodPoints) : 0,
|
||||
kind === 'membership'
|
||||
? parsePositiveInteger(membershipPeriodPoints)
|
||||
: 0,
|
||||
membershipPeriodDays:
|
||||
kind === 'membership' ? parsePositiveInteger(membershipPeriodDays) : 0,
|
||||
kind === 'membership'
|
||||
? parsePositiveInteger(membershipPeriodDays)
|
||||
: 0,
|
||||
membershipQueueLimit:
|
||||
kind === 'membership' ? parseNonNegativeInteger(membershipQueueLimit) : 0,
|
||||
kind === 'membership'
|
||||
? parseNonNegativeInteger(membershipQueueLimit)
|
||||
: 0,
|
||||
membershipDiscountBps:
|
||||
kind === 'membership' ? parseNonNegativeInteger(membershipDiscountBps) : 0,
|
||||
kind === 'membership'
|
||||
? parseNonNegativeInteger(membershipDiscountBps)
|
||||
: 0,
|
||||
enabled,
|
||||
sortOrder: parseInteger(sortOrder),
|
||||
});
|
||||
@@ -141,7 +155,9 @@ export function AdminRechargeProductPage({
|
||||
|
||||
function upsertEntry(next: ProfileRechargeProductConfigAdminResponse) {
|
||||
setEntries((current) => {
|
||||
const rest = current.filter((entry) => entry.productId !== next.productId);
|
||||
const rest = current.filter(
|
||||
(entry) => entry.productId !== next.productId,
|
||||
);
|
||||
return sortProducts([...rest, next]);
|
||||
});
|
||||
}
|
||||
@@ -230,7 +246,9 @@ export function AdminRechargeProductPage({
|
||||
setTier(tier === 'normal' ? 'starter' : tier);
|
||||
setDurationDays(durationDays === '0' ? '30' : durationDays);
|
||||
setMembershipPeriodDays(
|
||||
membershipPeriodDays === '0' ? '30' : membershipPeriodDays,
|
||||
membershipPeriodDays === '0'
|
||||
? '30'
|
||||
: membershipPeriodDays,
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -515,7 +533,9 @@ function formatTier(tier: ProfileMembershipTier) {
|
||||
return '普通';
|
||||
}
|
||||
|
||||
function formatProductContent(entry: ProfileRechargeProductConfigAdminResponse) {
|
||||
function formatProductContent(
|
||||
entry: ProfileRechargeProductConfigAdminResponse,
|
||||
) {
|
||||
if (entry.kind === 'points') {
|
||||
return `${entry.pointsAmount}+${entry.bonusPoints}`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {PowerOff, RefreshCcw, Save} from 'lucide-react';
|
||||
import {FormEvent, useEffect, useState} from 'react';
|
||||
import { PowerOff, RefreshCcw, Save } from 'lucide-react';
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
disableProfileRedeemCode,
|
||||
@@ -11,18 +11,18 @@ import type {
|
||||
ProfileRedeemCodeAdminResponse,
|
||||
ProfileRedeemCodeMode,
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {handlePageError, splitLines} from './pageUtils';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { handlePageError, splitLines } from './pageUtils';
|
||||
|
||||
interface AdminRedeemCodePageProps {
|
||||
token: string;
|
||||
onUnauthorized: (message?: string) => void;
|
||||
}
|
||||
|
||||
const redeemModes: Array<{value: ProfileRedeemCodeMode; label: string}> = [
|
||||
{value: 'public', label: '公共码'},
|
||||
{value: 'unique', label: '唯一码'},
|
||||
{value: 'private', label: '私有码'},
|
||||
const redeemModes: Array<{ value: ProfileRedeemCodeMode; label: string }> = [
|
||||
{ value: 'public', label: '公共码' },
|
||||
{ value: 'unique', label: '唯一码' },
|
||||
{ value: 'private', label: '私有码' },
|
||||
];
|
||||
|
||||
export function AdminRedeemCodePage({
|
||||
@@ -43,11 +43,13 @@ export function AdminRedeemCodePage({
|
||||
const [disableErrorMessage, setDisableErrorMessage] = useState('');
|
||||
const [listErrorMessage, setListErrorMessage] = useState('');
|
||||
const [entries, setEntries] = useState<ProfileRedeemCodeAdminResponse[]>([]);
|
||||
const [operations, setOperations] = useState<ProfileCodeOperationAdminResponse[]>([]);
|
||||
const [operations, setOperations] = useState<
|
||||
ProfileCodeOperationAdminResponse[]
|
||||
>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDisabling, setIsDisabling] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshRedeemCodes();
|
||||
@@ -447,7 +449,7 @@ function formatDateTime(value: string) {
|
||||
if (!Number.isFinite(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
return date.toLocaleString('zh-CN', {hour12: false});
|
||||
return date.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
function validateValidityWindow(startsAt: string, expiresAt: string) {
|
||||
@@ -494,10 +496,18 @@ function redeemValidityLabel(entry: ProfileRedeemCodeAdminResponse) {
|
||||
const now = Date.now();
|
||||
const startsAtTime = entry.startsAt ? Date.parse(entry.startsAt) : null;
|
||||
const expiresAtTime = entry.expiresAt ? Date.parse(entry.expiresAt) : null;
|
||||
if (startsAtTime !== null && Number.isFinite(startsAtTime) && now < startsAtTime) {
|
||||
if (
|
||||
startsAtTime !== null &&
|
||||
Number.isFinite(startsAtTime) &&
|
||||
now < startsAtTime
|
||||
) {
|
||||
return '未生效';
|
||||
}
|
||||
if (expiresAtTime !== null && Number.isFinite(expiresAtTime) && now >= expiresAtTime) {
|
||||
if (
|
||||
expiresAtTime !== null &&
|
||||
Number.isFinite(expiresAtTime) &&
|
||||
now >= expiresAtTime
|
||||
) {
|
||||
return '已过期';
|
||||
}
|
||||
if (entry.startsAt || entry.expiresAt) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {Eye, EyeOff, RefreshCcw} from 'lucide-react';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import { Eye, EyeOff, RefreshCcw } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
listAdminWorkVisibility,
|
||||
updateAdminWorkVisibility,
|
||||
} from '../api/adminApiClient';
|
||||
import type {AdminWorkVisibilityEntryPayload} from '../api/adminApiTypes';
|
||||
import {AdminUserReferenceButton} from '../components/AdminUserReferenceButton';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import type { AdminWorkVisibilityEntryPayload } from '../api/adminApiTypes';
|
||||
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminWorkVisibilityPageProps {
|
||||
token: string;
|
||||
@@ -37,7 +37,7 @@ export function AdminWorkVisibilityPage({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [savingKey, setSavingKey] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshEntries();
|
||||
@@ -81,7 +81,8 @@ export function AdminWorkVisibilityPage({
|
||||
|
||||
async function handleToggle(entry: AdminWorkVisibilityEntryPayload) {
|
||||
const nextVisible = !entry.visible;
|
||||
const target = entry.title.trim() || entry.publicWorkCode || entry.profileId;
|
||||
const target =
|
||||
entry.title.trim() || entry.publicWorkCode || entry.profileId;
|
||||
const confirmed = await confirmWrite({
|
||||
action: nextVisible ? '显示作品' : '隐藏作品',
|
||||
target,
|
||||
@@ -110,7 +111,9 @@ export function AdminWorkVisibilityPage({
|
||||
function upsertEntry(next: AdminWorkVisibilityEntryPayload) {
|
||||
setEntries((current) =>
|
||||
sortEntries([
|
||||
...current.filter((entry) => buildEntryKey(entry) !== buildEntryKey(next)),
|
||||
...current.filter(
|
||||
(entry) => buildEntryKey(entry) !== buildEntryKey(next),
|
||||
),
|
||||
next,
|
||||
]),
|
||||
);
|
||||
@@ -276,5 +279,5 @@ function formatMicros(value: number) {
|
||||
if (!Number.isFinite(date.getTime())) {
|
||||
return '-';
|
||||
}
|
||||
return date.toLocaleString('zh-CN', {hour12: false});
|
||||
return date.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {formatAdminApiError, isAdminApiError} from '../api/adminApiClient';
|
||||
import { formatAdminApiError, isAdminApiError } from '../api/adminApiClient';
|
||||
|
||||
export function handlePageError(
|
||||
error: unknown,
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
color: #3d1f10;
|
||||
background: #f8efe7;
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", sans-serif;
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
@@ -62,10 +67,8 @@ button:disabled {
|
||||
}
|
||||
|
||||
.admin-login-screen {
|
||||
background:
|
||||
linear-gradient(145deg, rgba(204, 117, 76, 0.14), transparent 36%),
|
||||
linear-gradient(315deg, rgba(226, 171, 134, 0.16), transparent 34%),
|
||||
#f8efe7;
|
||||
background: linear-gradient(145deg, rgba(204, 117, 76, 0.14), transparent 36%),
|
||||
linear-gradient(315deg, rgba(226, 171, 134, 0.16), transparent 34%), #f8efe7;
|
||||
}
|
||||
|
||||
.admin-login-panel,
|
||||
@@ -172,7 +175,7 @@ button:disabled {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.admin-nav-button[data-active="true"] {
|
||||
.admin-nav-button[data-active='true'] {
|
||||
color: #8f3f27;
|
||||
background: #f4e5d7;
|
||||
}
|
||||
@@ -341,7 +344,7 @@ button:disabled {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.admin-dashboard-tabs button[data-active="true"] {
|
||||
.admin-dashboard-tabs button[data-active='true'] {
|
||||
color: #8f3f27;
|
||||
background: #f4e5d7;
|
||||
}
|
||||
@@ -438,7 +441,7 @@ button:disabled {
|
||||
min-height: 132px;
|
||||
}
|
||||
|
||||
.admin-dashboard-metric-card[data-compact="true"] {
|
||||
.admin-dashboard-metric-card[data-compact='true'] {
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
@@ -615,7 +618,7 @@ button:disabled {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.admin-account-list-items > button[data-active="true"] {
|
||||
.admin-account-list-items > button[data-active='true'] {
|
||||
border-color: #c87955;
|
||||
background: #f9eee5;
|
||||
}
|
||||
@@ -696,7 +699,9 @@ button:disabled {
|
||||
|
||||
.admin-table-query-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1fr) minmax(160px, 1fr) minmax(96px, 0.45fr) auto;
|
||||
grid-template-columns:
|
||||
minmax(180px, 1fr) minmax(160px, 1fr) minmax(96px, 0.45fr)
|
||||
auto;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
@@ -756,16 +761,15 @@ button:disabled {
|
||||
}
|
||||
|
||||
.admin-mono-value {
|
||||
font-family:
|
||||
"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-table tbody tr[data-clickable="true"] {
|
||||
.admin-table tbody tr[data-clickable='true'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-table tbody tr[data-clickable="true"]:hover {
|
||||
.admin-table tbody tr[data-clickable='true']:hover {
|
||||
background: #fff7f0;
|
||||
}
|
||||
|
||||
@@ -1012,8 +1016,7 @@ button:disabled {
|
||||
|
||||
.admin-combobox-option span {
|
||||
color: #8f3f27;
|
||||
font-family:
|
||||
"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -1880,7 +1883,7 @@ button:disabled {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-database-table tbody tr[data-clickable="true"] {
|
||||
.admin-database-table tbody tr[data-clickable='true'] {
|
||||
cursor: pointer;
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
@@ -1889,7 +1892,7 @@ button:disabled {
|
||||
background: #fffbf7;
|
||||
}
|
||||
|
||||
.admin-database-table tbody tr[data-clickable="true"]:hover {
|
||||
.admin-database-table tbody tr[data-clickable='true']:hover {
|
||||
background: #fff3e9;
|
||||
}
|
||||
|
||||
@@ -1972,7 +1975,7 @@ button:disabled {
|
||||
|
||||
.admin-table-sort-button:hover,
|
||||
.admin-table-sort-button:focus-visible,
|
||||
.admin-table-sort-button[data-active="true"] {
|
||||
.admin-table-sort-button[data-active='true'] {
|
||||
color: #8f3f27;
|
||||
outline: none;
|
||||
}
|
||||
@@ -2680,7 +2683,7 @@ button:disabled {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-segmented-control button[data-active="true"] {
|
||||
.admin-segmented-control button[data-active='true'] {
|
||||
color: #8f3f27;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(112, 57, 30, 0.08);
|
||||
@@ -2895,7 +2898,7 @@ button:disabled {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-bottom-nav-button[data-active="true"] {
|
||||
.admin-bottom-nav-button[data-active='true'] {
|
||||
color: #8f3f27;
|
||||
background: #f4e5d7;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {dirname, resolve} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import {defineConfig, loadEnv} from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
|
||||
const adminWebRoot = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(adminWebRoot, '../..');
|
||||
|
||||
export default defineConfig(({command, mode}) => {
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
const repoEnv = loadEnv(mode, repoRoot, '');
|
||||
const appEnv = loadEnv(mode, adminWebRoot, '');
|
||||
const env = {...repoEnv, ...appEnv};
|
||||
const env = { ...repoEnv, ...appEnv };
|
||||
const apiTarget =
|
||||
env.ADMIN_API_TARGET ||
|
||||
env.GENARRATIVE_API_TARGET ||
|
||||
|
||||
@@ -3,8 +3,5 @@
|
||||
"identifier": "events",
|
||||
"description": "允许客户端窗口订阅并取消订阅 Rust Runtime 事件。",
|
||||
"windows": ["client", "developer", "main", "launcher", "supervisor-chat"],
|
||||
"permissions": [
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-unlisten"
|
||||
]
|
||||
"permissions": ["core:event:allow-listen", "core:event:allow-unlisten"]
|
||||
}
|
||||
|
||||
@@ -73,9 +73,7 @@
|
||||
"roleOverlays": [
|
||||
{
|
||||
"agentId": "project-planning",
|
||||
"sections": [
|
||||
"projectPlanningRoleBrief"
|
||||
]
|
||||
"sections": ["projectPlanningRoleBrief"]
|
||||
}
|
||||
],
|
||||
"providerFragments": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user