Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 103 additions & 140 deletions bun.lock

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "pushy-admin",
"private": true,
"scripts": {
"dev": "PUBLIC_API=https://5.rnupdate.online/api rsbuild dev",
"dev": "PUBLIC_API=https://update.reactnative.cn/api rsbuild dev",
"dev:local": "PUBLIC_API=http://localhost:9000 rsbuild dev",
"build": "NODE_ENV=production rsbuild build",
"preview": "rsbuild preview",
Expand All @@ -12,10 +12,10 @@
"dependencies": {
"@ant-design/charts": "^2.6.7",
"@ant-design/icons": "^6.1.1",
"@rsbuild/core": "^1.7.5",
"@rsbuild/plugin-react": "^1.4.6",
"@rsbuild/plugin-svgr": "^1.3.1",
"@tanstack/react-query": "^5.99.2",
"@rsbuild/core": "^2.0.1",
"@rsbuild/plugin-react": "^2.0.0",
"@rsbuild/plugin-svgr": "^2.0.1",
"@tanstack/react-query": "^5.100.5",
"antd": "^6.3.6",
"dayjs": "^1.11.20",
"git-url-parse": "^16.1.0",
Expand All @@ -24,25 +24,25 @@
"json-diff-kit": "^1.0.35",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.1",
"react-router-dom": "^7.14.2",
"ua-parser-js": "^2.0.9",
"vanilla-jsoneditor": "^3.12.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@biomejs/biome": "2.4.12",
"@tailwindcss/postcss": "^4.2.3",
"@biomejs/biome": "2.4.13",
"@tailwindcss/postcss": "^4.2.4",
"@testing-library/react": "^16.3.2",
"@types/bun": "^1.3.12",
"@types/bun": "^1.3.13",
"@types/git-url-parse": "^16.0.2",
"@types/node": "^25.6.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-router-dom": "^5.3.3",
"@typescript/native-preview": "^7.0.0-dev.20260420.1",
"@typescript/native-preview": "^7.0.0-dev.20260426.1",
"happy-dom": "^20.9.0",
"mitata": "^1.0.34",
"tailwindcss": "^4.2.3",
"tailwindcss": "^4.2.4",
"typescript": "^6.0.3"
},
"trustedDependencies": [
Expand Down
82 changes: 82 additions & 0 deletions src/components/create-app-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Form, Input, Modal, message, Select } from 'antd';
import { api } from '@/services/api';
import PlatformIcon from './platform-icon';

export const showCreateAppModal = ({
onCreated,
}: {
onCreated?: (id: number) => void | Promise<void>;
} = {}) => {
let name = '';
let platform = 'android';

Modal.confirm({
icon: null,
closable: true,
maskClosable: true,
content: (
<Form initialValues={{ platform }}>
<br />
<Form.Item label="应用名称" name="name">
<Input
placeholder="请输入应用名称"
onChange={({ target }) => {
name = target.value;
}}
/>
</Form.Item>
<Form.Item label="选择平台" name="platform">
<Select
onSelect={(value: string) => {
platform = value;
}}
options={[
{
value: 'android',
label: (
<>
<PlatformIcon platform="android" className="mr-2" /> Android
</>
),
},
{
value: 'ios',
label: (
<>
<PlatformIcon platform="ios" className="mr-2" /> iOS
</>
),
},
{
value: 'harmony',
label: (
<>
<PlatformIcon platform="harmony" className="mr-[10px]" />
HarmonyOS
</>
),
},
]}
/>
</Form.Item>
</Form>
),
async onOk() {
const trimmedName = name.trim();
if (!trimmedName) {
message.warning('请输入应用名称');
return false;
}

try {
const id = await api.createApp({ name: trimmedName, platform });
if (typeof id === 'number') {
await onCreated?.(id);
}
} catch (error) {
message.error((error as Error).message);
return false;
}
},
});
};
219 changes: 219 additions & 0 deletions src/components/daily-check-quota.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { WarningOutlined } from '@ant-design/icons';
import { Alert, Progress, Tag, Tooltip, Typography } from 'antd';
import dayjs from 'dayjs';
import { quotas } from '@/constants/quotas';
import { useUserInfo } from '@/utils/hooks';

const { Text } = Typography;

interface DailyCheckQuotaProps {
variant: 'account';
}

const useDailyCheckQuotaState = () => {
const { user } = useUserInfo();
const quota = user
? (user.quota ?? quotas[user.tier as keyof typeof quotas])
: undefined;
const dailyQuota = quota?.pv;
const remaining = user?.checkQuota;
const hasData = !!dailyQuota && typeof remaining === 'number';

const remainingRatio = hasData ? remaining / dailyQuota : 0;
const percent = Math.max(0, Math.min(100, remainingRatio * 100));
const isExceeded = hasData && remaining <= 0;
const isLow = !isExceeded && remainingRatio <= 0.2;
const status: 'exception' | 'normal' =
isExceeded || isLow ? 'exception' : 'normal';
const tooltip = (
<div className="text-xs">
{user && (
<>
<div>
套餐:
{quota?.title ??
quotas[user.tier as keyof typeof quotas]?.title ??
user.tier}
</div>
<div>
到期:
{user.tierExpiresAt
? dayjs(user.tierExpiresAt).format('YYYY-MM-DD')
: '无'}
</div>
</>
)}
{hasData && (
<>
<div>
今日剩余额度:{Math.max(0, remaining).toLocaleString()} /{' '}
{dailyQuota.toLocaleString()} 次
</div>
{remaining < 0 && (
<div>已超出:{Math.abs(remaining).toLocaleString()} 次</div>
)}
</>
)}
{user?.last7dAvg !== undefined && (
<div>7 日平均剩余额度:{user.last7dAvg.toLocaleString()} 次</div>
)}
</div>
);

return {
hasData,
isExceeded,
isLow,
percent,
status,
tooltip,
user,
};
};

export function DailyCheckQuotaUserTrigger({
compact = false,
showPlanDetails = false,
userName,
}: {
compact?: boolean;
showPlanDetails?: boolean;
userName: string;
}) {
const quotaState = useDailyCheckQuotaState();
const { user } = quotaState;
const strokeColor = quotaState.isExceeded
? '#ef4444'
: quotaState.isLow
? '#f59e0b'
: '#2563eb';
const tierTitle = user
? (user.quota?.title ??
quotas[user.tier as keyof typeof quotas]?.title ??
user.tier)
: '';
const expireLabel = user?.tierExpiresAt
? `${dayjs(user.tierExpiresAt).format('YYYY-MM-DD')} 到期`
: '无到期';
const warningIcon = (quotaState.isExceeded || quotaState.isLow) && (
<WarningOutlined
className={`shrink-0 ${
quotaState.isExceeded ? 'text-red-500' : 'text-amber-500'
}`}
/>
);
const progress = quotaState.hasData && (
<Progress
className="mt-0.5"
percent={quotaState.percent}
showInfo={false}
size="small"
status={quotaState.status}
strokeColor={strokeColor}
trailColor="#e5e7eb"
/>
);
const content = compact ? (
<span className="flex h-10 w-16 min-w-0 flex-col justify-center text-left">
<span className="flex min-w-0 items-center gap-1">
<span className="truncate font-medium text-[11px] text-slate-600 leading-4">
{userName}
</span>
{warningIcon}
</span>
{progress}
</span>
) : (
<span className="flex min-w-0 items-center gap-2.5 text-left">
<span className="min-w-0 flex-1">
<span className="block truncate font-medium text-slate-800 text-sm leading-5">
{userName}
</span>
{showPlanDetails && user && (
<span className="block truncate text-[11px] text-slate-500 leading-4">
{tierTitle} · {expireLabel}
</span>
)}
{progress}
</span>
{warningIcon}
</span>
);

if (!quotaState.hasData) {
return content;
}

return <Tooltip title={quotaState.tooltip}>{content}</Tooltip>;
}

export default function DailyCheckQuota(_props: DailyCheckQuotaProps) {
const quotaState = useDailyCheckQuotaState();
const { user } = quotaState;
if (!user) {
return null;
}

if (!quotaState.hasData) {
return (
<Text type="secondary">
暂无今日检查额度数据。额度用于客户端查询是否有可用热更新,按账户下所有应用汇总。
</Text>
);
}

const message = quotaState.isExceeded
? '今日检查额度已用尽,请升级套餐或等待每日额度重置。'
: quotaState.isLow
? '今日检查额度偏低,继续发布或大量客户端查询时需要留意。'
: '今日检查额度状态正常。';

return (
<div className="space-y-3">
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<div>
<div className="font-medium">每日检查额度</div>
<div className="text-gray-500 text-sm">
客户端检查热更新时消耗,按账户下所有应用合并计算,每日重置。
</div>
</div>
<Tag
color={
quotaState.isExceeded
? 'red'
: quotaState.isLow
? 'orange'
: 'green'
}
>
{quotaState.isExceeded
? '已用尽'
: quotaState.isLow
? '偏低'
: '正常'}
</Tag>
</div>
<Tooltip title={quotaState.tooltip}>
<Progress
percent={quotaState.percent}
showInfo={false}
status={quotaState.status}
strokeLinecap="round"
/>
</Tooltip>
<div className="mt-2 text-gray-500 text-xs">
页面不直接展示具体次数;鼠标悬停进度条可查看剩余额度、套餐上限和 7
日平均。
</div>
</div>
{(quotaState.isExceeded || quotaState.isLow) && (
<Alert
showIcon
type={quotaState.isExceeded ? 'error' : 'warning'}
message={message}
/>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Layout, Typography } from 'antd';

const Footer = () => (
<Layout.Footer className="text-center">
<Layout.Footer className="shrink-0 text-center">
<Typography.Paragraph type="secondary">
React Native 中文网 © {new Date().getFullYear()} 武汉青罗网络科技有限公司
</Typography.Paragraph>
Expand Down
Loading