master
lion 2 weeks ago
parent 43dc99264d
commit 73cc701ad9

@ -4,9 +4,11 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Activity;
use App\Support\VerifyPortalCode;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
@ -33,6 +35,9 @@ class ActivityController extends Controller
if ($request->filled('audit_status')) {
$query->where('audit_status', $request->string('audit_status'));
}
if ($request->filled('reservation_type')) {
$query->where('reservation_type', $request->string('reservation_type'));
}
$pageSize = max(1, min(100, (int) $request->input('page_size', 10)));
$page = $query->paginate($pageSize);
@ -52,7 +57,14 @@ class ActivityController extends Controller
{
$data = $request->validate([
'venue_id' => ['required', 'integer', 'exists:venues,id'],
'reservation_type' => ['nullable', 'string', Rule::in([Activity::RESERVATION_TYPE_ONLINE, Activity::RESERVATION_TYPE_OFFLINE, Activity::RESERVATION_TYPE_OTHER])],
'reservation_type' => ['nullable', 'string', Rule::in([
Activity::RESERVATION_TYPE_ONLINE,
Activity::RESERVATION_TYPE_OFFLINE,
Activity::RESERVATION_TYPE_OTHER,
Activity::RESERVATION_TYPE_PHONE,
Activity::RESERVATION_TYPE_WECHAT_MP,
Activity::RESERVATION_TYPE_OFFLINE_VISIT,
])],
'location' => ['required', 'string', 'max:500'],
'specific_time' => ['nullable', 'string', 'max:2000'],
'offline_reservation_method' => ['nullable', 'string', 'max:500'],
@ -81,6 +93,7 @@ class ActivityController extends Controller
'is_active' => ['boolean'],
]);
$data = $this->applyReservationTypeDefaults($data);
$this->assertTicketNoteWhenNeeded($data, null);
$this->assertOfflineMethodWhenNeeded($data, null);
$this->assertExternalUrlWhenOther($data, null);
@ -107,6 +120,10 @@ class ActivityController extends Controller
'audit_remark' => null,
'last_approved_snapshot' => null,
]);
if (! $activity->verify_portal_token) {
$activity->forceFill(['verify_portal_token' => (string) Str::uuid()])->save();
}
VerifyPortalCode::ensureForActivity($activity);
return response()->json($activity->load('venue:id,name'), 201);
}
@ -117,7 +134,14 @@ class ActivityController extends Controller
$data = $request->validate([
'venue_id' => ['sometimes', 'integer', 'exists:venues,id'],
'reservation_type' => ['nullable', 'string', Rule::in([Activity::RESERVATION_TYPE_ONLINE, Activity::RESERVATION_TYPE_OFFLINE, Activity::RESERVATION_TYPE_OTHER])],
'reservation_type' => ['nullable', 'string', Rule::in([
Activity::RESERVATION_TYPE_ONLINE,
Activity::RESERVATION_TYPE_OFFLINE,
Activity::RESERVATION_TYPE_OTHER,
Activity::RESERVATION_TYPE_PHONE,
Activity::RESERVATION_TYPE_WECHAT_MP,
Activity::RESERVATION_TYPE_OFFLINE_VISIT,
])],
'location' => ['required', 'string', 'max:500'],
'specific_time' => ['nullable', 'string', 'max:2000'],
'offline_reservation_method' => ['nullable', 'string', 'max:500'],
@ -146,6 +170,7 @@ class ActivityController extends Controller
'is_active' => ['boolean'],
]);
$data = $this->applyReservationTypeDefaults($data, $activity);
$this->assertTicketNoteWhenNeeded($data, $activity);
$this->assertOfflineMethodWhenNeeded($data, $activity);
$this->assertExternalUrlWhenOther($data, $activity);
@ -278,28 +303,57 @@ class ActivityController extends Controller
*/
private function applyReservationTypeDefaults(array $data, ?Activity $existing = null): array
{
$newTypes = [Activity::RESERVATION_TYPE_PHONE, Activity::RESERVATION_TYPE_WECHAT_MP, Activity::RESERVATION_TYPE_OFFLINE_VISIT];
if (array_key_exists('reservation_type', $data)) {
$t = (string) ($data['reservation_type'] ?? '');
$data['reservation_type'] = $t === '' ? Activity::RESERVATION_TYPE_ONLINE : $t;
$data['reservation_type'] = $t === '' ? Activity::RESERVATION_TYPE_PHONE : $t;
} elseif ($existing !== null) {
$data['reservation_type'] = (string) ($existing->reservation_type ?? Activity::RESERVATION_TYPE_ONLINE);
$data['reservation_type'] = (string) ($existing->reservation_type ?? Activity::RESERVATION_TYPE_PHONE);
} else {
$data['reservation_type'] = Activity::RESERVATION_TYPE_ONLINE;
$data['reservation_type'] = Activity::RESERVATION_TYPE_PHONE;
}
if ($data['reservation_type'] !== Activity::RESERVATION_TYPE_OFFLINE) {
$t = $data['reservation_type'];
if (in_array($t, $newTypes, true)) {
$data['external_url'] = null;
if (! array_key_exists('offline_reservation_method', $data) && $existing !== null) {
$data['offline_reservation_method'] = $existing->offline_reservation_method;
}
} elseif ($t === Activity::RESERVATION_TYPE_OFFLINE) {
$data['external_url'] = null;
if (! array_key_exists('offline_reservation_method', $data) && $existing !== null) {
$data['offline_reservation_method'] = $existing->offline_reservation_method;
}
} elseif ($t === Activity::RESERVATION_TYPE_OTHER) {
if (! array_key_exists('external_url', $data) && $existing !== null) {
$data['external_url'] = $existing->external_url;
}
$data['offline_reservation_method'] = null;
} elseif ($t === Activity::RESERVATION_TYPE_ONLINE) {
$data['offline_reservation_method'] = null;
} elseif (! array_key_exists('offline_reservation_method', $data) && $existing !== null) {
$data['offline_reservation_method'] = $existing->offline_reservation_method;
}
if ($data['reservation_type'] !== Activity::RESERVATION_TYPE_OTHER) {
$data['external_url'] = null;
} elseif (! array_key_exists('external_url', $data) && $existing !== null) {
$data['external_url'] = $existing->external_url;
}
return $data;
}
/**
* @param array<string, mixed> $data
*/
private function assertTicketNoteWhenNeeded(array $data, ?Activity $existing = null): void
{
$newTypes = [Activity::RESERVATION_TYPE_PHONE, Activity::RESERVATION_TYPE_WECHAT_MP, Activity::RESERVATION_TYPE_OFFLINE_VISIT];
if (! in_array(($data['reservation_type'] ?? ''), $newTypes, true)) {
return;
}
$m = trim((string) ($data['offline_reservation_method'] ?? $existing?->offline_reservation_method ?? ''));
if (! in_array($m, [Activity::TICKET_FREE, Activity::TICKET_PAID], true)) {
throw ValidationException::withMessages(['offline_reservation_method' => ['请选择门票说明(免费/收费)']]);
}
}
/**
* @param array<string, mixed> $data
*/

@ -40,6 +40,13 @@ class ActivityRegistrationController extends Controller
$query->whereIn('venue_id', $user->venues()->pluck('venues.id'));
}
if ($kind === 'ticket_grab' && $request->filled('ticket_grab_event_id')) {
$eid = (int) $request->input('ticket_grab_event_id');
if ($eid > 0) {
$query->where('ticket_grab_event_id', $eid);
}
}
if ($request->filled('status') && $request->string('status')->toString() !== 'all') {
$query->where('status', (string) $request->string('status'));
}

@ -22,15 +22,17 @@ class AdminMenuController extends Controller
->orderBy('id');
$user = $request->user();
if ($user && Schema::hasTable('role_menu_permissions') && ! $user->isSuperAdmin()) {
// 含超级管理员:一律按 role_menu_permissions 过滤侧栏,与角色管理中勾选一致
if ($user && Schema::hasTable('role_menu_permissions')) {
$roleMenuIds = RoleMenuPermission::query()
->where('role', (string) $user->role)
->pluck('menu_id')
->map(fn ($id) => (int) $id)
->values();
if ($roleMenuIds->isNotEmpty()) {
$query->whereIn('id', $roleMenuIds);
if ($roleMenuIds->isEmpty()) {
return response()->json([]);
}
$query->whereIn('id', $roleMenuIds);
}
$menus = $query->get(['id', 'name', 'path', 'icon', 'parent_id', 'sort']);

@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\AdminRole;
use App\Models\RoleMenuPermission;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
class AdminRoleController extends Controller
{
public function index(Request $request): JsonResponse
{
$this->ensureSuperAdmin($request);
$roles = AdminRole::query()->orderBy('sort')->orderBy('id')->get(['id', 'slug', 'name', 'is_system', 'full_access', 'sort']);
return response()->json($roles);
}
public function store(Request $request): JsonResponse
{
$this->ensureSuperAdmin($request);
$data = $request->validate([
'slug' => [
'required',
'string',
'max:50',
'regex:/^[a-z][a-z0-9_]{1,47}$/',
Rule::unique('admin_roles', 'slug'),
Rule::notIn(['super_admin', 'venue_admin']),
],
'name' => ['required', 'string', 'max:100'],
'full_access' => ['sometimes', 'boolean'],
]);
$slug = strtolower(trim($data['slug']));
$role = AdminRole::create([
'slug' => $slug,
'name' => trim($data['name']),
'is_system' => false,
'full_access' => (bool) ($data['full_access'] ?? false),
'sort' => (int) (AdminRole::query()->max('sort') ?? 0) + 1,
]);
return response()->json($role, 201);
}
public function update(Request $request, string $slug): JsonResponse
{
$this->ensureSuperAdmin($request);
$role = AdminRole::query()->where('slug', $slug)->firstOrFail();
$data = $request->validate([
'name' => ['sometimes', 'string', 'max:100'],
'full_access' => ['sometimes', 'boolean'],
]);
if (isset($data['name'])) {
$role->name = trim($data['name']);
}
if (array_key_exists('full_access', $data)) {
if ($role->slug === 'venue_admin') {
abort_if($data['full_access'], 422, '场馆管理员不可设为系统级全权限');
}
if ($role->slug === 'super_admin') {
// 固定为全权限
$role->full_access = true;
} else {
$role->full_access = (bool) $data['full_access'];
}
}
$role->save();
return response()->json($role->fresh());
}
public function destroy(Request $request, string $slug): JsonResponse
{
$this->ensureSuperAdmin($request);
$role = AdminRole::query()->where('slug', $slug)->firstOrFail();
abort_if($role->is_system, 422, '系统内置角色不可删除');
abort_if(
User::query()->where('role', $slug)->exists(),
422,
'仍有管理员使用该角色,无法删除'
);
DB::transaction(function () use ($slug) {
RoleMenuPermission::query()->where('role', $slug)->delete();
AdminRole::query()->where('slug', $slug)->delete();
});
return response()->json(['message' => '角色已删除']);
}
private function ensureSuperAdmin(Request $request): void
{
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可操作');
}
}

@ -7,6 +7,7 @@ use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
class AdminUserController extends Controller
{
@ -30,7 +31,7 @@ class AdminUserController extends Controller
'name' => ['required', 'string', 'max:100'],
'email' => ['nullable', 'email', 'max:100', 'unique:users,email'],
'password' => ['required', 'string', 'min:6'],
'role' => ['required', 'in:super_admin,venue_admin'],
'role' => ['required', 'string', Rule::exists('admin_roles', 'slug')],
'is_active' => ['boolean'],
'venue_ids' => ['array'],
'venue_ids.*' => ['integer', 'exists:venues,id'],
@ -61,7 +62,7 @@ class AdminUserController extends Controller
'name' => ['sometimes', 'string', 'max:100'],
'email' => ['nullable', 'email', 'max:100', 'unique:users,email,' . $user->id],
'password' => ['nullable', 'string', 'min:6'],
'role' => ['sometimes', 'in:super_admin,venue_admin'],
'role' => ['sometimes', 'string', Rule::exists('admin_roles', 'slug')],
'is_active' => ['sometimes', 'boolean'],
'venue_ids' => ['sometimes', 'array'],
'venue_ids.*' => ['integer', 'exists:venues,id'],

@ -22,7 +22,8 @@ class AuditLogController extends Controller
$query->where(function ($q) use ($keyword) {
$q->where('username', 'like', "%{$keyword}%")
->orWhere('path', 'like', "%{$keyword}%")
->orWhere('action', 'like', "%{$keyword}%");
->orWhere('action', 'like', "%{$keyword}%")
->orWhere('operation_summary', 'like', "%{$keyword}%");
});
}
if ($request->filled('method') && $request->input('method') !== 'all') {

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\AuditLog;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -18,17 +19,47 @@ class AuthController extends Controller
]);
$user = User::with('venues:id,name')->where('username', $data['username'])->first();
if (!$user || !$user->is_active || !Hash::check($data['password'], $user->password)) {
if (! $user || ! $user->is_active || ! Hash::check($data['password'], $user->password)) {
$this->recordLoginAudit(
$request,
null,
422,
'登录失败(账号或密码错误)',
['username' => $data['username']]
);
return response()->json(['message' => '账号或密码错误'], 422);
}
// 移动端核销登录:签发更长有效期的 tokenSanctum 仍会在 expires_at 到期后失效)
$isH5Verify = $request->input('client') === 'h5_verify';
if ($isH5Verify && ! $user->isSuperAdmin()) {
$this->recordLoginAudit(
$request,
$user,
403,
'登录失败(核销端仅限超级管理员账号)',
['username' => $data['username'], 'client' => 'h5_verify']
);
return response()->json(['message' => '场馆管理员请使用活动专用核销链接与账号登录'], 403);
}
$expiresAt = $isH5Verify ? now()->addMonths(6) : null;
$tokenName = $isH5Verify ? 'h5-verify' : 'admin-token';
$token = $user->createToken($tokenName, ['*'], $expiresAt)->plainTextToken;
$this->recordLoginAudit(
$request,
$user,
200,
'登录('.$user->username.'',
[
'username' => $data['username'],
'client' => $request->input('client'),
]
);
return response()->json([
'token' => $token,
'user' => [
@ -37,6 +68,7 @@ class AuthController extends Controller
'name' => $user->name,
'role' => $user->role,
'venues' => $user->venues,
'full_admin_access' => $user->isSuperAdmin(),
],
]);
}
@ -44,6 +76,30 @@ class AuthController extends Controller
public function logout(Request $request): JsonResponse
{
$request->user()?->currentAccessToken()?->delete();
return response()->json(['message' => '已退出登录']);
}
/**
* @param array<string, mixed> $payload
*/
private function recordLoginAudit(Request $request, ?User $user, int $statusCode, string $operationSummary, array $payload = []): void
{
try {
AuditLog::create([
'user_id' => $user?->id,
'username' => $user?->username ?? (isset($payload['username']) ? (string) $payload['username'] : null),
'role' => $user?->role,
'method' => 'POST',
'path' => '/'.ltrim($request->path(), '/'),
'action' => 'POST '.$request->path(),
'operation_summary' => $operationSummary,
'status_code' => $statusCode,
'ip' => $request->ip(),
'user_agent' => substr((string) $request->userAgent(), 0, 500),
'request_payload' => $payload,
]);
} catch (\Throwable) {
}
}
}

@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Activity;
use App\Models\Blacklist;
use App\Models\Reservation;
use App\Models\TicketGrabEvent;
use App\Models\TicketGrabEventVenue;
use App\Models\Venue;
@ -47,8 +46,6 @@ class DashboardController extends Controller
}
}
$dateOnReservation = 'COALESCE(activity_days.activity_date, DATE(reservations.created_at))';
/** 顶部五项:账号可见场馆范围内的全量累计,不受日期与「筛选场馆」影响 */
$scopeVenueIds = $allowedVenueIds;
$activeVenueCount = (int) DB::table('reservations')
@ -94,14 +91,7 @@ class DashboardController extends Controller
->distinct('visitor_phone')
->count('visitor_phone');
$publishedCount = 0;
$activitySums = null;
$arTotal = 0;
$arVerified = 0;
$arCancelled = 0;
$arPending = 0;
$arExpired = 0;
$arVerifyRate = 0.0;
$activityStatsByVenue = [];
if (! $datesBlank) {
@ -129,106 +119,28 @@ class DashboardController extends Controller
}
$statsActivityIds = $statsActivityIdsQuery->pluck('id');
$publishedCount = $statsActivityIds->isEmpty()
? 0
: (int) Activity::query()
->whereIn('id', $statsActivityIds)
->whereDate('created_at', '>=', $startDate)
->whereDate('created_at', '<=', $endDate)
->count();
$activitySums = $statsActivityIds->isEmpty()
? null
: Activity::query()
->whereIn('id', $statsActivityIds)
->selectRaw('COALESCE(SUM(view_count),0) as v')
->selectRaw('COALESCE(SUM(external_link_click_count),0) as l')
->first();
if ($statsActivityIds->isNotEmpty()) {
$ar = DB::table('reservations')
->leftJoin('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id')
->whereIn('reservations.venue_id', $venueIds)
->where('reservations.reservation_kind', Reservation::KIND_ACTIVITY)
->whereIn('reservations.activity_id', $statsActivityIds)
->whereRaw("{$dateOnReservation} >= ?", [$startDate])
->whereRaw("{$dateOnReservation} <= ?", [$endDate])
->selectRaw('COUNT(*) as total_count')
->selectRaw("SUM(CASE WHEN reservations.status = 'verified' THEN 1 ELSE 0 END) as verified_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'pending' THEN 1 ELSE 0 END) as pending_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'expired' THEN 1 ELSE 0 END) as expired_count")
->first();
$arTotal = (int) ($ar->total_count ?? 0);
$arVerified = (int) ($ar->verified_count ?? 0);
$arCancelled = (int) ($ar->cancelled_count ?? 0);
$arPending = (int) ($ar->pending_count ?? 0);
$arExpired = (int) ($ar->expired_count ?? 0);
$effective = max(0, $arTotal - $arCancelled);
$arVerifyRate = $effective > 0 ? round($arVerified / $effective, 4) : 0;
}
if ($statsActivityIds->isNotEmpty()) {
$actByVenueRows = Activity::query()
->whereIn('id', $statsActivityIds)
->selectRaw('venue_id')
->selectRaw('COALESCE(SUM(view_count), 0) as total_view_count')
->selectRaw('COALESCE(SUM(external_link_click_count), 0) as total_external_link_click_count')
->selectRaw('SUM(CASE WHEN DATE(created_at) >= ? AND DATE(created_at) <= ? THEN 1 ELSE 0 END) as published_count', [$startDate, $endDate])
->groupBy('venue_id')
->get();
$actByVenue = [];
foreach ($actByVenueRows as $row) {
$actByVenue[(int) $row->venue_id] = $row;
}
$names = Venue::query()->whereIn('id', $actByVenueRows->pluck('venue_id'))->pluck('name', 'id');
$resByVenueRows = DB::table('reservations')
->leftJoin('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id')
->whereIn('reservations.venue_id', $venueIds)
->where('reservations.reservation_kind', Reservation::KIND_ACTIVITY)
->whereIn('reservations.activity_id', $statsActivityIds)
->whereRaw("{$dateOnReservation} >= ?", [$startDate])
->whereRaw("{$dateOnReservation} <= ?", [$endDate])
->selectRaw('reservations.venue_id as venue_id')
->selectRaw('COUNT(*) as total_count')
->selectRaw("SUM(CASE WHEN reservations.status = 'verified' THEN 1 ELSE 0 END) as verified_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'pending' THEN 1 ELSE 0 END) as pending_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'expired' THEN 1 ELSE 0 END) as expired_count")
->groupBy('reservations.venue_id')
->get();
$resByVenue = [];
foreach ($resByVenueRows as $row) {
$resByVenue[(int) $row->venue_id] = $row;
}
$allVenueIds = collect(array_keys($actByVenue))->merge(array_keys($resByVenue))->unique()->sort()->values();
$names = Venue::query()->whereIn('id', $allVenueIds)->pluck('name', 'id');
foreach ($allVenueIds as $vid) {
$vid = (int) $vid;
$a = $actByVenue[$vid] ?? null;
$r = $resByVenue[$vid] ?? null;
$total = (int) ($r->total_count ?? 0);
$verified = (int) ($r->verified_count ?? 0);
$cancelled = (int) ($r->cancelled_count ?? 0);
$pending = (int) ($r->pending_count ?? 0);
$expired = (int) ($r->expired_count ?? 0);
$effective = max(0, $total - $cancelled);
$rate = $effective > 0 ? round($verified / $effective, 4) : 0.0;
foreach ($actByVenueRows as $row) {
$vid = (int) $row->venue_id;
$activityStatsByVenue[] = [
'venue_id' => (int) $vid,
'venue_id' => $vid,
'venue_name' => (string) ($names[$vid] ?? ('#'.$vid)),
'published_count' => (int) ($a->published_count ?? 0),
'total_view_count' => (int) ($a->total_view_count ?? 0),
'total_external_link_click_count' => (int) ($a->total_external_link_click_count ?? 0),
'total_count' => $total,
'verified_count' => $verified,
'cancelled_count' => $cancelled,
'pending_count' => $pending,
'expired_count' => $expired,
'verify_rate' => $rate,
'total_view_count' => (int) ($row->total_view_count ?? 0),
];
}
@ -253,15 +165,7 @@ class DashboardController extends Controller
'blacklisted_unique' => (int) $blacklistedUnique,
],
'activity_stats' => [
'published_count' => $publishedCount,
'total_view_count' => (int) ($activitySums->v ?? 0),
'total_external_link_click_count' => (int) ($activitySums->l ?? 0),
'total_count' => $arTotal,
'verified_count' => $arVerified,
'cancelled_count' => $arCancelled,
'pending_count' => $arPending,
'expired_count' => $arExpired,
'verify_rate' => $arVerifyRate,
],
'activity_stats_venues' => $activityStatsByVenue,
]);

