Merge branch 'cedar2025:master' into master

This commit is contained in:
socksprox 2025-01-25 19:06:49 +01:00 committed by GitHub
commit ec64fe0039
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 278 additions and 147 deletions

View File

@ -1,37 +1,39 @@
--- ---
name: 🐛 Bug Report name: 🐛 问题反馈 | Bug Report
about: Report an issue about: 提交使用过程中遇到的问题 | Report an issue
title: "Issue: " title: "问题:"
labels: '🐛 bug' labels: '🐛 bug'
assignees: '' assignees: ''
--- ---
<!-- 🔴 请注意XrayR等非XBoard问题请前往相应项目提问 -->
<!-- 🔴 Note: For XrayR and other non-XBoard issues, please report to their respective projects --> <!-- 🔴 Note: For XrayR and other non-XBoard issues, please report to their respective projects -->
> ⚠️ 请务必按照模板填写完整信息没有详细描述的issue可能会被忽略或关闭
> ⚠️ Please follow the template to provide complete information, issues without detailed description may be ignored or closed > ⚠️ Please follow the template to provide complete information, issues without detailed description may be ignored or closed
**Basic Info** **基本信息 | Basic Info**
```yaml ```yaml
Version: XBoard版本 | Version:
Deployment: [Docker/Manual] 部署方式 | Deployment: [Docker/手动部署]
PHP Version: PHP版本 | Version:
Database: 数据库 | Database:
``` ```
**Description** **问题描述 | Description**
<!-- Briefly describe the issue you encountered --> <!-- 简要描述你遇到的问题 -->
**Steps to Reproduce** **复现步骤 | Steps**
<!-- How to reproduce this issue? --> <!-- 如何复现这个问题? -->
1. 1.
2. 2.
**Screenshots** **相关截图 | Screenshots**
<!-- Drag and drop images here (please hide sensitive information) --> <!-- 拖拽图片到这里(请注意隐藏敏感信息)-->
**Logs** **日志信息 | Logs**
<!-- Logs from storage/logs directory --> <!-- storage/logs 目录下的日志 -->
```log ```log
// Paste log content here // 粘贴日志内容到这里
``` ```

View File

@ -1,27 +1,28 @@
--- ---
name: ✨ Feature Request name: ✨ 功能请求 | Feature Request
about: Suggest an idea about: 提交新功能建议或改进意见 | Suggest an idea
title: "Suggestion: " title: "建议:"
labels: '✨ enhancement' labels: '✨ enhancement'
assignees: '' assignees: ''
--- ---
> ⚠️ 请务必按照模板详细描述你的需求没有详细描述的issue可能会被忽略或关闭
> ⚠️ Please follow the template to describe your request in detail, issues without detailed description may be ignored or closed > ⚠️ Please follow the template to describe your request in detail, issues without detailed description may be ignored or closed
**Description** **需求描述 | Description**
<!-- Describe the feature or improvement you'd like to suggest --> <!-- 描述你希望添加的功能或改进建议 -->
**Use Case** **使用场景 | Use Case**
<!-- Describe the scenarios where this feature would be used and what problems it solves --> <!-- 描述这个功能会在什么场景下使用,解决什么问题 -->
**Suggestion** **功能建议 | Suggestion**
<!-- How do you expect this feature to work? You can describe the specific implementation --> <!-- 你期望这个功能是什么样的?可以描述一下具体实现方式 -->
```yaml ```yaml
Type: [New Feature/Feature Enhancement/UI Improvement] 功能形式 | Type: [新功能/功能优化/界面改进]
Expected: 预期效果 | Expected:
``` ```
**Additional Information** **补充说明 | Additional**
<!-- Any additional information or reference examples --> <!-- 其他补充说明或者参考示例 -->

View File

