master
parent
031d9e57f6
commit
9050460fc4
@ -0,0 +1,72 @@
|
||||
import { http } from '@/utils/http'
|
||||
import type { ApiBody, Paginated } from '@/api/types'
|
||||
|
||||
export type CrawlAddressTargetType = 'paper' | 'industry_news' | 'teacher'
|
||||
|
||||
export interface CrawlAddressRow {
|
||||
id: number
|
||||
target_type: CrawlAddressTargetType
|
||||
name: string
|
||||
request_url: string
|
||||
keyword?: string | null
|
||||
category_dict_item_id?: number | null
|
||||
category_label?: string | null
|
||||
university_id?: number | null
|
||||
university_name?: string | null
|
||||
department?: string | null
|
||||
sort: number
|
||||
status: number
|
||||
created_at?: string | null
|
||||
}
|
||||
|
||||
export async function fetchCrawlAddresses(params: Record<string, unknown>) {
|
||||
const { data } = await http.get<ApiBody<Paginated<CrawlAddressRow>>>('/admin/v1/crawl-addresses', {
|
||||
params,
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchCrawlAddressOptions(target_type?: CrawlAddressTargetType) {
|
||||
const { data } = await http.get<ApiBody<{ items: CrawlAddressRow[] }>>(
|
||||
'/admin/v1/crawl-addresses/options',
|
||||
{ params: target_type ? { target_type } : undefined },
|
||||
)
|
||||
return data.data.items
|
||||
}
|
||||
|
||||
export async function createCrawlAddress(payload: {
|
||||
target_type: CrawlAddressTargetType
|
||||
name: string
|
||||
request_url: string
|
||||
keyword?: string | null
|
||||
category_dict_item_id?: number | null
|
||||
university_id?: number | null
|
||||
department?: string | null
|
||||
sort?: number
|
||||
status: number
|
||||
}) {
|
||||
const { data } = await http.post<ApiBody<CrawlAddressRow>>('/admin/v1/crawl-addresses', payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateCrawlAddress(
|
||||
id: number,
|
||||
payload: Partial<{
|
||||
target_type: CrawlAddressTargetType
|
||||
name: string
|
||||
request_url: string
|
||||
keyword?: string | null
|
||||
category_dict_item_id?: number | null
|
||||
university_id?: number | null
|
||||
sort: number
|
||||
status: number
|
||||
}>,
|
||||
) {
|
||||
const { data } = await http.put<ApiBody<CrawlAddressRow>>(`/admin/v1/crawl-addresses/${id}`, payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deleteCrawlAddress(id: number) {
|
||||
const { data } = await http.delete<ApiBody<null>>(`/admin/v1/crawl-addresses/${id}`)
|
||||
return data
|
||||
}
|
||||
@ -0,0 +1,341 @@
|
||||
<script setup lang="ts">
|
||||
import PageTitle from '@/components/PageTitle.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { usePageLoad } from '@/composables/usePageLoad'
|
||||
import {
|
||||
fetchCrawlAddresses,
|
||||
createCrawlAddress,
|
||||
updateCrawlAddress,
|
||||
deleteCrawlAddress,
|
||||
type CrawlAddressRow,
|
||||
type CrawlAddressTargetType,
|
||||
} from '@/api/admin/crawl-addresses'
|
||||
import { fetchDictByCode } from '@/api/admin/dict'
|
||||
import { fetchUniversities } from '@/api/admin/teachers'
|
||||
import { enabledStatusClass } from '@/utils/admin-list'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const TARGET_TYPE_LABELS: Record<CrawlAddressTargetType, string> = {
|
||||
paper: '论文 → 论文库',
|
||||
industry_news: '行业资讯 → 资讯管理',
|
||||
teacher: '老师库 → 老师库',
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const items = ref<CrawlAddressRow[]>([])
|
||||
const meta = ref({ current_page: 1, per_page: 20, total: 0 })
|
||||
const keyword = ref('')
|
||||
const filterTargetType = ref<CrawlAddressTargetType | ''>('')
|
||||
const page = ref(1)
|
||||
|
||||
const newsCategoryOptions = ref<{ id: number; label: string }[]>([])
|
||||
const universityOptions = ref<{ id: number; name: string }[]>([])
|
||||
|
||||
const dialog = ref(false)
|
||||
const editing = ref<CrawlAddressRow | null>(null)
|
||||
const form = ref({
|
||||
target_type: 'paper' as CrawlAddressTargetType,
|
||||
name: '',
|
||||
request_url: '',
|
||||
keyword: '',
|
||||
category_dict_item_id: null as number | null,
|
||||
university_id: null as number | null,
|
||||
department: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
})
|
||||
|
||||
const showCategoryField = computed(() => form.value.target_type === 'industry_news')
|
||||
const showUniversityField = computed(() => form.value.target_type === 'teacher')
|
||||
|
||||
function targetTypeLabel(type: CrawlAddressTargetType) {
|
||||
return TARGET_TYPE_LABELS[type] || type
|
||||
}
|
||||
|
||||
async function loadOptionsForForm() {
|
||||
if (newsCategoryOptions.value.length === 0) {
|
||||
try {
|
||||
const res = await fetchDictByCode('news_category')
|
||||
newsCategoryOptions.value = res.items.map((o) => ({ id: o.id, label: o.label }))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (universityOptions.value.length === 0) {
|
||||
try {
|
||||
const res = await fetchUniversities({ page: 1, page_size: 500, simple: 1 })
|
||||
universityOptions.value = res.items.map((u) => ({ id: u.id, name: u.name }))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchCrawlAddresses({
|
||||
page: page.value,
|
||||
page_size: meta.value.per_page,
|
||||
keyword: keyword.value || undefined,
|
||||
target_type: filterTargetType.value || undefined,
|
||||
})
|
||||
items.value = res.items
|
||||
meta.value = res.meta
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetFormFieldsByType(type: CrawlAddressTargetType) {
|
||||
if (type !== 'industry_news') {
|
||||
form.value.category_dict_item_id = null
|
||||
}
|
||||
if (type !== 'teacher') {
|
||||
form.value.university_id = null
|
||||
form.value.department = ''
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null
|
||||
form.value = {
|
||||
target_type: 'paper',
|
||||
name: '',
|
||||
request_url: '',
|
||||
keyword: '',
|
||||
category_dict_item_id: null,
|
||||
university_id: null,
|
||||
department: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
}
|
||||
dialog.value = true
|
||||
void loadOptionsForForm()
|
||||
}
|
||||
|
||||
function openEdit(row: CrawlAddressRow) {
|
||||
editing.value = row
|
||||
form.value = {
|
||||
target_type: row.target_type,
|
||||
name: row.name,
|
||||
request_url: row.request_url,
|
||||
keyword: row.keyword || '',
|
||||
category_dict_item_id: row.category_dict_item_id ?? null,
|
||||
university_id: row.university_id ?? null,
|
||||
department: row.department || '',
|
||||
sort: row.sort,
|
||||
status: row.status,
|
||||
}
|
||||
dialog.value = true
|
||||
void loadOptionsForForm()
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.value.name.trim()) {
|
||||
ElMessage.warning('请填写地址名称')
|
||||
return
|
||||
}
|
||||
if (!form.value.request_url.trim()) {
|
||||
ElMessage.warning('请填写目标地址')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
target_type: form.value.target_type,
|
||||
name: form.value.name.trim(),
|
||||
request_url: form.value.request_url.trim(),
|
||||
keyword: form.value.keyword.trim() || null,
|
||||
category_dict_item_id: showCategoryField.value ? form.value.category_dict_item_id : null,
|
||||
university_id: showUniversityField.value ? form.value.university_id : null,
|
||||
department: showUniversityField.value ? form.value.department.trim() || null : null,
|
||||
sort: form.value.sort,
|
||||
status: form.value.status,
|
||||
}
|
||||
if (editing.value) {
|
||||
await updateCrawlAddress(editing.value.id, payload)
|
||||
} else {
|
||||
await createCrawlAddress(payload)
|
||||
}
|
||||
ElMessage.success('已保存')
|
||||
dialog.value = false
|
||||
await load()
|
||||
}
|
||||
|
||||
async function remove(row: CrawlAddressRow) {
|
||||
await ElMessageBox.confirm(`确定删除爬虫地址「${row.name}」?`, '提示', { type: 'warning' })
|
||||
await deleteCrawlAddress(row.id)
|
||||
ElMessage.success('已删除')
|
||||
await load()
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.value = 1
|
||||
void load()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
keyword.value = ''
|
||||
filterTargetType.value = ''
|
||||
page.value = 1
|
||||
void load()
|
||||
}
|
||||
|
||||
function onFormTargetTypeChange(type: CrawlAddressTargetType) {
|
||||
resetFormFieldsByType(type)
|
||||
}
|
||||
|
||||
usePageLoad(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list-page">
|
||||
<div class="page-header">
|
||||
<PageTitle />
|
||||
<el-button type="primary" size="small" class="btn-create" @click="openCreate">新增</el-button>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="admin-list-card">
|
||||
<div class="list-filter-bar">
|
||||
<el-select
|
||||
v-model="filterTargetType"
|
||||
clearable
|
||||
placeholder="入库类型"
|
||||
class="filter-select"
|
||||
@change="search"
|
||||
>
|
||||
<el-option label="论文 → 论文库" value="paper" />
|
||||
<el-option label="行业资讯 → 资讯管理" value="industry_news" />
|
||||
<el-option label="老师库 → 老师库" value="teacher" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="名称 / 地址 / 关键词"
|
||||
clearable
|
||||
class="filter-search"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-button type="primary" @click="search">搜索</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="items" row-key="id">
|
||||
<el-table-column label="入库类型" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ targetTypeLabel(row.target_type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="地址名称" min-width="140" />
|
||||
<el-table-column prop="request_url" label="目标地址" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="keyword" label="关键词" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="资讯分类" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.category_label || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="高校" width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.university_name || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="学院" width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.department || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="status-badge" :class="enabledStatusClass(row.status)">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-row-actions">
|
||||
<el-button class="btn-action-primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-action-brand" @click="remove(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="list-pager">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
layout="total, prev, pager, next"
|
||||
:total="meta.total"
|
||||
@current-change="load"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialog" :title="editing ? '编辑爬虫地址' : '新增爬虫地址'" width="560px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="入库类型" required>
|
||||
<el-radio-group v-model="form.target_type" @change="onFormTargetTypeChange">
|
||||
<el-radio label="paper">论文</el-radio>
|
||||
<el-radio label="industry_news">行业资讯</el-radio>
|
||||
<el-radio label="teacher">老师库</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="地址名称" required>
|
||||
<el-input v-model="form.name" maxlength="128" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="目标地址" required>
|
||||
<el-input v-model="form.request_url" type="url" placeholder="https://" />
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="form.keyword"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="多个关键词用逗号或换行分隔"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showCategoryField" label="资讯分类">
|
||||
<el-select v-model="form.category_dict_item_id" clearable filterable placeholder="请选择">
|
||||
<el-option
|
||||
v-for="opt in newsCategoryOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.label"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showUniversityField" label="高校">
|
||||
<el-select v-model="form.university_id" clearable filterable placeholder="请选择">
|
||||
<el-option
|
||||
v-for="opt in universityOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.name"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showUniversityField" label="学院">
|
||||
<el-input v-model="form.department" maxlength="128" placeholder="如:电子信息与电气工程学院" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-select {
|
||||
width: 180px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in new issue