@ -4,8 +4,11 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Reservation;
use App\Models\User;
use App\Models\VerifyPortalCredential;
use App\Services\ReservationExpiryService;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -18,8 +21,7 @@ class ReservationVerifyController extends Controller
{
$today = now()->toDateString();
if ((string) ($reservation->reservation_kind ?? 'activity') === \App\Models\Reservation::KIND_TICKET_GRAB) {
// entry_date 是 reservations 表的字段cast 为 date不是 relation
if ((string) ($reservation->reservation_kind ?? 'activity') === Reservation::KIND_TICKET_GRAB) {
$ed = $reservation->entry_date;
$dateStr = $ed instanceof CarbonInterface ? $ed->format('Y-m-d') : ($ed ? (string) $ed : '');
if ($dateStr === '') {
@ -34,7 +36,7 @@ class ReservationVerifyController extends Controller
$reservation->loadMissing('activityDay');
if (!$reservation->activity_day_id || !$reservation->activityDay) {
if (! $reservation->activity_day_id || ! $reservation->activityDay) {
return '该预约未关联活动场次,仅支持核销「活动日为今日」的预约';
}
@ -48,6 +50,70 @@ class ReservationVerifyController extends Controller
return null;
}
private function actingPortalCredential(Request $request): ?VerifyPortalCredential
{
$u = $request->user();
return $u instanceof VerifyPortalCredential ? $u : null;
}
private function assertPortalActiveOrAbort(VerifyPortalCredential $c): void
{
if (! $c->portalAcceptsVerification()) {
abort(403, '活动已结束,核销登录已失效');
}
}
private function applyPortalScope(Builder $query, VerifyPortalCredential $cred): void
{
$query->where('venue_id', $cred->venue_id);
if ($cred->portal_kind === VerifyPortalCredential::KIND_ACTIVITY) {
$query->where('activity_id', $cred->portal_id)
->where(function ($q) {
$q->whereNull('reservation_kind')
->orWhere('reservation_kind', Reservation::KIND_ACTIVITY);
});
} else {
$query->where('reservation_kind', Reservation::KIND_TICKET_GRAB)
->where('ticket_grab_event_id', $cred->portal_id);
}
}
private function ensureReservationAccess(Request $request, Reservation $reservation): void
{
$cred = $this->actingPortalCredential($request);
if ($cred !== null) {
$this->assertPortalActiveOrAbort($cred);
if ((int) $reservation->venue_id !== (int) $cred->venue_id) {
abort(403, '无权核销该预约');
}
if ($cred->portal_kind === VerifyPortalCredential::KIND_ACTIVITY) {
if ((int) ($reservation->activity_id ?? 0) !== (int) $cred->portal_id) {
abort(403, '该预约不属于本活动');
}
if ((string) ($reservation->reservation_kind ?? '') === Reservation::KIND_TICKET_GRAB) {
abort(403, '该预约不属于本活动');
}
} else {
if ((string) $reservation->reservation_kind !== Reservation::KIND_TICKET_GRAB
|| (int) ($reservation->ticket_grab_event_id ?? 0) !== (int) $cred->portal_id) {
abort(403, '该预约不属于本抢票活动');
}
}
return;
}
/** @var User $user */
$user = $request->user();
if ($user->isSuperAdmin()) {
return;
}
$allowed = $user->venues()->where('venues.id', $reservation->venue_id)->exists();
abort_unless($allowed, 403, '仅可核销已绑定场馆预约');
}
/**
* 扫码后先查询预约信息,不修改状态(用于移动端「确认后再核销」)。
*/
@ -70,7 +136,7 @@ class ReservationVerifyController extends Controller
return response()->json(['message' => '无效的二维码或预约不存在'], 404);
}
$this->ensureVenuePermission($request, $reservation->venue_id);
$this->ensureReservationAccess($request, $reservation);
$blockReason = null;
if ($reservation->status === 'verified') {
@ -101,30 +167,36 @@ class ReservationVerifyController extends Controller
public function index(Request $request): JsonResponse
{
$user = $request->user();
$cred = $this->actingPortalCredential($request);
$query = Reservation::with([
'venue:id,name,address,lat,lng,reservation_notice',
'activity:id,title,summary,cover_image,address,start_at,end_at,tags,lat,lng',
'ticketGrabEvent:id,title,summary,cover_image,address,start_at,end_at,tags',
'activityDay:id,activity_id,activity_date,session_name,session_start_at,session_end_at,booking_deadline_at',
])
// 核销端列表:待核销在前,已过期/已取消置后,同状态内按 id 倒序
->orderByRaw("CASE status WHEN 'pending' THEN 0 WHEN 'verified' THEN 1 WHEN 'expired' THEN 2 WHEN 'cancelled' THEN 3 ELSE 4 END")
->orderByDesc('id');
$rk = (string) $request->input('reservation_kind', '');
if ($rk === 'ticket_grab') {
$query->where('reservation_kind', \App\Models\Reservation::KIND_TICKET_GRAB);
} elseif ($rk === 'activity') {
$query->where(function ($q) {
$q->whereNull('reservation_kind')
->orWhere('reservation_kind', \App\Models\Reservation::KIND_ACTIVITY);
});
}
if ($cred !== null) {
$this->assertPortalActiveOrAbort($cred);
$this->applyPortalScope($query, $cred);
} else {
/** @var User $user */
$user = $request->user();
$rk = (string) $request->input('reservation_kind', '');
if ($rk === 'ticket_grab') {
$query->where('reservation_kind', Reservation::KIND_TICKET_GRAB);
} elseif ($rk === 'activity') {
$query->where(function ($q) {
$q->whereNull('reservation_kind')
->orWhere('reservation_kind', Reservation::KIND_ACTIVITY);
});
}
if (! $user->isSuperAdmin()) {
$venueIds = $user->venues()->pluck('venues.id');
$query->whereIn('venue_id', $venueIds);
if (! $user->isSuperAdmin()) {
$venueIds = $user->venues()->pluck('venues.id');
$query->whereIn('venue_id', $venueIds);
}
}
if ($request->filled('status') && $request->string('status')->toString() !== 'all') {
@ -144,7 +216,6 @@ class ReservationVerifyController extends Controller
if ($dateField === 'activity_day') {
$start = $request->filled('start_date') ? (string) $request->input('start_date') : null;
$end = $request->filled('end_date') ? (string) $request->input('end_date') : $start;
// 普通活动按 activity_days.activity_date抢票无 activity_day按入馆日 entry_date
$query->where(function ($q) use ($start, $end) {
$q->where(function ($q2) use ($start, $end) {
$q2->where(function ($q3) {
@ -200,26 +271,50 @@ class ReservationVerifyController extends Controller
*/
public function todaySummary(Request $request): JsonResponse
{
$user = $request->user();
$cred = $this->actingPortalCredential($request);
$today = now()->toDateString();
$tg = \App\Models\Reservation::KIND_TICKET_GRAB;
$base = Reservation::query()
->where(function ($q) use ($today, $tg) {
$q->where('reservation_kind', $tg)
$tg = Reservation::KIND_TICKET_GRAB;
if ($cred !== null) {
$this->assertPortalActiveOrAbort($cred);
if ($cred->portal_kind === VerifyPortalCredential::KIND_ACTIVITY) {
$base = Reservation::query()
->where('venue_id', $cred->venue_id)
->where('activity_id', $cred->portal_id)
->where(function ($q) {
$q->whereNull('reservation_kind')
->orWhere('reservation_kind', Reservation::KIND_ACTIVITY);
})
->whereHas('activityDay', function ($q3) use ($today) {
$q3->whereDate('activity_date', $today);
});
} else {
$base = Reservation::query()
->where('venue_id', $cred->venue_id)
->where('reservation_kind', $tg)
->where('ticket_grab_event_id', $cred->portal_id)
->whereDate('entry_date', $today);
})->orWhere(function ($q) use ($today, $tg) {
$q->where(function ($q2) use ($tg) {
$q2->whereNull('reservation_kind')
->orWhere('reservation_kind', '!=', $tg);
})->whereHas('activityDay', function ($q3) use ($today) {
$q3->whereDate('activity_date', $today);
}
} else {
/** @var User $user */
$user = $request->user();
$base = Reservation::query()
->where(function ($q) use ($today, $tg) {
$q->where('reservation_kind', $tg)
->whereDate('entry_date', $today);
})->orWhere(function ($q) use ($today, $tg) {
$q->where(function ($q2) use ($tg) {
$q2->whereNull('reservation_kind')
->orWhere('reservation_kind', '!=', $tg);
})->whereHas('activityDay', function ($q3) use ($today) {
$q3->whereDate('activity_date', $today);
});
});
});
if (!$user->isSuperAdmin()) {
$venueIds = $user->venues()->pluck('venues.id');
$base->whereIn('venue_id', $venueIds);
if (! $user->isSuperAdmin()) {
$venueIds = $user->venues()->pluck('venues.id');
$base->whereIn('venue_id', $venueIds);
}
}
$totalOrders = (clone $base)->whereNotIn('status', ['cancelled'])->count();
@ -245,7 +340,8 @@ class ReservationVerifyController extends Controller
'ticketGrabEvent:id,title,summary,cover_image,address,start_at,end_at,tags',
'activityDay:id,activity_id,activity_date,session_name,session_start_at,session_end_at,booking_deadline_at',
])->where('qr_token', $data['qr_token'])->firstOrFail();
$this->ensureVenuePermission($request, $reservation->venue_id);
$this->ensureReservationAccess($request, $reservation);
if ($reservation->status === 'verified') {
return response()->json(['message' => '该预约已核销'], 422);
@ -262,11 +358,21 @@ class ReservationVerifyController extends Controller
return response()->json(['message' => $dayBlock], 422);
}
$reservation->update([
$cred = $this->actingPortalCredential($request);
$payload = [
'status' => 'verified',
'verified_by' => $request->user()->id,
'verified_at' => now(),
]);
];
if ($cred !== null) {
$payload['verified_by'] = null;
$payload['verify_credential_id'] = $cred->id;
} else {
/** @var User $user */
$user = $request->user();
$payload['verified_by'] = $user->id;
$payload['verify_credential_id'] = null;
}
$reservation->update($payload);
$fresh = $reservation->fresh()->load([
'venue:id,name,address,lat,lng,reservation_notice',
@ -282,15 +388,4 @@ class ReservationVerifyController extends Controller
'reservation' => $h5->reservationToH5Array($fresh),
]);
}
private function ensureVenuePermission(Request $request, int $venueId): void
{
$user = $request->user();
if ($user->isSuperAdmin()) {
return;
}
$allowed = $user->venues()->where('venues.id', $venueId)->exists();
abort_unless($allowed, 403, '仅可核销已绑定场馆预约');
}
}

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\AdminMenu;
use App\Models\AdminRole;
use App\Models\RoleMenuPermission;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -18,27 +19,42 @@ class RoleMenuPermissionController extends Controller
->orderBy('id')
->get(['id', 'name', 'path', 'icon', 'parent_id', 'sort', 'is_visible']);
$roles = ['super_admin', 'venue_admin'];
$roleModels = AdminRole::query()->orderBy('sort')->orderBy('id')->get(['slug', 'name', 'is_system', 'full_access']);
$slugs = $roleModels->pluck('slug')->map(fn ($s) => (string) $s)->values()->all();
$permissionRows = RoleMenuPermission::query()
->whereIn('role', $roles)
->whereIn('role', $slugs)
->get(['role', 'menu_id'])
->groupBy('role')
->map(fn ($group) => $group->pluck('menu_id')->map(fn ($id) => (int) $id)->values())
->toArray();
$rolesPayload = $roleModels->map(function (AdminRole $r) use ($permissionRows) {
return [
'role' => $r->slug,
'label' => $r->name,
'menu_ids' => $permissionRows[$r->slug] ?? [],
'is_system' => $r->is_system,
'full_access' => $r->full_access,
];
})->values()->all();
return response()->json([
'menus' => $menus,
'roles' => [
['role' => 'super_admin', 'label' => '超级管理员', 'menu_ids' => $permissionRows['super_admin'] ?? []],
['role' => 'venue_admin', 'label' => '场馆管理员', 'menu_ids' => $permissionRows['venue_admin'] ?? []],
],
'roles' => $rolesPayload,
]);
}
public function update(Request $request, string $role): JsonResponse
{
$this->ensureSuperAdmin($request);
abort_unless(in_array($role, ['super_admin', 'venue_admin'], true), 422, '不支持的角色');
abort_unless(
AdminRole::query()->where('slug', $role)->exists(),
422,
'角色不存在'
);
$data = $request->validate([
'menu_ids' => ['required', 'array'],
@ -65,4 +81,3 @@ class RoleMenuPermissionController extends Controller
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可操作');
}
}

@ -0,0 +1,138 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SystemLogController extends Controller
{
private const MAX_LINES = 5000;
private const DEFAULT_LINES = 400;
/** 仅允许 laravel.log 与 laravel-YYYY-MM-DD.log */
private function isAllowedBasename(string $name): bool
{
return (bool) preg_match('/^laravel(-\d{4}-\d{2}-\d{2})?\.log$/', $name);
}
public function index(Request $request): JsonResponse
{
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可查看系统日志');
$logsDir = storage_path('logs');
$files = $this->listLogFiles($logsDir);
$requested = basename((string) $request->query('file', ''));
$selected = '';
if ($requested !== '' && $requested !== '.' && $this->isAllowedBasename($requested)) {
$candidate = $logsDir.DIRECTORY_SEPARATOR.$requested;
if (is_file($candidate)) {
$selected = $requested;
}
}
if ($selected === '' && $files !== []) {
$selected = $files[0]['name'];
}
$linesParam = (int) $request->query('lines', self::DEFAULT_LINES);
$lines = max(50, min(self::MAX_LINES, $linesParam));
$linesPayload = [];
$error = null;
if ($selected !== '') {
$fullPath = $logsDir.DIRECTORY_SEPARATOR.$selected;
if (! is_file($fullPath) || ! is_readable($fullPath)) {
$error = '日志文件不可读';
} else {
$linesPayload = $this->tailLines($fullPath, $lines);
}
}
return response()->json([
'files' => $files,
'file' => $selected,
'lines' => $linesPayload,
'lines_requested' => $lines,
'error' => $error,
]);
}
/**
* @return array<int, array{name: string, size_bytes: int, modified_at: string}>
*/
private function listLogFiles(string $logsDir): array
{
if (! is_dir($logsDir)) {
return [];
}
$pattern = $logsDir.DIRECTORY_SEPARATOR.'laravel*.log';
$paths = glob($pattern) ?: [];
$files = [];
foreach ($paths as $full) {
$base = basename($full);
if (! $this->isAllowedBasename($base)) {
continue;
}
$mtime = @filemtime($full);
$size = @filesize($full);
$files[] = [
'name' => $base,
'size_bytes' => $size !== false ? $size : 0,
'modified_at' => $mtime ? date('c', $mtime) : '',
];
}
usort($files, static function (array $a, array $b): int {
return strcmp($b['modified_at'], $a['modified_at']);
});
return array_values($files);
}
/**
* @return array<int, string>
*/
private function tailLines(string $path, int $maxLines): array
{
$size = filesize($path);
if ($size === false || $size === 0) {
return [];
}
$maxLines = max(1, min(self::MAX_LINES, $maxLines));
$chunk = min($size, 262144);
while ($chunk <= $size) {
$start = $size - $chunk;
$data = file_get_contents($path, false, null, $start, $chunk);
if ($data === false) {
return [];
}
if ($start > 0) {
$firstNl = strpos($data, "\n");
if ($firstNl !== false) {
$data = substr($data, $firstNl + 1);
}
}
$parts = preg_split('/\r\n|\r|\n/', $data);
if ($parts === false) {
$parts = [];
}
if (count($parts) >= $maxLines || $chunk >= $size) {
return array_values(array_slice($parts, -$maxLines));
}
$chunk = min($size, $chunk * 2);
}
return [];
}
}

@ -10,10 +10,12 @@ use App\Models\TicketGrabVenueReleaseDay;
use App\Models\Venue;
use App\Services\TicketGrabReleaseDayService;
use App\Support\CalendarDateFormat;
use App\Support\VerifyPortalCode;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class TicketGrabEventController extends Controller
@ -62,6 +64,32 @@ class TicketGrabEventController extends Controller
return response()->json($page);
}
/**
* 下拉筛选等场景:仅返回 id、标题默认最多 2000 条(不受 index 分页 page_size≤100 限制)。
* 支持 keyword 按标题模糊筛选;权限范围与 {@see index} 一致。
*/
public function options(Request $request): JsonResponse
{
$query = TicketGrabEvent::query()
->select(['id', 'title'])
->orderByDesc('id');
$this->restrictByEventVenues($request, $query);
if ($request->filled('keyword')) {
$keyword = trim((string) $request->input('keyword'));
if ($keyword !== '') {
$query->where('title', 'like', '%'.$keyword.'%');
}
}
$limit = max(1, min(2000, (int) $request->input('limit', 2000)));
return response()->json([
'data' => $query->limit($limit)->get(),
]);
}
public function store(Request $request): JsonResponse
{
$data = $this->validated($request, true);
@ -86,6 +114,10 @@ class TicketGrabEventController extends Controller
if ($e->booking_start_at && $e->booking_end_at && (count($data['venues'] ?? [])) > 0) {
TicketGrabReleaseDayService::rebuildAllReleaseDaysForEvent($e->id);
}
if (! $e->verify_portal_token) {
$e->forceFill(['verify_portal_token' => (string) Str::uuid()])->save();
}
VerifyPortalCode::ensureForTicketGrabEvent($e);
return response()->json($e->fresh()->load('venues:id,name,address', 'eventVenuePivots'), 201);
}
@ -142,6 +174,9 @@ class TicketGrabEventController extends Controller
->exists();
if (! $hasR) {
TicketGrabReleaseDayService::rebuildAllReleaseDaysForEvent($ticketGrabEvent->id);
} else {
// 已有预约时不可清空重建:按新预约窗口合并每日放票并插入缺失日期
TicketGrabReleaseDayService::syncReleaseDaysToBookingWindow($ticketGrabEvent->id);
}
}
}

@ -17,7 +17,7 @@ class VenueController extends Controller
public function store(Request $request): JsonResponse
{
$user = $request->user();
abort_unless($user && ($user->isSuperAdmin() || $user->role === 'venue_admin'), 403, '无权限');
abort_unless($user, 403, '无权限');
$data = $request->validate([
'name' => ['required', 'string', 'max:120'],

@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Activity;
use App\Models\TicketGrabEvent;
use App\Models\VerifyPortalCredential;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class VerifyPortalAuthController extends Controller
{
public function login(Request $request): JsonResponse
{
$data = $request->validate([
'portal_code' => ['nullable', 'string', 'max:16'],
'portal_token' => ['nullable', 'string', 'max:64'],
'username' => ['required', 'string', 'max:64'],
'password' => ['required', 'string', 'max:72'],
]);
$code = strtolower(trim((string) ($data['portal_code'] ?? '')));
$portalToken = trim((string) ($data['portal_token'] ?? ''));
if ($code === '' && ($portalToken === '' || strlen($portalToken) < 32)) {
return response()->json(['message' => '请提供核销入口短码或有效入口参数'], 422);
}
$activity = null;
$tg = null;
if ($code !== '') {
$activity = Activity::query()->where('verify_portal_code', $code)->first();
if (! $activity) {
$tg = TicketGrabEvent::query()->where('verify_portal_code', $code)->first();
}
} elseif ($portalToken !== '' && strlen($portalToken) >= 32) {
$activity = Activity::query()->where('verify_portal_token', $portalToken)->first();
if (! $activity) {
$tg = TicketGrabEvent::query()->where('verify_portal_token', $portalToken)->first();
}
}
if ($activity) {
$kind = VerifyPortalCredential::KIND_ACTIVITY;
$portalId = (int) $activity->id;
} elseif ($tg) {
$kind = VerifyPortalCredential::KIND_TICKET_GRAB;
$portalId = (int) $tg->id;
} else {
return response()->json(['message' => '无效的核销入口'], 404);
}
$cred = VerifyPortalCredential::query()
->where('portal_kind', $kind)
->where('portal_id', $portalId)
->where('username', $data['username'])
->first();
if (! $cred || ! Hash::check($data['password'], $cred->password)) {
return response()->json(['message' => '账号或密码错误'], 422);
}
if (! $cred->portalAcceptsVerification()) {
return response()->json(['message' => '活动已结束,无法登录核销'], 422);
}
$cred->tokens()->delete();
$expiresAt = now()->addMonths(6);
$plain = $cred->createToken('verify-portal', ['verify-portal'], $expiresAt)->plainTextToken;
return response()->json([
'token' => $plain,
'auth_mode' => 'verify_portal',
'portal_kind' => $kind,
'portal_id' => $portalId,
]);
}
public function me(Request $request): JsonResponse
{
$cred = $request->user();
if (! $cred instanceof VerifyPortalCredential) {
return response()->json(['message' => '非核销门户登录态'], 403);
}
if (! $cred->portalAcceptsVerification()) {
$cred->tokens()->delete();
return response()->json(['message' => '活动已结束,核销已失效'], 403);
}
$cred->load('venue:id,name');
$title = '';
if ($cred->portal_kind === VerifyPortalCredential::KIND_ACTIVITY) {
$title = (string) Activity::query()->where('id', $cred->portal_id)->value('title');
} else {
$title = (string) TicketGrabEvent::query()->where('id', $cred->portal_id)->value('title');
}
return response()->json([
'auth_mode' => 'verify_portal',
'portal_kind' => $cred->portal_kind,
'portal_id' => $cred->portal_id,
'username' => $cred->username,
'venue' => $cred->venue ? ['id' => $cred->venue->id, 'name' => $cred->venue->name] : null,
'event_title' => $title,
]);
}
}

@ -0,0 +1,274 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Activity;
use App\Models\TicketGrabEvent;
use App\Models\TicketGrabEventVenue;
use App\Models\VerifyPortalCredential;
use App\Support\VerifyPortalCode;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
class VerifyPortalManageController extends Controller
{
public function activityShow(Request $request, Activity $activity): JsonResponse
{
$this->ensureActivityVenueAdmin($request, $activity);
if ($activity->verify_portal_token === null || $activity->verify_portal_token === '') {
$activity->forceFill(['verify_portal_token' => (string) \Illuminate\Support\Str::uuid()])->save();
}
VerifyPortalCode::ensureForActivity($activity);
$activity->refresh();
$rows = VerifyPortalCredential::query()
->where('portal_kind', VerifyPortalCredential::KIND_ACTIVITY)
->where('portal_id', $activity->id)
->with('venue:id,name')
->orderBy('id')
->get()
->map(fn (VerifyPortalCredential $c) => $this->credentialToRow($c));
return response()->json([
'verify_portal_code' => $activity->verify_portal_code,
'credentials' => $rows,
]);
}
public function activityStore(Request $request, Activity $activity): JsonResponse
{
$this->ensureActivityVenueAdmin($request, $activity);
$data = $request->validate([
'username' => ['required', 'string', 'max:64'],
'password' => ['required', 'string', 'min:6', 'max:72'],
'note' => ['nullable', 'string', 'max:255'],
]);
$vid = (int) $activity->venue_id;
$exists = VerifyPortalCredential::query()
->where('portal_kind', VerifyPortalCredential::KIND_ACTIVITY)
->where('portal_id', $activity->id)
->where('venue_id', $vid)
->where('username', $data['username'])
->exists();
if ($exists) {
return response()->json(['message' => '该场馆下此用户名已存在'], 422);
}
$plain = (string) $data['password'];
$c = VerifyPortalCredential::query()->create([
'portal_kind' => VerifyPortalCredential::KIND_ACTIVITY,
'portal_id' => $activity->id,
'venue_id' => $vid,
'username' => $data['username'],
'password' => Hash::make($plain),
'password_plain_enc' => Crypt::encryptString($plain),
'note' => $data['note'] ?? null,
]);
return response()->json(['id' => $c->id], 201);
}
public function activityUpdate(Request $request, Activity $activity, VerifyPortalCredential $verifyPortalCredential): JsonResponse
{
$this->ensureActivityVenueAdmin($request, $activity);
$this->assertCredentialBelongsActivity($verifyPortalCredential, $activity);
$data = $request->validate([
'password' => ['sometimes', 'string', 'min:6', 'max:72'],
'note' => ['sometimes', 'nullable', 'string', 'max:255'],
]);
if (array_key_exists('password', $data)) {
$plain = (string) $data['password'];
$verifyPortalCredential->password = Hash::make($plain);
$verifyPortalCredential->password_plain_enc = Crypt::encryptString($plain);
}
if (array_key_exists('note', $data)) {
$verifyPortalCredential->note = $data['note'];
}
$verifyPortalCredential->save();
return response()->json(['message' => '已更新']);
}
public function activityDestroy(Request $request, Activity $activity, VerifyPortalCredential $verifyPortalCredential): JsonResponse
{
$this->ensureActivityVenueAdmin($request, $activity);
$this->assertCredentialBelongsActivity($verifyPortalCredential, $activity);
$verifyPortalCredential->tokens()->delete();
$verifyPortalCredential->delete();
return response()->json(['message' => '已删除']);
}
public function ticketGrabShow(Request $request, TicketGrabEvent $ticketGrabEvent): JsonResponse
{
$this->ensureTicketGrabAdmin($request, $ticketGrabEvent);
if ($ticketGrabEvent->verify_portal_token === null || $ticketGrabEvent->verify_portal_token === '') {
$ticketGrabEvent->forceFill(['verify_portal_token' => (string) \Illuminate\Support\Str::uuid()])->save();
}
VerifyPortalCode::ensureForTicketGrabEvent($ticketGrabEvent);
$ticketGrabEvent->refresh();
$rows = VerifyPortalCredential::query()
->where('portal_kind', VerifyPortalCredential::KIND_TICKET_GRAB)
->where('portal_id', $ticketGrabEvent->id)
->with('venue:id,name')
->orderBy('venue_id')
->orderBy('id')
->get()
->map(fn (VerifyPortalCredential $c) => $this->credentialToRow($c));
return response()->json([
'verify_portal_code' => $ticketGrabEvent->verify_portal_code,
'credentials' => $rows,
]);
}
public function ticketGrabStore(Request $request, TicketGrabEvent $ticketGrabEvent): JsonResponse
{
$this->ensureTicketGrabAdmin($request, $ticketGrabEvent);
$data = $request->validate([
'venue_id' => ['required', 'integer'],
'username' => ['required', 'string', 'max:64'],
'password' => ['required', 'string', 'min:6', 'max:72'],
'note' => ['nullable', 'string', 'max:255'],
]);
$vid = (int) $data['venue_id'];
$pivotOk = TicketGrabEventVenue::query()
->where('ticket_grab_event_id', $ticketGrabEvent->id)
->where('venue_id', $vid)
->exists();
abort_unless($pivotOk, 422, '场馆未参与本抢票活动');
$exists = VerifyPortalCredential::query()
->where('portal_kind', VerifyPortalCredential::KIND_TICKET_GRAB)
->where('portal_id', $ticketGrabEvent->id)
->where('venue_id', $vid)
->where('username', $data['username'])
->exists();
if ($exists) {
return response()->json(['message' => '该场馆下此用户名已存在'], 422);
}
$plain = (string) $data['password'];
$c = VerifyPortalCredential::query()->create([
'portal_kind' => VerifyPortalCredential::KIND_TICKET_GRAB,
'portal_id' => $ticketGrabEvent->id,
'venue_id' => $vid,
'username' => $data['username'],
'password' => Hash::make($plain),
'password_plain_enc' => Crypt::encryptString($plain),
'note' => $data['note'] ?? null,
]);
return response()->json(['id' => $c->id], 201);
}
public function ticketGrabUpdate(
Request $request,
TicketGrabEvent $ticketGrabEvent,
VerifyPortalCredential $verifyPortalCredential,
): JsonResponse {
$this->ensureTicketGrabAdmin($request, $ticketGrabEvent);
$this->assertCredentialBelongsTicketGrab($verifyPortalCredential, $ticketGrabEvent);
$data = $request->validate([
'password' => ['sometimes', 'string', 'min:6', 'max:72'],
'note' => ['sometimes', 'nullable', 'string', 'max:255'],
]);
if (array_key_exists('password', $data)) {
$plain = (string) $data['password'];
$verifyPortalCredential->password = Hash::make($plain);
$verifyPortalCredential->password_plain_enc = Crypt::encryptString($plain);
}
if (array_key_exists('note', $data)) {
$verifyPortalCredential->note = $data['note'];
}
$verifyPortalCredential->save();
return response()->json(['message' => '已更新']);
}
public function ticketGrabDestroy(
Request $request,
TicketGrabEvent $ticketGrabEvent,
VerifyPortalCredential $verifyPortalCredential,
): JsonResponse {
$this->ensureTicketGrabAdmin($request, $ticketGrabEvent);
$this->assertCredentialBelongsTicketGrab($verifyPortalCredential, $ticketGrabEvent);
$verifyPortalCredential->tokens()->delete();
$verifyPortalCredential->delete();
return response()->json(['message' => '已删除']);
}
/**
* @return array<string, mixed>
*/
private function credentialToRow(VerifyPortalCredential $c): array
{
$plain = null;
if ($c->password_plain_enc !== null && $c->password_plain_enc !== '') {
try {
$plain = Crypt::decryptString($c->password_plain_enc);
} catch (\Throwable) {
$plain = null;
}
}
return [
'id' => $c->id,
'venue_id' => $c->venue_id,
'venue_name' => $c->venue?->name,
'username' => $c->username,
'password_plain' => $plain,
'note' => $c->note,
'created_at' => $c->created_at?->toDateTimeString(),
];
}
private function ensureActivityVenueAdmin(Request $request, Activity $activity): void
{
$user = $request->user();
if ($user->isSuperAdmin()) {
return;
}
$allowed = $user->venues()->where('venues.id', $activity->venue_id)->exists();
abort_unless($allowed, 403, '仅可操作已绑定场馆');
}
private function ensureTicketGrabAdmin(Request $request, TicketGrabEvent $e): void
{
$pivots = TicketGrabEventVenue::query()
->where('ticket_grab_event_id', $e->id)
->pluck('venue_id')
->all();
if ($pivots === []) {
return;
}
$user = $request->user();
if ($user->isSuperAdmin()) {
return;
}
$allow = $user->venues()->pluck('venues.id');
foreach (array_unique(array_map('intval', $pivots)) as $id) {
if ($id > 0 && ! $allow->contains($id)) {
abort(403, '仅可操作已绑定场馆');
}
}
}
private function assertCredentialBelongsActivity(VerifyPortalCredential $c, Activity $activity): void
{
abort_unless(
$c->portal_kind === VerifyPortalCredential::KIND_ACTIVITY && (int) $c->portal_id === (int) $activity->id,
404
);
}
private function assertCredentialBelongsTicketGrab(VerifyPortalCredential $c, TicketGrabEvent $e): void
{
abort_unless(
$c->portal_kind === VerifyPortalCredential::KIND_TICKET_GRAB && (int) $c->portal_id === (int) $e->id,
404
);
}
}

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\WechatUser;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class WechatUserController extends Controller
{
public function index(Request $request): JsonResponse
{
$pageSize = max(1, min(100, (int) $request->input('page_size', 20)));
$query = WechatUser::query()->orderByDesc('id');
if ($request->filled('keyword')) {
$keyword = trim((string) $request->input('keyword'));
$query->where(function ($q) use ($keyword) {
$q->where('phone', 'like', "%{$keyword}%")
->orWhere('nickname', 'like', "%{$keyword}%")
->orWhere('real_name', 'like', "%{$keyword}%");
});
}
$user = $request->user();
if (! $user->isSuperAdmin()) {
$venueIds = $user->venues()->pluck('venues.id');
$query->whereExists(function ($sub) use ($venueIds) {
$sub->selectRaw('1')
->from('reservations')
->whereColumn('reservations.wechat_user_id', 'wechat_users.id')
->whereIn('reservations.venue_id', $venueIds);
});
}
return response()->json($query->paginate($pageSize));
}
}

@ -3,6 +3,7 @@
namespace App\Http\Middleware;
use App\Models\AuditLog;
use App\Support\AdminAuditOperationDescriber;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
@ -22,6 +23,10 @@ class AuditLogMiddleware
$user = $request->user();
try {
$sanitized = $this->sanitizePayload($request->all());
$statusCode = (int) $response->getStatusCode();
$summary = AdminAuditOperationDescriber::describe($request, $sanitized, $statusCode);
AuditLog::create([
'user_id' => $user?->id,
'username' => $user?->username ?: $user?->name,
@ -29,10 +34,11 @@ class AuditLogMiddleware
'method' => strtoupper($request->method()),
'path' => '/'.ltrim($request->path(), '/'),
'action' => strtoupper($request->method()).' '.$request->path(),
'status_code' => (int) $response->getStatusCode(),
'operation_summary' => $summary,
'status_code' => $statusCode,
'ip' => $request->ip(),
'user_agent' => substr((string) $request->userAgent(), 0, 500),
'request_payload' => $this->sanitizePayload($request->all()),
'request_payload' => $sanitized,
]);
} catch (\Throwable $e) {
Log::warning('audit_log_write_failed', [

@ -20,6 +20,18 @@ class Activity extends Model
public const RESERVATION_TYPE_OTHER = 'other';
/** 报名方式(二期兼容旧值 online/offline/other */
public const RESERVATION_TYPE_PHONE = 'phone';
public const RESERVATION_TYPE_WECHAT_MP = 'wechat_mp';
public const RESERVATION_TYPE_OFFLINE_VISIT = 'offline_visit';
/** 门票说明:写入 offline_reservation_method取值 free / paid */
public const TICKET_FREE = 'free';
public const TICKET_PAID = 'paid';
protected $fillable = [
'venue_id',
'reservation_type',

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AdminRole extends Model
{
protected $fillable = [
'slug',
'name',
'is_system',
'full_access',
'sort',
];
protected $casts = [
'is_system' => 'boolean',
'full_access' => 'boolean',
'sort' => 'integer',
];
}

@ -17,6 +17,7 @@ class AuditLog extends Model
'method',
'path',
'action',
'operation_summary',
'status_code',
'ip',
'user_agent',

@ -32,6 +32,7 @@ class Reservation extends Model
'qr_token',
'status',
'verified_by',
'verify_credential_id',
'verified_at',
'reservation_source',
];
@ -76,4 +77,9 @@ class Reservation extends Model
{
return $this->belongsTo(User::class, 'verified_by');
}
public function verifyCredential(): BelongsTo
{
return $this->belongsTo(VerifyPortalCredential::class, 'verify_credential_id');
}
}

@ -3,6 +3,7 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
@ -13,6 +14,9 @@ class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/** @var bool|null in-request memo for {@see isSuperAdmin()} */
private ?bool $isSuperAdminMemo = null;
/**
* The attributes that are mass assignable.
*
@ -55,6 +59,17 @@ class User extends Authenticatable
public function isSuperAdmin(): bool
{
return $this->role === 'super_admin';
if ($this->isSuperAdminMemo !== null) {
return $this->isSuperAdminMemo;
}
if ($this->role === 'super_admin') {
return $this->isSuperAdminMemo = true;
}
return $this->isSuperAdminMemo = AdminRole::query()
->where('slug', $this->role)
->where('full_access', true)
->exists();
}
}

@ -0,0 +1,67 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Sanctum\HasApiTokens;
class VerifyPortalCredential extends Model
{
use HasApiTokens;
public const KIND_ACTIVITY = 'activity';
public const KIND_TICKET_GRAB = 'ticket_grab';
protected $fillable = [
'portal_kind',
'portal_id',
'venue_id',
'username',
'password',
'password_plain_enc',
'note',
];
protected $hidden = [
'password',
'password_plain_enc',
];
protected function casts(): array
{
return [
'portal_id' => 'integer',
'venue_id' => 'integer',
];
}
public function venue(): BelongsTo
{
return $this->belongsTo(Venue::class);
}
/** 活动/抢票结束日(含当日)内可核销;超过 end 日则失效 */
public function portalAcceptsVerification(): bool
{
if ($this->portal_kind === self::KIND_ACTIVITY) {
$a = Activity::query()->find($this->portal_id);
if (! $a || ! $a->end_at) {
return false;
}
return now()->toDateString() <= $a->end_at->toDateString();
}
if ($this->portal_kind === self::KIND_TICKET_GRAB) {
$e = TicketGrabEvent::query()->find($this->portal_id);
if (! $e || ! $e->end_at) {
return false;
}
return now()->toDateString() <= $e->end_at->toDateString();
}
return false;
}
}

@ -128,6 +128,154 @@ class TicketGrabReleaseDayService
}
}
/**
* 预约起止变更后同步每日放票行:已有预约时不可整表重建,改为按新窗口增删日期并重新均分配额。
*
* @throws ValidationException 缩短窗口导致「窗口外」仍有已预约放票日时
*/
public static function syncReleaseDaysToBookingWindow(int $eventId): void
{
$event = TicketGrabEvent::query()->with('eventVenuePivots')->find($eventId);
if (! $event || ! $event->booking_start_at || ! $event->booking_end_at) {
return;
}
$tz = (string) config('app.timezone');
$start = Carbon::parse($event->booking_start_at, $tz)->copy()->startOfDay();
$end = Carbon::parse($event->booking_end_at, $tz)->copy()->startOfDay();
if ($end->lt($start)) {
return;
}
$dates = [];
foreach (CarbonPeriod::create($start, $end) as $d) {
$dates[] = $d->toDateString();
}
$n = count($dates);
if ($n === 0) {
return;
}
$invalid = TicketGrabVenueReleaseDay::query()
->where('ticket_grab_event_id', $eventId)
->whereNotIn('release_date', $dates)
->where('booked_count', '>', 0)
->exists();
if ($invalid) {
throw ValidationException::withMessages([
'booking_end_at' => ['所选预约窗口之外仍有已预约人数对应的放票日,无法缩小至此范围。可先扩大结束日或处理相关预约后再调整。'],
]);
}
DB::transaction(function () use ($eventId, $dates, $n) {
$pivots = TicketGrabEventVenue::query()->where('ticket_grab_event_id', $eventId)->get();
foreach ($pivots as $p) {
$total = (int) $p->venue_total_quota;
$alloc = self::equalSplit($total, $n);
foreach ($dates as $i => $dateStr) {
$target = (int) ($alloc[$i] ?? 0);
$row = TicketGrabVenueReleaseDay::query()
->where('ticket_grab_event_id', $eventId)
->where('venue_id', $p->venue_id)
->whereDate('release_date', $dateStr)
->first();
if (! $row) {
TicketGrabVenueReleaseDay::query()->create([
'ticket_grab_event_id' => $eventId,
'venue_id' => $p->venue_id,
'release_date' => $dateStr,
'day_quota' => $target,
'booked_count' => 0,
'carry_in' => 0,
]);
} else {
$booked = (int) $row->booked_count;
$row->day_quota = max($target, $booked);
$row->save();
}
}
TicketGrabVenueReleaseDay::query()
->where('ticket_grab_event_id', $eventId)
->where('venue_id', $p->venue_id)
->whereNotIn('release_date', $dates)
->delete();
self::rebalanceVenueReleaseDays($eventId, $p->venue_id, $total);
}
self::syncCarryInChain($eventId);
});
$sum = (int) TicketGrabEventVenue::query()
->where('ticket_grab_event_id', $eventId)
->sum('venue_total_quota');
TicketGrabEvent::query()->where('id', $eventId)->update(['total_quota' => $sum]);
}
/**
* 将某馆各日 day_quota 之和收敛为 venue_total_quota已尊重 booked_count 下限)。
*
* @throws ValidationException 无法在不跌破已预约数的前提下收敛时
*/
private static function rebalanceVenueReleaseDays(int $eventId, int $venueId, int $targetTotal): void
{
$rows = TicketGrabVenueReleaseDay::query()
->where('ticket_grab_event_id', $eventId)
->where('venue_id', $venueId)
->orderBy('release_date')
->get();
if ($rows->isEmpty()) {
return;
}
$sum = (int) $rows->sum('day_quota');
$diff = $targetTotal - $sum;
if ($diff === 0) {
return;
}
if ($diff > 0) {
$keys = $rows->keys()->all();
$k = count($keys);
$i = 0;
while ($diff > 0) {
$idx = $keys[$i % $k];
$row = $rows[$idx];
$row->day_quota = (int) $row->day_quota + 1;
$row->save();
$diff--;
$i++;
}
return;
}
$need = -$diff;
$sorted = $rows->sortByDesc(fn ($r) => (int) $r->day_quota - (int) $r->booked_count)->values();
foreach ($sorted as $row) {
if ($need <= 0) {
break;
}
$slack = max(0, (int) $row->day_quota - (int) $row->booked_count);
$dec = min($slack, $need);
if ($dec > 0) {
$row->day_quota = (int) $row->day_quota - $dec;
$row->save();
$need -= $dec;
}
}
if ($need > 0) {
throw ValidationException::withMessages([
'booking_end_at' => ['各日已预约数量较多,无法在给定预约窗口与放票总数下平衡每日配额,请检查后再试。'],
]);
}
}
public static function updateDayQuotasFromAdmin(int $eventId, int $venueId, array $dateToQuota): void
{
$pivot = TicketGrabEventVenue::query()

@ -0,0 +1,247 @@
<?php
namespace App\Support;
use Illuminate\Http\Request;
/**
* 根据后台 API 路径与请求体生成简短中文「操作项」说明。
*/
final class AdminAuditOperationDescriber
{
public static function describe(Request $request, array $payload, int $statusCode): string
{
$method = strtoupper($request->method());
$path = self::normalizeApiPath($request);
$fail = $statusCode >= 400 ? '(失败)' : '';
$name = static fn (?string $s): ?string => $s !== null && trim($s) !== '' ? trim($s) : null;
if ($path === 'auth/logout' && $method === 'POST') {
return '退出登录'.$fail;
}
// --- 场馆 ---
if ($path === 'venues' && $method === 'POST') {
$t = $name($payload['name'] ?? null);
return $t !== null ? '新增场馆「'.$t.'」'.$fail : '新增场馆'.$fail;
}
if (preg_match('#^venues/(\d+)$#', $path, $m)) {
$id = $m[1];
if ($method === 'PUT' || $method === 'PATCH') {
$t = $name($payload['name'] ?? null);
return $t !== null ? '编辑场馆「'.$t.'」'.$fail : '编辑场馆ID:'.$id.''.$fail;
}
if ($method === 'DELETE') {
return '删除场馆ID:'.$id.''.$fail;
}
}
if (preg_match('#^venues/(\d+)/audit/approve$#', $path, $m)) {
return '审核通过场馆ID:'.$m[1].''.$fail;
}
if (preg_match('#^venues/(\d+)/audit/reject$#', $path, $m)) {
return '驳回场馆审核ID:'.$m[1].''.$fail;
}
if ($path === 'venues/import/preview' && $method === 'POST') {
return '导入场馆·预览'.$fail;
}
if ($path === 'venues/import/confirm' && $method === 'POST') {
return '导入场馆·确认入库'.$fail;
}
// --- 活动 ---
if ($path === 'activities' && $method === 'POST') {
$t = $name($payload['title'] ?? null);
return $t !== null ? '新增活动「'.$t.'」'.$fail : '新增活动'.$fail;
}
if (preg_match('#^activities/(\d+)$#', $path, $m)) {
$id = $m[1];
if ($method === 'PUT' || $method === 'PATCH') {
$t = $name($payload['title'] ?? null);
return $t !== null ? '编辑活动「'.$t.'」'.$fail : '编辑活动ID:'.$id.''.$fail;
}
if ($method === 'DELETE') {
return '删除活动ID:'.$id.''.$fail;
}
}
if (preg_match('#^activities/(\d+)/audit/approve$#', $path, $m)) {
return '审核通过活动ID:'.$m[1].''.$fail;
}
if (preg_match('#^activities/(\d+)/audit/reject$#', $path, $m)) {
return '驳回活动审核ID:'.$m[1].''.$fail;
}
if (preg_match('#^activities/(\d+)/toggle$#', $path, $m)) {
return '切换活动启停ID:'.$m[1].''.$fail;
}
if (preg_match('#^activities/(\d+)/restore$#', $path, $m)) {
return '恢复活动ID:'.$m[1].''.$fail;
}
// --- 抢票活动 ---
if ($path === 'ticket-grab-events' && $method === 'POST') {
$t = $name($payload['title'] ?? null);
return $t !== null ? '新增抢票活动「'.$t.'」'.$fail : '新增抢票活动'.$fail;
}
if (preg_match('#^ticket-grab-events/(\d+)$#', $path, $m)) {
$id = $m[1];
if ($method === 'PUT' || $method === 'PATCH') {
$t = $name($payload['title'] ?? null);
return $t !== null ? '编辑抢票活动「'.$t.'」'.$fail : '编辑抢票活动ID:'.$id.''.$fail;
}
if ($method === 'DELETE') {
return '删除抢票活动ID:'.$id.''.$fail;
}
}
if (preg_match('#^ticket-grab-events/(\d+)/toggle$#', $path, $m)) {
return '切换抢票活动启停ID:'.$m[1].''.$fail;
}
// --- 管理员与权限 ---
if ($path === 'admin-users' && $method === 'POST') {
$u = $name($payload['username'] ?? null);
return $u !== null ? '新增管理员「'.$u.'」'.$fail : '新增管理员'.$fail;
}
if (preg_match('#^admin-users/(\d+)$#', $path, $m) && ($method === 'PUT' || $method === 'PATCH')) {
return '编辑管理员ID:'.$m[1].''.$fail;
}
if ($path === 'admin-roles' && $method === 'POST') {
$slug = $name($payload['slug'] ?? null);
$rn = $name($payload['name'] ?? null);
if ($slug !== null && $rn !== null) {
return '新增角色「'.$rn.'」('.$slug.''.$fail;
}
return '新增角色'.$fail;
}
if (preg_match('#^admin-roles/([a-z0-9_]+)$#', $path, $m) && ($method === 'PUT' || $method === 'PATCH')) {
return '编辑角色('.$m[1].''.$fail;
}
if (preg_match('#^admin-roles/([a-z0-9_]+)$#', $path, $m) && $method === 'DELETE') {
return '删除角色('.$m[1].''.$fail;
}
if ($path === 'admin-menus' && $method === 'POST') {
$t = $name($payload['name'] ?? null);
return $t !== null ? '新增菜单「'.$t.'」'.$fail : '新增菜单'.$fail;
}
if (preg_match('#^admin-menus/(\d+)$#', $path, $m)) {
$id = $m[1];
if ($method === 'PUT' || $method === 'PATCH') {
$t = $name($payload['name'] ?? null);
return $t !== null ? '编辑菜单「'.$t.'」'.$fail : '编辑菜单ID:'.$id.''.$fail;
}
if ($method === 'DELETE') {
return '删除菜单ID:'.$id.''.$fail;
}
}
if (preg_match('#^role-menu-permissions/(.+)$#', $path, $m) && ($method === 'PUT' || $method === 'PATCH')) {
return '保存角色菜单权限('.$m[1].''.$fail;
}
if ($path === 'role-permissions/batch-update' && $method === 'POST') {
return '批量更新功能权限'.$fail;
}
if (preg_match('#^role-permissions/(\d+)$#', $path, $m) && ($method === 'PUT' || $method === 'PATCH')) {
return '更新功能权限ID:'.$m[1].''.$fail;
}
// --- 上传 ---
if ($path === 'upload' && $method === 'POST') {
return '上传文件'.$fail;
}
if ($path === 'upload/delete' && $method === 'POST') {
return '删除已上传文件'.$fail;
}
// --- 核销 ---
if ($path === 'reservations/verify' && $method === 'POST') {
return '核销预约'.$fail;
}
if ($path === 'activity-registrations/quick-blacklist/batch' && $method === 'POST') {
return '批量拉黑(报名)'.$fail;
}
if (preg_match('#^activity-registrations/(\d+)/quick-blacklist$#', $path, $m) && $method === 'POST') {
return '一键拉黑(报名 ID:'.$m[1].''.$fail;
}
// --- 黑名单 ---
if ($path === 'blacklists' && $method === 'POST') {
return '新增黑名单记录'.$fail;
}
if (preg_match('#^blacklists/(\d+)$#', $path, $m)) {
$id = $m[1];
if ($method === 'PUT' || $method === 'PATCH') {
return '编辑黑名单ID:'.$id.''.$fail;
}
if ($method === 'DELETE') {
return '删除黑名单ID:'.$id.''.$fail;
}
}
if ($path === 'blacklists/batch-import' && $method === 'POST') {
return '批量导入黑名单'.$fail;
}
if ($path === 'user-management/blacklist' && $method === 'POST') {
return '批量拉黑用户'.$fail;
}
if ($path === 'user-management/unblacklist' && $method === 'POST') {
return '批量解除拉黑'.$fail;
}
// --- 研学 / 字典 ---
if ($path === 'study-tours' && $method === 'POST') {
$t = $name($payload['title'] ?? null);
return $t !== null ? '新增研学路线「'.$t.'」'.$fail : '新增研学路线'.$fail;
}
if (preg_match('#^study-tours/(\d+)$#', $path, $m)) {
$id = $m[1];
if ($method === 'PUT' || $method === 'PATCH') {
return '编辑研学路线ID:'.$id.''.$fail;
}
if ($method === 'DELETE') {
return '删除研学路线ID:'.$id.''.$fail;
}
}
if ($path === 'dict-items' && $method === 'POST') {
return '新增字典项'.$fail;
}
if (preg_match('#^dict-items/(\d+)$#', $path, $m)) {
$id = $m[1];
if ($method === 'PUT' || $method === 'PATCH') {
return '编辑字典项ID:'.$id.''.$fail;
}
if ($method === 'DELETE') {
return '删除字典项ID:'.$id.''.$fail;
}
}
// fallback
return self::fallbackLabel($method, $path).$fail;
}
private static function normalizeApiPath(Request $request): string
{
$p = trim($request->path(), '/');
if (str_starts_with($p, 'api/')) {
return substr($p, strlen('api/'));
}
return $p;
}
private static function fallbackLabel(string $method, string $path): string
{
return $method.' '.$path;
}
}

@ -0,0 +1,46 @@
<?php
namespace App\Support;
use App\Models\Activity;
use App\Models\TicketGrabEvent;
final class VerifyPortalCode
{
/** 8 位十六进制,便于手机录入;全库(活动+抢票)唯一 */
public static function generateUnique(): string
{
for ($i = 0; $i < 80; $i++) {
$c = strtolower(substr(bin2hex(random_bytes(4)), 0, 8));
if (! self::isTaken($c)) {
return $c;
}
}
return strtolower(substr(bin2hex(random_bytes(8)), 0, 12));
}
public static function isTaken(string $code): bool
{
$code = strtolower(trim($code));
return Activity::query()->where('verify_portal_code', $code)->exists()
|| TicketGrabEvent::query()->where('verify_portal_code', $code)->exists();
}
public static function ensureForActivity(Activity $activity): void
{
if ($activity->verify_portal_code !== null && $activity->verify_portal_code !== '') {
return;
}
$activity->forceFill(['verify_portal_code' => self::generateUnique()])->save();
}
public static function ensureForTicketGrabEvent(TicketGrabEvent $event): void
{
if ($event->verify_portal_code !== null && $event->verify_portal_code !== '') {
return;
}
$event->forceFill(['verify_portal_code' => self::generateUnique()])->save();
}
}

@ -0,0 +1,77 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
return new class extends Migration
{
public function up(): void
{
Schema::table('activities', function (Blueprint $table) {
$table->string('verify_portal_token', 36)->nullable()->unique()->after('id');
});
Schema::table('ticket_grab_events', function (Blueprint $table) {
$table->string('verify_portal_token', 36)->nullable()->unique()->after('id');
});
Schema::create('verify_portal_credentials', function (Blueprint $table) {
$table->id();
$table->string('portal_kind', 32);
$table->unsignedBigInteger('portal_id');
$table->foreignId('venue_id')->constrained('venues')->cascadeOnDelete();
$table->string('username', 64);
$table->string('password');
$table->string('note', 255)->nullable();
$table->timestamps();
$table->unique(['portal_kind', 'portal_id', 'venue_id', 'username'], 'vpc_portal_venue_username');
$table->index(['portal_kind', 'portal_id']);
});
Schema::table('reservations', function (Blueprint $table) {
$table->foreignId('verify_credential_id')
->nullable()
->after('verified_by')
->constrained('verify_portal_credentials')
->nullOnDelete();
});
if (Schema::hasTable('activities')) {
foreach (DB::table('activities')->whereNull('verify_portal_token')->pluck('id') as $id) {
DB::table('activities')->where('id', $id)->update([
'verify_portal_token' => Str::uuid()->toString(),
]);
}
}
if (Schema::hasTable('ticket_grab_events')) {
foreach (DB::table('ticket_grab_events')->whereNull('verify_portal_token')->pluck('id') as $id) {
DB::table('ticket_grab_events')->where('id', $id)->update([
'verify_portal_token' => Str::uuid()->toString(),
]);
}
}
}
public function down(): void
{
Schema::table('reservations', function (Blueprint $table) {
$table->dropConstrainedForeignId('verify_credential_id');
});
Schema::dropIfExists('verify_portal_credentials');
Schema::table('ticket_grab_events', function (Blueprint $table) {
$table->dropUnique(['verify_portal_token']);
$table->dropColumn('verify_portal_token');
});
Schema::table('activities', function (Blueprint $table) {
$table->dropUnique(['verify_portal_token']);
$table->dropColumn('verify_portal_token');
});
}
};

@ -0,0 +1,61 @@
<?php
use App\Models\Activity;
use App\Models\TicketGrabEvent;
use App\Support\VerifyPortalCode;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('activities', function (Blueprint $table) {
$table->string('verify_portal_code', 12)->nullable()->index()->after('verify_portal_token');
});
Schema::table('ticket_grab_events', function (Blueprint $table) {
$table->string('verify_portal_code', 12)->nullable()->index()->after('verify_portal_token');
});
Schema::table('verify_portal_credentials', function (Blueprint $table) {
$table->text('password_plain_enc')->nullable()->after('password');
});
Activity::query()->select('id')->orderBy('id')->chunkById(100, function ($rows) {
foreach ($rows as $row) {
$a = Activity::query()->find($row->id);
if ($a && ($a->verify_portal_code === null || $a->verify_portal_code === '')) {
$a->forceFill(['verify_portal_code' => VerifyPortalCode::generateUnique()])->save();
}
}
});
TicketGrabEvent::query()->select('id')->orderBy('id')->chunkById(100, function ($rows) {
foreach ($rows as $row) {
$e = TicketGrabEvent::query()->find($row->id);
if ($e && ($e->verify_portal_code === null || $e->verify_portal_code === '')) {
$e->forceFill(['verify_portal_code' => VerifyPortalCode::generateUnique()])->save();
}
}
});
}
public function down(): void
{
Schema::table('verify_portal_credentials', function (Blueprint $table) {
$table->dropColumn('password_plain_enc');
});
Schema::table('ticket_grab_events', function (Blueprint $table) {
$table->dropIndex(['verify_portal_code']);
$table->dropColumn('verify_portal_code');
});
Schema::table('activities', function (Blueprint $table) {
$table->dropIndex(['verify_portal_code']);
$table->dropColumn('verify_portal_code');
});
}
};

@ -0,0 +1,88 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('admin_menus')) {
return;
}
$活动管理Id = (int) (DB::table('admin_menus')->where('name', '活动管理')->where('parent_id', 0)->value('id') ?? 0);
if ($活动管理Id > 0) {
DB::table('admin_menus')
->where('parent_id', $活动管理Id)
->whereIn('path', ['/activities/registrations', '/activities/verify', '/activities/blacklist'])
->update(['is_visible' => false]);
}
$exists = DB::table('admin_menus')->where('path', '/wechat-users')->exists();
if ($exists) {
return;
}
$parentId = DB::table('admin_menus')->insertGetId([
'name' => '用户管理',
'path' => null,
'icon' => 'IconUser',
'parent_id' => 0,
'sort' => 31,
'is_visible' => true,
'created_at' => now(),
'updated_at' => now(),
]);
$childId = DB::table('admin_menus')->insertGetId([
'name' => '用户列表',
'path' => '/wechat-users',
'icon' => 'IconUserGroup',
'parent_id' => $parentId,
'sort' => 1,
'is_visible' => true,
'created_at' => now(),
'updated_at' => now(),
]);
if (Schema::hasTable('role_menu_permissions')) {
$now = now();
foreach (['super_admin', 'venue_admin'] as $role) {
foreach ([$parentId, $childId] as $mid) {
DB::table('role_menu_permissions')->insertOrIgnore([
'role' => $role,
'menu_id' => $mid,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}
}
public function down(): void
{
if (! Schema::hasTable('admin_menus')) {
return;
}
$活动管理Id = (int) (DB::table('admin_menus')->where('name', '活动管理')->where('parent_id', 0)->value('id') ?? 0);
if ($活动管理Id > 0) {
DB::table('admin_menus')
->where('parent_id', $活动管理Id)
->whereIn('path', ['/activities/registrations', '/activities/verify', '/activities/blacklist'])
->update(['is_visible' => true]);
}
$childRow = DB::table('admin_menus')->where('path', '/wechat-users')->first();
if ($childRow) {
$pid = (int) ($childRow->parent_id ?? 0);
DB::table('admin_menus')->where('id', $childRow->id)->delete();
if ($pid > 0) {
DB::table('admin_menus')->where('id', $pid)->where('name', '用户管理')->delete();
}
}
}
};

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('admin_roles', function (Blueprint $table) {
$table->boolean('full_access')->default(false)->after('is_system');
});
DB::table('admin_roles')->where('slug', 'super_admin')->update(['full_access' => true]);
}
public function down(): void
{
Schema::table('admin_roles', function (Blueprint $table) {
$table->dropColumn('full_access');
});
}
};

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('audit_logs', function (Blueprint $table) {
$table->string('operation_summary', 500)->nullable()->after('action');
});
}
public function down(): void
{
Schema::table('audit_logs', function (Blueprint $table) {
$table->dropColumn('operation_summary');
});
}
};

@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('admin_roles', function (Blueprint $table) {
$table->id();
$table->string('slug', 50)->unique();
$table->string('name', 100);
$table->boolean('is_system')->default(false);
$table->unsignedInteger('sort')->default(0);
$table->timestamps();
});
$now = now();
DB::table('admin_roles')->insert([
[
'slug' => 'super_admin',
'name' => '超级管理员',
'is_system' => true,
'sort' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'slug' => 'venue_admin',
'name' => '场馆管理员',
'is_system' => true,
'sort' => 2,
'created_at' => $now,
'updated_at' => $now,
],
]);
$driver = Schema::getConnection()->getDriverName();
if (in_array($driver, ['mysql', 'mariadb'], true)) {
DB::statement("ALTER TABLE users MODIFY COLUMN role VARCHAR(50) NOT NULL DEFAULT 'venue_admin'");
}
}
public function down(): void
{
Schema::dropIfExists('admin_roles');
$driver = Schema::getConnection()->getDriverName();
if (in_array($driver, ['mysql', 'mariadb'], true)) {
DB::statement("ALTER TABLE users MODIFY COLUMN role ENUM('super_admin','venue_admin') NOT NULL DEFAULT 'venue_admin'");
}
}
};

@ -173,6 +173,7 @@ class DatabaseSeeder extends Seeder
['name' => '工作台', 'path' => '/dashboard', 'icon' => 'IconDashboard', 'parent_id' => 0, 'sort' => 10],
['name' => '场馆管理', 'path' => null, 'icon' => 'IconApps', 'parent_id' => 0, 'sort' => 20],
['name' => '活动管理', 'path' => null, 'icon' => 'IconCalendar', 'parent_id' => 0, 'sort' => 30],
['name' => '用户管理', 'path' => null, 'icon' => 'IconUser', 'parent_id' => 0, 'sort' => 31],
['name' => '抢票管理', 'path' => null, 'icon' => 'IconThunderbolt', 'parent_id' => 0, 'sort' => 32],
['name' => '研学线路管理', 'path' => null, 'icon' => 'IconBook', 'parent_id' => 0, 'sort' => 40],
['name' => '客流监控', 'path' => null, 'icon' => 'IconBarChart', 'parent_id' => 0, 'sort' => 50],
@ -189,9 +190,7 @@ class DatabaseSeeder extends Seeder
$children = [
['name' => '场馆列表', 'path' => '/venues', 'icon' => 'IconList', 'parent' => '场馆管理', 'sort' => 1],
['name' => '活动列表', 'path' => '/activities', 'icon' => 'IconCalendarClock', 'parent' => '活动管理', 'sort' => 1],
['name' => '报名管理', 'path' => '/activities/registrations', 'icon' => 'IconUser', 'parent' => '活动管理', 'sort' => 2],
['name' => '现场核销', 'path' => '/activities/verify', 'icon' => 'IconScan', 'parent' => '活动管理', 'sort' => 3],
['name' => '用户管理', 'path' => '/activities/blacklist', 'icon' => 'IconLock', 'parent' => '活动管理', 'sort' => 4],
['name' => '用户列表', 'path' => '/wechat-users', 'icon' => 'IconUserGroup', 'parent' => '用户管理', 'sort' => 1],
['name' => '抢票列表', 'path' => '/ticket-grab-events', 'icon' => 'IconCalendar', 'parent' => '抢票管理', 'sort' => 1],
['name' => '抢票报名', 'path' => '/ticket-grab-events/registrations', 'icon' => 'IconUser', 'parent' => '抢票管理', 'sort' => 2],
['name' => '抢票核销', 'path' => '/ticket-grab-events/verify', 'icon' => 'IconScan', 'parent' => '抢票管理', 'sort' => 3],

@ -0,0 +1 @@
.activity-address-coord-row[data-v-0f0b3fd7]{flex-wrap:wrap;align-items:center;gap:12px;width:100%;display:flex}.activity-address-coord-row__address[data-v-0f0b3fd7]{flex:45%;min-width:320px;max-width:100%}.activity-address-coord-row__lng[data-v-0f0b3fd7],.activity-address-coord-row__lat[data-v-0f0b3fd7]{flex:180px;width:200px;min-width:180px}.activity-address-coord-row__map[data-v-0f0b3fd7]{flex-shrink:0}.activity-cover-carousel-wrap[data-v-0f0b3fd7]{flex-wrap:wrap;align-items:flex-start;gap:20px;width:100%;display:flex}.activity-cover-carousel-row__col[data-v-0f0b3fd7]{flex:320px;min-width:min(100%,320px)}.activity-cover-carousel-row__sub[data-v-0f0b3fd7]{color:var(--color-text-1);margin-bottom:8px;font-weight:500}.activity-cover-thumb[data-v-0f0b3fd7]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:120px;height:70px}.activity-gallery-grid[data-v-0f0b3fd7]{flex-wrap:wrap;align-items:flex-start;gap:12px;width:100%;display:flex}.activity-gallery-item[data-v-0f0b3fd7]{flex-direction:column;align-items:flex-start;gap:8px;display:flex}.activity-gallery-thumb[data-v-0f0b3fd7]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:120px;height:70px}.activity-gallery-thumb--video[data-v-0f0b3fd7]{display:block}.activity-form-tags[data-v-0f0b3fd7]{max-width:520px}.activity-form-tags__line[data-v-0f0b3fd7]{flex-flow:row;align-items:center;gap:8px;width:100%;display:flex}.activity-form-tags__input[data-v-0f0b3fd7]{flex:auto;min-width:0}.activity-form-tags__input[data-v-0f0b3fd7] .arco-input-wrapper{width:100%}.activity-form-tags__save[data-v-0f0b3fd7]{flex-shrink:0}.activity-form-tags__chips[data-v-0f0b3fd7]{flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px;display:flex}.activity-form-tags__empty[data-v-0f0b3fd7]{color:#86909c;font-size:12px}

@ -1 +0,0 @@
.activity-address-coord-row[data-v-dbd5747f]{flex-wrap:wrap;align-items:center;gap:12px;width:100%;display:flex}.activity-address-coord-row__address[data-v-dbd5747f]{flex:45%;min-width:320px;max-width:100%}.activity-address-coord-row__lng[data-v-dbd5747f],.activity-address-coord-row__lat[data-v-dbd5747f]{flex:180px;width:200px;min-width:180px}.activity-address-coord-row__map[data-v-dbd5747f]{flex-shrink:0}.activity-cover-carousel-wrap[data-v-dbd5747f]{flex-wrap:wrap;align-items:flex-start;gap:20px;width:100%;display:flex}.activity-cover-carousel-row__col[data-v-dbd5747f]{flex:320px;min-width:min(100%,320px)}.activity-cover-carousel-row__sub[data-v-dbd5747f]{color:var(--color-text-1);margin-bottom:8px;font-weight:500}.activity-cover-thumb[data-v-dbd5747f]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:120px;height:70px}.activity-gallery-grid[data-v-dbd5747f]{flex-wrap:wrap;align-items:flex-start;gap:12px;width:100%;display:flex}.activity-gallery-item[data-v-dbd5747f]{flex-direction:column;align-items:flex-start;gap:8px;display:flex}.activity-gallery-thumb[data-v-dbd5747f]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:120px;height:70px}.activity-gallery-thumb--video[data-v-dbd5747f]{display:block}.activity-form-tags[data-v-dbd5747f]{max-width:520px}.activity-form-tags__line[data-v-dbd5747f]{flex-flow:row;align-items:center;gap:8px;width:100%;display:flex}.activity-form-tags__input[data-v-dbd5747f]{flex:auto;min-width:0}.activity-form-tags__input[data-v-dbd5747f] .arco-input-wrapper{width:100%}.activity-form-tags__save[data-v-dbd5747f]{flex-shrink:0}.activity-form-tags__chips[data-v-dbd5747f]{flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px;display:flex}.activity-form-tags__empty[data-v-dbd5747f]{color:#86909c;font-size:12px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`Alerts`,setup(n){return(n,i)=>(e(),t(r,{title:`客流监控 / 异常告警`}))}});export{i as default};
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-C01qW8FK.js";var i=n({__name:`Alerts`,setup(n){return(n,i)=>(e(),t(r,{title:`客流监控 / 异常告警`}))}});export{i as default};

@ -0,0 +1 @@
.audit-api-endpoint[data-v-f4d2ff07]{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px}

@ -0,0 +1 @@
import{I as e,N as t,V as n,Y as r,_ as i,d as a,it as o,kt as s,nt as c,u as l,ut as u,v as d,y as f}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as p}from"./message-Dh9377vh.js";import{n as m}from"./http-BWP--XXK.js";import{i as h}from"./index-CLnpIFlv.js";import{t as g}from"./listTableRowIndex-Bl-nc9Qt.js";import{t as _}from"./datetime-DLy52ZIc.js";var v={class:`audit-api-endpoint`},y=1280,b=h(f({__name:`AuditLogs`,setup(f){let h=o(!1),b=o([]),x=c({current:1,pageSize:20,total:0}),S=c({keyword:``,method:`all`,status_code:void 0,dateRange:[]});async function C(){h.value=!0;try{let{data:e}=await m.get(`/audit-logs`,{params:{keyword:S.keyword||void 0,method:S.method,status_code:S.status_code||void 0,start_date:S.dateRange?.[0]||void 0,end_date:S.dateRange?.[1]||void 0,page:x.current,page_size:x.pageSize}});b.value=e.data,x.total=e.total}catch(e){p.error(e?.response?.data?.message??`加载操作日志失败`)}finally{h.value=!1}}function w(){x.current=1,C()}function T(e){x.current=e,C()}function E(e){return e.operation_summary&&String(e.operation_summary).trim()!==``?e.operation_summary:e.action&&String(e.action).trim()!==``?e.action:``}function D(e){let t=(e.method||``).toUpperCase()||``,n=(e.path||``).trim();return!n||n===`/`?t+``:(n.startsWith(`/`)||(n=`/`+n),`${t} ${n}`)}return t(C),(t,o)=>{let c=n(`a-alert`),f=n(`a-input`),p=n(`a-option`),m=n(`a-select`),C=n(`a-input-number`),O=n(`a-range-picker`),k=n(`a-button`),A=n(`a-space`),j=n(`a-table-column`),M=n(`a-table`),N=n(`a-card`);return e(),a(N,{title:`用户与权限 / 操作日志`},{default:r(()=>[d(c,{type:`info`,style:{"margin-bottom":`12px`},closable:``},{default:r(()=>[...o[4]||=[i(` 此处为「操作日志」(写操作审计)。系统设置里的「系统日志」页面用于查看服务器 Laravel 日志文件,不是本页。列表不包含 GET 类查询请求。 `,-1)]]),_:1}),d(A,{wrap:``,size:12,style:{"margin-bottom":`12px`}},{default:r(()=>[d(f,{modelValue:S.keyword,"onUpdate:modelValue":o[0]||=e=>S.keyword=e,placeholder:`操作人 / 操作项`,"allow-clear":``,style:{width:`240px`}},null,8,[`modelValue`]),d(m,{modelValue:S.method,"onUpdate:modelValue":o[1]||=e=>S.method=e,style:{width:`120px`}},{default:r(()=>[d(p,{value:`all`},{default:r(()=>[...o[5]||=[i(`全部方法`,-1)]]),_:1}),d(p,{value:`POST`},{default:r(()=>[...o[6]||=[i(`POST`,-1)]]),_:1}),d(p,{value:`PUT`},{default:r(()=>[...o[7]||=[i(`PUT`,-1)]]),_:1}),d(p,{value:`PATCH`},{default:r(()=>[...o[8]||=[i(`PATCH`,-1)]]),_:1}),d(p,{value:`DELETE`},{default:r(()=>[...o[9]||=[i(`DELETE`,-1)]]),_:1})]),_:1},8,[`modelValue`]),d(C,{modelValue:S.status_code,"onUpdate:modelValue":o[2]||=e=>S.status_code=e,min:100,max:599,placeholder:`状态码`,style:{width:`120px`}},null,8,[`modelValue`]),d(O,{modelValue:S.dateRange,"onUpdate:modelValue":o[3]||=e=>S.dateRange=e,style:{width:`260px`}},null,8,[`modelValue`]),d(k,{type:`primary`,onClick:w},{default:r(()=>[...o[10]||=[i(`查询`,-1)]]),_:1})]),_:1}),d(M,{class:`list-data-table`,scroll:{x:y},data:b.value,loading:h.value,"row-key":`id`,pagination:{current:x.current,pageSize:x.pageSize,total:x.total,showTotal:!0},onPageChange:T},{columns:r(()=>[d(j,{title:``,width:52,ellipsis:!0,tooltip:!0},{cell:r(({rowIndex:e})=>[i(s(u(g)(e,x.current,x.pageSize)),1)]),_:1}),d(j,{title:`操作人`,"data-index":`username`,width:140,ellipsis:!0,tooltip:!0},{cell:r(({record:e})=>[i(s(e.username||``),1)]),_:1}),d(j,{title:`操作时间`,width:178},{cell:r(({record:e})=>[i(s(u(_)(e.created_at)),1)]),_:1}),d(j,{title:`接口`,width:320,ellipsis:!0,tooltip:!0},{cell:r(({record:e})=>[l(`span`,v,s(D(e)),1)]),_:1}),d(j,{title:`操作项`,"min-width":320,ellipsis:!0,tooltip:!0},{cell:r(({record:e})=>[i(s(E(e)),1)]),_:1})]),_:1},8,[`scroll`,`data`,`loading`,`pagination`])]),_:1})}}}),[[`__scopeId`,`data-v-f4d2ff07`]]);export{b as default};

@ -1 +0,0 @@
import{n as e}from"./axios-CiYFffbI.js";import{I as t,N as n,V as r,Y as i,_ as a,d as o,it as s,kt as c,nt as l,ut as u,v as d,y as f}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{n as p}from"./index-Dyg4zy2p.js";import{t as m}from"./listTableRowIndex-CwNZabrG.js";import{t as h}from"./datetime-CjmbUMhc.js";var g=1600,_=f({__name:`AuditLogs`,setup(f){let _=s(!1),v=s([]),y=l({current:1,pageSize:20,total:0}),b=l({keyword:``,method:`all`,status_code:void 0,dateRange:[]});async function x(){_.value=!0;try{let{data:e}=await p.get(`/audit-logs`,{params:{keyword:b.keyword||void 0,method:b.method,status_code:b.status_code||void 0,start_date:b.dateRange?.[0]||void 0,end_date:b.dateRange?.[1]||void 0,page:y.current,page_size:y.pageSize}});v.value=e.data,y.total=e.total}catch(t){e.error(t?.response?.data?.message??`加载操作日志失败`)}finally{_.value=!1}}function S(){y.current=1,x()}function C(e){y.current=e,x()}function w(e){return e===`super_admin`?`超级管理员`:e===`venue_admin`?`场馆管理员`:`-`}return n(x),(e,n)=>{let s=r(`a-input`),l=r(`a-option`),f=r(`a-select`),p=r(`a-input-number`),T=r(`a-range-picker`),E=r(`a-button`),D=r(`a-space`),O=r(`a-table-column`),k=r(`a-typography-paragraph`),A=r(`a-table`),j=r(`a-card`);return t(),o(j,{title:`用户与权限 / 操作日志`},{default:i(()=>[d(D,{wrap:``,size:12,style:{"margin-bottom":`12px`}},{default:i(()=>[d(s,{modelValue:b.keyword,"onUpdate:modelValue":n[0]||=e=>b.keyword=e,placeholder:`操作人/路径/动作`,"allow-clear":``,style:{width:`240px`}},null,8,[`modelValue`]),d(f,{modelValue:b.method,"onUpdate:modelValue":n[1]||=e=>b.method=e,style:{width:`120px`}},{default:i(()=>[d(l,{value:`all`},{default:i(()=>[...n[4]||=[a(`全部方法`,-1)]]),_:1}),d(l,{value:`POST`},{default:i(()=>[...n[5]||=[a(`POST`,-1)]]),_:1}),d(l,{value:`PUT`},{default:i(()=>[...n[6]||=[a(`PUT`,-1)]]),_:1}),d(l,{value:`PATCH`},{default:i(()=>[...n[7]||=[a(`PATCH`,-1)]]),_:1}),d(l,{value:`DELETE`},{default:i(()=>[...n[8]||=[a(`DELETE`,-1)]]),_:1})]),_:1},8,[`modelValue`]),d(p,{modelValue:b.status_code,"onUpdate:modelValue":n[2]||=e=>b.status_code=e,min:100,max:599,placeholder:`状态码`,style:{width:`120px`}},null,8,[`modelValue`]),d(T,{modelValue:b.dateRange,"onUpdate:modelValue":n[3]||=e=>b.dateRange=e,style:{width:`260px`}},null,8,[`modelValue`]),d(E,{type:`primary`,onClick:S},{default:i(()=>[...n[9]||=[a(`查询`,-1)]]),_:1}),d(E,{onClick:x},{default:i(()=>[...n[10]||=[a(`刷新`,-1)]]),_:1})]),_:1}),d(A,{class:`list-data-table`,scroll:{x:g},data:v.value,loading:_.value,"row-key":`id`,pagination:{current:y.current,pageSize:y.pageSize,total:y.total,showTotal:!0},onPageChange:C},{columns:i(()=>[d(O,{title:``,width:50,ellipsis:!0,tooltip:!0},{cell:i(({rowIndex:e})=>[a(c(u(m)(e,y.current,y.pageSize)),1)]),_:1}),d(O,{title:`操作人`,"data-index":`username`,width:140,ellipsis:!0,tooltip:!0}),d(O,{title:`角色`,width:120},{cell:i(({record:e})=>[a(c(w(e.role)),1)]),_:1}),d(O,{title:`方法`,"data-index":`method`,width:90}),d(O,{title:`路径`,"data-index":`path`,width:260,ellipsis:!0,tooltip:!0}),d(O,{title:`动作`,"data-index":`action`,width:220,ellipsis:!0,tooltip:!0}),d(O,{title:`状态码`,"data-index":`status_code`,width:100}),d(O,{title:`IP`,"data-index":`ip`,width:140,ellipsis:!0,tooltip:!0}),d(O,{title:`时间`,width:190},{cell:i(({record:e})=>[a(c(u(h)(e.created_at)),1)]),_:1}),d(O,{title:`请求参数`,"min-width":260},{cell:i(({record:e})=>[d(k,{ellipsis:{rows:2}},{default:i(()=>[a(c(e.request_payload?JSON.stringify(e.request_payload):`-`),1)]),_:2},1024)]),_:1})]),_:1},8,[`scroll`,`data`,`loading`,`pagination`])]),_:1})}}});export{_ as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`Categories`,setup(n){return(n,i)=>(e(),t(r,{title:`数据统计 / 类别分析`}))}});export{i as default};
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-C01qW8FK.js";var i=n({__name:`Categories`,setup(n){return(n,i)=>(e(),t(r,{title:`数据统计 / 类别分析`}))}});export{i as default};

@ -1 +0,0 @@
.dashboard-wrap[data-v-c21a264b]{gap:14px;display:grid}.summary-scope-hint[data-v-c21a264b]{color:#86909c;margin-top:-4px;font-size:12px;line-height:1.5}.stats-cards-flow[data-v-c21a264b]{grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:10px;width:100%;display:grid}.stats-cards-flow--dense[data-v-c21a264b]{grid-template-columns:repeat(auto-fit,minmax(104px,1fr));gap:8px;margin-bottom:14px}.stats-cards-flow--tg[data-v-c21a264b]{grid-template-columns:repeat(auto-fit,minmax(124px,1fr));gap:8px;margin-bottom:14px}.stat-card-mini[data-v-c21a264b]{background:linear-gradient(#fff 0%,#f7faff 100%);border:1px solid #e8edf5;border-radius:10px;min-height:62px;padding:8px 10px;box-shadow:0 2px 8px #0f172a0f}.stat-card-mini--accent[data-v-c21a264b]{background:linear-gradient(#fff 0%,#f0f6ff 100%);border-color:#c8ddff}.stat-card-mini--tg[data-v-c21a264b]{background:#fafbfd;border:1px solid #e8ecf2;border-radius:10px;box-shadow:0 2px 8px #0f172a0d}.stat-card-mini__title[data-v-c21a264b]{color:#4e5969;word-break:break-all;font-size:11px;line-height:1.25}.stat-card-mini__value[data-v-c21a264b]{color:#165dff;margin-top:6px;font-size:20px;font-weight:700;line-height:1.1}.stat-card-mini__value--sm[data-v-c21a264b]{color:#1d2129;font-size:15px;font-weight:600}.stat-card-mini__value--green[data-v-c21a264b]{color:#00b42a}.stat-card-mini__value--yellow[data-v-c21a264b]{color:#d89614}.stat-card-mini__value--orange[data-v-c21a264b]{color:#f77234}.stat-card-mini__value--rate[data-v-c21a264b]{color:#1d2129;font-size:18px}.stat-card-mini__suffix[data-v-c21a264b]{color:#86909c;margin-left:2px;font-size:12px;font-weight:500}.stat-card-mini__unit[data-v-c21a264b]{color:#86909c;margin-left:2px;font-size:11px;font-weight:500}.stat-card-mini--tg-blue .stat-card-mini__value[data-v-c21a264b]{color:#165dff}.stat-card-mini--tg-green .stat-card-mini__value[data-v-c21a264b]{color:#00b42a}.stat-card-mini--tg-gold .stat-card-mini__value[data-v-c21a264b]{color:#d89614}.stat-card-mini--tg-rose .stat-card-mini__value[data-v-c21a264b]{color:#f53f3f}.activity-venue-table[data-v-c21a264b]{margin-top:4px}.activity-venue-table[data-v-c21a264b] .arco-card-header{padding-top:8px}.panel-card[data-v-c21a264b],.query-card[data-v-c21a264b]{background:linear-gradient(#fff 0%,#f9fbff 100%);border-radius:12px;box-shadow:0 4px 14px #0f172a14}.panel-hint[data-v-c21a264b]{color:#86909c;text-align:right;max-width:320px;font-size:12px}.tg-title-row[data-v-c21a264b]{align-items:center;gap:10px;display:inline-flex}.ticket-grab-panel .tg-subtitle[data-v-c21a264b]{color:#4e5969;margin-bottom:14px;font-size:13px}.tg-three-col[data-v-c21a264b]{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-bottom:16px;display:grid}.tg-box[data-v-c21a264b]{background:#fcfdff;border:1px dashed #c9cdd4;border-radius:10px;min-height:120px;padding:12px 14px}.tg-box-title[data-v-c21a264b]{color:#1d2129;margin-bottom:8px;font-weight:600}.tg-highlight-line[data-v-c21a264b],.tg-list-line[data-v-c21a264b]{color:#1d2129;margin-bottom:4px;font-size:13px;line-height:1.6}.tg-muted[data-v-c21a264b]{color:#86909c;font-size:13px}.inner-panel[data-v-c21a264b]{box-shadow:none;background:0 0;margin-bottom:12px}.inner-panel[data-v-c21a264b] .arco-card-header{padding-left:0}.inner-panel[data-v-c21a264b] .arco-card-body{padding-left:0;padding-right:0}.tg-footer[data-v-c21a264b]{color:#86909c;text-align:right;margin-top:8px;font-size:12px}[data-v-c21a264b] .query-card .arco-card-header-title,[data-v-c21a264b] .panel-card .arco-card-header-title{color:#1d2129}[data-v-c21a264b] .query-card .arco-card-body,[data-v-c21a264b] .panel-card .arco-card-body{background:0 0}[data-v-c21a264b] .panel-card .arco-table-container{border-radius:8px;overflow:hidden}@media (width<=1200px){.tg-three-col[data-v-c21a264b]{grid-template-columns:1fr}}

@ -0,0 +1 @@
.dashboard-wrap[data-v-4833f0bd]{gap:14px;display:grid}.summary-scope-hint[data-v-4833f0bd]{color:#86909c;margin-top:-4px;font-size:12px;line-height:1.5}.stats-cards-flow[data-v-4833f0bd]{grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:10px;width:100%;display:grid}.stats-cards-flow--dense[data-v-4833f0bd]{grid-template-columns:repeat(auto-fit,minmax(104px,1fr));gap:8px;margin-bottom:14px}.stats-cards-flow--tg[data-v-4833f0bd]{grid-template-columns:repeat(auto-fit,minmax(124px,1fr));gap:8px;margin-bottom:14px}.stat-card-mini[data-v-4833f0bd]{background:linear-gradient(#fff 0%,#f7faff 100%);border:1px solid #e8edf5;border-radius:10px;min-height:62px;padding:8px 10px;box-shadow:0 2px 8px #0f172a0f}.stat-card-mini--accent[data-v-4833f0bd]{background:linear-gradient(#fff 0%,#f0f6ff 100%);border-color:#c8ddff}.stat-card-mini--tg[data-v-4833f0bd]{background:#fafbfd;border:1px solid #e8ecf2;border-radius:10px;box-shadow:0 2px 8px #0f172a0d}.stat-card-mini__title[data-v-4833f0bd]{color:#4e5969;word-break:break-all;font-size:11px;line-height:1.25}.stat-card-mini__value[data-v-4833f0bd]{color:#165dff;margin-top:6px;font-size:20px;font-weight:700;line-height:1.1}.stat-card-mini__value--sm[data-v-4833f0bd]{color:#1d2129;font-size:15px;font-weight:600}.stat-card-mini__value--green[data-v-4833f0bd]{color:#00b42a}.stat-card-mini__value--yellow[data-v-4833f0bd]{color:#d89614}.stat-card-mini__value--orange[data-v-4833f0bd]{color:#f77234}.stat-card-mini__value--rate[data-v-4833f0bd]{color:#1d2129;font-size:18px}.stat-card-mini__suffix[data-v-4833f0bd]{color:#86909c;margin-left:2px;font-size:12px;font-weight:500}.stat-card-mini__unit[data-v-4833f0bd]{color:#86909c;margin-left:2px;font-size:11px;font-weight:500}.stat-card-mini--tg-blue .stat-card-mini__value[data-v-4833f0bd]{color:#165dff}.stat-card-mini--tg-green .stat-card-mini__value[data-v-4833f0bd]{color:#00b42a}.stat-card-mini--tg-gold .stat-card-mini__value[data-v-4833f0bd]{color:#d89614}.stat-card-mini--tg-rose .stat-card-mini__value[data-v-4833f0bd]{color:#f53f3f}.activity-venue-table[data-v-4833f0bd]{margin-top:4px}.activity-venue-table[data-v-4833f0bd] .arco-card-header{padding-top:8px}.panel-card[data-v-4833f0bd],.query-card[data-v-4833f0bd]{background:linear-gradient(#fff 0%,#f9fbff 100%);border-radius:12px;box-shadow:0 4px 14px #0f172a14}.panel-hint[data-v-4833f0bd]{color:#86909c;text-align:right;max-width:320px;font-size:12px}.tg-title-row[data-v-4833f0bd]{align-items:center;gap:10px;display:inline-flex}.ticket-grab-panel .tg-subtitle[data-v-4833f0bd]{color:#4e5969;margin-bottom:14px;font-size:13px}.tg-three-col[data-v-4833f0bd]{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-bottom:16px;display:grid}.tg-box[data-v-4833f0bd]{background:#fcfdff;border:1px dashed #c9cdd4;border-radius:10px;min-height:120px;padding:12px 14px}.tg-box-title[data-v-4833f0bd]{color:#1d2129;margin-bottom:8px;font-weight:600}.tg-highlight-line[data-v-4833f0bd],.tg-list-line[data-v-4833f0bd]{color:#1d2129;margin-bottom:4px;font-size:13px;line-height:1.6}.tg-muted[data-v-4833f0bd]{color:#86909c;font-size:13px}.inner-panel[data-v-4833f0bd]{box-shadow:none;background:0 0;margin-bottom:12px}.inner-panel[data-v-4833f0bd] .arco-card-header{padding-left:0}.inner-panel[data-v-4833f0bd] .arco-card-body{padding-left:0;padding-right:0}.tg-footer[data-v-4833f0bd]{color:#86909c;text-align:right;margin-top:8px;font-size:12px}[data-v-4833f0bd] .query-card .arco-card-header-title,[data-v-4833f0bd] .panel-card .arco-card-header-title{color:#1d2129}[data-v-4833f0bd] .query-card .arco-card-body,[data-v-4833f0bd] .panel-card .arco-card-body{background:0 0}[data-v-4833f0bd] .panel-card .arco-table-container{border-radius:8px;overflow:hidden}@media (width<=1200px){.tg-three-col[data-v-4833f0bd]{grid-template-columns:1fr}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`Exports`,setup(n){return(n,i)=>(e(),t(r,{title:`数据统计 / 报表导出`,desc:`后续接入按时间/区域/类别筛选与 Excel 导出下载。`}))}});export{i as default};
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-C01qW8FK.js";var i=n({__name:`Exports`,setup(n){return(n,i)=>(e(),t(r,{title:`数据统计 / 报表导出`,desc:`后续接入按时间/区域/类别筛选与 Excel 导出下载。`}))}});export{i as default};

@ -1 +1 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`Leaderboard`,setup(n){return(n,i)=>(e(),t(r,{title:`客流监控 / 活跃指数排行榜`}))}});export{i as default};
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-C01qW8FK.js";var i=n({__name:`Leaderboard`,setup(n){return(n,i)=>(e(),t(r,{title:`客流监控 / 活跃指数排行榜`}))}});export{i as default};

@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/dynamicAdminRoutes-BLw7NroI.js","assets/dynamicAdminRoutes-D4W5LZHg.js","assets/preload-helper-CII-I4WM.js","assets/http-BWP--XXK.js","assets/axios-DUaTpsmW.js"])))=>i.map(i=>d[i]);
import{I as e,V as t,Y as n,_ as r,it as i,nt as a,p as o,v as s,y as c}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as l}from"./message-Dh9377vh.js";import{n as u,t as d}from"./http-BWP--XXK.js";import{t as f}from"./preload-helper-CII-I4WM.js";import{n as p}from"./index-CLnpIFlv.js";var m={style:{height:`100vh`,display:`grid`,"place-items":`center`,background:`var(--color-fill-2)`}},h=c({__name:`Login`,setup(c){let h=p(),g=i(!1),_=a({username:`admin`,password:`admin123456`});async function v(){g.value=!0;try{let{data:e}=await u.post(`/auth/login`,_);localStorage.setItem(d,e.token),l.success(`登录成功`);let{getFirstMenuPath:t}=await f(async()=>{let{getFirstMenuPath:e}=await import(`./dynamicAdminRoutes-BLw7NroI.js`);return{getFirstMenuPath:e}},__vite__mapDeps([0,1,2,3,4]));h.replace(await t())}catch(e){l.error(e?.response?.data?.message??`登录失败`)}finally{g.value=!1}}return(i,a)=>{let c=t(`a-input`),l=t(`a-form-item`),u=t(`a-input-password`),d=t(`a-button`),f=t(`a-form`),p=t(`a-card`);return e(),o(`div`,m,[s(p,{title:`苏州市科普场馆地图管理后台登录`,style:{width:`380px`}},{default:n(()=>[s(f,{model:_,layout:`vertical`,onSubmitSuccess:v},{default:n(()=>[s(l,{field:`username`,label:`用户名`},{default:n(()=>[s(c,{modelValue:_.username,"onUpdate:modelValue":a[0]||=e=>_.username=e,placeholder:`请输入用户名`},null,8,[`modelValue`])]),_:1}),s(l,{field:`password`,label:`密码`},{default:n(()=>[s(u,{modelValue:_.password,"onUpdate:modelValue":a[1]||=e=>_.password=e,placeholder:`请输入密码`},null,8,[`modelValue`])]),_:1}),s(d,{type:`primary`,long:``,loading:g.value,onClick:v},{default:n(()=>[...a[2]||=[r(`登录`,-1)]]),_:1},8,[`loading`])]),_:1},8,[`model`])]),_:1})])}}});export{h as default};

@ -1 +0,0 @@
import{n as e}from"./axios-CiYFffbI.js";import{I as t,V as n,Y as r,_ as i,it as a,nt as o,p as s,v as c,y as l}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{i as u,n as d,t as f}from"./index-Dyg4zy2p.js";var p={style:{height:`100vh`,display:`grid`,"place-items":`center`,background:`var(--color-fill-2)`}},m=l({__name:`Login`,setup(l){let m=u(),h=a(!1),g=o({username:`admin`,password:`admin123456`});async function _(){h.value=!0;try{let{data:t}=await d.post(`/auth/login`,g);localStorage.setItem(f,t.token),e.success(`登录成功`),m.replace(`/dashboard`)}catch(t){e.error(t?.response?.data?.message??`登录失败`)}finally{h.value=!1}}return(e,a)=>{let o=n(`a-input`),l=n(`a-form-item`),u=n(`a-input-password`),d=n(`a-button`),f=n(`a-form`),m=n(`a-card`);return t(),s(`div`,p,[c(m,{title:`苏州市科普场馆地图管理后台登录`,style:{width:`380px`}},{default:r(()=>[c(f,{model:g,layout:`vertical`,onSubmitSuccess:_},{default:r(()=>[c(l,{field:`username`,label:`用户名`},{default:r(()=>[c(o,{modelValue:g.username,"onUpdate:modelValue":a[0]||=e=>g.username=e,placeholder:`请输入用户名`},null,8,[`modelValue`])]),_:1}),c(l,{field:`password`,label:`密码`},{default:r(()=>[c(u,{modelValue:g.password,"onUpdate:modelValue":a[1]||=e=>g.password=e,placeholder:`请输入密码`},null,8,[`modelValue`])]),_:1}),c(d,{type:`primary`,long:``,loading:h.value,onClick:_},{default:r(()=>[...a[2]||=[i(`登录`,-1)]]),_:1},8,[`loading`])]),_:1},8,[`model`])]),_:1})])}}});export{m as default};

@ -1 +0,0 @@
import{I as e,V as t,Y as n,_ as r,d as i,v as a}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{o}from"./index-Dyg4zy2p.js";var s={};function c(o,s){let c=t(`a-alert`),l=t(`a-descriptions-item`),u=t(`a-descriptions`),d=t(`a-card`);return e(),i(d,{title:`系统设置 / 地图与第三方配置`},{default:n(()=>[a(c,{type:`info`,style:{"margin-bottom":`12px`}},{default:n(()=>[...s[0]||=[r(` 当前后台场馆地图选点已使用腾讯地图,坐标统一为 GCJ-02火星坐标系`,-1)]]),_:1}),a(u,{column:1,bordered:``},{default:n(()=>[a(l,{label:`前端地图Key`},{default:n(()=>[...s[1]||=[r(" 在 `code/szkp-map-web/.env` 配置 `VITE_TENCENT_MAP_KEY=你的腾讯地图JS_KEY` ",-1)]]),_:1}),a(l,{label:`地图外链 referer`},{default:n(()=>[...s[2]||=[r(" 在 `code/szkp-map-web/.env` 配置 `VITE_TENCENT_MAP_REFERER=你的应用标识` ",-1)]]),_:1}),a(l,{label:`后端服务Key`},{default:n(()=>[...s[3]||=[r(" 在 `code/szkp-map-service/.env` 配置 `TENCENT_MAP_SERVER_KEY=你的腾讯地图WebService_KEY` ",-1)]]),_:1})]),_:1})]),_:1})}var l=o(s,[[`render`,c]]);export{l as default};

@ -0,0 +1 @@
import{I as e,V as t,Y as n,_ as r,d as i,v as a}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{i as o}from"./index-CLnpIFlv.js";var s={};function c(o,s){let c=t(`a-alert`),l=t(`a-descriptions-item`),u=t(`a-descriptions`),d=t(`a-card`);return e(),i(d,{title:`系统设置 / 地图与第三方配置`},{default:n(()=>[a(c,{type:`info`,style:{"margin-bottom":`12px`}},{default:n(()=>[...s[0]||=[r(` 当前后台场馆地图选点已使用腾讯地图,坐标统一为 GCJ-02火星坐标系`,-1)]]),_:1}),a(u,{column:1,bordered:``},{default:n(()=>[a(l,{label:`前端地图Key`},{default:n(()=>[...s[1]||=[r(" 在 `code/szkp-map-web/.env` 配置 `VITE_TENCENT_MAP_KEY=你的腾讯地图JS_KEY` ",-1)]]),_:1}),a(l,{label:`地图外链 referer`},{default:n(()=>[...s[2]||=[r(" 在 `code/szkp-map-web/.env` 配置 `VITE_TENCENT_MAP_REFERER=你的应用标识` ",-1)]]),_:1}),a(l,{label:`后端服务Key`},{default:n(()=>[...s[3]||=[r(" 在 `code/szkp-map-service/.env` 配置 `TENCENT_MAP_SERVER_KEY=你的腾讯地图WebService_KEY` ",-1)]]),_:1})]),_:1})]),_:1})}var l=o(s,[[`render`,c]]);export{l as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`Monitor`,setup(n){return(n,i)=>(e(),t(r,{title:`客流监控 / 实时客流监控`,desc:`后续接入 50 个重点场馆实时客流数据源。`}))}});export{i as default};
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-C01qW8FK.js";var i=n({__name:`Monitor`,setup(n){return(n,i)=>(e(),t(r,{title:`客流监控 / 实时客流监控`,desc:`后续接入 50 个重点场馆实时客流数据源。`}))}});export{i as default};

@ -1 +1 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`Notifications`,setup(n){return(n,i)=>(e(),t(r,{title:`系统设置 / 消息通知`}))}});export{i as default};
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-C01qW8FK.js";var i=n({__name:`Notifications`,setup(n){return(n,i)=>(e(),t(r,{title:`系统设置 / 消息通知`}))}});export{i as default};

