From 2346afa291172cf25a5ac4a9ffaa00bc5fba4cd3 Mon Sep 17 00:00:00 2001 From: Qi HU Date: Sun, 7 Dec 2025 12:57:08 +0800 Subject: [PATCH] feat: enhance torrent state scheduling and display Signed-off-by: Qi HU --- .../Resources/System/TorrentStateResource.php | 90 ++++-- .../Pages/ManageTorrentStates.php | 5 +- app/Models/TorrentState.php | 267 +++++++++++++++++- app/Policies/TorrentStatePolicy.php | 8 +- ...000_add_remark_to_torrents_state_table.php | 32 +++ include/functions.php | 35 ++- include/globalfunctions.php | 19 +- lang/chs/lang_functions.php | 2 + lang/cht/lang_functions.php | 2 + lang/en/lang_functions.php | 2 + resources/lang/en/label.php | 7 + resources/lang/zh_CN/label.php | 7 + resources/lang/zh_TW/label.php | 7 + 13 files changed, 436 insertions(+), 47 deletions(-) create mode 100644 database/migrations/2024_08_26_000000_add_remark_to_torrents_state_table.php diff --git a/app/Filament/Resources/System/TorrentStateResource.php b/app/Filament/Resources/System/TorrentStateResource.php index c9acdbf3..0880216b 100644 --- a/app/Filament/Resources/System/TorrentStateResource.php +++ b/app/Filament/Resources/System/TorrentStateResource.php @@ -5,21 +5,18 @@ namespace App\Filament\Resources\System; use Filament\Schemas\Schema; use Filament\Forms\Components\Select; use Filament\Forms\Components\DateTimePicker; +use Filament\Forms\Components\Textarea; use Filament\Tables\Columns\TextColumn; use Filament\Actions\EditAction; +use Filament\Actions\DeleteAction; +use Filament\Actions\DeleteBulkAction; use App\Filament\Resources\System\TorrentStateResource\Pages\ManageTorrentStates; -use App\Filament\Resources\System\TorrentStateResource\Pages; -use App\Filament\Resources\System\TorrentStateResource\RelationManagers; -use App\Models\Setting; use App\Models\Torrent; use App\Models\TorrentState; -use Filament\Forms; +use Carbon\Carbon; use Filament\Resources\Resource; use Filament\Tables\Table; -use Filament\Tables; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; -use Nexus\Database\NexusDB; class TorrentStateResource extends Resource { @@ -46,13 +43,28 @@ class TorrentStateResource extends Resource return $schema ->components([ Select::make('global_sp_state') - ->options(Torrent::listPromotionTypes(true)) + ->options(function () { + $options = Torrent::listPromotionTypes(true); + unset($options[Torrent::PROMOTION_NORMAL]); + return $options; + }) ->label(__('label.torrent_state.global_sp_state')) ->required(), DateTimePicker::make('begin') - ->label(__('label.begin')), + ->label(__('label.begin')) + ->required(), DateTimePicker::make('deadline') - ->label(__('label.deadline')), + ->label(__('label.deadline')) + ->required() + ->after('begin') + ->validationMessages([ + 'after' => __('label.torrent_state.deadline_after_begin'), + ]), + Textarea::make('remark') + ->label(__('label.comment')) + ->rows(2) + ->columnSpanFull() + ->maxLength(255), ])->columns(1); } @@ -63,21 +75,63 @@ class TorrentStateResource extends Resource TextColumn::make('global_sp_state_text')->label(__('label.torrent_state.global_sp_state')), TextColumn::make('begin')->label(__('label.begin')), TextColumn::make('deadline')->label(__('label.deadline')), + TextColumn::make('promotion_status') + ->label(__('label.torrent_state.status')) + ->state(function (TorrentState $record) { + $now = Carbon::now(); + $begin = $record->begin ? Carbon::parse($record->begin) : null; + $deadline = $record->deadline ? Carbon::parse($record->deadline) : null; + + if ($deadline && $deadline->lt($now)) { + return 'expired'; + } + if ($begin && $begin->gt($now)) { + return 'upcoming'; + } + return 'ongoing'; + }) + ->formatStateUsing(function (string $state) { + return match ($state) { + 'expired' => __('label.torrent_state.status_expired'), + 'upcoming' => __('label.torrent_state.status_upcoming'), + default => __('label.torrent_state.status_ongoing'), + }; + }) + ->badge() + ->sortable(query: function (Builder $query, string $direction): Builder { + $now = Carbon::now()->toDateTimeString(); + // expired=0, ongoing=1, upcoming=2 + return $query->orderByRaw( + "CASE + WHEN deadline IS NOT NULL AND deadline < ? THEN 0 + WHEN begin IS NOT NULL AND begin > ? THEN 2 + ELSE 1 + END {$direction}", + [$now, $now] + ); + }) + ->color(fn (string $state) => match ($state) { + 'expired' => 'danger', + 'upcoming' => 'info', + default => 'success', + }) + ->icon(fn (string $state) => match ($state) { + 'expired' => 'heroicon-o-x-circle', + 'upcoming' => 'heroicon-o-clock', + default => 'heroicon-o-check-circle', + }) + ->iconPosition('before'), + TextColumn::make('remark')->label(__('label.comment'))->limit(50), ]) ->filters([ // ]) ->recordActions([ - EditAction::make()->after(function () { - do_log("cache_del: global_promotion_state"); - NexusDB::cache_del(Setting::TORRENT_GLOBAL_STATE_CACHE_KEY); - do_log("publish_model_event: global_promotion_state_updated"); - publish_model_event("global_promotion_state_updated", 0); - }), -// Tables\Actions\DeleteAction::make(), + EditAction::make(), + DeleteAction::make(), ]) ->toolbarActions([ -// Tables\Actions\DeleteBulkAction::make(), + DeleteBulkAction::make(), ]); } diff --git a/app/Filament/Resources/System/TorrentStateResource/Pages/ManageTorrentStates.php b/app/Filament/Resources/System/TorrentStateResource/Pages/ManageTorrentStates.php index 8f2fa8fa..d16c634c 100644 --- a/app/Filament/Resources/System/TorrentStateResource/Pages/ManageTorrentStates.php +++ b/app/Filament/Resources/System/TorrentStateResource/Pages/ManageTorrentStates.php @@ -3,9 +3,8 @@ namespace App\Filament\Resources\System\TorrentStateResource\Pages; use App\Filament\Resources\System\TorrentStateResource; -use Filament\Pages\Actions; +use Filament\Actions\CreateAction; use Filament\Resources\Pages\ManageRecords; -use Nexus\Database\NexusDB; class ManageTorrentStates extends ManageRecords { @@ -14,7 +13,7 @@ class ManageTorrentStates extends ManageRecords protected function getHeaderActions(): array { return [ -// Actions\CreateAction::make(), + CreateAction::make(), ]; } diff --git a/app/Models/TorrentState.php b/app/Models/TorrentState.php index d9b57447..47044764 100644 --- a/app/Models/TorrentState.php +++ b/app/Models/TorrentState.php @@ -4,17 +4,282 @@ namespace App\Models; use App\Models\Traits\NexusActivityLogTrait; +use App\Models\Setting; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Request; +use Illuminate\Validation\ValidationException; +use Nexus\Database\NexusDB; class TorrentState extends NexusModel { use NexusActivityLogTrait; - protected $fillable = ['global_sp_state', 'deadline', 'begin']; + protected $fillable = ['global_sp_state', 'deadline', 'begin', 'remark']; protected $table = 'torrents_state'; + protected $casts = [ + 'begin' => 'datetime', + 'deadline' => 'datetime', + ]; + + protected static function booted() + { + parent::booted(); + + static::saving(function (TorrentState $state) { + $state->validateTimeRange(); + $state->ensureNoOverlap(); + }); + + static::saved(function () { + static::flushCache(); + }); + + static::deleted(function () { + static::flushCache(); + }); + } + public function getGlobalSpStateTextAttribute() { return Torrent::$promotionTypes[$this->global_sp_state]['text'] ?? ''; } + + public function scopeActive(Builder $query, ?Carbon $moment = null): Builder + { + $moment = $moment ?? Carbon::now(); + + return $query + ->where('global_sp_state', '!=', Torrent::PROMOTION_NORMAL) + ->where(function (Builder $query) use ($moment) { + $query->whereNull('begin')->orWhere('begin', '<=', $moment); + }) + ->where(function (Builder $query) use ($moment) { + $query->whereNull('deadline')->orWhere('deadline', '>=', $moment); + }) + ->orderBy('begin') + ->orderBy('id'); + } + + public function scopeUpcoming(Builder $query, ?Carbon $moment = null): Builder + { + $moment = $moment ?? Carbon::now(); + + return $query + ->where('global_sp_state', '!=', Torrent::PROMOTION_NORMAL) + ->whereNotNull('begin') + ->where('begin', '>', $moment) + ->orderBy('begin') + ->orderBy('id'); + } + + public static function current(?Carbon $moment = null): ?self + { + return self::query()->active($moment)->first(); + } + + public static function next(?Carbon $moment = null): ?self + { + return self::query()->upcoming($moment)->first(); + } + + public static function cachedStates(): array + { + return NexusDB::remember(Setting::TORRENT_GLOBAL_STATE_CACHE_KEY, 600, function () { + return self::query() + ->where('global_sp_state', '!=', Torrent::PROMOTION_NORMAL) + ->orderByRaw('begin is null') + ->orderBy('begin') + ->orderBy('id') + ->get() + ->toArray(); + }); + } + + public static function flushCache(): void + { + do_log("cache_del: " . Setting::TORRENT_GLOBAL_STATE_CACHE_KEY); + NexusDB::cache_del(Setting::TORRENT_GLOBAL_STATE_CACHE_KEY); + do_log("publish_model_event: global_promotion_state_updated"); + publish_model_event("global_promotion_state_updated", 0); + } + + public static function resolveTimeline(?Carbon $moment = null): array + { + $moment = $moment ?? Carbon::now(); + $states = self::cachedStates(); + $current = null; + $upcoming = null; + + foreach ($states as $state) { + $begin = self::parseDateTimeValue($state['begin'] ?? null); + $deadline = self::parseDateTimeValue($state['deadline'] ?? null); + + $hasBegun = !$begin || $begin->lessThanOrEqualTo($moment); + $notExpired = !$deadline || $deadline->greaterThanOrEqualTo($moment); + + if ($hasBegun && $notExpired) { + if (!$current) { + $current = $state; + } + continue; + } + + if ($begin && $begin->greaterThan($moment)) { + if (!$upcoming) { + $upcoming = $state; + continue; + } + $upcomingBegin = self::parseDateTimeValue($upcoming['begin'] ?? null); + if ($upcomingBegin && $begin->lessThan($upcomingBegin)) { + $upcoming = $state; + } + } + } + + return [ + 'current' => $current, + 'upcoming' => $upcoming, + ]; + } + + protected function validateTimeRange(): void + { + $begin = self::parseDateTimeValue($this->begin); + $deadline = self::parseDateTimeValue($this->deadline); + + if ($begin && $deadline && $deadline->lessThanOrEqualTo($begin)) { + throw ValidationException::withMessages([ + self::errorFieldKey('deadline') => __('label.torrent_state.deadline_after_begin'), + ]); + } + } + + protected function ensureNoOverlap(): void + { + self::validateNoOverlap($this->attributesToArray(), $this->id); + } + + protected function getRangeForComparison(TorrentState $state): array + { + $min = Carbon::createFromTimestamp(0); + $max = Carbon::create(9999, 12, 31, 23, 59, 59); + + $begin = self::parseDateTimeValue($state->begin) ?? $min; + + $deadline = self::parseDateTimeValue($state->deadline) ?? $max; + + return [ + 'begin' => $begin, + 'end' => $deadline, + ]; + } + + protected static function parseDateTimeValue(mixed $value): ?Carbon + { + if ($value instanceof Carbon) { + return $value; + } + + if (empty($value) || $value === '0000-00-00 00:00:00') { + return null; + } + + return Carbon::parse($value); + } + + public static function validateNoOverlap(array $attributes, ?int $ignoreId = null): void + { + $globalState = (int) Arr::get($attributes, 'global_sp_state', Torrent::PROMOTION_NORMAL); + if ($globalState === Torrent::PROMOTION_NORMAL) { + return; + } + + $range = self::getRangeForArray($attributes); + + $conflicts = self::query() + ->where('global_sp_state', '!=', Torrent::PROMOTION_NORMAL) + ->when($ignoreId, fn (Builder $query) => $query->whereKeyNot($ignoreId)) + ->get(['id', 'begin', 'deadline']); + + $beginConflict = $conflicts->first(function (TorrentState $state) use ($range) { + $other = $state->getRangeForComparison($state); + return $range['begin']->greaterThanOrEqualTo($other['begin']) && $range['begin']->lessThanOrEqualTo($other['end']); + }); + + $endConflict = $conflicts->first(function (TorrentState $state) use ($range) { + $other = $state->getRangeForComparison($state); + return $range['end']->greaterThanOrEqualTo($other['begin']) && $range['end']->lessThanOrEqualTo($other['end']); + }); + + $coverageConflict = $conflicts->first(function (TorrentState $state) use ($range) { + $other = $state->getRangeForComparison($state); + return $range['begin']->lt($other['begin']) && $range['end']->gt($other['end']); + }); + + if ($beginConflict || $endConflict || $coverageConflict) { + $errors = []; + + if ($beginConflict) { + $errors[self::errorFieldKey('begin')] = self::buildOverlapMessage($beginConflict); + } + + if ($endConflict) { + $errors[self::errorFieldKey('deadline')] = self::buildOverlapMessage($endConflict); + } + + if (empty($errors) && $coverageConflict) { + $msg = self::buildOverlapMessage($coverageConflict); + $errors[self::errorFieldKey('begin')] = $msg; + $errors[self::errorFieldKey('deadline')] = $msg; + } + + if (empty($errors)) { + $msg = __('label.torrent_state.time_overlaps'); + $errors[self::errorFieldKey('begin')] = $msg; + $errors[self::errorFieldKey('deadline')] = $msg; + } + + throw ValidationException::withMessages($errors); + } + } + + protected static function getRangeForArray(array $attributes): array + { + $min = Carbon::createFromTimestamp(0); + $max = Carbon::create(9999, 12, 31, 23, 59, 59); + + $begin = self::parseDateTimeValue($attributes['begin'] ?? null) ?? $min; + $deadline = self::parseDateTimeValue($attributes['deadline'] ?? null) ?? $max; + + return [ + 'begin' => $begin, + 'end' => $deadline, + ]; + } + + protected static function errorFieldKey(string $field): string + { + $prefix = 'mountedActions.0.data.'; + + return $prefix . $field; + } + + protected static function buildOverlapMessage(TorrentState $conflict): string + { + $begin = self::parseDateTimeValue($conflict->begin); + $deadline = self::parseDateTimeValue($conflict->deadline); + + $beginText = $begin ? $begin->toDateTimeString() : '-∞'; + $deadlineText = $deadline ? $deadline->toDateTimeString() : '∞'; + + return __('label.torrent_state.time_overlaps_with', [ + 'id' => $conflict->id, + 'begin' => $beginText, + 'end' => $deadlineText, + ]); + } } diff --git a/app/Policies/TorrentStatePolicy.php b/app/Policies/TorrentStatePolicy.php index aeee0478..013117e2 100644 --- a/app/Policies/TorrentStatePolicy.php +++ b/app/Policies/TorrentStatePolicy.php @@ -42,7 +42,7 @@ class TorrentStatePolicy extends BasePolicy */ public function create(User $user) { - return false; + return $this->can($user); } /** @@ -66,7 +66,7 @@ class TorrentStatePolicy extends BasePolicy */ public function delete(User $user, TorrentState $torrentState) { - + return $this->can($user); } /** @@ -78,7 +78,7 @@ class TorrentStatePolicy extends BasePolicy */ public function restore(User $user, TorrentState $torrentState) { - + return $this->can($user); } /** @@ -90,7 +90,7 @@ class TorrentStatePolicy extends BasePolicy */ public function forceDelete(User $user, TorrentState $torrentState) { - // + return $this->can($user); } private function can(User $user) diff --git a/database/migrations/2024_08_26_000000_add_remark_to_torrents_state_table.php b/database/migrations/2024_08_26_000000_add_remark_to_torrents_state_table.php new file mode 100644 index 00000000..44975ae6 --- /dev/null +++ b/database/migrations/2024_08_26_000000_add_remark_to_torrents_state_table.php @@ -0,0 +1,32 @@ +string('remark')->nullable()->after('deadline'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('torrents_state', function (Blueprint $table) { + if (Schema::hasColumn('torrents_state', 'remark')) { + $table->dropColumn('remark'); + } + }); + } +}; diff --git a/include/functions.php b/include/functions.php index d43eec8e..fd395a6a 100644 --- a/include/functions.php +++ b/include/functions.php @@ -2823,15 +2823,34 @@ print '
'; } if ($msgalert) { - $spStateGlobal = get_global_sp_state(); - if ($spStateGlobal != \App\Models\Torrent::PROMOTION_NORMAL) { - $torrentGlobalStateRow = \Nexus\Database\NexusDB::cache_get(\App\Models\Setting::TORRENT_GLOBAL_STATE_CACHE_KEY); - $msg = sprintf($lang_functions['full_site_promotion_in_effect'], \App\Models\Torrent::$promotionTypes[$spStateGlobal]['text']); - if (!empty($torrentGlobalStateRow['begin']) || !empty($torrentGlobalStateRow['deadline'])) { - $timeRange = sprintf($lang_functions['full_site_promotion_time_range'], $torrentGlobalStateRow['begin'] ?? '-∞', $torrentGlobalStateRow['deadline'] ?? '∞'); - $msg .= $timeRange; + $timeline = \App\Models\TorrentState::resolveTimeline(); + $currentPromotion = $timeline['current'] ?? null; + $upcomingPromotion = $timeline['upcoming'] ?? null; + $remarkTpl = $lang_functions['full_site_promotion_remark'] ?? 'Remark: %s'; + + if ($currentPromotion) { + $promotionText = \App\Models\Torrent::$promotionTypes[$currentPromotion['global_sp_state']]['text'] ?? ''; + $msg = sprintf($lang_functions['full_site_promotion_in_effect'], $promotionText); + if (!empty($currentPromotion['begin']) || !empty($currentPromotion['deadline'])) { + $timeRange = sprintf($lang_functions['full_site_promotion_time_range'], $currentPromotion['begin'] ?? '-∞', $currentPromotion['deadline'] ?? '∞'); + $msg .= '
' . $timeRange; + } + if (!empty($currentPromotion['remark'])) { + $msg .= '
' . sprintf($remarkTpl, $currentPromotion['remark']); } msgalert("torrents.php", $msg, "green"); + } + if ($upcomingPromotion) { + $promotionText = \App\Models\Torrent::$promotionTypes[$upcomingPromotion['global_sp_state']]['text'] ?? ''; + $msg = sprintf($lang_functions['full_site_promotion_upcoming'] ?? 'Upcoming full site [%s]', $promotionText); + if (!empty($upcomingPromotion['begin']) || !empty($upcomingPromotion['deadline'])) { + $timeRange = sprintf($lang_functions['full_site_promotion_time_range'], $upcomingPromotion['begin'] ?? '-∞', $upcomingPromotion['deadline'] ?? '∞'); + $msg .= '
' . $timeRange; + } + if (!empty($upcomingPromotion['remark'])) { + $msg .= '
' . sprintf($remarkTpl, $upcomingPromotion['remark']); + } + msgalert("torrents.php", $msg, "blue"); } if($CURUSER['leechwarn'] == 'yes') { @@ -5994,7 +6013,7 @@ function get_ip_location_from_geoip($ip): bool|array function msgalert($url, $text, $bgcolor = "red") { - print("
\n"); + print("
\n"); if (!empty($url)) { print("".$text.""); } else { diff --git a/include/globalfunctions.php b/include/globalfunctions.php index 36754299..57037c84 100644 --- a/include/globalfunctions.php +++ b/include/globalfunctions.php @@ -3,21 +3,14 @@ function get_global_sp_state() { static $global_promotion_state; - $cacheKey = \App\Models\Setting::TORRENT_GLOBAL_STATE_CACHE_KEY; if (is_null($global_promotion_state)) { - $row = \Nexus\Database\NexusDB::remember($cacheKey, 600, function () use ($cacheKey) { - return \Nexus\Database\NexusDB::getOne('torrents_state', 1); - }); - if (is_array($row) && isset($row['deadline']) && $row['deadline'] < date('Y-m-d H:i:s')) { - //expired - $global_promotion_state = \App\Models\Torrent::PROMOTION_NORMAL; - } elseif (is_array($row) && isset($row['begin']) && $row['begin'] > date('Y-m-d H:i:s')) { - //Not begin - $global_promotion_state = \App\Models\Torrent::PROMOTION_NORMAL; - } elseif (is_array($row)) { - $global_promotion_state = $row["global_sp_state"]; + $timeline = \App\Models\TorrentState::resolveTimeline(); + $current = $timeline['current'] ?? null; + + if (is_array($current) && isset($current['global_sp_state'])) { + $global_promotion_state = $current['global_sp_state']; } else { - $global_promotion_state = $row; + $global_promotion_state = \App\Models\Torrent::PROMOTION_NORMAL; } } return $global_promotion_state; diff --git a/lang/chs/lang_functions.php b/lang/chs/lang_functions.php index ab9236ac..3221d2b3 100644 --- a/lang/chs/lang_functions.php +++ b/lang/chs/lang_functions.php @@ -327,6 +327,8 @@ $lang_functions = array 'text_contactstaff' => '联系管理组', 'full_site_promotion_in_effect' => '全站 [%s] 生效中!', 'full_site_promotion_time_range' => '时间:%s ~ %s', + 'full_site_promotion_remark' => '备注:%s', + 'full_site_promotion_upcoming' => '即将生效的全站 [%s]', 'text_torrent_to_approval' => '有 %s%u 个待审核的种子%s', 'std_confirm_remove' => '确定要删除吗?', 'select_an_user_class' => '选择一个用户等级', diff --git a/lang/cht/lang_functions.php b/lang/cht/lang_functions.php index 058fa452..1f038eff 100644 --- a/lang/cht/lang_functions.php +++ b/lang/cht/lang_functions.php @@ -334,6 +334,8 @@ $lang_functions = array 'text_contactstaff' => '聯系管理組', 'full_site_promotion_in_effect' => '全站 [%s] 生效中!', 'full_site_promotion_time_range' => '時間:%s ~ %s', + 'full_site_promotion_remark' => '備註:%s', + 'full_site_promotion_upcoming' => '即將生效的全站 [%s]', 'text_torrent_to_approval' => '有 %s%u 個待審核的種子%s', 'std_confirm_remove' => '確定要刪除嗎?', 'select_an_user_class' => '選擇一個用戶等級', diff --git a/lang/en/lang_functions.php b/lang/en/lang_functions.php index c75b564a..a9b92907 100644 --- a/lang/en/lang_functions.php +++ b/lang/en/lang_functions.php @@ -335,6 +335,8 @@ $lang_functions = array 'text_contactstaff' => 'Contact staff', 'full_site_promotion_in_effect' => 'Full site [%s] in effect!', 'full_site_promotion_time_range' => 'Time range: %s ~ %s', + 'full_site_promotion_remark' => 'Note: %s', + 'full_site_promotion_upcoming' => 'Upcoming full site [%s]', 'text_torrent_to_approval' => 'There %s%u not approval torrent%s.', 'std_confirm_remove' => 'Are you sure you want to delete it?', 'select_an_user_class' => 'Select an user class', diff --git a/resources/lang/en/label.php b/resources/lang/en/label.php index e33091f4..0369ac8c 100644 --- a/resources/lang/en/label.php +++ b/resources/lang/en/label.php @@ -328,6 +328,13 @@ return [ 'torrent_state' => [ 'label' => 'Global promotion', 'global_sp_state' => 'Global promotion state', + 'deadline_after_begin' => 'End time must be later than start time.', + 'status' => 'Status', + 'status_expired' => 'Expired', + 'status_ongoing' => 'In progress', + 'status_upcoming' => 'Upcoming', + 'time_overlaps' => 'Time overlaps with another promotion. Please adjust the window.', + 'time_overlaps_with' => 'Overlaps with promotion ID :id (time: :begin ~ :end).', ], 'role' => [ 'class' => 'Relate user class', diff --git a/resources/lang/zh_CN/label.php b/resources/lang/zh_CN/label.php index 34a81cf4..c9eb84cf 100644 --- a/resources/lang/zh_CN/label.php +++ b/resources/lang/zh_CN/label.php @@ -370,6 +370,13 @@ return [ 'torrent_state' => [ 'label' => '全站优惠', 'global_sp_state' => '全站优惠', + 'deadline_after_begin' => '结束时间必须晚于开始时间。', + 'status' => '状态', + 'status_expired' => '已过期', + 'status_ongoing' => '进行中', + 'status_upcoming' => '未开始', + 'time_overlaps' => '时间与已有活动重叠,请调整时间段。', + 'time_overlaps_with' => '与活动 ID :id (时间::begin ~ :end)重叠,请调整时间段。', ], 'role' => [ 'class' => '关联用户等级', diff --git a/resources/lang/zh_TW/label.php b/resources/lang/zh_TW/label.php index da6f3699..0cb57b39 100644 --- a/resources/lang/zh_TW/label.php +++ b/resources/lang/zh_TW/label.php @@ -327,6 +327,13 @@ return [ 'torrent_state' => [ 'label' => '全站優惠', 'global_sp_state' => '全站優惠', + 'deadline_after_begin' => '結束時間必須晚於開始時間。', + 'status' => '狀態', + 'status_expired' => '已過期', + 'status_ongoing' => '進行中', + 'status_upcoming' => '未開始', + 'time_overlaps' => '時間與已有活動重疊,請調整時間段。', + 'time_overlaps_with' => '與活動 ID :id (時間::begin ~ :end)重疊,請調整時間段。', ], 'role' => [ 'class' => '關聯用户等級',