feat: 过滤规则引入视频总长度和联合投稿 (#684)
This commit is contained in:
@@ -37,13 +37,22 @@ impl Evaluatable<usize> for Condition<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Evaluatable<&NaiveDateTime> for Condition<NaiveDateTime> {
|
||||
fn evaluate(&self, value: &NaiveDateTime) -> bool {
|
||||
impl Evaluatable<NaiveDateTime> for Condition<NaiveDateTime> {
|
||||
fn evaluate(&self, value: NaiveDateTime) -> bool {
|
||||
match self {
|
||||
Condition::Equals(expected) => expected == value,
|
||||
Condition::GreaterThan(threshold) => value > threshold,
|
||||
Condition::LessThan(threshold) => value < threshold,
|
||||
Condition::Between(start, end) => value > start && value < end,
|
||||
Condition::Equals(expected) => *expected == value,
|
||||
Condition::GreaterThan(threshold) => value > *threshold,
|
||||
Condition::LessThan(threshold) => value < *threshold,
|
||||
Condition::Between(start, end) => value > *start && value < *end,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Evaluatable<bool> for Condition<bool> {
|
||||
fn evaluate(&self, value: bool) -> bool {
|
||||
match self {
|
||||
Condition::Equals(expected) => *expected == value,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -65,13 +74,20 @@ impl FieldEvaluatable for RuleTarget {
|
||||
.favtime
|
||||
.try_as_ref()
|
||||
.map(|fav_time| fav_time.and_utc().with_timezone(&Local).naive_local()) // 数据库中保存的一律是 utc 时间,转换为 local 时间再比较
|
||||
.is_some_and(|fav_time| cond.evaluate(&fav_time)),
|
||||
.is_some_and(|fav_time| cond.evaluate(fav_time)),
|
||||
RuleTarget::PubTime(cond) => video
|
||||
.pubtime
|
||||
.try_as_ref()
|
||||
.map(|pub_time| pub_time.and_utc().with_timezone(&Local).naive_local())
|
||||
.is_some_and(|pub_time| cond.evaluate(&pub_time)),
|
||||
.is_some_and(|pub_time| cond.evaluate(pub_time)),
|
||||
RuleTarget::PageCount(cond) => cond.evaluate(pages.len()),
|
||||
RuleTarget::SumVideoLength(cond) => pages
|
||||
.iter()
|
||||
.try_fold(0usize, |acc, page| {
|
||||
page.duration.try_as_ref().map(|d| acc + *d as usize).ok_or(())
|
||||
})
|
||||
.is_ok_and(|total_length| cond.evaluate(total_length)),
|
||||
RuleTarget::MultiUpper(cond) => cond.evaluate(video.staff.as_ref().is_some()),
|
||||
RuleTarget::Not(inner) => !inner.evaluate(video, pages),
|
||||
}
|
||||
}
|
||||
@@ -86,9 +102,13 @@ impl FieldEvaluatable for RuleTarget {
|
||||
.tags
|
||||
.as_ref()
|
||||
.is_some_and(|tags| tags.0.iter().any(|tag| cond.evaluate(tag))),
|
||||
RuleTarget::FavTime(cond) => cond.evaluate(&video.favtime.and_utc().with_timezone(&Local).naive_local()),
|
||||
RuleTarget::PubTime(cond) => cond.evaluate(&video.pubtime.and_utc().with_timezone(&Local).naive_local()),
|
||||
RuleTarget::FavTime(cond) => cond.evaluate(video.favtime.and_utc().with_timezone(&Local).naive_local()),
|
||||
RuleTarget::PubTime(cond) => cond.evaluate(video.pubtime.and_utc().with_timezone(&Local).naive_local()),
|
||||
RuleTarget::PageCount(cond) => cond.evaluate(pages.len()),
|
||||
RuleTarget::SumVideoLength(cond) => {
|
||||
cond.evaluate(pages.iter().fold(0usize, |acc, page| acc + page.duration as usize))
|
||||
}
|
||||
RuleTarget::MultiUpper(cond) => cond.evaluate(video.staff.is_some()),
|
||||
RuleTarget::Not(inner) => !inner.evaluate_model(video, pages),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ pub enum RuleTarget {
|
||||
FavTime(Condition<DateTime>),
|
||||
PubTime(Condition<DateTime>),
|
||||
PageCount(Condition<usize>),
|
||||
SumVideoLength(Condition<usize>),
|
||||
MultiUpper(Condition<bool>),
|
||||
Not(Box<RuleTarget>),
|
||||
}
|
||||
|
||||
@@ -63,6 +65,8 @@ impl Display for RuleTarget {
|
||||
RuleTarget::FavTime(_) => "收藏时间",
|
||||
RuleTarget::PubTime(_) => "发布时间",
|
||||
RuleTarget::PageCount(_) => "视频分页数量",
|
||||
RuleTarget::SumVideoLength(_) => "视频总时长",
|
||||
RuleTarget::MultiUpper(_) => "联合投稿",
|
||||
RuleTarget::Not(inner) => {
|
||||
if depth == 0 {
|
||||
get_field_name(inner, depth + 1)
|
||||
@@ -79,14 +83,16 @@ impl Display for RuleTarget {
|
||||
RuleTarget::FavTime(cond) | RuleTarget::PubTime(cond) => {
|
||||
write!(f, "{}不{}", field_name, cond)
|
||||
}
|
||||
RuleTarget::PageCount(cond) => write!(f, "{}不{}", field_name, cond),
|
||||
RuleTarget::PageCount(cond) | RuleTarget::SumVideoLength(cond) => write!(f, "{}不{}", field_name, cond),
|
||||
RuleTarget::MultiUpper(cond) => write!(f, "{}不{}", field_name, cond),
|
||||
RuleTarget::Not(_) => write!(f, "格式化失败"),
|
||||
},
|
||||
RuleTarget::Title(cond) | RuleTarget::Tags(cond) => write!(f, "{}{}", field_name, cond),
|
||||
RuleTarget::FavTime(cond) | RuleTarget::PubTime(cond) => {
|
||||
write!(f, "{}{}", field_name, cond)
|
||||
}
|
||||
RuleTarget::PageCount(cond) => write!(f, "{}{}", field_name, cond),
|
||||
RuleTarget::PageCount(cond) | RuleTarget::SumVideoLength(cond) => write!(f, "{}{}", field_name, cond),
|
||||
RuleTarget::MultiUpper(cond) => write!(f, "{}{}", field_name, cond),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import * as Select from '$lib/components/ui/select/index.js';
|
||||
import { PlusIcon, MinusIcon, XIcon } from '@lucide/svelte/icons';
|
||||
import type { Rule, RuleTarget, Condition } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -21,7 +22,9 @@
|
||||
{ value: 'tags', label: '标签' },
|
||||
{ value: 'favTime', label: '收藏时间' },
|
||||
{ value: 'pubTime', label: '发布时间' },
|
||||
{ value: 'pageCount', label: '视频分页数量' }
|
||||
{ value: 'pageCount', label: '视频分页数量' },
|
||||
{ value: 'sumVideoLength', label: '视频总时长' },
|
||||
{ value: 'multiUpper', label: '联合投稿' }
|
||||
];
|
||||
|
||||
const getOperatorOptions = (field: string) => {
|
||||
@@ -37,6 +40,7 @@
|
||||
{ value: 'matchesRegex', label: '匹配正则' }
|
||||
];
|
||||
case 'pageCount':
|
||||
case 'sumVideoLength':
|
||||
return [
|
||||
{ value: 'equals', label: '等于' },
|
||||
{ value: 'greaterThan', label: '大于' },
|
||||
@@ -51,6 +55,8 @@
|
||||
{ value: 'lessThan', label: '早于' },
|
||||
{ value: 'between', label: '时间范围' }
|
||||
];
|
||||
case 'multiUpper':
|
||||
return [{ value: 'equals', label: '等于' }];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -80,7 +86,9 @@
|
||||
}
|
||||
});
|
||||
|
||||
function convertRuleTargetToLocal(target: RuleTarget<string | number | Date>): LocalCondition {
|
||||
function convertRuleTargetToLocal(
|
||||
target: RuleTarget<string | number | boolean | Date>
|
||||
): LocalCondition {
|
||||
if (typeof target.rule === 'object' && 'field' in target.rule) {
|
||||
// 嵌套的 not
|
||||
const innerCondition = convertRuleTargetToLocal(target.rule);
|
||||
@@ -93,10 +101,10 @@
|
||||
let value = '';
|
||||
let value2 = '';
|
||||
if (Array.isArray(condition.value)) {
|
||||
value = String(condition.value[0] || '');
|
||||
value2 = String(condition.value[1] || '');
|
||||
value = String(condition.value[0] ?? '');
|
||||
value2 = String(condition.value[1] ?? '');
|
||||
} else {
|
||||
value = String(condition.value || '');
|
||||
value = String(condition.value ?? '');
|
||||
}
|
||||
return {
|
||||
field: target.field,
|
||||
@@ -111,8 +119,8 @@
|
||||
if (localRule.length === 0) return null;
|
||||
return localRule.map((andGroup) =>
|
||||
andGroup.conditions.map((condition) => {
|
||||
let value: string | number | Date | (string | number | Date)[];
|
||||
if (condition.field === 'pageCount') {
|
||||
let value: string | number | boolean | Date | (string | number | boolean | Date)[];
|
||||
if (condition.field === 'pageCount' || condition.field === 'sumVideoLength') {
|
||||
if (condition.operator === 'between') {
|
||||
value = [parseInt(condition.value) || 0, parseInt(condition.value2 || '0') || 0];
|
||||
} else {
|
||||
@@ -124,6 +132,8 @@
|
||||
} else {
|
||||
value = condition.value;
|
||||
}
|
||||
} else if (condition.field === 'multiUpper') {
|
||||
value = condition.value === 'true';
|
||||
} else {
|
||||
if (condition.operator === 'between') {
|
||||
value = [condition.value, condition.value2 || ''];
|
||||
@@ -131,12 +141,12 @@
|
||||
value = condition.value;
|
||||
}
|
||||
}
|
||||
const conditionObj: Condition<string | number | Date> = {
|
||||
const conditionObj: Condition<string | number | boolean | Date> = {
|
||||
operator: condition.operator,
|
||||
value
|
||||
};
|
||||
|
||||
let target: RuleTarget<string | number | Date> = {
|
||||
let target: RuleTarget<string | number | boolean | Date> = {
|
||||
field: condition.field,
|
||||
rule: conditionObj
|
||||
};
|
||||
@@ -187,7 +197,7 @@
|
||||
condition.field = value;
|
||||
const operators = getOperatorOptions(value);
|
||||
condition.operator = operators[0]?.value || 'equals';
|
||||
condition.value = '';
|
||||
condition.value = value === 'multiUpper' ? 'false' : '';
|
||||
condition.value2 = '';
|
||||
} else if (field === 'operator') {
|
||||
condition.operator = value;
|
||||
@@ -290,36 +300,43 @@
|
||||
<!-- 字段选择 -->
|
||||
<div>
|
||||
<Label class="text-muted-foreground text-xs">字段</Label>
|
||||
<select
|
||||
class="border-input bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={condition.field}
|
||||
onchange={(e) =>
|
||||
updateCondition(groupIndex, conditionIndex, 'field', e.currentTarget.value)}
|
||||
onValueChange={(v) => updateCondition(groupIndex, conditionIndex, 'field', v)}
|
||||
>
|
||||
{#each FIELD_OPTIONS as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Select.Trigger class="w-full">
|
||||
{FIELD_OPTIONS.find((o) => o.value === condition.field)?.label ??
|
||||
condition.field}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each FIELD_OPTIONS as option (option.value)}
|
||||
<Select.Item value={option.value} label={option.label} />
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<!-- 操作符选择 -->
|
||||
<div>
|
||||
<Label class="text-muted-foreground text-xs">操作符</Label>
|
||||
<select
|
||||
class="border-input bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={condition.operator}
|
||||
onchange={(e) =>
|
||||
updateCondition(
|
||||
groupIndex,
|
||||
conditionIndex,
|
||||
'operator',
|
||||
e.currentTarget.value
|
||||
)}
|
||||
onValueChange={(v) =>
|
||||
updateCondition(groupIndex, conditionIndex, 'operator', v)}
|
||||
>
|
||||
{#each getOperatorOptions(condition.field) as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Select.Trigger class="w-full">
|
||||
{getOperatorOptions(condition.field).find(
|
||||
(o) => o.value === condition.operator
|
||||
)?.label ?? condition.operator}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each getOperatorOptions(condition.field) as option (option.value)}
|
||||
<Select.Item value={option.value} label={option.label} />
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -328,7 +345,7 @@
|
||||
<Label class="text-muted-foreground text-xs">值</Label>
|
||||
{#if condition.operator === 'between'}
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#if condition.field === 'pageCount'}
|
||||
{#if condition.field === 'pageCount' || condition.field === 'sumVideoLength'}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="最小值"
|
||||
@@ -411,7 +428,7 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if condition.field === 'pageCount'}
|
||||
{:else if condition.field === 'pageCount' || condition.field === 'sumVideoLength'}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="输入数值"
|
||||
@@ -434,6 +451,20 @@
|
||||
e.currentTarget.value + ':00'
|
||||
)}
|
||||
/>
|
||||
{:else if condition.field === 'multiUpper'}
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={condition.value}
|
||||
onValueChange={(v) => updateCondition(groupIndex, conditionIndex, 'value', v)}
|
||||
>
|
||||
<Select.Trigger class="w-full">
|
||||
{condition.value === 'true' ? 'true' : 'false'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="true" label="true" />
|
||||
<Select.Item value="false" label="false" />
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{:else}
|
||||
<Input
|
||||
type="text"
|
||||
|
||||
@@ -213,7 +213,7 @@ export interface RuleTarget<T> {
|
||||
rule: Condition<T> | RuleTarget<T>;
|
||||
}
|
||||
|
||||
export type AndGroup = RuleTarget<string | number | Date>[];
|
||||
export type AndGroup = RuleTarget<string | number | boolean | Date>[];
|
||||
export type Rule = AndGroup[];
|
||||
|
||||
export interface VideoSourceDetail {
|
||||
|
||||
Reference in New Issue
Block a user