@ -1 +1 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`Overview`,setup(n){return(n,i)=>(e(),t(r,{title:`数据统计 / 综合统计`}))}});export{i as default};
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-C01qW8FK.js";var i=n({__name:`Overview`,setup(n){return(n,i)=>(e(),t(r,{title:`数据统计 / 综合统计`}))}});export{i as default};

@ -1 +1 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`Regions`,setup(n){return(n,i)=>(e(),t(r,{title:`数据统计 / 区域分析`}))}});export{i as default};
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-C01qW8FK.js";var i=n({__name:`Regions`,setup(n){return(n,i)=>(e(),t(r,{title:`数据统计 / 区域分析`}))}});export{i as default};

@ -1 +0,0 @@
import{n as e}from"./axios-CiYFffbI.js";import{I as t,N as n,V as r,Y as i,_ as a,d as o,f as s,i as c,it as l,kt as u,l as d,p as f,v as p,y as m,z as h}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{n as g}from"./index-Dyg4zy2p.js";var _=m({__name:`Roles`,setup(m){let _=l(!1),v=l(!1),y=l(!1),b=l([]),x=l([]),S=l(`super_admin`),C=l([]),w=d(()=>{let e=new Map;b.value.forEach(t=>e.set(t.id,{key:t.id,title:t.name,children:[]}));let t=[];return b.value.forEach(n=>{let r=e.get(n.id);n.parent_id>0&&e.has(n.parent_id)?e.get(n.parent_id).children.push(r):t.push(r)}),t}),T=d(()=>x.value.find(e=>e.role===S.value));async function E(){let{data:e}=await g.get(`/me`);y.value=e?.role===`super_admin`}async function D(){_.value=!0;try{let{data:e}=await g.get(`/role-menu-permissions`);b.value=e.menus||[],x.value=e.roles||[];let t=x.value[0];t&&(S.value=t.role,C.value=[...t.menu_ids||[]])}catch(t){e.error(t?.response?.data?.message??`加载角色菜单权限失败`)}finally{_.value=!1}}function O(e){S.value=e,C.value=[...x.value.find(t=>t.role===e)?.menu_ids||[]]}function k(e){C.value=e.map(e=>Number(e))}async function A(){if(y.value){v.value=!0;try{await g.put(`/role-menu-permissions/${S.value}`,{menu_ids:C.value});let t=x.value.find(e=>e.role===S.value);t&&(t.menu_ids=[...C.value]),e.success(`角色菜单权限保存成功`)}catch(t){e.error(t?.response?.data?.message??`保存失败`)}finally{v.value=!1}}}return n(async()=>{await E(),await D()}),(e,n)=>{let l=r(`a-alert`),d=r(`a-button`),m=r(`a-space`),g=r(`a-card`),b=r(`a-tree`),E=r(`a-spin`);return t(),o(g,{title:`用户与权限 / 角色管理(菜单权限)`},{default:i(()=>[p(l,{style:{"margin-bottom":`12px`}},{default:i(()=>[...n[0]||=[a(` 当前仅控制“每个角色可查看哪些菜单”;接口级细粒度权限后续可继续扩展。 `,-1)]]),_:1}),y.value?s(``,!0):(t(),o(l,{key:0,type:`info`,style:{"margin-bottom":`12px`}},{default:i(()=>[...n[1]||=[a(` 当前为只读模式,仅超级管理员可以修改角色菜单权限。 `,-1)]]),_:1})),p(E,{loading:_.value},{default:i(()=>[p(m,{align:`start`,fill:``},{default:i(()=>[p(g,{title:`角色列表`,size:`small`,style:{width:`220px`}},{default:i(()=>[p(m,{direction:`vertical`,fill:``},{default:i(()=>[(t(!0),f(c,null,h(x.value,e=>(t(),o(d,{key:e.role,type:S.value===e.role?`primary`:`secondary`,long:``,onClick:t=>O(e.role)},{default:i(()=>[a(u(e.label),1)]),_:2},1032,[`type`,`onClick`]))),128))]),_:1})]),_:1}),p(g,{title:`${T.value?.label||``} - 菜单权限`,size:`small`,style:{flex:`1`}},{extra:i(()=>[p(d,{type:`primary`,disabled:!y.value,loading:v.value,onClick:A},{default:i(()=>[...n[2]||=[a(`保存当前角色`,-1)]]),_:1},8,[`disabled`,`loading`])]),default:i(()=>[p(b,{checkable:``,"block-node":``,data:w.value,"checked-keys":C.value,"default-expand-all":!0,onCheck:k},null,8,[`data`,`checked-keys`])]),_:1},8,[`title`])]),_:1})]),_:1},8,[`loading`])]),_:1})}}});export{_ as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
.system-logs-tool-label[data-v-00758b3f]{color:var(--color-text-2);font-size:13px}.system-logs-panel[data-v-00758b3f]{border:1px solid var(--color-border-2);border-radius:var(--border-radius-medium);background:var(--color-fill-1);max-height:min(70vh,720px);padding:12px;overflow:auto}.system-logs-pre[data-v-00758b3f]{white-space:pre-wrap;word-break:break-word;color:var(--color-text-1);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px;line-height:1.45}.system-logs-empty[data-v-00758b3f]{text-align:center;color:var(--color-text-3);padding:24px}

@ -0,0 +1,2 @@
import{I as e,N as t,O as n,V as r,Y as i,_ as a,d as o,f as s,i as c,it as l,kt as u,l as d,p as f,q as p,u as m,ut as h,v as g,y as _,z as v}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as y}from"./message-Dh9377vh.js";import{n as b}from"./http-BWP--XXK.js";import{i as x}from"./index-CLnpIFlv.js";import{t as S}from"./datetime-DLy52ZIc.js";var C={key:0,class:`system-logs-pre`},w={key:1,class:`system-logs-empty`},T=x(_({__name:`SystemLogs`,setup(_){let x=l(!1),T=l(``),E=l(400),D=l(``),O=l([]),k=l([]),A=l(null),j=l(null),M=l(!1),N=d(()=>{let e=T.value.trim().toLowerCase();return e?k.value.filter(t=>t.toLowerCase().includes(e)):k.value});function P(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(2)} MB`}async function F(){await n();let e=j.value;e&&(e.scrollTop=e.scrollHeight)}async function I(){x.value=!0,A.value=null;try{let{data:e}=await b.get(`/system-logs`,{params:{file:D.value||void 0,lines:E.value}});O.value=e.files??[],M.value=!0,D.value=e.file??``,await n(),M.value=!1,k.value=e.lines??[],A.value=e.error??null,await F()}catch(e){y.error(e?.response?.data?.message??`加载系统日志失败`),k.value=[],M.value=!1}finally{x.value=!1}}return p(D,()=>{M.value||I()}),t(I),(t,n)=>{let l=r(`a-alert`),d=r(`a-option`),p=r(`a-select`),_=r(`a-input-number`),y=r(`a-input`),b=r(`a-button`),k=r(`a-space`),M=r(`a-spin`),F=r(`a-card`);return e(),o(F,{title:`系统设置 / 系统日志`},{default:i(()=>[g(l,{type:`warning`,style:{"margin-bottom":`12px`}},{default:i(()=>[...n[3]||=[a(` 展示服务器 `,-1),m(`code`,null,`storage/logs`,-1),a(` 下 Laravel 应用日志文件尾部(按行)。仅超级管理员可访问;请勿将日志内容外传。 `,-1)]]),_:1}),g(k,{wrap:``,size:12,style:{"margin-bottom":`12px`}},{default:i(()=>[n[5]||=m(`span`,{class:`system-logs-tool-label`},`日志文件`,-1),g(p,{modelValue:D.value,"onUpdate:modelValue":n[0]||=e=>D.value=e,"allow-clear":``,placeholder:`选择文件`,style:{width:`260px`},loading:x.value},{default:i(()=>[(e(!0),f(c,null,v(O.value,t=>(e(),o(d,{key:t.name,value:t.name},{default:i(()=>[a(u(t.name)+``+u(P(t.size_bytes))+``+u(h(S)(t.modified_at))+` `,1)]),_:2},1032,[`value`]))),128))]),_:1},8,[`modelValue`,`loading`]),n[6]||=m(`span`,{class:`system-logs-tool-label`},`尾部行数`,-1),g(_,{modelValue:E.value,"onUpdate:modelValue":n[1]||=e=>E.value=e,min:50,max:5e3,step:50,style:{width:`120px`}},null,8,[`modelValue`]),g(y,{modelValue:T.value,"onUpdate:modelValue":n[2]||=e=>T.value=e,placeholder:`在行内筛选关键字`,"allow-clear":``,style:{width:`220px`}},null,8,[`modelValue`]),g(b,{type:`primary`,loading:x.value,onClick:I},{default:i(()=>[...n[4]||=[a(`刷新`,-1)]]),_:1},8,[`loading`])]),_:1}),A.value?(e(),o(l,{key:0,type:`error`,style:{"margin-bottom":`8px`}},{default:i(()=>[a(u(A.value),1)]),_:1})):s(``,!0),m(`div`,{ref_key:`scrollEl`,ref:j,class:`system-logs-panel`,tabindex:`0`,role:`region`,"aria-label":`日志内容`},[g(M,{loading:x.value,style:{display:`block`,"min-height":`120px`}},{default:i(()=>[N.value.length?(e(),f(`pre`,C,u(N.value.join(`
`)),1)):x.value?s(``,!0):(e(),f(`div`,w,`暂无日志内容或无权限读取文件`))]),_:1},8,[`loading`])],512)]),_:1})}}}),[[`__scopeId`,`data-v-00758b3f`]]);export{T as default};

@ -1 +0,0 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`SystemLogs`,setup(n){return(n,i)=>(e(),t(r,{title:`系统设置 / 系统日志`}))}});export{i as default};

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
.tg-venue-block[data-v-4ab0fb79]{width:100%}.tg-venue-block__add[data-v-4ab0fb79]{margin-bottom:10px}.tg-venue-table-scroll[data-v-4ab0fb79]{box-sizing:border-box;width:100%;max-width:100%;overflow-x:auto}.tg-venue-table[data-v-4ab0fb79]{width:100%;min-width:0}.tg-venue-quota-input[data-v-4ab0fb79] .arco-input-wrapper{min-width:120px}.tg-venue-actions[data-v-4ab0fb79]{box-sizing:border-box;flex-wrap:nowrap;justify-content:center;align-items:center;gap:0;width:100%;display:inline-flex}.tg-venue-actions[data-v-4ab0fb79] .arco-btn-size-small{padding-left:4px;padding-right:4px}.tg-list-actions[data-v-4ab0fb79]{flex-wrap:wrap;justify-content:flex-start;row-gap:2px;max-width:100%}.activity-cover-carousel-wrap[data-v-4ab0fb79]{flex-wrap:wrap;align-items:flex-start;gap:20px;width:100%;display:flex}.activity-cover-carousel-row__col[data-v-4ab0fb79]{flex:320px;min-width:min(100%,320px)}.activity-cover-carousel-row__sub[data-v-4ab0fb79]{color:var(--color-text-1);margin-bottom:8px;font-weight:500}.activity-cover-thumb[data-v-4ab0fb79]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:120px;height:70px}.activity-gallery-grid[data-v-4ab0fb79]{flex-wrap:wrap;align-items:flex-start;gap:12px;width:100%;display:flex}.activity-gallery-item[data-v-4ab0fb79]{flex-direction:column;align-items:flex-start;gap:8px;display:flex}.activity-gallery-thumb[data-v-4ab0fb79]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:120px;height:80px}.activity-gallery-thumb--video[data-v-4ab0fb79]{cursor:default}.activity-address-coord-row[data-v-4ab0fb79]{flex-wrap:wrap;align-items:center;gap:12px;width:100%;display:flex}.activity-address-coord-row__address[data-v-4ab0fb79]{flex:45%;min-width:220px;max-width:100%}.activity-address-coord-row__lng[data-v-4ab0fb79],.activity-address-coord-row__lat[data-v-4ab0fb79]{flex:180px;width:200px;min-width:160px}.activity-address-coord-row__map[data-v-4ab0fb79]{flex-shrink:0}.tg-venue-contact-row[data-v-4ab0fb79]{flex-wrap:wrap;align-items:flex-start;gap:16px;width:100%;display:flex}.tg-venue-contact-row__col[data-v-4ab0fb79]{flex:200px;min-width:180px;max-width:100%}.tg-venue-contact-row__sub[data-v-4ab0fb79]{color:var(--color-text-1);margin-bottom:8px;font-size:13px;font-weight:500}

@ -1 +0,0 @@
.tg-venue-block[data-v-1889475c]{width:100%}.tg-venue-block__add[data-v-1889475c]{margin-bottom:10px}.tg-venue-table-scroll[data-v-1889475c]{box-sizing:border-box;width:100%;max-width:100%;overflow-x:auto}.tg-venue-table[data-v-1889475c]{width:100%;min-width:0}.tg-venue-quota-input[data-v-1889475c] .arco-input-wrapper{min-width:120px}.tg-venue-actions[data-v-1889475c]{box-sizing:border-box;flex-wrap:nowrap;justify-content:center;align-items:center;gap:0;width:100%;display:inline-flex}.tg-venue-actions[data-v-1889475c] .arco-btn-size-small{padding-left:4px;padding-right:4px}.tg-list-actions[data-v-1889475c]{flex-wrap:wrap;justify-content:flex-start;row-gap:2px;max-width:100%}.activity-cover-carousel-wrap[data-v-1889475c]{flex-wrap:wrap;align-items:flex-start;gap:20px;width:100%;display:flex}.activity-cover-carousel-row__col[data-v-1889475c]{flex:320px;min-width:min(100%,320px)}.activity-cover-carousel-row__sub[data-v-1889475c]{color:var(--color-text-1);margin-bottom:8px;font-weight:500}.activity-cover-thumb[data-v-1889475c]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:120px;height:70px}.activity-gallery-grid[data-v-1889475c]{flex-wrap:wrap;align-items:flex-start;gap:12px;width:100%;display:flex}.activity-gallery-item[data-v-1889475c]{flex-direction:column;align-items:flex-start;gap:8px;display:flex}.activity-gallery-thumb[data-v-1889475c]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:120px;height:80px}.activity-gallery-thumb--video[data-v-1889475c]{cursor:default}.activity-address-coord-row[data-v-1889475c]{flex-wrap:wrap;align-items:center;gap:12px;width:100%;display:flex}.activity-address-coord-row__address[data-v-1889475c]{flex:45%;min-width:220px;max-width:100%}.activity-address-coord-row__lng[data-v-1889475c],.activity-address-coord-row__lat[data-v-1889475c]{flex:180px;width:200px;min-width:160px}.activity-address-coord-row__map[data-v-1889475c]{flex-shrink:0}.tg-venue-contact-row[data-v-1889475c]{flex-wrap:wrap;align-items:flex-start;gap:16px;width:100%;display:flex}.tg-venue-contact-row__col[data-v-1889475c]{flex:200px;min-width:180px;max-width:100%}.tg-venue-contact-row__sub[data-v-1889475c]{color:var(--color-text-1);margin-bottom:8px;font-size:13px;font-weight:500}

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
import{I as e,N as t,V as n,Y as r,_ as i,d as a,i as o,it as s,kt as c,nt as l,p as u,u as d,ut as f,v as p,y as m,z as h}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as g}from"./message-Dh9377vh.js";import{n as _}from"./http-BWP--XXK.js";import{t as v}from"./listTableRowIndex-Bl-nc9Qt.js";import{n as y,t as b}from"./datetime-DLy52ZIc.js";import{t as x}from"./bookingType-sDQIPutU.js";import{t as S}from"./reservationStatus-DPnogIlu.js";var C={style:{"font-family":`monospace`,"font-size":`12px`}},w=1680,T=m({__name:`TicketGrabRegistrations`,setup(m){let T=s(!1),E=s(`all`),D=s(``),O=s(void 0),k=s([]),A=l({current:1,pageSize:10,total:0}),j=s([]),M=s([]);async function N(){try{let{data:e}=await _.get(`/ticket-grab-events/options`,{params:{limit:2e3}});M.value=e.data??[]}catch{M.value=[]}}async function P(){T.value=!0;try{let{data:e}=await _.get(`/activity-registrations`,{params:{reservation_kind:`ticket_grab`,ticket_grab_event_id:O.value||void 0,status:E.value,keyword:D.value||void 0,start_date:k.value?.[0]||void 0,end_date:k.value?.[1]||void 0,page:A.current,page_size:A.pageSize}});j.value=e.data,A.total=e.total}catch(e){g.error(e?.response?.data?.message??`加载失败`)}finally{T.value=!1}}function F(e){A.current=e,P()}function I(e){A.pageSize=e,A.current=1,P()}return t(async()=>{await N(),await P()}),(t,s)=>{let l=n(`a-option`),m=n(`a-select`),g=n(`a-radio`),_=n(`a-radio-group`),N=n(`a-input`),L=n(`a-range-picker`),R=n(`a-button`),z=n(`a-space`),B=n(`a-table-column`),V=n(`a-tag`),H=n(`a-table`),U=n(`a-card`);return e(),a(U,{title:`抢票管理 / 抢票报名`,bordered:!1},{default:r(()=>[p(z,{direction:`vertical`,fill:``},{default:r(()=>[p(z,{wrap:``,size:12},{default:r(()=>[p(m,{modelValue:O.value,"onUpdate:modelValue":s[0]||=e=>O.value=e,"allow-clear":``,placeholder:`抢票活动(全部)`,style:{width:`260px`},"allow-search":``,onChange:s[1]||=()=>{A.current=1,P()}},{default:r(()=>[(e(!0),u(o,null,h(M.value,t=>(e(),a(l,{key:t.id,value:t.id},{default:r(()=>[i(c(t.title),1)]),_:2},1032,[`value`]))),128))]),_:1},8,[`modelValue`]),p(_,{modelValue:E.value,"onUpdate:modelValue":s[2]||=e=>E.value=e,type:`button`,size:`small`,onChange:P},{default:r(()=>[p(g,{value:`all`},{default:r(()=>[...s[7]||=[i(`全部`,-1)]]),_:1}),p(g,{value:`pending`},{default:r(()=>[...s[8]||=[i(`待核销`,-1)]]),_:1}),p(g,{value:`verified`},{default:r(()=>[...s[9]||=[i(`已核销`,-1)]]),_:1}),p(g,{value:`cancelled`},{default:r(()=>[...s[10]||=[i(`已取消`,-1)]]),_:1}),p(g,{value:`expired`},{default:r(()=>[...s[11]||=[i(`已过期`,-1)]]),_:1})]),_:1},8,[`modelValue`]),p(N,{modelValue:D.value,"onUpdate:modelValue":s[3]||=e=>D.value=e,placeholder:`姓名 / 身份证 / token`,"allow-clear":``,style:{width:`220px`}},null,8,[`modelValue`]),p(L,{modelValue:k.value,"onUpdate:modelValue":s[4]||=e=>k.value=e,style:{width:`260px`}},null,8,[`modelValue`]),p(R,{type:`primary`,onClick:s[5]||=()=>{A.current=1,P()}},{default:r(()=>[...s[12]||=[i(` 查询 `,-1)]]),_:1}),p(R,{onClick:s[6]||=()=>{E.value=`all`,D.value=``,O.value=void 0,k.value=[],A.current=1,P()}},{default:r(()=>[...s[13]||=[i(` 重置 `,-1)]]),_:1})]),_:1}),p(H,{scroll:{x:w},data:j.value,loading:T.value,"row-key":`id`,pagination:{current:A.current,pageSize:A.pageSize,total:A.total,showTotal:!0,onChange:F,onPageSizeChange:I}},{columns:r(()=>[p(B,{title:``,width:50,ellipsis:!0,tooltip:!0},{cell:r(({rowIndex:e})=>[i(c(f(v)(e,A.current,A.pageSize)),1)]),_:1}),p(B,{title:`抢票活动`,width:200,ellipsis:!0,tooltip:!0},{cell:r(({record:e})=>[i(c(e.ticket_grab_event?.title??`-`),1)]),_:1}),p(B,{title:`场馆`,width:160,ellipsis:!0,tooltip:!0},{cell:r(({record:e})=>[i(c(e.venue?.name??`-`),1)]),_:1}),p(B,{title:`姓名`,"data-index":`visitor_name`,width:100}),p(B,{title:`身份证`,"data-index":`id_card`,width:180,ellipsis:!0,tooltip:!0}),p(B,{title:`入馆日`,width:120},{cell:r(({record:e})=>[i(c(e.entry_date?f(y)(String(e.entry_date)):`-`),1)]),_:1}),p(B,{title:`预约类型`,width:100},{cell:r(({record:e})=>[i(c(f(x)(e.booking_type,e.ticket_count)),1)]),_:1}),p(B,{title:`票数`,width:80},{cell:r(({record:e})=>[i(c(e.ticket_count??1),1)]),_:1}),p(B,{title:`状态`,width:100},{cell:r(({record:e})=>[p(V,{color:e.status===`verified`?`green`:e.status===`pending`?`arcoblue`:`gray`},{default:r(()=>[i(c(f(S)(e.status)),1)]),_:2},1032,[`color`])]),_:1}),p(B,{title:`下单时间`,width:170},{cell:r(({record:e})=>[i(c(f(b)(e.created_at)),1)]),_:1}),p(B,{title:`核销时间`,width:170},{cell:r(({record:e})=>[i(c(e.verified_at?f(b)(String(e.verified_at)):`-`),1)]),_:1}),p(B,{title:`核销 Token`,width:220,ellipsis:!0,tooltip:!0},{cell:r(({record:e})=>[d(`span`,C,c(e.qr_token),1)]),_:1})]),_:1},8,[`scroll`,`data`,`loading`,`pagination`])]),_:1})]),_:1})}}});export{T as default};

@ -1 +0,0 @@
import{n as e}from"./axios-CiYFffbI.js";import{I as t,N as n,V as r,Y as i,_ as a,d as o,it as s,kt as c,nt as l,ut as u,v as d,y as f}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{n as p}from"./index-Dyg4zy2p.js";import{t as m}from"./listTableRowIndex-CwNZabrG.js";import{n as h,t as g}from"./datetime-CjmbUMhc.js";import{t as _}from"./bookingType-CGtYk0HZ.js";import{t as v}from"./reservationStatus-DfEgE0qr.js";var y=1280,b=f({__name:`TicketGrabRegistrations`,setup(f){let b=s(!1),x=s(`all`),S=s(``),C=s([]),w=l({current:1,pageSize:10,total:0}),T=s([]);async function E(){b.value=!0;try{let{data:e}=await p.get(`/activity-registrations`,{params:{reservation_kind:`ticket_grab`,status:x.value,keyword:S.value||void 0,start_date:C.value?.[0]||void 0,end_date:C.value?.[1]||void 0,page:w.current,page_size:w.pageSize}});T.value=e.data,w.total=e.total}catch(t){e.error(t?.response?.data?.message??`加载失败`)}finally{b.value=!1}}function D(e){w.current=e,E()}function O(e){w.pageSize=e,w.current=1,E()}return n(E),(e,n)=>{let s=r(`a-radio`),l=r(`a-radio-group`),f=r(`a-input`),p=r(`a-range-picker`),k=r(`a-button`),A=r(`a-space`),j=r(`a-table-column`),M=r(`a-tag`),N=r(`a-table`),P=r(`a-card`);return t(),o(P,{title:`抢票管理 / 抢票报名`,bordered:!1},{default:i(()=>[d(A,{direction:`vertical`,fill:``},{default:i(()=>[d(A,{wrap:``,size:12},{default:i(()=>[d(l,{modelValue:x.value,"onUpdate:modelValue":n[0]||=e=>x.value=e,type:`button`,size:`small`,onChange:E},{default:i(()=>[d(s,{value:`all`},{default:i(()=>[...n[5]||=[a(`全部`,-1)]]),_:1}),d(s,{value:`pending`},{default:i(()=>[...n[6]||=[a(`待核销`,-1)]]),_:1}),d(s,{value:`verified`},{default:i(()=>[...n[7]||=[a(`已核销`,-1)]]),_:1}),d(s,{value:`cancelled`},{default:i(()=>[...n[8]||=[a(`已取消`,-1)]]),_:1}),d(s,{value:`expired`},{default:i(()=>[...n[9]||=[a(`已过期`,-1)]]),_:1})]),_:1},8,[`modelValue`]),d(f,{modelValue:S.value,"onUpdate:modelValue":n[1]||=e=>S.value=e,placeholder:`姓名 / 身份证 / token`,"allow-clear":``,style:{width:`220px`}},null,8,[`modelValue`]),d(p,{modelValue:C.value,"onUpdate:modelValue":n[2]||=e=>C.value=e,style:{width:`260px`}},null,8,[`modelValue`]),d(k,{type:`primary`,onClick:n[3]||=()=>{w.current=1,E()}},{default:i(()=>[...n[10]||=[a(` 查询 `,-1)]]),_:1}),d(k,{onClick:n[4]||=()=>{x.value=`all`,S.value=``,C.value=[],w.current=1,E()}},{default:i(()=>[...n[11]||=[a(` 重置 `,-1)]]),_:1}),d(k,{onClick:E},{default:i(()=>[...n[12]||=[a(`刷新`,-1)]]),_:1})]),_:1}),d(N,{scroll:{x:y},data:T.value,loading:b.value,"row-key":`id`,pagination:{current:w.current,pageSize:w.pageSize,total:w.total,showTotal:!0,onChange:D,onPageSizeChange:O}},{columns:i(()=>[d(j,{title:``,width:50,ellipsis:!0,tooltip:!0},{cell:i(({rowIndex:e})=>[a(c(u(m)(e,w.current,w.pageSize)),1)]),_:1}),d(j,{title:`抢票活动`,width:200,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.ticket_grab_event?.title??`-`),1)]),_:1}),d(j,{title:`场馆`,width:160,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.venue?.name??`-`),1)]),_:1}),d(j,{title:`姓名`,"data-index":`visitor_name`,width:100}),d(j,{title:`身份证`,"data-index":`id_card`,width:180,ellipsis:!0,tooltip:!0}),d(j,{title:`入馆日`,width:120},{cell:i(({record:e})=>[a(c(e.entry_date?u(h)(String(e.entry_date)):`-`),1)]),_:1}),d(j,{title:`预约类型`,width:100},{cell:i(({record:e})=>[a(c(u(_)(e.booking_type,e.ticket_count)),1)]),_:1}),d(j,{title:`票数`,width:80},{cell:i(({record:e})=>[a(c(e.ticket_count??1),1)]),_:1}),d(j,{title:`状态`,width:100},{cell:i(({record:e})=>[d(M,{color:e.status===`verified`?`green`:e.status===`pending`?`arcoblue`:`gray`},{default:i(()=>[a(c(u(v)(e.status)),1)]),_:2},1032,[`color`])]),_:1}),d(j,{title:`下单时间`,width:170},{cell:i(({record:e})=>[a(c(u(g)(e.created_at)),1)]),_:1})]),_:1},8,[`scroll`,`data`,`loading`,`pagination`])]),_:1})]),_:1})}}});export{b as default};

@ -1 +0,0 @@
import{n as e}from"./axios-CiYFffbI.js";import{I as t,N as n,V as r,Y as i,_ as a,d as o,it as s,kt as c,nt as l,u,ut as d,v as f,y as p}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{n as m,o as h}from"./index-Dyg4zy2p.js";import{t as g}from"./listTableRowIndex-CwNZabrG.js";import{n as _,t as v}from"./datetime-CjmbUMhc.js";import{t as y}from"./bookingType-CGtYk0HZ.js";import{t as b}from"./reservationStatus-DfEgE0qr.js";var x={class:`verify-list-toolbar`},S=1780,C=h(p({__name:`TicketGrabVerify`,setup(p){let h=s(!1),C=s([]),w=s(``),T=s(!1),E=s(`all`),D=s(``),O=s([]),k=l({current:1,pageSize:10});async function A(){h.value=!0;try{let e={status:E.value,keyword:D.value||void 0,reservation_kind:`ticket_grab`};O.value?.length===2&&(e.start_date=O.value[0],e.end_date=O.value[1],e.date_field=`entry_date`);let{data:t}=await m.get(`/reservations`,{params:e});C.value=t,k.current=1}catch(t){e.error(t?.response?.data?.message??`加载预约列表失败`)}finally{h.value=!1}}function j(){A()}function M(){E.value=`all`,D.value=``,O.value=[],A()}async function N(){if(!w.value){e.warning(`请输入二维码 token`);return}T.value=!0;try{await m.post(`/reservations/verify`,{qr_token:w.value}),e.success(`核销成功`),w.value=``,await A()}catch(t){e.error(t?.response?.data?.message??`核销失败`)}finally{T.value=!1}}return n(A),(e,n)=>{let s=r(`a-alert`),l=r(`a-input`),p=r(`a-button`),m=r(`a-space`),P=r(`a-radio`),F=r(`a-radio-group`),I=r(`a-range-picker`),L=r(`a-table-column`),R=r(`a-tag`),z=r(`a-table`),B=r(`a-card`);return t(),o(B,{title:`抢票管理 / 抢票核销`,bordered:!1},{default:i(()=>[f(m,{direction:`vertical`,fill:``},{default:i(()=>[f(s,null,{default:i(()=>[...n[5]||=[a(`抢票预约按「入馆日」为当天方可核销。输入二维码 token 核销。`,-1)]]),_:1}),f(m,{wrap:``,size:12},{default:i(()=>[f(l,{modelValue:w.value,"onUpdate:modelValue":n[0]||=e=>w.value=e,style:{width:`min(100%, 420px)`},placeholder:`请输入二维码 token`,"allow-clear":``},null,8,[`modelValue`]),f(p,{type:`primary`,loading:T.value,onClick:N},{default:i(()=>[...n[6]||=[a(`立即核销`,-1)]]),_:1},8,[`loading`])]),_:1}),u(`div`,x,[f(m,{wrap:``,size:12},{default:i(()=>[f(F,{modelValue:E.value,"onUpdate:modelValue":n[1]||=e=>E.value=e,type:`button`,size:`small`,onChange:A},{default:i(()=>[f(P,{value:`all`},{default:i(()=>[...n[7]||=[a(`全部`,-1)]]),_:1}),f(P,{value:`pending`},{default:i(()=>[...n[8]||=[a(`待核销`,-1)]]),_:1}),f(P,{value:`verified`},{default:i(()=>[...n[9]||=[a(`已核销`,-1)]]),_:1}),f(P,{value:`cancelled`},{default:i(()=>[...n[10]||=[a(`已取消`,-1)]]),_:1}),f(P,{value:`expired`},{default:i(()=>[...n[11]||=[a(`已过期`,-1)]]),_:1})]),_:1},8,[`modelValue`]),f(l,{modelValue:D.value,"onUpdate:modelValue":n[2]||=e=>D.value=e,placeholder:`姓名 / 手机 / 身份证 / token`,"allow-clear":``,style:{width:`240px`}},null,8,[`modelValue`]),f(I,{modelValue:O.value,"onUpdate:modelValue":n[3]||=e=>O.value=e,style:{width:`260px`}},null,8,[`modelValue`]),f(p,{type:`primary`,onClick:j},{default:i(()=>[...n[12]||=[a(`查询`,-1)]]),_:1}),f(p,{onClick:M},{default:i(()=>[...n[13]||=[a(`重置`,-1)]]),_:1}),f(p,{onClick:A},{default:i(()=>[...n[14]||=[a(`刷新列表`,-1)]]),_:1})]),_:1})]),f(z,{class:`list-data-table verify-table`,scroll:{x:S},data:C.value,loading:h.value,"row-key":`id`,pagination:{current:k.current,pageSize:k.pageSize,total:C.value.length,showTotal:!0},onPageChange:n[4]||=e=>k.current=e},{columns:i(()=>[f(L,{title:``,width:50,ellipsis:!0,tooltip:!0},{cell:i(({rowIndex:e})=>[a(c(d(g)(e,k.current,k.pageSize)),1)]),_:1}),f(L,{title:`预约场次`,width:220,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.ticket_grab_event?.title??`-`),1)]),_:1}),f(L,{title:`场馆`,width:180,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.venue?.name??`-`),1)]),_:1}),f(L,{title:`姓名`,"data-index":`visitor_name`,width:100}),f(L,{title:`身份证`,"data-index":`id_card`,width:180,ellipsis:!0,tooltip:!0}),f(L,{title:`手机号`,"data-index":`visitor_phone`,width:120}),f(L,{title:`预约类型`,width:100},{cell:i(({record:e})=>[a(c(d(y)(e.booking_type,e.ticket_count)),1)]),_:1}),f(L,{title:`场次时间`,width:140},{cell:i(({record:e})=>[a(c(e.entry_date?d(_)(String(e.entry_date)):`-`),1)]),_:1}),f(L,{title:`状态`,width:100},{cell:i(({record:e})=>[f(R,{color:e.status===`verified`?`green`:e.status===`pending`?`arcoblue`:e.status===`expired`?`orange`:`gray`},{default:i(()=>[a(c(d(b)(e.status)),1)]),_:2},1032,[`color`])]),_:1}),f(L,{title:`预约时间`,width:175},{cell:i(({record:e})=>[a(c(d(v)(e.created_at)),1)]),_:1}),f(L,{title:`核销时间`,width:175},{cell:i(({record:e})=>[a(c(d(v)(e.verified_at)),1)]),_:1}),f(L,{title:`二维码 token`,"data-index":`qr_token`,width:200,ellipsis:!0,tooltip:!0,fixed:`right`})]),_:1},8,[`scroll`,`data`,`loading`,`pagination`])]),_:1})]),_:1})}}}),[[`__scopeId`,`data-v-f1a484a3`]]);export{C as default};

@ -0,0 +1 @@
import{I as e,N as t,V as n,Y as r,_ as i,d as a,it as o,kt as s,nt as c,u as l,ut as u,v as d,y as f}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as p}from"./message-Dh9377vh.js";import{n as m}from"./http-BWP--XXK.js";import{i as h}from"./index-CLnpIFlv.js";import{t as g}from"./listTableRowIndex-Bl-nc9Qt.js";import{n as _,t as v}from"./datetime-DLy52ZIc.js";import{t as y}from"./bookingType-sDQIPutU.js";import{t as b}from"./reservationStatus-DPnogIlu.js";var x={class:`verify-list-toolbar`},S=1780,C=h(f({__name:`TicketGrabVerify`,setup(f){let h=o(!1),C=o([]),w=o(``),T=o(!1),E=o(`all`),D=o(``),O=o([]),k=c({current:1,pageSize:10});async function A(){h.value=!0;try{let e={status:E.value,keyword:D.value||void 0,reservation_kind:`ticket_grab`};O.value?.length===2&&(e.start_date=O.value[0],e.end_date=O.value[1],e.date_field=`entry_date`);let{data:t}=await m.get(`/reservations`,{params:e});C.value=t,k.current=1}catch(e){p.error(e?.response?.data?.message??`加载预约列表失败`)}finally{h.value=!1}}function j(){A()}function M(){E.value=`all`,D.value=``,O.value=[],A()}async function N(){if(!w.value){p.warning(`请输入二维码 token`);return}T.value=!0;try{await m.post(`/reservations/verify`,{qr_token:w.value}),p.success(`核销成功`),w.value=``,await A()}catch(e){p.error(e?.response?.data?.message??`核销失败`)}finally{T.value=!1}}return t(A),(t,o)=>{let c=n(`a-alert`),f=n(`a-input`),p=n(`a-button`),m=n(`a-space`),P=n(`a-radio`),F=n(`a-radio-group`),I=n(`a-range-picker`),L=n(`a-table-column`),R=n(`a-tag`),z=n(`a-table`),B=n(`a-card`);return e(),a(B,{title:`抢票管理 / 抢票核销`,bordered:!1},{default:r(()=>[d(m,{direction:`vertical`,fill:``},{default:r(()=>[d(c,null,{default:r(()=>[...o[5]||=[i(`抢票预约按「入馆日」为当天方可核销。输入二维码 token 核销。`,-1)]]),_:1}),d(m,{wrap:``,size:12},{default:r(()=>[d(f,{modelValue:w.value,"onUpdate:modelValue":o[0]||=e=>w.value=e,style:{width:`min(100%, 420px)`},placeholder:`请输入二维码 token`,"allow-clear":``},null,8,[`modelValue`]),d(p,{type:`primary`,loading:T.value,onClick:N},{default:r(()=>[...o[6]||=[i(`立即核销`,-1)]]),_:1},8,[`loading`])]),_:1}),l(`div`,x,[d(m,{wrap:``,size:12},{default:r(()=>[d(F,{modelValue:E.value,"onUpdate:modelValue":o[1]||=e=>E.value=e,type:`button`,size:`small`,onChange:A},{default:r(()=>[d(P,{value:`all`},{default:r(()=>[...o[7]||=[i(`全部`,-1)]]),_:1}),d(P,{value:`pending`},{default:r(()=>[...o[8]||=[i(`待核销`,-1)]]),_:1}),d(P,{value:`verified`},{default:r(()=>[...o[9]||=[i(`已核销`,-1)]]),_:1}),d(P,{value:`cancelled`},{default:r(()=>[...o[10]||=[i(`已取消`,-1)]]),_:1}),d(P,{value:`expired`},{default:r(()=>[...o[11]||=[i(`已过期`,-1)]]),_:1})]),_:1},8,[`modelValue`]),d(f,{modelValue:D.value,"onUpdate:modelValue":o[2]||=e=>D.value=e,placeholder:`姓名 / 手机 / 身份证 / token`,"allow-clear":``,style:{width:`240px`}},null,8,[`modelValue`]),d(I,{modelValue:O.value,"onUpdate:modelValue":o[3]||=e=>O.value=e,style:{width:`260px`}},null,8,[`modelValue`]),d(p,{type:`primary`,onClick:j},{default:r(()=>[...o[12]||=[i(`查询`,-1)]]),_:1}),d(p,{onClick:M},{default:r(()=>[...o[13]||=[i(`重置`,-1)]]),_:1}),d(p,{onClick:A},{default:r(()=>[...o[14]||=[i(`刷新列表`,-1)]]),_:1})]),_:1})]),d(z,{class:`list-data-table verify-table`,scroll:{x:S},data:C.value,loading:h.value,"row-key":`id`,pagination:{current:k.current,pageSize:k.pageSize,total:C.value.length,showTotal:!0},onPageChange:o[4]||=e=>k.current=e},{columns:r(()=>[d(L,{title:``,width:50,ellipsis:!0,tooltip:!0},{cell:r(({rowIndex:e})=>[i(s(u(g)(e,k.current,k.pageSize)),1)]),_:1}),d(L,{title:`预约场次`,width:220,ellipsis:!0,tooltip:!0},{cell:r(({record:e})=>[i(s(e.ticket_grab_event?.title??`-`),1)]),_:1}),d(L,{title:`场馆`,width:180,ellipsis:!0,tooltip:!0},{cell:r(({record:e})=>[i(s(e.venue?.name??`-`),1)]),_:1}),d(L,{title:`姓名`,"data-index":`visitor_name`,width:100}),d(L,{title:`身份证`,"data-index":`id_card`,width:180,ellipsis:!0,tooltip:!0}),d(L,{title:`手机号`,"data-index":`visitor_phone`,width:120}),d(L,{title:`预约类型`,width:100},{cell:r(({record:e})=>[i(s(u(y)(e.booking_type,e.ticket_count)),1)]),_:1}),d(L,{title:`场次时间`,width:140},{cell:r(({record:e})=>[i(s(e.entry_date?u(_)(String(e.entry_date)):`-`),1)]),_:1}),d(L,{title:`状态`,width:100},{cell:r(({record:e})=>[d(R,{color:e.status===`verified`?`green`:e.status===`pending`?`arcoblue`:e.status===`expired`?`orange`:`gray`},{default:r(()=>[i(s(u(b)(e.status)),1)]),_:2},1032,[`color`])]),_:1}),d(L,{title:`预约时间`,width:175},{cell:r(({record:e})=>[i(s(u(v)(e.created_at)),1)]),_:1}),d(L,{title:`核销时间`,width:175},{cell:r(({record:e})=>[i(s(u(v)(e.verified_at)),1)]),_:1}),d(L,{title:`二维码 token`,"data-index":`qr_token`,width:200,ellipsis:!0,tooltip:!0,fixed:`right`})]),_:1},8,[`scroll`,`data`,`loading`,`pagination`])]),_:1})]),_:1})}}}),[[`__scopeId`,`data-v-f1a484a3`]]);export{C as default};

@ -0,0 +1 @@
.import-file-label[data-v-db5c7e55]{cursor:pointer;display:inline-block}.import-file-input[data-v-db5c7e55]{opacity:0;width:0;height:0;position:absolute;overflow:hidden}.import-file-btn[data-v-db5c7e55]{border:1px solid var(--color-border-2);border-radius:var(--border-radius-small);padding:0 15px;line-height:32px;display:inline-block}.import-file-label:hover .import-file-btn[data-v-db5c7e55]{border-color:rgb(var(--primary-6));color:rgb(var(--primary-6))}.venue-address-coord-row[data-v-db5c7e55]{flex-wrap:wrap;align-items:center;gap:12px;width:100%;display:flex}.venue-address-coord-row__address[data-v-db5c7e55]{flex:45%;min-width:320px;max-width:100%}.venue-address-coord-row__lng[data-v-db5c7e55],.venue-address-coord-row__lat[data-v-db5c7e55]{flex:180px;width:200px;min-width:180px}.venue-address-coord-row__map[data-v-db5c7e55]{flex-shrink:0}.venue-form-split-label[data-v-db5c7e55]{color:var(--color-text-2);margin-bottom:8px;font-size:13px;font-weight:500}.venue-cover-carousel-wrap[data-v-db5c7e55]{flex-wrap:wrap;align-items:flex-start;gap:20px;width:100%;display:flex}.venue-cover-carousel-row__col[data-v-db5c7e55]{flex:320px;min-width:min(100%,320px)}.venue-cover-carousel-row__sub[data-v-db5c7e55]{color:var(--color-text-1);margin-bottom:8px;font-weight:500}.venue-gallery-grid[data-v-db5c7e55]{flex-wrap:wrap;align-items:flex-start;gap:12px;width:100%;display:flex}.venue-gallery-item[data-v-db5c7e55]{flex-direction:column;align-items:flex-start;gap:8px;display:flex}.venue-gallery-thumb[data-v-db5c7e55]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:80px;height:50px}.venue-gallery-thumb--video[data-v-db5c7e55]{display:block}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
.import-file-label[data-v-8d39421c]{cursor:pointer;display:inline-block}.import-file-input[data-v-8d39421c]{opacity:0;width:0;height:0;position:absolute;overflow:hidden}.import-file-btn[data-v-8d39421c]{border:1px solid var(--color-border-2);border-radius:var(--border-radius-small);padding:0 15px;line-height:32px;display:inline-block}.import-file-label:hover .import-file-btn[data-v-8d39421c]{border-color:rgb(var(--primary-6));color:rgb(var(--primary-6))}.venue-address-coord-row[data-v-8d39421c]{flex-wrap:wrap;align-items:center;gap:12px;width:100%;display:flex}.venue-address-coord-row__address[data-v-8d39421c]{flex:45%;min-width:320px;max-width:100%}.venue-address-coord-row__lng[data-v-8d39421c],.venue-address-coord-row__lat[data-v-8d39421c]{flex:180px;width:200px;min-width:180px}.venue-address-coord-row__map[data-v-8d39421c]{flex-shrink:0}.venue-form-split-label[data-v-8d39421c]{color:var(--color-text-2);margin-bottom:8px;font-size:13px;font-weight:500}.venue-cover-carousel-wrap[data-v-8d39421c]{flex-wrap:wrap;align-items:flex-start;gap:20px;width:100%;display:flex}.venue-cover-carousel-row__col[data-v-8d39421c]{flex:320px;min-width:min(100%,320px)}.venue-cover-carousel-row__sub[data-v-8d39421c]{color:var(--color-text-1);margin-bottom:8px;font-weight:500}.venue-gallery-grid[data-v-8d39421c]{flex-wrap:wrap;align-items:flex-start;gap:12px;width:100%;display:flex}.venue-gallery-item[data-v-8d39421c]{flex-direction:column;align-items:flex-start;gap:8px;display:flex}.venue-gallery-thumb[data-v-8d39421c]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:80px;height:50px}.venue-gallery-thumb--video[data-v-8d39421c]{display:block}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
import{I as e,N as t,V as n,Y as r,_ as i,it as a,kt as o,nt as s,p as c,u as l,v as u,y as d}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as f}from"./message-Dh9377vh.js";import{i as p,n as m,t as h}from"./index-CLnpIFlv.js";import{i as g,n as _,o as v,r as y,t as b}from"./h5Http-DeskuLbk.js";var x={class:`m-verify-page`},S={class:`m-verify-hero`},C={class:`m-verify-sub`},w={class:`m-verify-card`},T={key:0,class:`m-verify-tip`},E={key:1,class:`m-verify-tip`},D=p(d({__name:`VerifyLogin`,setup(d){let p=m(),D=h(),O=a(!1),k=a(``),A=a(``),j=s({username:``,password:``});function M(e){let t=D.query[e];return(typeof t==`string`?t:Array.isArray(t)?String(t[0]??``):``).trim()}t(()=>{let e=M(`v`).toLowerCase(),t=M(`portal`);e.length>=6?(k.value=e,A.value=``,localStorage.setItem(y,e),localStorage.removeItem(g)):t.length>=32?(A.value=t,k.value=``,localStorage.setItem(g,t),localStorage.removeItem(y)):(k.value=localStorage.getItem(`szkp_verify_portal_code`)||``,A.value=localStorage.getItem(`szkp_verify_portal_legacy_token`)||``)});function N(){return D.path.startsWith(`/m/`)?`/m/verify`:`/h5/verify/scan`}async function P(){O.value=!0;try{if(k.value.length>=6){let{data:e}=await v.post(`/verify-portal/login`,{portal_code:k.value,username:j.username.trim(),password:j.password});localStorage.setItem(b,e.token),localStorage.setItem(`${b}_saved_at`,String(Date.now())),localStorage.setItem(_,`portal`),f.success(`登录成功`),p.replace(N());return}if(A.value.length>=32){let{data:e}=await v.post(`/verify-portal/login`,{portal_token:A.value,username:j.username.trim(),password:j.password});localStorage.setItem(b,e.token),localStorage.setItem(`${b}_saved_at`,String(Date.now())),localStorage.setItem(_,`portal`),f.success(`登录成功`),p.replace(N());return}let{data:e}=await v.post(`/auth/login`,{...j,client:`h5_verify`});localStorage.setItem(b,e.token),localStorage.setItem(`${b}_saved_at`,String(Date.now())),localStorage.setItem(_,`admin`),localStorage.removeItem(y),localStorage.removeItem(g),f.success(`登录成功`),p.replace(N())}catch(e){f.error(e?.response?.data?.message??`登录失败`)}finally{O.value=!1}}return(t,a)=>{let s=n(`a-input`),d=n(`a-form-item`),f=n(`a-input-password`),p=n(`a-button`),m=n(`a-form`);return e(),c(`div`,x,[l(`div`,S,[a[2]||=l(`div`,{class:`m-verify-title`},`苏州市科普场馆地图`,-1),l(`div`,C,o(k.value.length>=6||A.value.length>=32?`活动专用核销登录(活动结束后账号失效)`:`超级管理员核销登录`),1)]),l(`div`,w,[u(m,{model:j,layout:`vertical`,onSubmitSuccess:P},{default:r(()=>[u(d,{label:`用户名`},{default:r(()=>[u(s,{modelValue:j.username,"onUpdate:modelValue":a[0]||=e=>j.username=e,placeholder:`请输入账号`,size:`large`,"allow-clear":``},null,8,[`modelValue`])]),_:1}),u(d,{label:`密码`},{default:r(()=>[u(f,{modelValue:j.password,"onUpdate:modelValue":a[1]||=e=>j.password=e,placeholder:`请输入密码`,size:`large`,"allow-clear":``},null,8,[`modelValue`])]),_:1}),u(p,{type:`primary`,long:``,size:`large`,loading:O.value,onClick:P},{default:r(()=>[...a[3]||=[i(`登录`,-1)]]),_:1},8,[`loading`])]),_:1},8,[`model`]),k.value.length>=6||A.value.length>=32?(e(),c(`p`,T,[...a[4]||=[i(` 请使用后台配置的核销用户名与密码。链接中的 `,-1),l(`strong`,null,`v=`,-1),i(` 为活动短码,无需输入长串 ID。 `,-1)]])):(e(),c(`p`,E,[...a[5]||=[i(``,-1),l(`strong`,null,`超级管理员`,-1),i(`可使用后台账号登录本页。场馆工作人员请打开管理员提供的带 `,-1),l(`strong`,null,`?v=短码`,-1),i(` 的专用链接。 `,-1)]])),a[6]||=l(`p`,{class:`m-verify-tip`},`登录状态将保持较长时间;若已失效会自动回到本页。`,-1)])])}}}),[[`__scopeId`,`data-v-071c78f9`]]);export{D as default};

@ -1 +0,0 @@
import{n as e}from"./axios-CiYFffbI.js";import{I as t,V as n,Y as r,_ as i,it as a,nt as o,p as s,u as c,v as l,y as u}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{i as d,o as f,r as p}from"./index-Dyg4zy2p.js";import{n as m,t as h}from"./h5Http-B_zh1Fd1.js";var g={class:`m-verify-page`},_={class:`m-verify-card`},v=f(u({__name:`VerifyLogin`,setup(u){let f=d(),v=p(),y=a(!1),b=o({username:``,password:``});function x(){return v.path.startsWith(`/m/`)?`/m/verify`:`/h5/verify/scan`}async function S(){y.value=!0;try{let{data:t}=await m.post(`/auth/login`,{...b,client:`h5_verify`});localStorage.setItem(h,t.token),localStorage.setItem(`${h}_saved_at`,String(Date.now())),e.success(`登录成功`),f.replace(x())}catch(t){e.error(t?.response?.data?.message??`登录失败`)}finally{y.value=!1}}return(e,a)=>{let o=n(`a-input`),u=n(`a-form-item`),d=n(`a-input-password`),f=n(`a-button`),p=n(`a-form`);return t(),s(`div`,g,[a[4]||=c(`div`,{class:`m-verify-hero`},[c(`div`,{class:`m-verify-title`},`苏州市科普场馆地图`),c(`div`,{class:`m-verify-sub`},`移动端核销登录`)],-1),c(`div`,_,[l(p,{model:b,layout:`vertical`,onSubmitSuccess:S},{default:r(()=>[l(u,{label:`用户名`},{default:r(()=>[l(o,{modelValue:b.username,"onUpdate:modelValue":a[0]||=e=>b.username=e,placeholder:`请输入账号`,size:`large`,"allow-clear":``},null,8,[`modelValue`])]),_:1}),l(u,{label:`密码`},{default:r(()=>[l(d,{modelValue:b.password,"onUpdate:modelValue":a[1]||=e=>b.password=e,placeholder:`请输入密码`,size:`large`,"allow-clear":``},null,8,[`modelValue`])]),_:1}),l(f,{type:`primary`,long:``,size:`large`,loading:y.value,onClick:S},{default:r(()=>[...a[2]||=[i(`登录`,-1)]]),_:1},8,[`loading`])]),_:1},8,[`model`]),a[3]||=c(`p`,{class:`m-verify-tip`},`登录状态将保持较长时间;若已失效会自动回到本页。`,-1)])])}}}),[[`__scopeId`,`data-v-b762ebdf`]]);export{v as default};

@ -0,0 +1 @@
.m-verify-page[data-v-071c78f9]{box-sizing:border-box;background:linear-gradient(165deg,#e8f1ff 0%,#f5f7fa 45%,#fff 100%);min-height:100dvh;padding:20px 16px 32px}.m-verify-hero[data-v-071c78f9]{text-align:center;padding:28px 8px 20px}.m-verify-title[data-v-071c78f9]{color:#1d2129;letter-spacing:.02em;font-size:20px;font-weight:700}.m-verify-sub[data-v-071c78f9]{color:#86909c;margin-top:8px;font-size:14px}.m-verify-card[data-v-071c78f9]{background:#fff;border-radius:14px;max-width:420px;margin:0 auto;padding:20px;box-shadow:0 8px 28px #0f172a14}.m-verify-tip[data-v-071c78f9]{color:#86909c;margin-top:14px;font-size:12px;line-height:1.5}

@ -1 +0,0 @@
.m-verify-page[data-v-b762ebdf]{box-sizing:border-box;background:linear-gradient(165deg,#e8f1ff 0%,#f5f7fa 45%,#fff 100%);min-height:100dvh;padding:20px 16px 32px}.m-verify-hero[data-v-b762ebdf]{text-align:center;padding:28px 8px 20px}.m-verify-title[data-v-b762ebdf]{color:#1d2129;letter-spacing:.02em;font-size:20px;font-weight:700}.m-verify-sub[data-v-b762ebdf]{color:#86909c;margin-top:8px;font-size:14px}.m-verify-card[data-v-b762ebdf]{background:#fff;border-radius:14px;max-width:420px;margin:0 auto;padding:20px;box-shadow:0 8px 28px #0f172a14}.m-verify-tip[data-v-b762ebdf]{color:#86909c;margin-top:14px;font-size:12px;line-height:1.5}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
.m-scan[data-v-c9326014]{min-height:100dvh;padding:16px;padding-bottom:calc(16px + env(safe-area-inset-bottom));box-sizing:border-box;background:linear-gradient(#f0f5ff 0%,#f7f8fa 40%,#fff 100%)}.m-scan-head[data-v-c9326014]{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:28px;display:flex}.m-scan-brand[data-v-c9326014]{color:#1d2129;font-size:22px;font-weight:700}.m-scan-sub[data-v-c9326014]{color:#86909c;margin-top:4px;font-size:13px}.m-scan-venue[data-v-c9326014]{color:#1d2129;word-break:break-all;margin-top:8px;font-size:14px;font-weight:600;line-height:1.45}.m-scan-stats[data-v-c9326014]{box-sizing:border-box;gap:12px;max-width:420px;margin:0 auto 20px;padding:0 4px;display:flex}.m-scan-stats-loading[data-v-c9326014]{text-align:center;color:#86909c;background:#ffffffb3;border:1px solid #e5e6eb;border-radius:12px;width:100%;padding:12px;font-size:13px}.m-scan-stat[data-v-c9326014]{text-align:center;background:#fff;border:1px solid #e5e6eb;border-radius:12px;flex:1;min-width:0;padding:14px 12px;box-shadow:0 2px 8px #0000000a}.m-scan-stat--verified[data-v-c9326014]{background:linear-gradient(#f6ffed 0%,#fff 100%);border-color:#00b42a59}.m-scan-stat-label[data-v-c9326014]{color:#86909c;margin-bottom:6px;font-size:12px}.m-scan-stat-num[data-v-c9326014]{color:#1d2129;font-variant-numeric:tabular-nums;font-size:26px;font-weight:700;line-height:1.2}.m-scan-stat--verified .m-scan-stat-num[data-v-c9326014]{color:#00b42a}.m-scan-main[data-v-c9326014]{flex-direction:column;align-items:center;gap:16px;max-width:420px;margin:0 auto;display:flex}.m-scan-btn[data-v-c9326014]{color:#fff;cursor:pointer;background:linear-gradient(145deg,#165dff 0%,#0e42d2 100%);border:none;border-radius:50%;flex-direction:column;justify-content:center;align-items:center;gap:10px;width:168px;height:168px;font-size:18px;font-weight:600;display:flex;box-shadow:0 12px 32px #165dff59}.m-scan-btn[data-v-c9326014]:disabled{opacity:.65;cursor:not-allowed}.m-scan-btn-icon[data-v-c9326014]{background:#ffffff40;border-radius:8px;width:40px;height:40px;position:relative}.m-scan-btn-icon[data-v-c9326014]:before,.m-scan-btn-icon[data-v-c9326014]:after{content:"";border:3px solid #fff;border-radius:4px;position:absolute}.m-scan-btn-icon[data-v-c9326014]:before{inset:6px}.m-scan-btn-icon[data-v-c9326014]:after{inset:10px}.m-scan-hint[data-v-c9326014]{text-align:center;color:#86909c;margin:0;padding:0 8px;font-size:13px;line-height:1.5}.m-scan-hint--sub[data-v-c9326014]{margin-top:6px;font-size:12px}.m-scan-secondary[data-v-c9326014]{max-width:280px}.cam-wrap[data-v-c9326014]{flex-direction:column;gap:12px;display:flex}.cam-video[data-v-c9326014]{object-fit:cover;background:#000;border-radius:10px;width:100%;min-height:220px}.cam-tip[data-v-c9326014]{color:#86909c;text-align:center;margin:0;font-size:13px}.today-list[data-v-c9326014]{flex-direction:column;gap:10px;max-height:min(60vh,420px);display:flex;overflow:auto}.today-item[data-v-c9326014]{background:#f7f8fa;border:1px solid #e5e6eb;border-radius:10px;padding:12px}.today-row[data-v-c9326014]{justify-content:space-between;align-items:center;gap:8px;display:flex}.today-name[data-v-c9326014]{color:#1d2129;font-size:15px;font-weight:600}.today-act[data-v-c9326014]{color:#4e5969;margin-top:6px;font-size:14px}.today-meta[data-v-c9326014]{color:#86909c;margin-top:4px;font-size:12px;line-height:1.45}.today-meta--sub[data-v-c9326014]{color:#a3a6ad;margin-top:2px;font-size:11px}.today-modal-summary[data-v-c9326014]{color:#4e5969;background:#f7f8fa;border-radius:8px;margin:-4px 0 12px;padding:8px 10px;font-size:13px;line-height:1.5}.today-modal-summary strong[data-v-c9326014]{color:#1d2129;font-variant-numeric:tabular-nums}.m-verify-res-modal.arco-modal{max-width:calc(100vw - 24px);margin:12px}.m-verify-res-modal .arco-modal-header{padding:14px 16px 10px}.m-verify-res-modal .arco-modal-title{font-size:17px;font-weight:600}.m-verify-res-modal .arco-modal-footer{padding:10px 16px 16px;padding-bottom:calc(16px + env(safe-area-inset-bottom));border-top:1px solid var(--color-border-2,#e5e6eb);flex-direction:column;gap:10px;display:flex}.m-verify-res-modal .arco-modal-footer .arco-btn{width:100%;margin:0!important}.m-verify-res-desc .arco-descriptions-item-label{vertical-align:top;color:#86909c;width:88px;padding-bottom:10px}.m-verify-res-desc .arco-descriptions-item-value{word-break:break-all;padding-bottom:10px}@media (width>=480px){.m-verify-res-modal .arco-modal-footer{flex-direction:row;justify-content:flex-end}.m-verify-res-modal .arco-modal-footer .arco-btn{width:auto;min-width:100px}}
.m-scan[data-v-e1b23e8d]{min-height:100dvh;padding:16px;padding-bottom:calc(16px + env(safe-area-inset-bottom));box-sizing:border-box;background:linear-gradient(#f0f5ff 0%,#f7f8fa 40%,#fff 100%)}.m-scan-head[data-v-e1b23e8d]{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:28px;display:flex}.m-scan-brand[data-v-e1b23e8d]{color:#1d2129;font-size:22px;font-weight:700}.m-scan-sub[data-v-e1b23e8d]{color:#86909c;margin-top:4px;font-size:13px}.m-scan-venue[data-v-e1b23e8d]{color:#1d2129;word-break:break-all;margin-top:8px;font-size:14px;font-weight:600;line-height:1.45}.m-scan-stats[data-v-e1b23e8d]{box-sizing:border-box;gap:12px;max-width:420px;margin:0 auto 20px;padding:0 4px;display:flex}.m-scan-stats-loading[data-v-e1b23e8d]{text-align:center;color:#86909c;background:#ffffffb3;border:1px solid #e5e6eb;border-radius:12px;width:100%;padding:12px;font-size:13px}.m-scan-stat[data-v-e1b23e8d]{text-align:center;background:#fff;border:1px solid #e5e6eb;border-radius:12px;flex:1;min-width:0;padding:14px 12px;box-shadow:0 2px 8px #0000000a}.m-scan-stat--verified[data-v-e1b23e8d]{background:linear-gradient(#f6ffed 0%,#fff 100%);border-color:#00b42a59}.m-scan-stat-label[data-v-e1b23e8d]{color:#86909c;margin-bottom:6px;font-size:12px}.m-scan-stat-num[data-v-e1b23e8d]{color:#1d2129;font-variant-numeric:tabular-nums;font-size:26px;font-weight:700;line-height:1.2}.m-scan-stat--verified .m-scan-stat-num[data-v-e1b23e8d]{color:#00b42a}.m-scan-main[data-v-e1b23e8d]{flex-direction:column;align-items:center;gap:16px;max-width:420px;margin:0 auto;display:flex}.m-scan-btn[data-v-e1b23e8d]{color:#fff;cursor:pointer;background:linear-gradient(145deg,#165dff 0%,#0e42d2 100%);border:none;border-radius:50%;flex-direction:column;justify-content:center;align-items:center;gap:10px;width:168px;height:168px;font-size:18px;font-weight:600;display:flex;box-shadow:0 12px 32px #165dff59}.m-scan-btn[data-v-e1b23e8d]:disabled{opacity:.65;cursor:not-allowed}.m-scan-btn-icon[data-v-e1b23e8d]{background:#ffffff40;border-radius:8px;width:40px;height:40px;position:relative}.m-scan-btn-icon[data-v-e1b23e8d]:before,.m-scan-btn-icon[data-v-e1b23e8d]:after{content:"";border:3px solid #fff;border-radius:4px;position:absolute}.m-scan-btn-icon[data-v-e1b23e8d]:before{inset:6px}.m-scan-btn-icon[data-v-e1b23e8d]:after{inset:10px}.m-scan-hint[data-v-e1b23e8d]{text-align:center;color:#86909c;margin:0;padding:0 8px;font-size:13px;line-height:1.5}.m-scan-hint--sub[data-v-e1b23e8d]{margin-top:6px;font-size:12px}.m-scan-secondary[data-v-e1b23e8d]{max-width:280px}.cam-wrap[data-v-e1b23e8d]{flex-direction:column;gap:12px;display:flex}.cam-video[data-v-e1b23e8d]{object-fit:cover;background:#000;border-radius:10px;width:100%;min-height:220px}.cam-tip[data-v-e1b23e8d]{color:#86909c;text-align:center;margin:0;font-size:13px}.today-list[data-v-e1b23e8d]{flex-direction:column;gap:10px;max-height:min(60vh,420px);display:flex;overflow:auto}.today-item[data-v-e1b23e8d]{background:#f7f8fa;border:1px solid #e5e6eb;border-radius:10px;padding:12px}.today-row[data-v-e1b23e8d]{justify-content:space-between;align-items:center;gap:8px;display:flex}.today-name[data-v-e1b23e8d]{color:#1d2129;font-size:15px;font-weight:600}.today-act[data-v-e1b23e8d]{color:#4e5969;margin-top:6px;font-size:14px}.today-meta[data-v-e1b23e8d]{color:#86909c;margin-top:4px;font-size:12px;line-height:1.45}.today-meta--sub[data-v-e1b23e8d]{color:#a3a6ad;margin-top:2px;font-size:11px}.today-modal-summary[data-v-e1b23e8d]{color:#4e5969;background:#f7f8fa;border-radius:8px;margin:-4px 0 12px;padding:8px 10px;font-size:13px;line-height:1.5}.today-modal-summary strong[data-v-e1b23e8d]{color:#1d2129;font-variant-numeric:tabular-nums}.m-verify-res-modal.arco-modal{max-width:calc(100vw - 24px);margin:12px}.m-verify-res-modal .arco-modal-header{padding:14px 16px 10px}.m-verify-res-modal .arco-modal-title{font-size:17px;font-weight:600}.m-verify-res-modal .arco-modal-footer{padding:10px 16px 16px;padding-bottom:calc(16px + env(safe-area-inset-bottom));border-top:1px solid var(--color-border-2,#e5e6eb);flex-direction:column;gap:10px;display:flex}.m-verify-res-modal .arco-modal-footer .arco-btn{width:100%;margin:0!important}.m-verify-res-desc .arco-descriptions-item-label{vertical-align:top;color:#86909c;width:88px;padding-bottom:10px}.m-verify-res-desc .arco-descriptions-item-value{word-break:break-all;padding-bottom:10px}@media (width>=480px){.m-verify-res-modal .arco-modal-footer{flex-direction:row;justify-content:flex-end}.m-verify-res-modal .arco-modal-footer .arco-btn{width:auto;min-width:100px}}

@ -1 +1 @@
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-0iDIbJ_3.js";var i=n({__name:`Wechat`,setup(n){return(n,i)=>(e(),t(r,{title:`系统设置 / 微信配置`}))}});export{i as default};
import{I as e,d as t,y as n}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as r}from"./PagePlaceholder-C01qW8FK.js";var i=n({__name:`Wechat`,setup(n){return(n,i)=>(e(),t(r,{title:`系统设置 / 微信配置`}))}});export{i as default};

@ -0,0 +1 @@
import{I as e,N as t,V as n,Y as r,_ as i,d as a,it as o,kt as s,nt as c,p as l,ut as u,v as d,y as f}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{t as p}from"./message-Dh9377vh.js";import{n as m}from"./http-BWP--XXK.js";import{t as h}from"./listTableRowIndex-Bl-nc9Qt.js";var g=[`src`],_={key:1},v=f({__name:`WechatUsers`,setup(f){let v=o(!1),y=o([]),b=c({current:1,pageSize:15,total:0}),x=o(``);async function S(){v.value=!0;try{let{data:e}=await m.get(`/wechat-users`,{params:{page:b.current,page_size:b.pageSize,keyword:x.value.trim()||void 0}});y.value=e.data??[],b.total=e.total??0}catch(e){p.error(e?.response?.data?.message??`加载失败`)}finally{v.value=!1}}function C(){b.current=1,S()}function w(e){b.current=e,S()}return t(()=>void S()),(t,o)=>{let c=n(`a-input`),f=n(`a-button`),p=n(`a-space`),m=n(`a-table-column`),S=n(`a-avatar`),T=n(`a-table`),E=n(`a-card`);return e(),a(E,{title:`用户管理 / 用户列表`},{default:r(()=>[d(p,{style:{"margin-bottom":`12px`},wrap:``},{default:r(()=>[d(c,{modelValue:x.value,"onUpdate:modelValue":o[0]||=e=>x.value=e,placeholder:`搜索手机号或昵称`,style:{width:`240px`},"allow-clear":``,onPressEnter:C},null,8,[`modelValue`]),d(f,{type:`primary`,onClick:C},{default:r(()=>[...o[1]||=[i(`查询`,-1)]]),_:1})]),_:1}),d(T,{class:`list-data-table`,data:y.value,loading:v.value,"row-key":`id`,pagination:{current:b.current,pageSize:b.pageSize,total:b.total,showTotal:!0},onPageChange:w},{columns:r(()=>[d(m,{title:``,width:52},{cell:r(({rowIndex:e})=>[i(s(u(h)(e,b.current,b.pageSize)),1)]),_:1}),d(m,{title:`头像`,width:72},{cell:r(({record:t})=>[d(S,{size:40},{default:r(()=>[t.avatar_url?(e(),l(`img`,{key:0,src:t.avatar_url,alt:``},null,8,g)):(e(),l(`span`,_,`访`))]),_:2},1024)]),_:1}),d(m,{title:`手机号`,"data-index":`phone`,width:140}),d(m,{title:`昵称`,"data-index":`nickname`,width:140,ellipsis:``,tooltip:``}),d(m,{title:`姓名`,"data-index":`real_name`,width:120,ellipsis:``,tooltip:``}),d(m,{title:`注册时间`,"data-index":`created_at`,width:180})]),_:1},8,[`data`,`loading`,`pagination`])]),_:1})}}});export{v as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save