setVisible((v) => !v)}
aria-label={visible ? '隐藏密码' : '显示密码'}
- className="text-text-placeholder-1"
+ className="text-text-placeholder-2"
>
diff --git a/web/src/features/auth/components/VerificationCodeButton.tsx b/web/src/features/auth/components/VerificationCodeButton.tsx
index 248c3585..6ca4f6fb 100644
--- a/web/src/features/auth/components/VerificationCodeButton.tsx
+++ b/web/src/features/auth/components/VerificationCodeButton.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { requestVerificationCode } from '@/lib/api/auth';
import { ImApiError } from '@/lib/api/client';
@@ -18,6 +18,11 @@ export function VerificationCodeButton({ phone, scene, onChallenge, onError }: V
const [busy, setBusy] = useState(false);
const timerRef = useRef(undefined);
+ // Without this, navigating away mid-countdown (e.g. switching from register back to login)
+ // leaves the interval running against an unmounted component indefinitely until it naturally
+ // reaches 0 -- a real resource leak, not just a React warning, since nothing else ever clears it.
+ useEffect(() => () => window.clearInterval(timerRef.current), []);
+
const tick = (initial: number) => {
window.clearInterval(timerRef.current);
setSeconds(initial);
diff --git a/web/src/lib/api/tenant.ts b/web/src/lib/api/tenant.ts
index 2d7a19a2..f5999622 100644
--- a/web/src/lib/api/tenant.ts
+++ b/web/src/lib/api/tenant.ts
@@ -1,4 +1,6 @@
-import { imRequest } from './client';
+import { API_KEY } from '@/lib/config';
+
+import { apiBase, currentTenantCode, imPost, imRequest, ImApiError, requestId } from './client';
/**
* Shape verified against a real `POST /v1/im/tenants/resolve` response (not assumed from memory/
@@ -37,3 +39,66 @@ export async function resolveTenant(tenantCode: string): Promise {
+ const form = new FormData();
+ form.append('purpose', purpose);
+ form.append('file', file);
+ const res = await fetch(`${apiBase()}/public/uploads`, {
+ method: 'POST',
+ headers: {
+ 'X-MeetingIM-APIKey': API_KEY,
+ 'X-IM-Schema': '1',
+ 'X-Tenant-Code': currentTenantCode(),
+ 'X-Request-ID': requestId(),
+ },
+ body: form,
+ });
+ const text = await res.text();
+ let payload: { code?: number; data?: PublicUploadResult; message?: string; reason?: string } = {};
+ try {
+ payload = JSON.parse(text);
+ } catch {
+ throw new ImApiError('文件上传失败:服务器返回异常', undefined, res.status);
+ }
+ if (!res.ok || (payload.code && payload.code !== 0) || !payload.data) {
+ throw new ImApiError(payload.message || payload.reason || '文件上传失败', payload.reason, res.status);
+ }
+ return payload.data;
+}
+
+export interface TenantApplicationResult {
+ application_id?: string;
+ estimated_review_hours?: number;
+}
+
+export function submitTenantApplication(body: { credit_code: string; enterprise_email?: string; license_file_id?: string; tenant_code_hint?: string }) {
+ return imPost('/tenant-applications', { client_context: clientContext(), ...body }, { auth: false });
+}
+
+export function submitTenantEntryFeedback(body: { description: string; tenant_code_hint?: string; screenshot_file_ids?: string[]; contact?: string }) {
+ return imPost<{ received: boolean }>('/tenant-entry-feedback', { client_context: clientContext(), ...body }, { auth: false });
+}