问卷 新闻

master
lion 1 day ago
parent 492c01d8c5
commit 45c3911066

@ -15,3 +15,6 @@ export const reviewMany = data => request({ url: base + 'review-many', method: '
export const newsCategories = () => request({ url: base + 'categories', method: 'get', isLoading: false })
export const saveCategorySettings = data => request({ url: base + 'settings', method: 'post', data, isLoading: false })
export const getAutoSettings = () => request({ url: base + 'auto-settings', method: 'get', isLoading: false })
export const saveAutoSettings = data => request({ url: base + 'auto-settings', method: 'post', data, isLoading: false })

@ -6,17 +6,19 @@
<div class="toolbar">
<el-button type="primary" :loading="starting" :disabled="running || !batchLoaded || !!configurationError" @click="start()">获取更新</el-button>
<el-button @click="openSettings">分类留存设置</el-button>
<el-button @click="openAutoSettings">自动获取更新设置</el-button>
<el-button @click="openHistory">历史批次</el-button>
<el-button v-if="batch" @click="openTasks(batch.id, 'failed')">失败明细</el-button>
<el-button v-if="batch && batch.failed && !running" :loading="starting" :disabled="!!configurationError" @click="start(batch.id)">仅重试失败企业</el-button>
<span class="hint">获取全部候选和已占额企业新闻,不受下方筛选影响;不更改企业户状态。</span>
<span class="hint">获取全部候选和已占额企业新闻,不受下方筛选影响;候选企业成功返回有效业务数据后转为已占额。</span>
</div>
<template v-if="batch">
<p>批次 #{{ batch.id }} · {{ running ? '采集中' : '已结束' }} · 开始时间 {{ batch.created_at }}</p>
<p>批次 #{{ batch.id }}({{ batch.scheduled_date ? '自动' : '手动' }}) · {{ running ? '采集中' : '已结束' }} · 开始时间 {{ batch.created_at }}</p>
<div class="metrics">
<span>企业总数 <b>{{ batch.total }}</b></span><span>已处理 <b>{{ batch.processed }}</b></span>
<span>成功 <b>{{ batch.success }}</b></span><span>失败 <b>{{ batch.failed }}</b></span><span>新增新闻 <b>{{ batch.inserted }}</b></span>
</div>
<p v-if="batch.group_size" class="hint">每组 {{ batch.group_size }} 家 · 共 {{ batch.group_total }} 组 · 已完成 {{ batch.group_processed }} 组</p>
<el-progress :percentage="batch.progress" :status="running ? undefined : (batch.failed ? 'warning' : 'success')" />
<p v-if="running" class="hint">{{ batch.current_company ? '当前处理:' + batch.current_company : '等待后台调度继续处理' }}。关闭页面不影响采集。</p>
<p v-if="batch.invalid" class="hint">有 {{ batch.invalid }} 条新闻格式异常,详见失败明细;正常新闻已保存。</p>
@ -67,6 +69,18 @@
</el-table>
<el-pagination :current-page="query.page" :page-size="20" :total="total" layout="total, prev, pager, next" @current-change="pageNews" />
<el-dialog title="自动获取更新设置" :visible.sync="autoVisible" width="600px" :close-on-click-modal="false">
<el-form label-width="130px">
<el-form-item label="启用每日采集"><el-switch v-model="autoForm.enabled" /></el-form-item>
<el-form-item label="每天开始时间"><el-time-picker v-model="autoForm.start_time" value-format="HH:mm" format="HH:mm" :clearable="false" placeholder="选择开始时间" /></el-form-item>
<el-form-item label="每组企业数"><el-input-number v-model="autoForm.group_size" :min="1" :max="100" :precision="0" /></el-form-item>
</el-form>
<p class="hint">时区:{{ autoTimezone }}。建议先使用每组 10 家;每家企业返回的全部新闻处理完后,再处理下一家,单家失败不影响后续企业。</p>
<p class="hint">常驻进程每组结束后等待 5 秒继续;使用计划任务时按调度继续。请确保服务器采集进程或 Laravel 调度正在运行。</p>
<p class="hint">每天自动创建一个批次,已有批次未完成则等待。关闭开关只停止新建自动批次,当前批次继续完成。每组数量及分类留存设置在新建批次时固定。</p>
<p class="hint">如果当天已过开始时间且尚未自动采集,启用后会在下次调度开始。重复新闻不会再次入库,沿用分类留存设置。</p>
<span slot="footer"><el-button @click="autoVisible = false">取消</el-button><el-button type="primary" :loading="autoSaving" @click="saveAutomation">保存设置</el-button></span>
</el-dialog>
<el-dialog title="分类留存设置" custom-class="category-settings-dialog" :visible.sync="settingsVisible" width="650px" top="8vh" :close-on-click-modal="false">
<p>命中任意一个已选分类即自动留存;不选则全部不留存。数量为全部已入库新闻数量,一条新闻可属于多个分类。</p>
<div class="category-actions">
@ -130,12 +144,13 @@
</template>
<script>
import { newsCategories, saveCategorySettings, listNews, companies, showNews, saveNews, reviewNews, reviewMany, newsLogs, startBatch, latestBatch, listBatches, listTasks } from '@/api/companyNews'
import { getAutoSettings, saveAutoSettings, newsCategories, saveCategorySettings, listNews, companies, showNews, saveNews, reviewNews, reviewMany, newsLogs, startBatch, latestBatch, listBatches, listTasks } from '@/api/companyNews'
export default {
name: 'CompanyNews',
data() {
return {
query: { category_codes: [], company_id: '', keyword: '', review_status: 'retained', page: 1 }, dates: [], rows: [], selection: [], total: 0, loading: false,
autoVisible: false, autoSaving: false, autoTimezone: 'Asia/Shanghai', autoForm: { enabled: false, start_time: '22:00', group_size: 10 },
categoryOptions: [], savedRetainedCategories: [], retainedCategories: [], categorySequence: 0, settingsVisible: false, settingsSaving: false,
companyOptions: [], companyLoading: false, companySequence: 0, companyKeyword: '', companyPage: 0, companyTotal: 0, companyHasMore: false, companyScroll: null, listSequence: 0,
configurationError: '', batch: null, batchLoaded: false, starting: false, pollError: false, timer: null, active: false, pollEpoch: 0,
@ -157,6 +172,24 @@ export default {
deactivated() { this.stop(); this.unbindCompanyScroll() },
beforeDestroy() { this.stop(); this.unbindCompanyScroll(); this.companySequence++ },
methods: {
async openAutoSettings() {
try {
const result = await getAutoSettings()
this.autoForm = { enabled: result.enabled, start_time: result.start_time, group_size: result.group_size }
this.autoTimezone = result.timezone
this.autoVisible = true
} catch (_) { /* Request interceptor displays errors. */ }
},
async saveAutomation() {
if (this.autoSaving) return
if (!this.autoForm.start_time || !Number.isInteger(this.autoForm.group_size) || this.autoForm.group_size < 1 || this.autoForm.group_size > 100) return this.$message.warning('请填写开始时间及 1~100 的每组企业数')
this.autoSaving = true
try {
await saveAutoSettings({ ...this.autoForm })
this.$message.success('自动获取更新设置已保存')
this.autoVisible = false
} catch (_) { /* Request interceptor displays errors. */ } finally { this.autoSaving = false }
},
async loadCategories() {
const sequence = ++this.categorySequence
try {

@ -50,10 +50,10 @@ export default {
</script>
<style scoped>
.alumni-news { display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; }
.alumni-news-list { flex: 1; min-height: 0; display: grid; grid-template-rows: repeat(6, minmax(0, 1fr)); overflow: hidden; }
.alumni-news-row { display: flex; align-items: center; min-width: 0; padding: 0 0.8vw; font-size: 1vw; font-weight: bold; color: #fff; }
.alumni-news-list { flex: 1; min-height: 0; display: grid; grid-template-rows: repeat(8, minmax(0, 1fr)); overflow: hidden; }
.alumni-news-row { display: flex; align-items: center; min-width: 0; min-height: 0; overflow: hidden; padding: 0 0.8vw; font-size: 0.95vw; font-weight: bold; color: #fff; }
.alumni-news-row:nth-child(even) { background: rgba(21, 37, 123, 0.5); }
.alumni-news-row a, .alumni-news-row span { display: block; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; line-height: 1.5; }
.alumni-news-row a, .alumni-news-row span { display: block; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; line-height: 1.3; }
.alumni-news-row a:hover, .alumni-news-row a:focus-visible { color: #93ccfd; text-decoration: underline; }
.alumni-news-empty { grid-row: 1 / -1; align-self: center; text-align: center; color: #93ccfd; font-size: 0.9vw; }
.alumni-news-pagination { display: flex; align-items: center; justify-content: center; gap: 0.7vw; padding-top: 1vh; flex-shrink: 0; color: #93ccfd; font-size: 0.8vw; }

@ -1585,14 +1585,35 @@ export default {
font-size: 1vw;
}
.course-system-container .system-table {
flex: 1;
height: auto;
min-height: 0;
}
.course-system-container .table-header,
.course-system-container .table-footer {
flex-shrink: 0;
}
.course-system-container .table-body {
max-height: none;
overflow: hidden;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior-y: contain;
scrollbar-width: none;
-ms-overflow-style: none;
position: relative;
min-height: 0;
flex: 1;
}
.course-system-container .table-body::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
.course-system-container .table-footer {
margin-top: auto;
}
@ -1957,7 +1978,7 @@ export default {
flex-direction: column;
padding: 3.4vh 1.5vw !important;
min-height: 0;
gap: 1.5vh !important;
gap: 0.4vh !important;
backdrop-filter: blur(10px);
}

@ -6,11 +6,11 @@ const flush = async() => { await Promise.resolve(); await Promise.resolve() }
let wrapper
afterEach(() => { if (wrapper) wrapper.destroy(); jest.clearAllMocks() })
it('shows single-line linked titles and requests the next page', async() => {
alumniNews.mockResolvedValueOnce({ data: [{ id: 1, title: '企业新闻', source_url: 'https://example.com/article' }], total: 7, current_page: 1, last_page: 2 })
alumniNews.mockResolvedValueOnce({ data: [{ id: 1, title: '企业新闻', source_url: 'https://example.com/article' }], total: 9, current_page: 1, last_page: 2 })
wrapper = shallowMount(AlumniNews); await flush()
expect(wrapper.find('a').attributes()).toMatchObject({ href: 'https://example.com/article', target: '_blank', rel: 'noopener noreferrer', title: '企业新闻' })
expect(wrapper.findAll('button').at(0).attributes('disabled')).toBeDefined()
alumniNews.mockResolvedValueOnce({ data: [{ id: 2, title: '下一页新闻', source_url: 'javascript:alert(1)' }], total: 7, current_page: 2, last_page: 2 })
alumniNews.mockResolvedValueOnce({ data: [{ id: 2, title: '下一页新闻', source_url: 'javascript:alert(1)' }], total: 9, current_page: 2, last_page: 2 })
await wrapper.findAll('button').at(1).trigger('click'); await flush()
expect(alumniNews).toHaveBeenLastCalledWith({ page: 2 })
expect(wrapper.find('a').exists()).toBe(false)

@ -1,10 +1,10 @@
import { shallowMount, createLocalVue } from '@vue/test-utils'
import ElementUI from 'element-ui'
import News from '@/views/companyNews/index.vue'
import { newsCategories, saveCategorySettings, listNews, companies, latestBatch, startBatch, reviewNews, showNews, saveNews } from '@/api/companyNews'
import { getAutoSettings, saveAutoSettings, newsCategories, saveCategorySettings, listNews, companies, latestBatch, startBatch, reviewNews, showNews, saveNews } from '@/api/companyNews'
jest.mock('@/api/companyNews', () => ({
newsCategories: jest.fn(), saveCategorySettings: jest.fn(), listNews: jest.fn(), companies: jest.fn(), showNews: jest.fn(), saveNews: jest.fn(), reviewNews: jest.fn(),
getAutoSettings: jest.fn(), saveAutoSettings: jest.fn(), newsCategories: jest.fn(), saveCategorySettings: jest.fn(), listNews: jest.fn(), companies: jest.fn(), showNews: jest.fn(), saveNews: jest.fn(), reviewNews: jest.fn(),
newsLogs: jest.fn(), startBatch: jest.fn(), latestBatch: jest.fn(), listBatches: jest.fn(), listTasks: jest.fn()
}))
const localVue = createLocalVue()
@ -24,6 +24,8 @@ describe('Enterprise news management', () => {
jest.useFakeTimers()
newsCategories.mockResolvedValue({ categories: [{ code: '12014', name: '产品信息', count: 317 }], retained_categories: ['12014'] })
saveCategorySettings.mockResolvedValue({})
getAutoSettings.mockResolvedValue({ enabled: false, start_time: '22:00', group_size: 10, timezone: 'Asia/Shanghai' })
saveAutoSettings.mockResolvedValue({})
listNews.mockResolvedValue({ data: [], total: 0 })
companies.mockResolvedValue({ data: [], total: 0, current_page: 1, last_page: 1 })
latestBatch.mockResolvedValue({ batch: null })
@ -47,6 +49,19 @@ describe('Enterprise news management', () => {
expect(wrapper.vm.query.category_codes).toEqual([])
expect(wrapper.vm.query.review_status).toBe('retained')
})
it('loads and saves the daily schedule without starting a collection directly', async() => {
wrapper = create(); await flush()
await wrapper.vm.openAutoSettings()
expect(wrapper.vm.autoForm).toEqual({ enabled: false, start_time: '22:00', group_size: 10 })
wrapper.setData({ autoForm: { enabled: true, start_time: '22:30', group_size: 5 }})
await wrapper.vm.saveAutomation()
expect(saveAutoSettings).toHaveBeenCalledWith({ enabled: true, start_time: '22:30', group_size: 5 })
expect(startBatch).not.toHaveBeenCalled()
expect(wrapper.vm.autoVisible).toBe(false)
wrapper.setData({ autoForm: { enabled: true, start_time: '22:30', group_size: 0 }})
await wrapper.vm.saveAutomation()
expect(saveAutoSettings).toHaveBeenCalledTimes(1)
})
it('restores running progress and prevents starting a duplicate batch', async() => {
latestBatch.mockResolvedValue({ batch: { id: 3, status: 'running', total: 100, processed: 40, success: 38, failed: 2, inserted: 8, progress: 40 }})
wrapper = create(); await flush()

Loading…
Cancel
Save