feat: recover transient PDD sold-out pages (#27)
This commit is contained in:
@@ -67,6 +67,15 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-section" aria-labelledby="recovery-settings-title">
|
||||
<div class="section-heading"><div><h3 id="recovery-settings-title">商品页假售罄恢复</h3><p>仅在商品页精确显示“商品已售罄”且没有正常商品证据时触发;规格项售罄不会触发。</p></div><el-switch v-model="form.recovery.enabled" active-text="启用" inactive-text="关闭" /></div>
|
||||
<div v-if="form.recovery.enabled" class="form-grid hook-grid">
|
||||
<el-form-item label="下拉次数"><el-input-number v-model="form.recovery.pullDownCount" :min="range('pageRecovery', 'pullDownCount').min" :max="range('pageRecovery', 'pullDownCount').max" controls-position="right" /></el-form-item>
|
||||
<el-form-item label="下拉间隔(毫秒)"><el-input-number v-model="form.recovery.intervalMs" :min="range('pageRecovery', 'intervalMs').min" :max="range('pageRecovery', 'intervalMs').max" :step="100" controls-position="right" /></el-form-item>
|
||||
<el-form-item label="刷新后等待(毫秒)"><el-input-number v-model="form.recovery.settleMs" :min="range('pageRecovery', 'settleMs').min" :max="range('pageRecovery', 'settleMs').max" :step="100" controls-position="right" /></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-collapse class="json-collapse"><el-collapse-item title="查看只读 JSON 预览" name="preview"><pre class="json-preview-block" tabindex="0">{{ previewText }}</pre></el-collapse-item></el-collapse>
|
||||
</template>
|
||||
|
||||
@@ -88,7 +97,8 @@ const defaultConstraints = {
|
||||
aliasCount: { min: 1, max: 20 }, aliasLength: { min: 1, max: 30 },
|
||||
timeoutsMs: { page: { min: 100, max: 60000 }, specPanel: { min: 100, max: 30000 }, selection: { min: 100, max: 10000 }, price: { min: 100, max: 10000 }, overall: { min: 1000, max: 600000 }},
|
||||
limits: { goodsPageVerticalSwipes: { min: 0, max: 10 }, specHorizontalSwipes: { min: 0, max: 30 }, specVerticalSwipes: { min: 0, max: 30 }, stableEdgeReads: { min: 1, max: 5 }, stablePriceReads: { min: 2, max: 5 }, maxSkuCount: { min: 1, max: 2000 }},
|
||||
swipe: { count: { min: 1, max: 5 }, settleMs: { min: 0, max: 2000 }}
|
||||
swipe: { count: { min: 1, max: 5 }, settleMs: { min: 0, max: 2000 }},
|
||||
pageRecovery: { pullDownCount: { min: 1, max: 3 }, intervalMs: { min: 500, max: 2000 }, settleMs: { min: 500, max: 5000 }, maxAttempts: { min: 1, max: 1 }}
|
||||
}
|
||||
const timeoutFields = [{ key: 'page', label: '商品页', step: 1000 }, { key: 'specPanel', label: '规格面板', step: 500 }, { key: 'selection', label: '选中确认', step: 100 }, { key: 'price', label: '价格稳定', step: 100 }, { key: 'overall', label: '任务总时长', step: 5000 }]
|
||||
const limitFields = [{ key: 'goodsPageVerticalSwipes', label: '商品页纵向滑动' }, { key: 'specHorizontalSwipes', label: '规格横向滑动' }, { key: 'specVerticalSwipes', label: '规格纵向滑动' }, { key: 'stableEdgeReads', label: '边界稳定读取' }, { key: 'stablePriceReads', label: '价格稳定读取' }, { key: 'maxSkuCount', label: '最大 SKU 数量' }]
|
||||
@@ -130,16 +140,16 @@ export default {
|
||||
},
|
||||
created() { this.form = this.emptyForm(); this.getList() },
|
||||
methods: {
|
||||
emptyForm() { return { mode: 'v2', name: '', contentText: '', browserPackage: '', activityName: '', colorAliases: [], sizeAliases: [], timeouts: {}, limits: {}, hook: { enabled: false, direction: 'up', count: 1, settleMs: 350 }} },
|
||||
emptyForm() { return { mode: 'v2', name: '', contentText: '', browserPackage: '', activityName: '', colorAliases: [], sizeAliases: [], timeouts: {}, limits: {}, hook: { enabled: false, direction: 'up', count: 1, settleMs: 350 }, recovery: { enabled: false, pullDownCount: 2, intervalMs: 1000, settleMs: 2000 }} },
|
||||
clone(value) { return JSON.parse(JSON.stringify(value)) },
|
||||
range(group, key) { return this.constraints[group]?.[key] || { min: 0, max: 999999 } },
|
||||
async ensureTemplate() { if (this.serverTemplate) return; const response = await getCollectionRuleTemplate(); this.serverTemplate = this.clone(response.data.content); this.constraints = response.data.constraints || defaultConstraints },
|
||||
fillV2(content, name = '') { this.contentBase = this.clone(content); const hook = content.hooks?.afterSpecPanelOpen?.[0]; this.form = { mode: 'v2', name, contentText: '', browserPackage: content.navigation.steps[0].packageName, activityName: content.pageEvidence.activityName, colorAliases: [...content.collector.dimensionAliases.color], sizeAliases: [...content.collector.dimensionAliases.size], timeouts: { ...content.collector.timeoutsMs }, limits: { ...content.collector.limits }, hook: { enabled: Boolean(hook), direction: hook?.direction || 'up', count: hook?.count || 1, settleMs: hook?.settleMs ?? 350 }} },
|
||||
buildV2Content() { if (!this.contentBase) return {}; const content = this.clone(this.contentBase); content.navigation.steps[0].packageName = this.form.browserPackage; content.pageEvidence.activityName = this.form.activityName.trim(); content.collector.dimensionAliases.color = this.form.colorAliases.map(item => item.trim()).filter(Boolean); content.collector.dimensionAliases.size = this.form.sizeAliases.map(item => item.trim()).filter(Boolean); content.collector.timeoutsMs = { ...this.form.timeouts }; content.collector.limits = { ...this.form.limits }; content.hooks.afterSpecPanelOpen = this.form.hook.enabled ? [{ action: 'swipe', target: 'specPanel', direction: this.form.hook.direction, count: this.form.hook.count, settleMs: this.form.hook.settleMs }] : []; return content },
|
||||
fillV2(content, name = '') { this.contentBase = this.clone(content); const hook = content.hooks?.afterSpecPanelOpen?.[0]; const recovery = content.pageRecovery?.transientSoldOut; this.form = { mode: 'v2', name, contentText: '', browserPackage: content.navigation.steps[0].packageName, activityName: content.pageEvidence.activityName, colorAliases: [...content.collector.dimensionAliases.color], sizeAliases: [...content.collector.dimensionAliases.size], timeouts: { ...content.collector.timeoutsMs }, limits: { ...content.collector.limits }, hook: { enabled: Boolean(hook), direction: hook?.direction || 'up', count: hook?.count || 1, settleMs: hook?.settleMs ?? 350 }, recovery: { enabled: Boolean(recovery?.enabled), pullDownCount: recovery?.pullDownCount || 2, intervalMs: recovery?.intervalMs || 1000, settleMs: recovery?.settleMs || 2000 }} },
|
||||
buildV2Content() { if (!this.contentBase) return {}; const content = this.clone(this.contentBase); content.navigation.steps[0].packageName = this.form.browserPackage; content.pageEvidence.activityName = this.form.activityName.trim(); content.collector.dimensionAliases.color = this.form.colorAliases.map(item => item.trim()).filter(Boolean); content.collector.dimensionAliases.size = this.form.sizeAliases.map(item => item.trim()).filter(Boolean); content.collector.timeoutsMs = { ...this.form.timeouts }; content.collector.limits = { ...this.form.limits }; content.hooks.afterSpecPanelOpen = this.form.hook.enabled ? [{ action: 'swipe', target: 'specPanel', direction: this.form.hook.direction, count: this.form.hook.count, settleMs: this.form.hook.settleMs }] : []; content.pageRecovery = { transientSoldOut: { enabled: this.form.recovery.enabled, exactText: '商品已售罄', pullDownCount: this.form.recovery.pullDownCount, intervalMs: this.form.recovery.intervalMs, settleMs: this.form.recovery.settleMs, maxAttempts: 1 }}; return content },
|
||||
async getList() { this.loading = true; try { const response = await listCollectionRules(this.query); this.rulesList = response.data.items; this.total = response.data.total } finally { this.loading = false } },
|
||||
handleQuery() { this.query.page = 1; this.getList() }, resetQuery() { this.query = { page: 1, pageSize: 20, name: '' }; this.getList() },
|
||||
ruleType(content) { return content.schemaVersion === 2 ? 'PDD 商品详情 v2' : `旧版 v${content.schemaVersion || '?'}` },
|
||||
summary(content) { if (content.schemaVersion !== 2) { const text = JSON.stringify(content); return text.length > 100 ? `${text.slice(0, 100)}…` : text } const aliases = content.collector?.dimensionAliases; const hookCount = content.hooks?.afterSpecPanelOpen?.[0]?.count || 0; return `颜色别名 ${aliases?.color?.length || 0} · 尺码别名 ${aliases?.size?.length || 0} · 附加滑动 ${hookCount} 次` },
|
||||
summary(content) { if (content.schemaVersion !== 2) { const text = JSON.stringify(content); return text.length > 100 ? `${text.slice(0, 100)}…` : text } const aliases = content.collector?.dimensionAliases; const hookCount = content.hooks?.afterSpecPanelOpen?.[0]?.count || 0; const recovery = content.pageRecovery?.transientSoldOut?.enabled ? '假售罄恢复已启用' : '假售罄恢复关闭'; return `颜色别名 ${aliases?.color?.length || 0} · 尺码别名 ${aliases?.size?.length || 0} · 附加滑动 ${hookCount} 次 · ${recovery}` },
|
||||
async openCreate() { this.dialog = { open: true, loading: true, saving: false, ruleId: null }; try { await this.ensureTemplate(); this.fillV2(this.serverTemplate, 'PDD 商品详情采集') } finally { this.dialog.loading = false; this.$nextTick(() => this.$refs.ruleForm?.clearValidate()) } },
|
||||
async openEdit(row) { this.dialog = { open: true, loading: true, saving: false, ruleId: row.id }; try { if (row.content.schemaVersion === 2) { await this.ensureTemplate(); this.fillV2(row.content, row.name) } else { this.contentBase = null; this.form = { ...this.emptyForm(), mode: 'v1', name: row.name, contentText: JSON.stringify(row.content, null, 2) } } } finally { this.dialog.loading = false; this.$nextTick(() => this.$refs.ruleForm?.clearValidate()) } },
|
||||
async save() { const valid = await this.$refs.ruleForm.validate().catch(() => false); if (!valid) return; this.dialog.saving = true; const content = this.form.mode === 'v2' ? this.buildV2Content() : JSON.parse(this.form.contentText); const payload = { requestId: window.crypto.randomUUID(), name: this.form.name.trim(), content }; try { if (this.dialog.ruleId) await updateCollectionRule(this.dialog.ruleId, payload); else await createCollectionRule(payload); ElMessage.success(this.dialog.ruleId ? '规则已更新,新任务将使用新内容' : '规则已创建并立即生效'); this.dialog.open = false; await this.getList() } finally { this.dialog.saving = false } },
|
||||
|
||||
Reference in New Issue
Block a user