@ -94,7 +94,7 @@ class ClientController extends Controller
*/ */
private function isBrowserAccess(Request $request): bool private function isBrowserAccess(Request $request): bool
{ {
$userAgent = strtolower($request->input('flag', $request->header('User-Agent'))); $userAgent = strtolower($request->input('flag', $request->header('User-Agent', '')));
return str_contains($userAgent, 'mozilla') return str_contains($userAgent, 'mozilla')
|| str_contains($userAgent, 'chrome') || str_contains($userAgent, 'chrome')
|| str_contains($userAgent, 'safari') || str_contains($userAgent, 'safari')

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException; use App\Exceptions\ApiException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Resources\CouponResource;
use App\Services\CouponService; use App\Services\CouponService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -11,11 +12,6 @@ class CouponController extends Controller
{ {
public function check(Request $request) public function check(Request $request)
{ {
// $request->validate([
// 'code' => 'required|string',
// 'plan_id' => 'required|integer',
// 'period' => 'nullable|string',
// ]);
if (empty($request->input('code'))) { if (empty($request->input('code'))) {
return $this->fail([422, __('Coupon cannot be empty')]); return $this->fail([422, __('Coupon cannot be empty')]);
} }
@ -24,6 +20,6 @@ class CouponController extends Controller
$couponService->setUserId($request->user()->id); $couponService->setUserId($request->user()->id);
$couponService->setPeriod($request->input('period')); $couponService->setPeriod($request->input('period'));
$couponService->check(); $couponService->check();
return $this->success($couponService->getCoupon()); return $this->success(CouponResource::make($couponService->getCoupon()));
} }
} }

View File

@ -70,7 +70,7 @@ class TicketController extends Controller
*/ */
private function fetchTickets(Request $request) private function fetchTickets(Request $request)
{ {
$ticketModel = Ticket::query() $ticketModel = Ticket::with('user')
->when($request->has('status'), function ($query) use ($request) { ->when($request->has('status'), function ($query) use ($request) {
$query->where('status', $request->input('status')); $query->where('status', $request->input('status'));
}) })

View File

@ -13,6 +13,7 @@ use App\Services\AuthService;
use App\Utils\Helper; use App\Utils\Helper;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class UserController extends Controller class UserController extends Controller
@ -397,4 +398,35 @@ class UserController extends Controller
return $this->success(true); return $this->success(true);
} }
/**
* 删除用户及其关联数据
*
* @param Request $request
* @return JsonResponse
*/
public function destroy(Request $request)
{
$request->validate([
'id' => 'required|exists:App\Models\User,id'
], [
'id.required' => '用户ID不能为空',
'id.exists' => '用户不存在'
]);
$user = User::find($request->input('id'));
try {
DB::beginTransaction();
$user->orders()->delete();
$user->codes()->delete();
$user->stat()->delete();
$user->tickets()->delete();
$user->delete();
DB::commit();
return $this->success(true);
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500, '删除失败']);
}
}
} }

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* 优惠券资源类
*
* @property array|null $limit_plan_ids 限制可用的套餐ID列表
*/
class CouponResource extends JsonResource
{
/**
* 将资源转换为数组
*
* @param Request $request 请求实例
* @return array<string, mixed> 转换后的数组
*/
public function toArray(Request $request): array
{
return [
...$this->resource->toArray(),
'limit_plan_ids' => $this->when(
!empty($this->limit_plan_ids),
fn() => collect($this->limit_plan_ids)
->map(fn(mixed $id): string => (string) $id)
->values()
->all()
)
];
}
}

View File

@ -108,6 +108,7 @@ class AdminRoute
$router->post('/ban', [UserController::class, 'ban']); $router->post('/ban', [UserController::class, 'ban']);
$router->post('/resetSecret', [UserController::class, 'resetSecret']); $router->post('/resetSecret', [UserController::class, 'resetSecret']);
$router->post('/setInviteUser', [UserController::class, 'setInviteUser']); $router->post('/setInviteUser', [UserController::class, 'setInviteUser']);
$router->post('/destroy', [UserController::class, 'destroy']);
}); });
// Stat // Stat

View File

