feat: UOOC/Zhihuishu dual-platform brushing platform

- ASP.NET Core 9 Web API backend with JWT auth, EF Core MySQL
- Vue 3 + Vite + Pinia + Ant Design Vue frontend
- Multi-platform connection management (UOOC & Zhihuishu)
- Video brushing with AES-CBC encryption for Zhihuishu
- Multi-task queue with cross-platform parallel execution
- Task persistence via MySQL database
- Progress tracking with inline catalog enrichment
- Mobile-responsive UI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 15:31:32 +08:00
co-authored by Claude Fable 5
commit cd486a36ef
84 changed files with 16242 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { message } from 'ant-design-vue';
import { uoocLogin } from '../lib/api';
import { usePlatformStore } from '../stores/platform';
import type { PlatformConnectionDto } from '../types/api';
declare global {
interface Window {
AliyunCaptchaConfig?: { region: string; prefix: string };
initAliyunCaptcha?: (config: Record<string, unknown>) => void;
}
}
const props = defineProps<{ platformId: number; connectionName: string }>();
const emit = defineEmits<{ success: [connection: PlatformConnectionDto]; close: [] }>();
const account = ref(''); const password = ref('');
const loading = ref(false); const error = ref('');
const captchaLoading = ref(false);
let scriptLoaded = false;
onMounted(() => { loadScript(); });
async function loadScript() {
if (scriptLoaded || document.querySelector('script[src*="aliyunCaptcha"]')) { scriptLoaded = true; return; }
return new Promise<void>(resolve => {
const s = document.createElement('script');
s.src = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js';
s.onload = () => { scriptLoaded = true; resolve(); };
s.onerror = () => resolve();
document.head.appendChild(s);
});
}
function waitReady(): Promise<void> {
return new Promise((resolve, reject) => {
let n = 0;
const check = () => { if (typeof window.initAliyunCaptcha === 'function') resolve(); else if (++n > 30) reject(new Error('验证码 SDK 超时。')); else setTimeout(check, 200); };
check();
});
}
function triggerCaptcha(): Promise<string> {
return new Promise((resolve, reject) => {
captchaLoading.value = true;
window.AliyunCaptchaConfig = { region: 'cn', prefix: 'dq1y8x' };
const m = document.getElementById('uooc-captcha-mount');
const t = document.getElementById('uooc-captcha-trigger');
if (!m || !t) { captchaLoading.value = false; reject(new Error('组件未就绪。')); return; }
m.innerHTML = '';
window.initAliyunCaptcha!({
SceneId: '137o3jmc', mode: 'popup',
element: '#uooc-captcha-mount', button: '#uooc-captcha-trigger',
success(p: string) { captchaLoading.value = false; resolve(p); },
fail() { captchaLoading.value = false; reject(new Error('验证未通过。')); },
getInstance() {},
slideStyle: { width: 360, height: 40 },
});
setTimeout(() => t.click(), 300);
});
}
async function submit() {
error.value = '';
const acc = account.value.trim(); const pwd = password.value.trim();
if (!acc) { error.value = '请输入手机号。'; return; }
if (!pwd) { error.value = '请输入密码。'; return; }
loading.value = true;
try {
await loadScript(); await waitReady();
const param = await triggerCaptcha();
const r = await uoocLogin({ platformId: props.platformId, connectionName: props.connectionName.trim() || null, account: acc, password: pwd, captchaVerifyParam: param });
await usePlatformStore().loadConnections();
if (r.connection) { message.success(r.message || '登录成功。'); emit('success', r.connection); }
} catch (err) { error.value = err instanceof Error ? err.message : '登录失败。'; }
finally { loading.value = false; }
}
</script>
<template>
<a-modal :open="true" title="登录 UOOC 学习平台" width="480px" :footer="null" :mask-closable="false" @cancel="emit('close')">
<a-alert v-if="error" type="error" :message="error" closable style="margin-bottom: 16px;" @close="error = ''" />
<a-form layout="vertical" :disabled="loading">
<a-form-item label="手机号" required>
<a-input v-model:value="account" placeholder="请输入 UOOC 手机号" autocomplete="tel" />
</a-form-item>
<a-form-item label="密码" required>
<a-input-password v-model:value="password" placeholder="请输入 UOOC 密码" autocomplete="current-password" />
<template #help>
密码将通过 HTTPS 加密传输至 UOOC 服务器
</template>
</a-form-item>
</a-form>
<div id="uooc-captcha-mount" />
<button id="uooc-captcha-trigger" type="button" style="display:none;">验证</button>
<a-tag v-if="captchaLoading" color="blue" style="margin: 8px 0;">请完成弹出的滑块验证...</a-tag>
<a-flex justify="end" :gap="8" style="margin-top: 16px;">
<a-button @click="emit('close')" :disabled="loading">取消</a-button>
<a-button type="primary" :loading="loading || captchaLoading" @click="submit">
{{ captchaLoading ? '等待验证...' : loading ? '登录中...' : '登录 UOOC 平台' }}
</a-button>
</a-flex>
</a-modal>
</template>
@@ -0,0 +1,168 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { message } from 'ant-design-vue';
import { zhihuishuLogin } from '../lib/api';
import { usePlatformStore } from '../stores/platform';
import type { PlatformConnectionDto } from '../types/api';
declare global {
interface Window {
initNECaptcha?: (config: Record<string, unknown>, onload: (instance: any) => void, onerror: (err: unknown) => void) => void;
}
}
const props = defineProps<{ platformId: number; connectionName: string }>();
const emit = defineEmits<{ success: [connection: PlatformConnectionDto]; close: [] }>();
const account = ref(''); const password = ref('');
const loading = ref(false); const error = ref('');
const captchaReady = ref(false);
let captchaInstance: any = null;
let pendingResolve: ((v: string) => void) | null = null;
let pendingReject: ((e: Error) => void) | null = null;
onMounted(() => { initCaptcha(); });
onUnmounted(() => {
const mount = document.getElementById('zhs-captcha-mount');
if (mount) mount.innerHTML = '';
});
async function initCaptcha(): Promise<void> {
// Load SDK script
if (!document.querySelector('script[src*="cstaticdun.126.net/load.min.js"]')) {
await new Promise<void>(resolve => {
const s = document.createElement('script');
s.src = 'https://cstaticdun.126.net/load.min.js';
s.onload = () => resolve();
s.onerror = () => resolve();
document.head.appendChild(s);
});
}
// Wait for SDK to be available
const ready = await new Promise<boolean>(resolve => {
let n = 0;
const check = () => {
if (typeof window.initNECaptcha === 'function') { resolve(true); return; }
if (++n > 50) { resolve(false); return; }
setTimeout(check, 200);
};
check();
});
if (!ready) {
console.error('[Zhihuishu] 网易易盾 SDK 未就绪');
return;
}
const mount = document.getElementById('zhs-captcha-mount');
if (!mount) return;
mount.innerHTML = '';
window.initNECaptcha!(
{
captchaId: '75f9f716460a422f89a628f50fd8cc2b',
element: '#zhs-captcha-mount',
mode: 'popup',
onVerify(err: unknown, data: { validate: string }) {
if (err) {
pendingReject?.(new Error('验证未通过。'));
} else {
pendingResolve?.(data.validate);
}
pendingResolve = null;
pendingReject = null;
},
},
(instance: any) => {
captchaInstance = instance;
captchaReady.value = true;
console.log('[Zhihuishu] 网易易盾 captcha ready');
},
(err: unknown) => {
console.error('[Zhihuishu] captcha init error:', err);
captchaReady.value = false;
},
);
}
function triggerCaptcha(): Promise<string> {
return new Promise((resolve, reject) => {
if (!captchaInstance) {
reject(new Error('验证码组件未就绪,请刷新页面后重试。'));
return;
}
pendingResolve = resolve;
pendingReject = reject;
captchaInstance.popUp();
});
}
async function submit() {
error.value = '';
const acc = account.value.trim(); const pwd = password.value.trim();
if (!acc) { error.value = '请输入手机号。'; return; }
if (!pwd) { error.value = '请输入密码。'; return; }
loading.value = true;
try {
if (!captchaInstance) {
await initCaptcha();
if (!captchaInstance) throw new Error('验证码初始化失败,请刷新页面后重试。');
}
const captchaValidate = await triggerCaptcha();
const r = await zhihuishuLogin({
platformId: props.platformId,
connectionName: props.connectionName.trim() || null,
account: acc,
password: pwd,
captchaValidate,
});
await usePlatformStore().loadConnections();
if (r.connection) {
message.success(r.message || '智慧树登录成功。');
emit('success', r.connection);
}
} catch (err) {
error.value = err instanceof Error ? err.message : '登录失败。';
} finally {
loading.value = false;
}
}
</script>
<template>
<a-modal :open="true" title="登录智慧树平台" width="480px" :footer="null" :mask-closable="false" @cancel="emit('close')">
<a-alert
v-if="!captchaReady"
type="warning"
message="正在加载网易易盾验证码组件,请稍候..."
style="margin-bottom: 16px;"
/>
<a-alert v-if="error" type="error" :message="error" closable style="margin-bottom: 16px;" @close="error = ''" />
<a-form layout="vertical" :disabled="loading">
<a-form-item label="手机号" required>
<a-input v-model:value="account" placeholder="请输入智慧树绑定的手机号" autocomplete="tel" />
</a-form-item>
<a-form-item label="密码" required>
<a-input-password v-model:value="password" placeholder="请输入智慧树密码" autocomplete="current-password" />
<template #help>
密码将通过加密传输至智慧树服务器系统不会存储明文密码
</template>
</a-form-item>
</a-form>
<div id="zhs-captcha-mount" style="margin: 8px 0; min-height: 40px;" />
<a-flex justify="end" :gap="8" style="margin-top: 16px;">
<a-button @click="emit('close')" :disabled="loading">取消</a-button>
<a-button type="primary" :loading="loading" :disabled="!captchaReady" @click="submit">
{{ loading ? '登录中...' : '登录智慧树' }}
</a-button>
</a-flex>
</a-modal>
</template>