交大智能研究院

master
lion 17 hours ago
parent 9f5d078be6
commit cbef170c43

@ -13,6 +13,9 @@ export interface CrawlAddressOption {
university_id?: number | null
university_name?: string | null
department?: string | null
crawl_source_id?: number | null
crawl_source_name?: string | null
adapter_code?: string | null
}
export const crawlAddressApi = {

@ -22,13 +22,18 @@ export interface CrawlJobResult {
items_imported?: number
papers_imported?: number
teacher_leads_imported?: number
teacher_duplicates_skipped?: number
news_imported?: number
result_summary?: string
preview_teacher_lead_count?: number
}
export const crawlerApi = {
resolveUrl(payload: { request_url: string; target_type: CrawlTargetType }) {
resolveUrl(payload: {
request_url: string
target_type: CrawlTargetType
crawl_address_id?: number
}) {
return request<CrawlResolveResult>('/crawl-jobs/resolve-url', {
method: 'POST',
data: payload,
@ -38,6 +43,7 @@ export const crawlerApi = {
submit(payload: {
target_type: CrawlTargetType
request_url: string
crawl_address_id?: number
params?: Record<string, unknown>
teacher_defaults?: {
university_id?: number

@ -18,9 +18,14 @@
@change="onBannerChange"
>
<swiper-item v-for="banner in displayBanners" :key="banner.id">
<view class="course-banner-v2" :class="{ 'course-banner-v2--cover': !!banner.cover_url }" @tap="onBannerTap(banner)">
<image v-if="banner.cover_url" class="banner-cover-bg" :src="banner.cover_url" mode="aspectFill" />
<view v-if="banner.cover_url" class="banner-cover-overlay" />
<view
v-if="banner.cover_url"
class="course-banner-v2 course-banner-v2--image-only"
@tap="onBannerTap(banner)"
>
<image class="banner-cover-image" :src="banner.cover_url" mode="aspectFill" />
</view>
<view v-else class="course-banner-v2" @tap="onBannerTap(banner)">
<view class="course-banner-main">
<text v-if="showBannerKicker(banner)" class="course-banner-kicker">{{ banner.kicker }}</text>
<text class="course-banner-title">{{ banner.title }}</text>
@ -32,7 +37,7 @@
<text v-if="showBannerDatetime(banner)">{{ banner.datetime_text }}</text>
</view>
</view>
<view v-if="!banner.cover_url" class="course-banner-visual">
<view class="course-banner-visual">
<view class="campus-skyline">
<view class="skyline-block skyline-block--1" />
<view class="skyline-block skyline-block--2" />
@ -426,47 +431,24 @@ function onSignupSuccess(message: string) {
box-shadow: 0 pr(10) pr(22) rgba(68, 76, 92, 0.12);
}
.course-banner-v2--cover {
.course-banner-v2--image-only {
display: block;
height: 100%;
background: #eef2f7;
}
.banner-cover-bg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
.banner-cover-image {
display: block;
width: 100%;
height: 100%;
}
.banner-cover-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.94) 0%,
rgba(255, 255, 255, 0.82) 42%,
rgba(255, 255, 255, 0.28) 72%,
rgba(255, 255, 255, 0.08) 100%
);
}
.course-banner-main {
position: relative;
z-index: 2;
padding: pr(22) 0 pr(18) pr(22);
}
.course-banner-v2--cover .course-banner-main {
padding: pr(22) pr(22) pr(18);
}
.course-banner-kicker {
display: inline-flex;
align-items: center;

@ -47,12 +47,13 @@
<view v-if="form.target_type === 'paper' || form.target_type === 'industry_news' || form.target_type === 'teacher'" class="field">
<text class="form-label">抓取页数</text>
<input v-model.number="maxPages" class="input" type="number" />
<input v-model.number="maxPages" class="input" type="number" :disabled="isAiSjtuResearchCenter" />
<text v-if="form.target_type === 'paper'" class="hint">arXiv 50 </text>
<text v-if="form.target_type === 'paper'" class="hint"></text>
<text v-else-if="form.target_type === 'industry_news'" class="hint">虎嗅投资界清科等列表页建议 35 正文将自动补全入库</text>
<text v-else-if="isAiSjtuResearchCenter" class="hint">交大 AI 研究院研究中心为 API 一次性拉取无需分页</text>
<text v-else-if="form.target_type === 'teacher'" class="hint">多页列表 Sudy CMS博山 CMS交大 tsites请适当增大页数</text>
<text v-else-if="form.target_type === 'teacher'" class="hint">大批量抓取时仅部分老师会访问主页补邮箱避免请求超时</text>
<text v-else-if="form.target_type === 'teacher' && !isAiSjtuResearchCenter" class="hint">大批量抓取时仅部分老师会访问主页补邮箱避免请求超时</text>
</view>
<view class="field">
@ -60,6 +61,7 @@
<input v-model.number="maxResults" class="input" type="number" />
<text v-if="form.target_type === 'paper'" class="hint"> 200 </text>
<text v-else-if="form.target_type === 'teacher'" class="hint">师资列表最多 500 </text>
<text v-if="isAiSjtuResearchCenter" class="hint"></text>
<text v-else-if="form.target_type === 'industry_news'" class="hint">资讯最多 50 URL 已入库将跳过不重写正文空正文需先删旧记录再重抓</text>
<text v-if="form.target_type === 'industry_news'" class="hint">使 HTML</text>
</view>
@ -74,6 +76,9 @@
<text v-if="lastResult.adapter_code" class="result-line">{{ formatAdapterLabel(lastResult.adapter_code) }}</text>
<text v-if="lastResult.result_summary" class="result-line">{{ lastResult.result_summary }}</text>
<text v-else class="result-line">已入库 {{ lastResult.items_imported ?? 0 }} </text>
<text v-if="lastResult.teacher_duplicates_skipped" class="result-line hint-line">
跳过 {{ lastResult.teacher_duplicates_skipped }} 老师库中已有相同邮箱或同校同院系同名老师
</text>
<text v-if="lastResult.items_fetched" class="result-line"> {{ lastResult.items_fetched }} </text>
</view>
</view>
@ -118,14 +123,20 @@ const ADAPTER_LABELS: Record<string, string> = {
faculty_list_html: '师资 HTML',
generic_news_html: '通用资讯',
arxiv_api: 'arXiv API',
ai_sjtu_research_center_api: '交大 AI 研究中心',
}
function formatAdapterLabel(code: string) {
return ADAPTER_LABELS[code] || code
}
const isAiSjtuResearchCenter = computed(
() => resolvedAdapter.value === 'ai_sjtu_research_center_api',
)
const lastResult = ref<Awaited<ReturnType<typeof crawlerApi.submit>> | null>(null)
const addressOptions = ref<CrawlAddressOption[]>([])
const addressIndex = ref(0)
const selectedCrawlAddressId = ref<number | null>(null)
const crawlDefaults = ref<{
category_dict_item_id?: number
category_label?: string
@ -133,6 +144,7 @@ const crawlDefaults = ref<{
university_id?: number
university_name?: string
department?: string
adapter_code?: string
}>({})
const addressLabels = computed(() => [
@ -161,6 +173,9 @@ const selectedAddressHint = computed(() => {
if (crawlDefaults.value.university_name) {
parts.push(`默认高校:${crawlDefaults.value.university_name}`)
}
if (crawlDefaults.value.adapter_code) {
parts.push(`采集适配器:${formatAdapterLabel(crawlDefaults.value.adapter_code)}`)
}
return parts.length > 0 ? parts.join('') : ''
})
@ -214,6 +229,7 @@ async function loadAddresses() {
addressOptions.value = []
}
addressIndex.value = 0
selectedCrawlAddressId.value = null
crawlDefaults.value = {}
}
@ -235,6 +251,9 @@ function applyCrawlDefaults(addr: CrawlAddressOption) {
if (addr.department) {
crawlDefaults.value.department = addr.department
}
if (addr.adapter_code) {
crawlDefaults.value.adapter_code = addr.adapter_code
}
}
function syncFromCrawlAddress(url: string, options?: { fillKeyword?: boolean }) {
@ -244,10 +263,12 @@ function syncFromCrawlAddress(url: string, options?: { fillKeyword?: boolean })
)
if (!matched) {
addressIndex.value = 0
selectedCrawlAddressId.value = null
crawlDefaults.value = {}
return
}
addressIndex.value = addressOptions.value.indexOf(matched) + 1
selectedCrawlAddressId.value = matched.id
if (options?.fillKeyword && matched.keyword) {
keyword.value = matched.keyword
}
@ -291,11 +312,13 @@ function onTypeChange(event: UniHelper.PickerChangeEvent) {
function onAddressPick(event: UniHelper.PickerChangeEvent) {
addressIndex.value = Number(event.detail.value)
if (addressIndex.value <= 0) {
selectedCrawlAddressId.value = null
crawlDefaults.value = {}
return
}
const addr = addressOptions.value[addressIndex.value - 1]
if (!addr) return
selectedCrawlAddressId.value = addr.id
form.request_url = addr.request_url
if (addr.keyword) {
keyword.value = addr.keyword
@ -311,6 +334,7 @@ async function onUrlBlur() {
resolvedAdapter.value = ''
resolvedUrl.value = ''
addressIndex.value = 0
selectedCrawlAddressId.value = null
crawlDefaults.value = {}
return
}
@ -327,10 +351,15 @@ async function onUrlBlur() {
const res = await crawlerApi.resolveUrl({
request_url: normalized,
target_type: form.target_type,
crawl_address_id: selectedCrawlAddressId.value ?? undefined,
})
resolvedName.value = res.source_name
resolvedAdapter.value = res.adapter_code || ''
resolvedUrl.value = normalized
if (res.adapter_code === 'ai_sjtu_research_center_api') {
maxPages.value = 1
maxResults.value = Math.max(maxResults.value, 200)
}
} catch (error) {
resolvedName.value = ''
resolvedAdapter.value = ''
@ -371,24 +400,24 @@ function buildParams(): Record<string, unknown> {
return params
}
function buildSuccessMessage(result: Awaited<ReturnType<typeof crawlerApi.submit>>): string {
if (result.result_summary) {
return result.result_summary
function buildToastMessage(result: Awaited<ReturnType<typeof crawlerApi.submit>>): string {
if (result.target_type === 'teacher') {
const imported = result.items_imported ?? 0
const skipped = result.teacher_duplicates_skipped ?? 0
if (skipped > 0) {
return `已入库${imported}位,跳过${skipped}`
}
return `已入库${imported}位老师`
}
if (result.target_type === 'paper') {
const papers = result.papers_imported ?? result.items_imported ?? 0
const leads = result.teacher_leads_imported ?? 0
return `已入库 ${papers} 篇论文、${leads} 位作者`
return `已入库${papers}篇论文`
}
if (result.target_type === 'industry_news') {
const news = result.items_imported ?? 0
return `已入库 ${news} 条资讯`
}
if (result.target_type === 'teacher') {
const teachers = result.items_imported ?? 0
return `已入库 ${teachers} 位老师`
return `已入库${news}条资讯`
}
return `已入库 ${result.items_imported ?? 0}`
return '抓取完成'
}
function resolveNewsSourceName(url: string): string {
@ -428,6 +457,9 @@ async function submit() {
request_url: normalizedUrl,
params: buildParams(),
}
if (selectedCrawlAddressId.value) {
payload.crawl_address_id = selectedCrawlAddressId.value
}
if (form.target_type === 'industry_news') {
const newsDefaults = buildNewsDefaults(normalizedUrl)
if (newsDefaults) {
@ -448,8 +480,9 @@ async function submit() {
}
lastResult.value = await crawlerApi.submit(payload)
uni.showToast({
title: buildSuccessMessage(lastResult.value),
icon: 'success',
title: buildToastMessage(lastResult.value),
icon: 'none',
duration: 2500,
})
} catch (error) {
uni.showToast({
@ -540,6 +573,11 @@ async function submit() {
font-size: 28rpx;
}
.input[disabled] {
background: #f3f4f6;
color: #9ca3af;
}
.textarea {
min-height: 240rpx;
padding: 24rpx;
@ -590,6 +628,11 @@ async function submit() {
line-height: 1.5;
}
.hint-line {
color: #6b7280;
font-size: 24rpx;
}
.result-summary {
margin-top: 8rpx;
line-height: 1.6;

@ -11,6 +11,14 @@
<text class="detail-label">所属学院</text>
<text class="detail-value">{{ teacher.department }}</text>
</view>
<view v-if="teacher.email" class="detail-row">
<text class="detail-label">电子邮箱</text>
<text class="detail-value contact-value" @tap="copyContact(teacher.email)">{{ teacher.email }}</text>
</view>
<view v-if="teacher.phone" class="detail-row">
<text class="detail-label">联系电话</text>
<text class="detail-value contact-value" @tap="copyContact(teacher.phone)">{{ teacher.phone }}</text>
</view>
<view v-if="teacher.research_direction" class="detail-row">
<text class="detail-label">研究方向</text>
<text class="detail-value">{{ teacher.research_direction }}</text>
@ -50,11 +58,22 @@ interface TeacherDetail {
city?: string | null
university_name?: string | null
research_direction?: string | null
email?: string | null
phone?: string | null
}
const loading = ref(true)
const teacher = ref<TeacherDetail | null>(null)
function copyContact(value?: string | null) {
const text = value?.trim()
if (!text) return
uni.setClipboardData({
data: text,
success: () => uni.showToast({ title: '已复制', icon: 'success' }),
})
}
onLoad(async (query) => {
const id = Number(query?.id)
if (!id) {
@ -111,4 +130,9 @@ onLoad(async (query) => {
font-size: pr(14);
line-height: 1.6;
}
.contact-value {
color: #2563eb;
word-break: break-all;
}
</style>

Loading…
Cancel
Save