@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class StatServerJob implements ShouldQueue class StatServerJob implements ShouldQueue
{ {
@ -22,6 +23,15 @@ class StatServerJob implements ShouldQueue
public $tries = 3; public $tries = 3;
public $timeout = 60; public $timeout = 60;
public $maxExceptions = 3;
/**
* Calculate the number of seconds to wait before retrying the job.
*/
public function backoff(): array
{
return [1, 5, 10];
}
/** /**
* Create a new job instance. * Create a new job instance.
@ -40,7 +50,6 @@ class StatServerJob implements ShouldQueue
*/ */
public function handle(): void public function handle(): void
{ {
// Calculate record timestamp
$recordAt = $this->recordType === 'm' $recordAt = $this->recordType === 'm'
? strtotime(date('Y-m-01')) ? strtotime(date('Y-m-01'))
: strtotime(date('Y-m-d')); : strtotime(date('Y-m-d'));
@ -51,19 +60,20 @@ class StatServerJob implements ShouldQueue
$u += $traffic[0]; $u += $traffic[0];
$d += $traffic[1]; $d += $traffic[1];
} }
DB::transaction(function () use ($u, $d, $recordAt) {
$stat = StatServer::lockForUpdate()
->where('record_at', $recordAt)
->where('server_id', $this->server['id'])
->where('server_type', $this->protocol)
->where('record_type', $this->recordType)
->first();
if ($stat) { try {
$stat->u += $u; DB::transaction(function () use ($u, $d, $recordAt) {
$stat->d += $d; $affected = StatServer::where([
$stat->save(); 'record_at' => $recordAt,
} else { 'server_id' => $this->server['id'],
'server_type' => $this->protocol,
'record_type' => $this->recordType,
])->update([
'u' => DB::raw('u + ' . $u),
'd' => DB::raw('d + ' . $d),
]);
if (!$affected) {
StatServer::create([ StatServer::create([
'record_at' => $recordAt, 'record_at' => $recordAt,
'server_id' => $this->server['id'], 'server_id' => $this->server['id'],
@ -73,6 +83,10 @@ class StatServerJob implements ShouldQueue
'd' => $d, 'd' => $d,
]); ]);
} }
}); }, 3);
} catch (\Exception $e) {
Log::error('StatServerJob failed for server ' . $this->server['id'] . ': ' . $e->getMessage());
throw $e;
}
} }
} }

View File

@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class StatUserJob implements ShouldQueue class StatUserJob implements ShouldQueue
{ {
@ -22,6 +23,15 @@ class StatUserJob implements ShouldQueue
public $tries = 3; public $tries = 3;
public $timeout = 60; public $timeout = 60;
public $maxExceptions = 3;
/**
* Calculate the number of seconds to wait before retrying the job.
*/
public function backoff(): array
{
return [1, 5, 10];
}
/** /**
* Create a new job instance. * Create a new job instance.
@ -40,24 +50,24 @@ class StatUserJob implements ShouldQueue
*/ */
public function handle(): void public function handle(): void
{ {
// Calculate record timestamp
$recordAt = $this->recordType === 'm' $recordAt = $this->recordType === 'm'
? strtotime(date('Y-m-01')) ? strtotime(date('Y-m-01'))
: strtotime(date('Y-m-d')); : strtotime(date('Y-m-d'));
foreach ($this->data as $uid => $v) { foreach ($this->data as $uid => $v) {
try {
DB::transaction(function () use ($uid, $v, $recordAt) { DB::transaction(function () use ($uid, $v, $recordAt) {
$stat = StatUser::lockForUpdate() $affected = StatUser::where([
->where('user_id', $uid) 'user_id' => $uid,
->where('server_rate', $this->server['rate']) 'server_rate' => $this->server['rate'],
->where('record_at', $recordAt) 'record_at' => $recordAt,
->where('record_type', $this->recordType) 'record_type' => $this->recordType,
->first(); ])->update([
if ($stat) { 'u' => DB::raw('u + ' . ($v[0] * $this->server['rate'])),
$stat->u += ($v[0] * $this->server['rate']); 'd' => DB::raw('d + ' . ($v[1] * $this->server['rate'])),
$stat->d += ($v[1] * $this->server['rate']); ]);
$stat->save();
} else { if (!$affected) {
StatUser::create([ StatUser::create([
'user_id' => $uid, 'user_id' => $uid,
'server_rate' => $this->server['rate'], 'server_rate' => $this->server['rate'],
@ -67,7 +77,11 @@ class StatUserJob implements ShouldQueue
'd' => ($v[1] * $this->server['rate']), 'd' => ($v[1] * $this->server['rate']),
]); ]);
} }
}); }, 3);
} catch (\Exception $e) {
Log::error('StatUserJob failed for user ' . $uid . ': ' . $e->getMessage());
throw $e;
}
} }
} }
} }

View File

@ -19,22 +19,9 @@ class Coupon extends Model
public function getLimitPeriodAttribute($value) public function getLimitPeriodAttribute($value)
{ {
return collect(json_decode($value, true))->map(function ($item) { return collect(json_decode((string) $value, true))->map(function ($item) {
return PlanService::getPeriodKey($item); return PlanService::getPeriodKey($item);
})->toArray(); })->toArray();
} }
public function getLimitPlanIdsAttribute($value)
{
$planIds = json_decode($value, true);
if (blank($planIds)) {
return null;
}
return collect($planIds)
->map(fn($id) => (string) $id)
->values()
->all();
}
} }

View File

@ -42,6 +42,16 @@ class User extends Authenticatable
return $this->hasMany(InviteCode::class, 'user_id', 'id'); return $this->hasMany(InviteCode::class, 'user_id', 'id');
} }
public function orders()
{
return $this->hasMany(Order::class, 'user_id', 'id');
}
public function stat()
{
return $this->hasMany(StatUser::class, 'user_id', 'id');
}
// 关联工单列表 // 关联工单列表
public function tickets() public function tickets()
{ {

View File

@ -90,7 +90,7 @@ class Helper
return true; return true;
} }
public static function trafficConvert(int $byte) public static function trafficConvert(float $byte)
{ {
$kb = 1024; $kb = 1024;
$mb = 1048576; $mb = 1048576;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -815,7 +815,11 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"success": "Success", "success": "Success",
"save": "Save", "save": "Save",
"cancel": "Cancel", "cancel": "Cancel",
"delete": "Delete", "confirm": "Confirm",
"delete": {
"success": "Deleted successfully",
"failed": "Failed to delete"
},
"edit": "Edit", "edit": "Edit",
"view": "View", "view": "View",
"toggleNavigation": "Toggle Navigation", "toggleNavigation": "Toggle Navigation",
@ -832,6 +836,7 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"logout": "Logout", "logout": "Logout",
"copy": { "copy": {
"success": "Copied successfully", "success": "Copied successfully",
"failed": "Failed to copy",
"error": "Copy failed", "error": "Copy failed",
"errorLog": "Error copying to clipboard" "errorLog": "Error copying to clipboard"
}, },
@ -1337,7 +1342,7 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
}, },
"ticket": { "ticket": {
"title": "Ticket Management", "title": "Ticket Management",
"description": "Here you can view user tickets, including viewing, replying, and closing operations.", "description": "View and manage user tickets, including viewing, replying, and closing operations.",
"columns": { "columns": {
"id": "Ticket ID", "id": "Ticket ID",
"subject": "Subject", "subject": "Subject",
@ -1354,13 +1359,13 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"processing": "Processing" "processing": "Processing"
}, },
"level": { "level": {
"low": "Low", "low": "Low Priority",
"medium": "Medium", "medium": "Medium Priority",
"high": "High" "high": "High Priority"
}, },
"filter": { "filter": {
"placeholder": "Search {field}...", "placeholder": "Search {field}...",
"no_results": "No results found.", "no_results": "No results found",
"selected": "{count} selected", "selected": "{count} selected",
"clear": "Clear filters" "clear": "Clear filters"
}, },
@ -1368,8 +1373,8 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"view_details": "View Details", "view_details": "View Details",
"close_ticket": "Close Ticket", "close_ticket": "Close Ticket",
"close_confirm_title": "Confirm Close Ticket", "close_confirm_title": "Confirm Close Ticket",
"close_confirm_description": "After closing, you will not be able to reply. Are you sure you want to close this ticket?", "close_confirm_description": "Are you sure you want to close this ticket? You won't be able to reply after closing.",
"close_confirm_button": "Close Ticket", "close_confirm_button": "Confirm Close",
"close_success": "Ticket closed successfully", "close_success": "Ticket closed successfully",
"view_ticket": "View Ticket" "view_ticket": "View Ticket"
}, },
@ -1385,6 +1390,12 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"sending": "Sending...", "sending": "Sending...",
"send": "Send" "send": "Send"
} }
},
"list": {
"title": "Ticket List",
"search_placeholder": "Search ticket subject or user email",
"no_tickets": "No pending tickets",
"no_search_results": "No matching tickets found"
} }
}, },
"server": { "server": {
@ -1740,7 +1751,10 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"reset_secret": "Reset UUID & URL", "reset_secret": "Reset UUID & URL",
"orders": "Orders", "orders": "Orders",
"invites": "Invites", "invites": "Invites",
"traffic_records": "Traffic Records" "traffic_records": "Traffic Records",
"delete": "Delete",
"delete_confirm_title": "Confirm Delete User",
"delete_confirm_description": "This action will permanently delete user {{email}} and all associated data, including orders, coupons, traffic records, and support tickets. This action cannot be undone. Do you want to continue?"
} }
}, },
"filter": { "filter": {

View File

@ -815,7 +815,11 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"success": "성공", "success": "성공",
"save": "저장", "save": "저장",
"cancel": "취소", "cancel": "취소",
"delete": "삭제", "confirm": "확인",
"delete": {
"success": "삭제되었습니다",
"failed": "삭제에 실패했습니다"
},
"edit": "편집", "edit": "편집",
"view": "보기", "view": "보기",
"toggleNavigation": "네비게이션 전환", "toggleNavigation": "네비게이션 전환",
@ -831,9 +835,8 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"settings": "설정", "settings": "설정",
"logout": "로그아웃", "logout": "로그아웃",
"copy": { "copy": {
"success": "복사 성공", "success": "복사되었습니다",
"error": "복사 실패", "failed": "복사에 실패했습니다"
"errorLog": "클립보드에 복사하는 중 오류 발생"
}, },
"table": { "table": {
"noData": "데이터가 없습니다", "noData": "데이터가 없습니다",
@ -1337,54 +1340,60 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
}, },
"ticket": { "ticket": {
"title": "티켓 관리", "title": "티켓 관리",
"description": "여기에서 사용자 티켓을 확인할 수 있으며, 조회, 답변 및 종료 작업을 수행할 수 있습니다.", "description": "사용자 티켓을 보고, 답변하고, 닫는 등의 작업을 관리합니다.",
"columns": { "columns": {
"id": "티켓 ID", "id": "티켓 번호",
"subject": "제목", "subject": "제목",
"level": "우선순위", "level": "우선순위",
"status": "상태", "status": "상태",
"updated_at": "최근 업데이트", "updated_at": "최근 업데이트",
"created_at": "생성 시간", "created_at": "생성",
"actions": "작업" "actions": "작업"
}, },
"status": { "status": {
"closed": "종료됨", "closed": "닫힘",
"replied": "답변", "replied": "답변완료",
"pending": "대기중", "pending": "대기중",
"processing": "처리중" "processing": "처리중"
}, },
"level": { "level": {
"low": "낮", "low": "낮은 우선순위",
"medium": "중간", "medium": "중간 우선순위",
"high": "높" "high": "높은 우선순위"
}, },
"filter": { "filter": {
"placeholder": "{field} 검색...", "placeholder": "{field} 검색...",
"no_results": "검색 결과가 없습니다.", "no_results": "결과를 찾을 수 없습니다",
"selected": "{count}개 선택됨", "selected": "{count}개 선택됨",
"clear": "필터 초기화" "clear": "필터 초기화"
}, },
"actions": { "actions": {
"view_details": "상세 보기", "view_details": "상세 보기",
"close_ticket": "티켓 종료", "close_ticket": "티켓 닫기",
"close_confirm_title": "티켓 종료 확인", "close_confirm_title": "티켓 닫기 확인",
"close_confirm_description": "종료 후에는 답변할 수 없습니다. 이 티켓을 종료하시겠습니까?", "close_confirm_description": "이 티켓을 닫으시겠습니까? 닫은 후에는 답변할 수 없습니다.",
"close_confirm_button": "티켓 종료", "close_confirm_button": "닫기 확인",
"close_success": "티켓이 성공적으로 종료되었습니다", "close_success": "티켓이 성공적으로 닫혔습니다",
"view_ticket": "티켓 보기" "view_ticket": "티켓 보기"
}, },
"detail": { "detail": {
"no_messages": "메시지가 아직 없습니다", "no_messages": "메시지가 없습니다",
"created_at": "생성 시간", "created_at": "생성",
"user_info": "사용자 정보", "user_info": "사용자 정보",
"traffic_records": "트래픽 기록", "traffic_records": "트래픽 기록",
"order_records": "주문 기록", "order_records": "주문 기록",
"input": { "input": {
"closed_placeholder": "티켓이 종료되었습니다", "closed_placeholder": "티켓이 닫혔습니다",
"reply_placeholder": "답변을 입력하세요...", "reply_placeholder": "답변을 입력하세요...",
"sending": "전송중...", "sending": "전송중...",
"send": "전송" "send": "전송"
} }
},
"list": {
"title": "티켓 목록",
"search_placeholder": "티켓 제목 또는 사용자 이메일 검색",
"no_tickets": "대기중인 티켓이 없습니다",
"no_search_results": "일치하는 티켓을 찾을 수 없습니다"
} }
}, },
"server": { "server": {
@ -1692,9 +1701,12 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"assign_order": "주문 할당", "assign_order": "주문 할당",
"copy_url": "구독 URL 복사", "copy_url": "구독 URL 복사",
"reset_secret": "UUID 및 URL 재설정", "reset_secret": "UUID 및 URL 재설정",
"orders": "주문", "orders": "주문 내역",
"invites": "초대", "invites": "초대 내역",
"traffic_records": "트래픽 기록" "traffic_records": "트래픽 기록",
"delete": "삭제",
"delete_confirm_title": "사용자 삭제 확인",
"delete_confirm_description": "이 작업은 사용자 {{email}}와 관련된 모든 데이터(주문, 쿠폰, 트래픽 기록, 지원 티켓 등)를 영구적으로 삭제합니다. 이 작업은 취소할 수 없습니다. 계속하시겠습니까?"
} }
}, },
"filter": { "filter": {

View File

@ -820,7 +820,11 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"success": "成功", "success": "成功",
"save": "保存", "save": "保存",
"cancel": "取消", "cancel": "取消",
"delete": "删除", "confirm": "确认",
"delete": {
"success": "删除成功",
"failed": "删除失败"
},
"edit": "编辑", "edit": "编辑",
"view": "查看", "view": "查看",
"toggleNavigation": "切换导航", "toggleNavigation": "切换导航",
@ -837,6 +841,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"logout": "退出登录", "logout": "退出登录",
"copy": { "copy": {
"success": "复制成功", "success": "复制成功",
"failed": "复制失败",
"error": "复制失败", "error": "复制失败",
"errorLog": "复制到剪贴板时出错" "errorLog": "复制到剪贴板时出错"
}, },
@ -1346,9 +1351,9 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"processing": "处理中" "processing": "处理中"
}, },
"level": { "level": {
"low": "低", "low": "低优先",
"medium": "中", "medium": "中优先",
"high": "高" "high": "高优先"
}, },
"filter": { "filter": {
"placeholder": "搜索{field}...", "placeholder": "搜索{field}...",
@ -1360,8 +1365,8 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"view_details": "查看详情", "view_details": "查看详情",
"close_ticket": "关闭工单", "close_ticket": "关闭工单",
"close_confirm_title": "确认关闭工单", "close_confirm_title": "确认关闭工单",
"close_confirm_description": "关闭后将无法继续回复,是否确认关闭该工单?", "close_confirm_description": "确定要关闭这个工单吗?关闭后将无法继续回复。",
"close_confirm_button": "关闭工单", "close_confirm_button": "确认关闭",
"close_success": "工单已关闭", "close_success": "工单已关闭",
"view_ticket": "查看工单" "view_ticket": "查看工单"
}, },
@ -1373,10 +1378,16 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"order_records": "订单记录", "order_records": "订单记录",
"input": { "input": {
"closed_placeholder": "工单已关闭", "closed_placeholder": "工单已关闭",
"reply_placeholder": "输入回复内容...", "reply_placeholder": "输入回复内容...",
"sending": "发送中...", "sending": "发送中...",
"send": "发送" "send": "发送"
} }
},
"list": {
"title": "工单列表",
"search_placeholder": "搜索工单标题或用户邮箱",
"no_tickets": "暂无待处理工单",
"no_search_results": "未找到匹配的工单"
} }
}, },
"server": { "server": {
@ -1707,7 +1718,10 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"reset_secret": "重置UUID及订阅URL", "reset_secret": "重置UUID及订阅URL",
"orders": "TA的订单", "orders": "TA的订单",
"invites": "TA的邀请", "invites": "TA的邀请",
"traffic_records": "TA的流量记录" "traffic_records": "TA的流量记录",
"delete": "删除",
"delete_confirm_title": "确认删除用户",
"delete_confirm_description": "此操作将永久删除用户 {{email}} 及其所有相关数据,包括订单、优惠码、流量记录、工单记录等信息。删除后无法恢复,是否继续?"
} }
}, },
"filter": { "filter": {
@ -1923,12 +1937,12 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"device": { "device": {
"label": "设备限制", "label": "设备限制",
"placeholder": "请输入设备限制", "placeholder": "请输入设备限制",
"unit": "台设备" "unit": "台"
}, },
"capacity": { "capacity": {
"label": "容量限制", "label": "容量限制",
"placeholder": "请输入容量限制", "placeholder": "请输入容量限制",
"unit": "个用户" "unit": ""
}, },
"reset_method": { "reset_method": {
"label": "流量重置方式", "label": "流量重置方式",