自动填充

master
lion 13 hours ago
parent 887a16dd84
commit 0c6b5f047d

@ -0,0 +1,21 @@
<?php
namespace App\Console\Commands;
use App\Services\NoShowBlacklistService;
use Illuminate\Console\Command;
class SyncNoShowBlacklistCommand extends Command
{
protected $signature = 'reservations:sync-no-show-blacklist';
protected $description = '累计3次预约未核销的手机号写入全平台禁约按活动开始日+3个月';
public function handle(NoShowBlacklistService $service): int
{
$n = $service->syncAll();
$this->info("应用禁约:{$n} 个手机号。");
return self::SUCCESS;
}
}

@ -12,7 +12,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
$schedule->command('reservations:sync-no-show-blacklist')->dailyAt('01:00');
}
/**

@ -26,12 +26,25 @@ class ActivityController extends Controller
if ($request->filled('is_active')) {
$query->where('is_active', (bool) $request->boolean('is_active'));
}
if ($request->filled('schedule_status')) {
$query->whereComputedScheduleStatus($request->string('schedule_status'));
}
if ($request->filled('audit_status')) {
$query->where('audit_status', $request->string('audit_status'));
}
$pageSize = max(1, min(100, (int) $request->input('page_size', 10)));
return response()->json($query->paginate($pageSize));
$page = $query->paginate($pageSize);
$page->getCollection()->transform(function (Activity $a) {
$a->setAttribute(
'schedule_status',
Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at)
);
return $a;
});
return response()->json($page);
}
public function store(Request $request): JsonResponse
@ -68,6 +81,10 @@ class ActivityController extends Controller
}
$this->normalizeActivityDates($data);
$data['schedule_status'] = Activity::computeScheduleStatusFromBounds(
$data['start_at'] ?? null,
$data['end_at'] ?? null
);
$auditStatus = $request->user()->isSuperAdmin() ? Activity::AUDIT_APPROVED : Activity::AUDIT_PENDING;
@ -128,6 +145,10 @@ class ActivityController extends Controller
$this->normalizeActivityDates($data);
$start = array_key_exists('start_at', $data) ? $data['start_at'] : $activity->start_at;
$end = array_key_exists('end_at', $data) ? $data['end_at'] : $activity->end_at;
$data['schedule_status'] = Activity::computeScheduleStatusFromBounds($start, $end);
$activity->fill($data)->save();
if (array_key_exists('tags', $data)) {
$activity->tags = array_values($data['tags'] ?? []);

@ -40,6 +40,7 @@ class H5ContentController extends Controller
'venue_lng' => $a->venue?->lng,
'start_at' => optional($a->start_at)?->toIso8601String(),
'end_at' => optional($a->end_at)?->toIso8601String(),
'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at),
'registered_count' => (int) ($a->registered_count ?? 0),
'tags' => array_values($a->tags ?? []),
];
@ -77,6 +78,7 @@ class H5ContentController extends Controller
'lng' => $a->lng,
'start_at' => optional($a->start_at)?->toIso8601String(),
'end_at' => optional($a->end_at)?->toIso8601String(),
'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at),
'registered_count' => (int) ($a->registered_count ?? 0),
'tags' => array_values($a->tags ?? []),
'reservation_notice' => $a->reservation_notice,
@ -156,6 +158,7 @@ class H5ContentController extends Controller
'cover_image' => $a->cover_image,
'start_at' => optional($a->start_at)?->toIso8601String(),
'end_at' => optional($a->end_at)?->toIso8601String(),
'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at),
'registered_count' => (int) ($a->registered_count ?? 0),
'address' => $a->address,
];

@ -131,6 +131,7 @@ class H5HomeController extends Controller
'address' => $a->address,
'start_at' => optional($a->start_at)?->toIso8601String(),
'end_at' => optional($a->end_at)?->toIso8601String(),
'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at),
'registered_count' => (int) ($a->registered_count ?? 0),
'is_bookable' => $isBookable,
'tags' => array_values($a->tags ?? []),

@ -5,8 +5,11 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Activity;
use App\Models\ActivityDay;
use App\Models\Blacklist;
use App\Models\PhoneBookingBan;
use App\Models\Reservation;
use App\Models\WechatUser;
use App\Services\NoShowBlacklistService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -82,13 +85,16 @@ class H5ReservationController extends Controller
'max_people_per_order' => $maxPeople,
'booking_modes' => $this->bookingModesFor($mode),
'reservation_notice' => $activity->reservation_notice,
'start_at' => optional($activity->start_at)?->toIso8601String(),
'end_at' => optional($activity->end_at)?->toIso8601String(),
'schedule_status' => Activity::computeScheduleStatusFromBounds($activity->start_at, $activity->end_at),
'venue' => $activity->venue,
],
'days' => $days,
]);
}
public function create(Request $request, int $activityId): JsonResponse
public function create(Request $request, int $activityId, NoShowBlacklistService $noShowBlacklistService): JsonResponse
{
$activity = Activity::query()
->with('venue:id,appointment_type')
@ -137,6 +143,23 @@ class H5ReservationController extends Controller
$wechatUser = $this->authWechatUser($request);
$tz = (string) config('app.timezone');
$noShowBlacklistService->syncForPhone((string) $data['visitor_phone']);
if (PhoneBookingBan::isGloballyBlocked((string) $data['visitor_phone'])) {
$until = PhoneBookingBan::activeBanUntil((string) $data['visitor_phone']);
$untilText = $until ? $until->timezone($tz)->format('Y-m-d') : '';
throw ValidationException::withMessages([
'visitor_phone' => [
'您因累计 3 次预约成功后未核销,已限制预约所有场馆活动至 '.$untilText.',期满后方可再次预约。',
],
]);
}
if (Blacklist::query()->where('venue_id', $activity->venue_id)->where('visitor_phone', $data['visitor_phone'])->exists()) {
throw ValidationException::withMessages([
'visitor_phone' => ['您已被该场馆限制预约,如有疑问请联系场馆。'],
]);
}
$reservation = DB::transaction(function () use ($activity, $day, $data, $peopleCount, $bookingType, $wechatUser) {
$day->refresh();
$available = (int) $day->day_quota - (int) $day->booked_count;

@ -27,7 +27,10 @@ class VenueController extends Controller
'open_mode' => ['nullable', 'string', 'max:40', Rule::in(['fulltime', 'scheduled', 'appointment'])],
'open_time' => ['nullable', 'string', 'max:120'],
'reservation_notice' => ['nullable', 'string'],
'study_courses' => ['nullable', 'string'],
'ticket_content' => ['nullable', 'string'],
'booking_method' => ['nullable', 'string'],
'visit_form' => ['nullable', 'string'],
'consultation_hours' => ['nullable', 'string'],
'address' => ['nullable', 'string', 'max:255'],
'contact_phone' => ['nullable', 'string', 'max:20'],
'lat' => ['nullable', 'numeric'],
@ -82,7 +85,10 @@ class VenueController extends Controller
->orWhere('unit_name', 'like', '%' . $keyword . '%')
->orWhere('open_time', 'like', '%' . $keyword . '%')
->orWhere('reservation_notice', 'like', '%' . $keyword . '%')
->orWhere('study_courses', 'like', '%' . $keyword . '%');
->orWhere('ticket_content', 'like', '%' . $keyword . '%')
->orWhere('booking_method', 'like', '%' . $keyword . '%')
->orWhere('visit_form', 'like', '%' . $keyword . '%')
->orWhere('consultation_hours', 'like', '%' . $keyword . '%');
});
}
@ -133,7 +139,10 @@ class VenueController extends Controller
'open_mode' => ['nullable', 'string', 'max:40', Rule::in(['fulltime', 'scheduled', 'appointment'])],
'open_time' => ['nullable', 'string', 'max:120'],
'reservation_notice' => ['nullable', 'string'],
'study_courses' => ['nullable', 'string'],
'ticket_content' => ['nullable', 'string'],
'booking_method' => ['nullable', 'string'],
'visit_form' => ['nullable', 'string'],
'consultation_hours' => ['nullable', 'string'],
'address' => ['nullable', 'string', 'max:255'],
'contact_phone' => ['nullable', 'string', 'max:20'],
'lat' => ['nullable', 'numeric'],

@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\VenueImportService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\StreamedResponse;
class VenueImportController extends Controller
{
public function __construct(
private VenueImportService $venueImportService
) {}
public function template(Request $request): StreamedResponse
{
$user = $request->user();
abort_unless($user?->isSuperAdmin(), 403, '仅超级管理员可下载导入模板');
$spreadsheet = $this->venueImportService->buildTemplateSpreadsheet();
$writer = new Xlsx($spreadsheet);
return response()->streamDownload(function () use ($writer) {
$writer->save('php://output');
}, '场馆导入模板.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}
public function preview(Request $request): JsonResponse
{
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可导入场馆');
$request->validate([
'file' => ['required', 'file', 'mimes:xlsx,xls', 'max:10240'],
]);
$path = $request->file('file')->getRealPath();
if ($path === false) {
return response()->json(['message' => '无法读取上传文件'], 422);
}
$result = $this->venueImportService->previewFromPath($path);
return response()->json($result);
}
public function confirm(Request $request): JsonResponse
{
$user = $request->user();
abort_unless($user?->isSuperAdmin(), 403, '仅超级管理员可导入场馆');
$data = $request->validate([
'rows' => ['required', 'array', 'min:1'],
'rows.*' => ['required', 'array'],
]);
$created = [];
$errors = [];
DB::beginTransaction();
try {
foreach ($data['rows'] as $index => $row) {
$errs = $this->venueImportService->validatePayload($row);
if ($errs !== []) {
$errors[] = ['index' => $index, 'errors' => $errs];
continue;
}
$clean = $this->venueImportService->stripInternalKeys($row);
$created[] = $this->venueImportService->createVenueFromPayload($clean, $user)->toArray();
}
if ($errors !== []) {
DB::rollBack();
return response()->json([
'message' => '部分数据校验失败,未写入任何场馆',
'failures' => $errors,
], 422);
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
return response()->json([
'created' => $created,
'count' => count($created),
], 201);
}
}

@ -2,6 +2,7 @@
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
@ -34,6 +35,7 @@ class Activity extends Model
'open_time',
'sort',
'is_active',
'schedule_status',
'booking_audience',
'total_quota',
'min_people_per_order',
@ -100,6 +102,55 @@ class Activity extends Model
->orderByDesc('id');
}
/**
* 按活动开始/结束日(应用时区下的日历日)计算「未开始 / 进行中 / 已结束」,与后台列表筛选一致。
*/
public static function computeScheduleStatusFromBounds(?Carbon $start, ?Carbon $end, ?string $tz = null): string
{
$tz = $tz ?? (string) config('app.timezone');
$today = Carbon::now($tz)->toDateString();
$startD = $start ? $start->copy()->timezone($tz)->toDateString() : null;
$endD = $end ? $end->copy()->timezone($tz)->toDateString() : null;
if (! $startD && ! $endD) {
return 'ongoing';
}
if ($startD && ! $endD) {
return $today < $startD ? 'not_started' : 'ongoing';
}
if (! $startD && $endD) {
return $today > $endD ? 'ended' : 'ongoing';
}
if ($today < $startD) {
return 'not_started';
}
if ($today > $endD) {
return 'ended';
}
return 'ongoing';
}
/** 列表筛选:按日期实时计算,不依赖 schedule_status 字段的缓存值。 */
public function scopeWhereComputedScheduleStatus(Builder $query, string $status): Builder
{
$tz = (string) config('app.timezone');
$today = Carbon::now($tz)->toDateString();
return match ($status) {
'not_started' => $query->whereNotNull('start_at')->whereDate('start_at', '>', $today),
'ended' => $query->whereNotNull('end_at')->whereDate('end_at', '<', $today),
'ongoing' => $query->where(function (Builder $q) use ($today) {
$q->where(function (Builder $q2) use ($today) {
$q2->whereNull('start_at')->orWhereDate('start_at', '<=', $today);
})->where(function (Builder $q2) use ($today) {
$q2->whereNull('end_at')->orWhereDate('end_at', '>=', $today);
});
}),
default => $query,
};
}
/**
* 按未取消预约的票数合计,回写 activities.registered_count展示用「已预约总人数」
*/

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
class PhoneBookingBan extends Model
{
protected $fillable = [
'visitor_phone',
'ban_until',
'reason',
];
protected $casts = [
'ban_until' => 'datetime',
];
/** 当前是否处于全平台禁约期ban_until 为期满后可预约的最早时刻)。 */
public static function isGloballyBlocked(string $visitorPhone): bool
{
return static::query()
->where('visitor_phone', $visitorPhone)
->where('ban_until', '>', now())
->exists();
}
public static function activeBanUntil(string $visitorPhone): ?Carbon
{
$row = static::query()
->where('visitor_phone', $visitorPhone)
->where('ban_until', '>', now())
->orderByDesc('ban_until')
->first();
return $row?->ban_until;
}
}

@ -23,7 +23,10 @@ class Venue extends Model
'open_mode',
'open_time',
'reservation_notice',
'study_courses',
'ticket_content',
'booking_method',
'visit_form',
'consultation_hours',
'address',
'contact_phone',
'lat',
@ -48,7 +51,8 @@ class Venue extends Model
/** @var list<string> */
public const SNAPSHOT_KEYS = [
'name', 'venue_type', 'venue_types', 'unit_name', 'district', 'ticket_type',
'appointment_type', 'open_mode', 'open_time', 'reservation_notice', 'study_courses',
'appointment_type', 'open_mode', 'open_time', 'reservation_notice',
'ticket_content', 'booking_method', 'visit_form', 'consultation_hours',
'address', 'contact_phone', 'lat', 'lng', 'cover_image', 'gallery_media',
'detail_html', 'live_people_count', 'sort', 'is_active',
];

@ -0,0 +1,128 @@
<?php
namespace App\Services;
use App\Models\PhoneBookingBan;
use App\Models\Reservation;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class NoShowBlacklistService
{
/**
* 扫描「活动日已过、仍为待核销、从未核销」的预约,累计满 3 次则写入全平台禁约。
* 禁约截止 = 第 3 条对应活动的「活动开始日」+ 3 个月(应用时区日历)。
* 若该手机号已有未过期禁约,则跳过;若曾禁约已过期,只统计禁约期满日之后的未核销场次。
*
* @return int 本次新写入或更新的禁约条数
*/
public function syncAll(): int
{
$tz = (string) config('app.timezone');
$today = Carbon::now($tz)->toDateString();
$phones = Reservation::query()
->join('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id')
->where('reservations.status', 'pending')
->whereNull('reservations.verified_at')
->whereNotNull('reservations.activity_day_id')
->whereDate('activity_days.activity_date', '<', $today)
->distinct()
->pluck('reservations.visitor_phone');
$applied = 0;
foreach ($phones as $phone) {
if (PhoneBookingBan::isGloballyBlocked((string) $phone)) {
continue;
}
if ($this->applyBanIfNeeded((string) $phone, $tz, $today)) {
$applied++;
}
}
return $applied;
}
/** 预约前对单号同步一次,避免仅依赖定时任务产生空窗期。 */
public function syncForPhone(string $phone): bool
{
$tz = (string) config('app.timezone');
$today = Carbon::now($tz)->toDateString();
if (PhoneBookingBan::isGloballyBlocked($phone)) {
return false;
}
return $this->applyBanIfNeeded($phone, $tz, $today);
}
private function applyBanIfNeeded(string $phone, string $tz, string $today): bool
{
$cutoff = $this->noShowCutoffDateString($phone);
/** @var Collection<int, Reservation> $rows */
$rows = Reservation::query()
->join('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id')
->join('activities', 'activities.id', '=', 'reservations.activity_id')
->where('reservations.visitor_phone', $phone)
->where('reservations.status', 'pending')
->whereNull('reservations.verified_at')
->whereNotNull('reservations.activity_day_id')
->whereDate('activity_days.activity_date', '<', $today)
->when($cutoff, fn ($q) => $q->whereDate('activity_days.activity_date', '>=', $cutoff))
->orderByRaw('activities.start_at IS NULL ASC')
->orderBy('activities.start_at')
->orderBy('activity_days.activity_date')
->orderBy('reservations.id')
->select('reservations.*')
->get();
if ($rows->count() < 3) {
return false;
}
$third = $rows->values()->get(2);
$third->loadMissing(['activity', 'activityDay']);
$activity = $third->activity;
$day = $third->activityDay;
if (! $activity || ! $day) {
return false;
}
$anchor = $activity->start_at
? $activity->start_at->copy()->timezone($tz)->startOfDay()
: Carbon::parse($day->activity_date->format('Y-m-d'), $tz)->startOfDay();
$banUntil = $anchor->copy()->addMonths(3)->startOfDay();
if ($banUntil->lte(now())) {
return false;
}
PhoneBookingBan::query()->updateOrCreate(
['visitor_phone' => $phone],
[
'ban_until' => $banUntil,
'reason' => '累计3次预约成功后未核销',
],
);
return true;
}
/**
* 禁约期满后,新统计的「未核销」场次须从该日期(含)起算。
*/
private function noShowCutoffDateString(string $phone): ?string
{
$max = PhoneBookingBan::query()
->where('visitor_phone', $phone)
->where('ban_until', '<=', now())
->max('ban_until');
if (! $max) {
return null;
}
return Carbon::parse($max)->timezone(config('app.timezone'))->toDateString();
}
}

@ -0,0 +1,382 @@
<?php
namespace App\Services;
use App\Models\DictItem;
use App\Models\User;
use App\Models\Venue;
use Illuminate\Support\Facades\Validator;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
class VenueImportService
{
/** @var array<string, string> 中文主题名 => item_value */
public const THEME_LABEL_TO_VALUE = [
'自然生态' => 'theme_nature_ecology',
'科技产业' => 'theme_tech_industry',
'苏工苏艺' => 'theme_su_craft',
'生命健康' => 'theme_life_health',
'弘扬科学家精神' => 'theme_scientist_spirit',
];
private const TICKET_TYPE_LABEL = [
'免费' => 'free',
'收费' => 'paid',
];
private const APPOINTMENT_LABEL = [
'仅团队' => 'team_only',
'个人团队均可' => 'individual_and_team',
];
private const OPEN_MODE_LABEL = [
'常态化全时开放' => 'fulltime',
'常态化定时开放' => 'scheduled',
'预约开放' => 'appointment',
];
public function buildTemplateSpreadsheet(): Spreadsheet
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('导入数据');
// 列顺序与后台「新增/编辑场馆」表单一致(不含封面、轮播、地图选点)
$headers = [
'场馆名称*',
'主题*',
'行政区*',
'预约类型',
'门票类型',
'开放模式',
'所属单位',
'预约方式',
'参观形式',
'开放时间',
'咨询预约时间',
'咨询预约联系电话',
'排序',
'启用',
'场馆地址',
'经度',
'纬度',
'门票说明',
'场馆详情',
'预约须知',
];
foreach (range(1, count($headers)) as $i) {
$col = Coordinate::stringFromColumnIndex($i);
$sheet->setCellValue($col . '1', $headers[$i - 1]);
}
$sheet->setCellValue(
'A2',
'说明:主题多个请用英文逗号分隔;启用填「是」或「否」;经纬度可留空,导入后请在列表编辑中通过「地图选点」填写;经度与纬度须成对填写或同时留空。'
);
$opt = $spreadsheet->createSheet();
$opt->setTitle('选项');
$districts = DictItem::query()
->where('dict_type', 'district')
->where('is_active', true)
->orderBy('sort')
->orderBy('id')
->pluck('item_label')
->all();
$opt->setCellValue('A1', '行政区');
$r = 2;
foreach ($districts as $d) {
$opt->setCellValue('A' . $r, $d);
$r++;
}
$lastD = max(2, $r - 1);
$opt->setCellValue('B1', '主题');
$tr = 2;
foreach (array_keys(self::THEME_LABEL_TO_VALUE) as $label) {
$opt->setCellValue('B' . $tr, $label);
$tr++;
}
$lastT = max(2, $tr - 1);
$opt->setCellValue('C1', '门票类型');
$opt->setCellValue('C2', '免费');
$opt->setCellValue('C3', '收费');
$opt->setCellValue('D1', '预约类型');
$opt->setCellValue('D2', '仅团队');
$opt->setCellValue('D3', '个人团队均可');
$opt->setCellValue('E1', '开放模式');
$opt->setCellValue('E2', '常态化全时开放');
$opt->setCellValue('E3', '常态化定时开放');
$opt->setCellValue('E4', '预约开放');
// B=主题 C=行政区 D=预约类型 E=门票类型 F=开放模式(与表头列位一致)
$this->applyListValidation($sheet, 'B2:B5000', '=选项!$B$2:$B$' . $lastT);
$this->applyListValidation($sheet, 'C2:C5000', '=选项!$A$2:$A$' . $lastD);
$this->applyListValidation($sheet, 'D2:D5000', '=选项!$D$2:$D$3');
$this->applyListValidation($sheet, 'E2:E5000', '=选项!$C$2:$C$3');
$this->applyListValidation($sheet, 'F2:F5000', '=选项!$E$2:$E$4');
$spreadsheet->setActiveSheetIndex(0);
return $spreadsheet;
}
private function applyListValidation(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, string $range, string $formula): void
{
$validation = new DataValidation();
$validation->setType(DataValidation::TYPE_LIST);
$validation->setErrorStyle(DataValidation::STYLE_INFORMATION);
$validation->setAllowBlank(true);
$validation->setShowInputMessage(true);
$validation->setShowErrorMessage(true);
$validation->setShowDropDown(true);
$validation->setFormula1($formula);
$sheet->setDataValidation($range, $validation);
}
/**
* @return array{rows: array<int, array<string, mixed>>, summary: array{total: int, valid: int, invalid: int}}
*/
public function previewFromPath(string $path): array
{
$spreadsheet = IOFactory::load($path);
$sheet = $spreadsheet->getSheet(0);
$highestRow = (int) $sheet->getHighestRow();
$out = [];
$valid = 0;
$invalid = 0;
for ($row = 2; $row <= $highestRow; $row++) {
$cells = [];
for ($col = 1; $col <= 20; $col++) {
$colLetter = Coordinate::stringFromColumnIndex($col);
$cells[] = trim((string) $sheet->getCell($colLetter . $row)->getValue());
}
if ($this->rowIsEmpty($cells)) {
continue;
}
$parsed = $this->normalizeRowFromCells($cells);
$errors = $this->validatePayload($parsed);
$ok = $errors === [];
if ($ok) {
$valid++;
} else {
$invalid++;
}
$out[] = [
'row_index' => $row,
'ok' => $ok,
'errors' => $errors,
'payload' => $parsed,
];
}
return [
'rows' => $out,
'summary' => [
'total' => count($out),
'valid' => $valid,
'invalid' => $invalid,
],
];
}
/**
* @param array<int, string> $cells
* @return array<string, mixed>
*/
public function normalizeRowFromCells(array $cells): array
{
$g = fn (int $i) => isset($cells[$i]) ? trim($cells[$i]) : '';
$themeRaw = str_replace('', ',', $g(1));
$themeParts = array_filter(array_map('trim', explode(',', $themeRaw)), fn ($s) => $s !== '');
$venueTypes = [];
foreach ($themeParts as $part) {
if (isset(self::THEME_LABEL_TO_VALUE[$part])) {
$venueTypes[] = self::THEME_LABEL_TO_VALUE[$part];
}
}
// 与表头3 预约类型、4 门票类型、5 开放模式 一致(从 0 起算列索引)
$rawApp = $g(3);
$rawTicket = $g(4);
$rawOpen = $g(5);
$ticketType = $rawTicket === '' ? null : (self::TICKET_TYPE_LABEL[$rawTicket] ?? null);
$appointmentType = $rawApp === '' ? null : (self::APPOINTMENT_LABEL[$rawApp] ?? null);
$openMode = $rawOpen === '' ? null : (self::OPEN_MODE_LABEL[$rawOpen] ?? null);
$lng = $this->parseFloat($g(15));
$lat = $this->parseFloat($g(16));
$sort = $g(12) === '' ? 0 : (int) $g(12);
return [
'name' => $g(0),
'venue_types' => $venueTypes,
'district' => $g(2),
'appointment_type' => $appointmentType,
'ticket_type' => $ticketType,
'open_mode' => $openMode,
'unit_name' => $g(6) === '' ? null : $g(6),
'booking_method' => $g(7) === '' ? null : $g(7),
'visit_form' => $g(8) === '' ? null : $g(8),
'open_time' => $g(9) === '' ? null : $g(9),
'consultation_hours' => $g(10) === '' ? null : $g(10),
'contact_phone' => $g(11) === '' ? null : $g(11),
'sort' => $sort,
'is_active' => $this->parseBool($g(13)),
'address' => $g(14) === '' ? null : $g(14),
'lng' => $lng,
'lat' => $lat,
'ticket_content' => $g(17) === '' ? null : $g(17),
'detail_html' => $g(18) === '' ? null : $g(18),
'reservation_notice' => $g(19) === '' ? null : $g(19),
'_raw_appointment_label' => $rawApp,
'_raw_open_mode_label' => $rawOpen,
'_raw_ticket_type_label' => $rawTicket,
];
}
/**
* @param array<string, mixed> $data
* @return list<string>
*/
public function validatePayload(array $data): array
{
$errors = [];
if (trim((string) ($data['name'] ?? '')) === '') {
$errors[] = '场馆名称不能为空';
}
if (empty($data['venue_types']) || ! is_array($data['venue_types'])) {
$errors[] = '主题无效或为空(须为模板「选项」表中的主题名称,多个用英文逗号分隔)';
}
if (trim((string) ($data['district'] ?? '')) === '') {
$errors[] = '行政区不能为空';
}
$lng = $data['lng'] ?? null;
$lat = $data['lat'] ?? null;
$hasPartialCoord = ($lng === null) !== ($lat === null);
if ($hasPartialCoord) {
$errors[] = '经度与纬度须同时填写或同时留空(留空时请在列表编辑中通过地图选点补充)';
}
if (($data['_raw_ticket_type_label'] ?? '') !== '' && ($data['ticket_type'] ?? null) === null) {
$errors[] = '门票类型须为「免费」或「收费」或留空';
}
if (($data['_raw_appointment_label'] ?? '') !== '' && ($data['appointment_type'] ?? null) === null) {
$errors[] = '预约类型须为「仅团队」「个人团队均可」或留空';
}
if (($data['_raw_open_mode_label'] ?? '') !== '' && ($data['open_mode'] ?? null) === null) {
$errors[] = '开放模式须为三种选项之一或留空';
}
$v = Validator::make($this->stripInternalKeys($data), [
'name' => ['required', 'string', 'max:120'],
'venue_types' => ['required', 'array', 'min:1'],
'venue_types.*' => ['string', 'max:80'],
'unit_name' => ['nullable', 'string', 'max:120'],
'district' => ['required', 'string', 'max:80'],
'ticket_type' => ['nullable', 'string', 'max:80'],
'appointment_type' => ['nullable', 'string', 'max:40'],
'open_mode' => ['nullable', 'string', 'max:40'],
'open_time' => ['nullable', 'string', 'max:120'],
'ticket_content' => ['nullable', 'string'],
'booking_method' => ['nullable', 'string'],
'visit_form' => ['nullable', 'string'],
'consultation_hours' => ['nullable', 'string'],
'detail_html' => ['nullable', 'string'],
'reservation_notice' => ['nullable', 'string'],
'address' => ['nullable', 'string', 'max:255'],
'contact_phone' => ['nullable', 'string', 'max:20'],
'lat' => ['nullable', 'numeric'],
'lng' => ['nullable', 'numeric'],
'sort' => ['nullable', 'integer', 'min:0'],
'is_active' => ['boolean'],
]);
if ($v->fails()) {
foreach ($v->errors()->all() as $msg) {
$errors[] = $msg;
}
}
return array_values(array_unique($errors));
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function stripInternalKeys(array $data): array
{
foreach (array_keys($data) as $k) {
if (is_string($k) && str_starts_with($k, '_')) {
unset($data[$k]);
}
}
return $data;
}
/**
* @param array<string, mixed> $payload
*/
public function createVenueFromPayload(array $payload, User $user): Venue
{
$clean = $this->stripInternalKeys($payload);
$auditStatus = $user->isSuperAdmin() ? Venue::AUDIT_APPROVED : Venue::AUDIT_PENDING;
$venue = Venue::create($clean + [
'cover_image' => null,
'gallery_media' => [],
'detail_html' => $clean['detail_html'] ?? null,
'live_people_count' => 0,
'audit_status' => $auditStatus,
'audit_remark' => null,
'last_approved_snapshot' => null,
]);
if (! $user->isSuperAdmin()) {
$user->venues()->syncWithoutDetaching([$venue->id]);
}
return $venue->fresh();
}
private function rowIsEmpty(array $cells): bool
{
foreach ($cells as $c) {
if (trim((string) $c) !== '') {
return false;
}
}
return true;
}
private function parseFloat(?string $s): ?float
{
if ($s === null || trim($s) === '') {
return null;
}
$n = (float) str_replace([',', ' '], '', $s);
return is_finite($n) ? $n : null;
}
private function parseBool(string $s): bool
{
$t = mb_strtolower(trim($s));
if ($t === '' || in_array($t, ['1', '是', 'y', 'yes', 'true', '启用'], true)) {
return true;
}
if (in_array($t, ['0', '否', 'n', 'no', 'false', '禁用'], true)) {
return false;
}
return true;
}
}

@ -9,7 +9,8 @@
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8"
"laravel/tinker": "^2.8",
"phpoffice/phpspreadsheet": "^5.3"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",

401
composer.lock generated

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9c491b8531eec05ba41a11d9276a5749",
"content-hash": "cdbad6a527684d62da061a1712efa143",
"packages": [
{
"name": "brick/math",
@ -147,6 +147,91 @@
],
"time": "2023-12-11T17:09:12+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
},
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@ -2027,6 +2112,208 @@
],
"time": "2024-01-28T23:22:08+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.1.1",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "6187e9cc4493da94b9b63eb2315821552015fca9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9",
"reference": "6187e9cc4493da94b9b63eb2315821552015fca9",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.1"
},
"require-dev": {
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^10.0",
"vimeo/psalm": "^5.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2024-10-10T12:33:01+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "monolog/monolog",
"version": "3.9.0",
@ -2570,6 +2857,118 @@
],
"time": "2024-11-21T10:36:35+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "5.3.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "4d597c1aacdde1805a33c525b9758113ea0d90df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/4d597c1aacdde1805a33c525b9758113ea0d90df",
"reference": "4d597c1aacdde1805a33c525b9758113ea0d90df",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^8.1",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.5",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1 || ^2.0",
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.3.0"
},
"time": "2025-11-24T15:47:10+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",

@ -0,0 +1,44 @@
<?php
use App\Models\Activity;
use Carbon\Carbon;
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('schedule_status', 20)->default('ongoing')->after('is_active');
});
$tz = config('app.timezone');
Activity::query()->orderBy('id')->chunkById(100, function ($activities) use ($tz): void {
foreach ($activities as $a) {
/** @var Activity $a */
$today = Carbon::now($tz)->toDateString();
$startD = $a->start_at ? $a->start_at->copy()->timezone($tz)->toDateString() : null;
$endD = $a->end_at ? $a->end_at->copy()->timezone($tz)->toDateString() : null;
$status = 'ongoing';
if ($startD && $today < $startD) {
$status = 'not_started';
} elseif ($endD && $today > $endD) {
$status = 'ended';
}
$a->schedule_status = $status;
$a->saveQuietly();
}
});
}
public function down(): void
{
Schema::table('activities', function (Blueprint $table) {
$table->dropColumn('schedule_status');
});
}
};

@ -0,0 +1,26 @@
<?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::create('phone_booking_bans', function (Blueprint $table) {
$table->id();
$table->string('visitor_phone', 20);
$table->dateTime('ban_until');
$table->string('reason')->nullable();
$table->timestamps();
$table->unique('visitor_phone');
});
}
public function down(): void
{
Schema::dropIfExists('phone_booking_bans');
}
};

@ -0,0 +1,68 @@
<?php
use App\Models\DictItem;
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('venues', function (Blueprint $table) {
if (Schema::hasColumn('venues', 'study_courses')) {
$table->dropColumn('study_courses');
}
$table->text('ticket_content')->nullable()->after('reservation_notice')->comment('门票(长文本)');
$table->text('booking_method')->nullable()->after('ticket_content')->comment('预约方式');
$table->text('visit_form')->nullable()->after('booking_method')->comment('参观形式');
$table->text('consultation_hours')->nullable()->after('visit_form')->comment('咨询预约时间');
});
DictItem::query()->where('dict_type', 'venue_type')->update(['is_active' => false]);
$themes = [
['item_label' => '自然生态', 'item_value' => 'theme_nature_ecology', 'item_remark' => '#2E7D32', 'sort' => 10],
['item_label' => '科技产业', 'item_value' => 'theme_tech_industry', 'item_remark' => '#1565C0', 'sort' => 20],
['item_label' => '苏工苏艺', 'item_value' => 'theme_su_craft', 'item_remark' => '#455A64', 'sort' => 30],
['item_label' => '生命健康', 'item_value' => 'theme_life_health', 'item_remark' => '#00796B', 'sort' => 40],
['item_label' => '弘扬科学家精神', 'item_value' => 'theme_scientist_spirit', 'item_remark' => '#C62828', 'sort' => 50],
];
foreach ($themes as $row) {
DictItem::updateOrCreate(
['dict_type' => 'venue_type', 'item_value' => $row['item_value']],
[
'dict_name' => '场馆主题',
'item_label' => $row['item_label'],
'item_remark' => $row['item_remark'],
'sort' => $row['sort'],
'is_active' => true,
]
);
}
DictItem::updateOrCreate(
['dict_type' => 'venue_appointment_type', 'item_value' => 'individual_and_team'],
[
'dict_name' => '场馆预约类型',
'item_label' => '个人团队均可',
'item_remark' => '',
'sort' => 20,
'is_active' => true,
]
);
}
public function down(): void
{
DictItem::query()
->where('dict_type', 'venue_appointment_type')
->where('item_value', 'individual_and_team')
->delete();
Schema::table('venues', function (Blueprint $table) {
$table->dropColumn(['ticket_content', 'booking_method', 'visit_form', 'consultation_hours']);
$table->text('study_courses')->nullable()->after('reservation_notice');
});
}
};

@ -52,18 +52,17 @@ class DatabaseSeeder extends Seeder
}
$venueTypes = [
['item_label' => '科技场馆类', 'item_value' => 'science_venue', 'item_remark' => '#165DFF'],
['item_label' => '教育科研类', 'item_value' => 'education_research', 'item_remark' => '#00B42A'],
['item_label' => '“三农”类', 'item_value' => 'agriculture', 'item_remark' => '#86909C'],
['item_label' => '企业类', 'item_value' => 'enterprise', 'item_remark' => '#722ED1'],
['item_label' => '自然资源类', 'item_value' => 'nature_resource', 'item_remark' => '#FF7D00'],
['item_label' => '其他类', 'item_value' => 'other', 'item_remark' => '#F53F3F'],
['item_label' => '自然生态', 'item_value' => 'theme_nature_ecology', 'item_remark' => '#2E7D32'],
['item_label' => '科技产业', 'item_value' => 'theme_tech_industry', 'item_remark' => '#1565C0'],
['item_label' => '苏工苏艺', 'item_value' => 'theme_su_craft', 'item_remark' => '#455A64'],
['item_label' => '生命健康', 'item_value' => 'theme_life_health', 'item_remark' => '#00796B'],
['item_label' => '弘扬科学家精神', 'item_value' => 'theme_scientist_spirit', 'item_remark' => '#C62828'],
];
foreach ($venueTypes as $index => $row) {
DictItem::updateOrCreate(
['dict_type' => 'venue_type', 'item_value' => $row['item_value']],
[
'dict_name' => '场馆类型',
'dict_name' => '场馆主题',
'item_label' => $row['item_label'],
'item_remark' => $row['item_remark'],
'sort' => ($index + 1) * 10,
@ -99,6 +98,16 @@ class DatabaseSeeder extends Seeder
'is_active' => true,
]
);
DictItem::updateOrCreate(
['dict_type' => 'venue_appointment_type', 'item_value' => 'individual_and_team'],
[
'dict_name' => '场馆预约类型',
'item_label' => '个人团队均可',
'item_remark' => '',
'sort' => 20,
'is_active' => true,
]
);
$openModes = [
['item_value' => 'fulltime', 'item_label' => '常态化全时开放', 'sort' => 10],

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 @@
.cover-preview[data-v-11c1fb8c]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:240px;height:140px}

@ -0,0 +1 @@
.activity-address-coord-row[data-v-0af9b2a9]{flex-wrap:wrap;align-items:center;gap:12px;width:100%;display:flex}.activity-address-coord-row__address[data-v-0af9b2a9]{flex:45%;min-width:320px;max-width:100%}.activity-address-coord-row__lng[data-v-0af9b2a9],.activity-address-coord-row__lat[data-v-0af9b2a9]{flex:180px;width:200px;min-width:180px}.activity-address-coord-row__map[data-v-0af9b2a9]{flex-shrink:0}.activity-cover-carousel-wrap[data-v-0af9b2a9]{flex-wrap:wrap;align-items:flex-start;gap:20px;width:100%;display:flex}.activity-cover-carousel-row__col[data-v-0af9b2a9]{flex:320px;min-width:min(100%,320px)}.activity-cover-carousel-row__sub[data-v-0af9b2a9]{color:var(--color-text-1);margin-bottom:8px;font-weight:500}.activity-cover-thumb[data-v-0af9b2a9]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:120px;height:70px}.activity-gallery-grid[data-v-0af9b2a9]{grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:12px;width:100%;display:grid}.activity-gallery-card[data-v-0af9b2a9]{width:90px}.activity-gallery-thumb[data-v-0af9b2a9]{object-fit:cover;cursor:zoom-in;border:1px solid #e5e6eb;border-radius:4px;width:70px;height:45px}.activity-gallery-thumb--video[data-v-0af9b2a9]{display:block}

File diff suppressed because one or more lines are too long

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

@ -1 +1 @@
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-DhmwAFpV.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};
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-BLul_Qh8.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 +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{o}from"./index-DhmwAFpV.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};
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-BLul_Qh8.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 @@
.menu-tree-row[data-v-8817ad31]{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.menu-tree-row .left[data-v-8817ad31]{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.menu-tree-row .name[data-v-8817ad31]{font-weight:500}.menu-tree-row .right[data-v-8817ad31]{flex-shrink:0}

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
.menu-tree-row[data-v-98deafc5]{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.menu-tree-row .left[data-v-98deafc5]{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.menu-tree-row .name[data-v-98deafc5]{font-weight:500}.menu-tree-row .right[data-v-98deafc5]{flex-shrink:0}

@ -1 +0,0 @@
.reg-toolbar[data-v-135e066b]{box-sizing:border-box;width:100%;max-width:100%;margin-bottom:12px}.reg-export-bar[data-v-135e066b]{box-sizing:border-box;grid-template-columns:130px minmax(0,1fr) auto;align-items:start;gap:12px;width:100%;margin-top:12px;display:grid}.reg-export-scope[data-v-135e066b]{width:130px}.reg-export-fields[data-v-135e066b]{width:100%;min-width:0}.reg-export-fields[data-v-135e066b] .arco-select-view{max-width:100%}.reg-export-btn[data-v-135e066b]{justify-self:start}@media (width<=720px){.reg-export-bar[data-v-135e066b]{grid-template-columns:minmax(0,1fr) auto;grid-template-areas:"scope btn""fields fields"}.reg-export-scope[data-v-135e066b]{grid-area:scope}.reg-export-fields[data-v-135e066b]{grid-area:fields}.reg-export-btn[data-v-135e066b]{grid-area:btn;justify-self:end}}.registrations-table[data-v-135e066b] .arco-table-td .arco-table-cell{white-space:normal;word-break:break-word}.registrations-table[data-v-135e066b] .arco-table-text-ellipsis{white-space:nowrap}

@ -0,0 +1 @@
.reg-toolbar[data-v-cd6913f8]{box-sizing:border-box;width:100%;max-width:100%;margin-bottom:12px}.reg-export-bar[data-v-cd6913f8]{box-sizing:border-box;grid-template-columns:130px minmax(0,1fr) auto;align-items:start;gap:12px;width:100%;margin-top:12px;display:grid}.reg-export-scope[data-v-cd6913f8]{width:130px}.reg-export-fields[data-v-cd6913f8]{width:100%;min-width:0}.reg-export-fields[data-v-cd6913f8] .arco-select-view{max-width:100%}.reg-export-btn[data-v-cd6913f8]{justify-self:start}@media (width<=720px){.reg-export-bar[data-v-cd6913f8]{grid-template-columns:minmax(0,1fr) auto;grid-template-areas:"scope btn""fields fields"}.reg-export-scope[data-v-cd6913f8]{grid-area:scope}.reg-export-fields[data-v-cd6913f8]{grid-area:fields}.reg-export-btn[data-v-cd6913f8]{grid-area:btn;justify-self:end}}.registrations-table[data-v-cd6913f8] .arco-table-td .arco-table-cell{white-space:normal;word-break:break-word}.registrations-table[data-v-cd6913f8] .arco-table-text-ellipsis{white-space:nowrap}

@ -1 +1 @@
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-DhmwAFpV.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};
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-BLul_Qh8.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};

@ -0,0 +1 @@
.study-tour-cover-carousel-wrap[data-v-8f02c653]{flex-wrap:wrap;align-items:flex-start;gap:20px;width:100%;display:flex}.study-tour-cover-carousel-row__col[data-v-8f02c653]{flex:320px;min-width:min(100%,320px)}.study-tour-cover-carousel-row__sub[data-v-8f02c653]{color:var(--color-text-1);margin-bottom:8px;font-weight:500}.study-tour-cover-thumb[data-v-8f02c653]{object-fit:contain;border:1px solid #e5e6eb;border-radius:4px;width:auto;max-width:120px;height:auto;max-height:80px}.study-tour-gallery-card[data-v-8f02c653]{width:90px}.study-tour-gallery-thumb-wrap[data-v-8f02c653]{background:#f7f8fa;border-radius:4px;width:80px;height:50px;overflow:hidden}.study-tour-gallery-thumb[data-v-8f02c653]{object-fit:cover;cursor:pointer;width:100%;height:100%}.study-tour-tags-field[data-v-8f02c653]{width:100%}.study-tour-tags-input-row[data-v-8f02c653]{flex-wrap:nowrap;align-items:center;gap:8px;width:100%;display:flex}.study-tour-tags-input[data-v-8f02c653]{flex:1;min-width:0}.study-tour-tags-list[data-v-8f02c653]{flex-wrap:wrap;gap:8px;width:100%;margin-top:8px;display:flex}

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 @@
.import-file-label[data-v-6069076d]{cursor:pointer;display:inline-block}.import-file-input[data-v-6069076d]{opacity:0;width:0;height:0;position:absolute;overflow:hidden}.import-file-btn[data-v-6069076d]{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-6069076d]{border-color:rgb(var(--primary-6));color:rgb(var(--primary-6))}.venue-address-coord-row[data-v-6069076d]{flex-wrap:wrap;align-items:center;gap:12px;width:100%;display:flex}.venue-address-coord-row__address[data-v-6069076d]{flex:45%;min-width:320px;max-width:100%}.venue-address-coord-row__lng[data-v-6069076d],.venue-address-coord-row__lat[data-v-6069076d]{flex:180px;width:200px;min-width:180px}.venue-address-coord-row__map[data-v-6069076d]{flex-shrink:0}.venue-cover-carousel-wrap[data-v-6069076d]{flex-wrap:wrap;align-items:flex-start;gap:20px;width:100%;display:flex}.venue-cover-carousel-row__col[data-v-6069076d]{flex:320px;min-width:min(100%,320px)}.venue-cover-carousel-row__sub[data-v-6069076d]{color:var(--color-text-1);margin-bottom:8px;font-weight:500}.venue-gallery-grid[data-v-6069076d]{grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:12px;width:100%;display:grid}

File diff suppressed because one or more lines are too long

@ -1 +1 @@
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,u as l,ut as u,v as d,y as f}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{n as p,o as m}from"./index-DhmwAFpV.js";import{n as h,t as g}from"./datetime-3T8f3S0H.js";import{t as _}from"./listTable-CVMJfkhr.js";import{n as v,t as y}from"./reservationStatus-DNE9Cq6e.js";var b={class:`verify-list-toolbar`},x=m(f({__name:`Verify`,setup(f){let m=s(!1),x=s([]),S=s(``),C=s(!1),w=s(`all`),T=s(``),E=s([]);async function D(){m.value=!0;try{let{data:e}=await p.get(`/reservations`,{params:{status:w.value,keyword:T.value||void 0,start_date:E.value?.[0]||void 0,end_date:E.value?.[1]||void 0}});x.value=e}catch(t){e.error(t?.response?.data?.message??`加载预约列表失败`)}finally{m.value=!1}}function O(){D()}function k(){w.value=`all`,T.value=``,E.value=[],D()}async function A(){if(!S.value){e.warning(`请输入二维码 token`);return}C.value=!0;try{await p.post(`/reservations/verify`,{qr_token:S.value}),e.success(`核销成功`),S.value=``,await D()}catch(t){e.error(t?.response?.data?.message??`核销失败`)}finally{C.value=!1}}return n(D),(e,n)=>{let s=r(`a-alert`),f=r(`a-input`),p=r(`a-button`),j=r(`a-space`),M=r(`a-radio`),N=r(`a-radio-group`),P=r(`a-range-picker`),F=r(`a-table-column`),I=r(`a-tag`),L=r(`a-table`),R=r(`a-card`);return t(),o(R,{title:`活动管理 / 现场核销`},{default:i(()=>[d(j,{direction:`vertical`,fill:``},{default:i(()=>[d(s,null,{default:i(()=>[...n[4]||=[a(` 输入预约二维码 token 进行核销。场馆管理员仅可核销自己绑定场馆的预约。 `,-1)]]),_:1}),d(j,{wrap:``,size:12},{default:i(()=>[d(f,{modelValue:S.value,"onUpdate:modelValue":n[0]||=e=>S.value=e,style:{width:`min(100%, 420px)`},placeholder:`请输入二维码 token`,"allow-clear":``},null,8,[`modelValue`]),d(p,{type:`primary`,loading:C.value,onClick:A},{default:i(()=>[...n[5]||=[a(`立即核销`,-1)]]),_:1},8,[`loading`])]),_:1}),l(`div`,b,[d(j,{wrap:``,size:12},{default:i(()=>[d(N,{modelValue:w.value,"onUpdate:modelValue":n[1]||=e=>w.value=e,type:`button`,size:`small`,onChange:D},{default:i(()=>[d(M,{value:`all`},{default:i(()=>[...n[6]||=[a(`全部`,-1)]]),_:1}),d(M,{value:`pending`},{default:i(()=>[...n[7]||=[a(`待核销`,-1)]]),_:1}),d(M,{value:`verified`},{default:i(()=>[...n[8]||=[a(`已核销`,-1)]]),_:1}),d(M,{value:`cancelled`},{default:i(()=>[...n[9]||=[a(`已取消`,-1)]]),_:1})]),_:1},8,[`modelValue`]),d(f,{modelValue:T.value,"onUpdate:modelValue":n[2]||=e=>T.value=e,placeholder:`报名人/手机/token`,"allow-clear":``,style:{width:`220px`}},null,8,[`modelValue`]),d(P,{modelValue:E.value,"onUpdate:modelValue":n[3]||=e=>E.value=e,style:{width:`260px`}},null,8,[`modelValue`]),d(p,{type:`primary`,onClick:O},{default:i(()=>[...n[10]||=[a(`查询`,-1)]]),_:1}),d(p,{onClick:k},{default:i(()=>[...n[11]||=[a(`重置`,-1)]]),_:1}),d(p,{onClick:D},{default:i(()=>[...n[12]||=[a(`刷新列表`,-1)]]),_:1})]),_:1})]),d(L,{class:`list-data-table verify-table`,scroll:{x:u(_)},data:x.value,loading:m.value,"row-key":`id`,pagination:{pageSize:10,showTotal:!0}},{columns:i(()=>[d(F,{title:`ID`,"data-index":`id`,width:88}),d(F,{title:`活动`,width:240,"min-width":180,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.activity?.title??`-`),1)]),_:1}),d(F,{title:`场馆`,width:200,"min-width":160,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.venue?.name??`-`),1)]),_:1}),d(F,{title:`报名人`,"data-index":`visitor_name`,width:120,ellipsis:!0,tooltip:!0}),d(F,{title:`手机号`,"data-index":`visitor_phone`,width:130,ellipsis:!0,tooltip:!0}),d(F,{title:`预约类型`,width:100},{cell:i(({record:e})=>[a(c(u(v)(e.booking_type,e.ticket_count)),1)]),_:1}),d(F,{title:`预约票数`,width:100},{cell:i(({record:e})=>[a(c(e.ticket_count??1),1)]),_:1}),d(F,{title:`预约入馆日期`,width:140,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.activity_day?.activity_date?u(h)(e.activity_day.activity_date):`-`),1)]),_:1}),d(F,{title:`状态`,width:100},{cell:i(({record:e})=>[d(I,{color:e.status===`verified`?`green`:e.status===`pending`?`arcoblue`:`gray`},{default:i(()=>[a(c(u(y)(e.status)),1)]),_:2},1032,[`color`])]),_:1}),d(F,{title:`预约时间`,width:175,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(u(g)(e.created_at)),1)]),_:1}),d(F,{title:`核销时间`,width:175,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(u(g)(e.verified_at)),1)]),_:1}),d(F,{title:`二维码 token`,"data-index":`qr_token`,width:220,"min-width":180,ellipsis:!0,tooltip:!0,fixed:`right`,align:`left`})]),_:1},8,[`scroll`,`data`,`loading`])]),_:1})]),_:1})}}}),[[`__scopeId`,`data-v-a0bd6091`]]);export{x as default};
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,u as l,ut as u,v as d,y as f}from"./runtime-core.esm-bundler-CnFWH3R5.js";import{n as p,o as m}from"./index-BLul_Qh8.js";import{n as h,t as g}from"./datetime-3T8f3S0H.js";import{t as _}from"./listTable-DKAruyoN.js";import{n as v,t as y}from"./reservationStatus-DNE9Cq6e.js";var b={class:`verify-list-toolbar`},x=m(f({__name:`Verify`,setup(f){let m=s(!1),x=s([]),S=s(``),C=s(!1),w=s(`all`),T=s(``),E=s([]);async function D(){m.value=!0;try{let{data:e}=await p.get(`/reservations`,{params:{status:w.value,keyword:T.value||void 0,start_date:E.value?.[0]||void 0,end_date:E.value?.[1]||void 0}});x.value=e}catch(t){e.error(t?.response?.data?.message??`加载预约列表失败`)}finally{m.value=!1}}function O(){D()}function k(){w.value=`all`,T.value=``,E.value=[],D()}async function A(){if(!S.value){e.warning(`请输入二维码 token`);return}C.value=!0;try{await p.post(`/reservations/verify`,{qr_token:S.value}),e.success(`核销成功`),S.value=``,await D()}catch(t){e.error(t?.response?.data?.message??`核销失败`)}finally{C.value=!1}}return n(D),(e,n)=>{let s=r(`a-alert`),f=r(`a-input`),p=r(`a-button`),j=r(`a-space`),M=r(`a-radio`),N=r(`a-radio-group`),P=r(`a-range-picker`),F=r(`a-table-column`),I=r(`a-tag`),L=r(`a-table`),R=r(`a-card`);return t(),o(R,{title:`活动管理 / 现场核销`},{default:i(()=>[d(j,{direction:`vertical`,fill:``},{default:i(()=>[d(s,null,{default:i(()=>[...n[4]||=[a(` 输入预约二维码 token 进行核销。场馆管理员仅可核销自己绑定场馆的预约。 `,-1)]]),_:1}),d(j,{wrap:``,size:12},{default:i(()=>[d(f,{modelValue:S.value,"onUpdate:modelValue":n[0]||=e=>S.value=e,style:{width:`min(100%, 420px)`},placeholder:`请输入二维码 token`,"allow-clear":``},null,8,[`modelValue`]),d(p,{type:`primary`,loading:C.value,onClick:A},{default:i(()=>[...n[5]||=[a(`立即核销`,-1)]]),_:1},8,[`loading`])]),_:1}),l(`div`,b,[d(j,{wrap:``,size:12},{default:i(()=>[d(N,{modelValue:w.value,"onUpdate:modelValue":n[1]||=e=>w.value=e,type:`button`,size:`small`,onChange:D},{default:i(()=>[d(M,{value:`all`},{default:i(()=>[...n[6]||=[a(`全部`,-1)]]),_:1}),d(M,{value:`pending`},{default:i(()=>[...n[7]||=[a(`待核销`,-1)]]),_:1}),d(M,{value:`verified`},{default:i(()=>[...n[8]||=[a(`已核销`,-1)]]),_:1}),d(M,{value:`cancelled`},{default:i(()=>[...n[9]||=[a(`已取消`,-1)]]),_:1})]),_:1},8,[`modelValue`]),d(f,{modelValue:T.value,"onUpdate:modelValue":n[2]||=e=>T.value=e,placeholder:`报名人/手机/token`,"allow-clear":``,style:{width:`220px`}},null,8,[`modelValue`]),d(P,{modelValue:E.value,"onUpdate:modelValue":n[3]||=e=>E.value=e,style:{width:`260px`}},null,8,[`modelValue`]),d(p,{type:`primary`,onClick:O},{default:i(()=>[...n[10]||=[a(`查询`,-1)]]),_:1}),d(p,{onClick:k},{default:i(()=>[...n[11]||=[a(`重置`,-1)]]),_:1}),d(p,{onClick:D},{default:i(()=>[...n[12]||=[a(`刷新列表`,-1)]]),_:1})]),_:1})]),d(L,{class:`list-data-table verify-table`,scroll:{x:u(_)},data:x.value,loading:m.value,"row-key":`id`,pagination:{pageSize:10,showTotal:!0}},{columns:i(()=>[d(F,{title:`ID`,"data-index":`id`,width:88}),d(F,{title:`活动`,width:240,"min-width":180,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.activity?.title??`-`),1)]),_:1}),d(F,{title:`场馆`,width:200,"min-width":160,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.venue?.name??`-`),1)]),_:1}),d(F,{title:`报名人`,"data-index":`visitor_name`,width:120,ellipsis:!0,tooltip:!0}),d(F,{title:`手机号`,"data-index":`visitor_phone`,width:130,ellipsis:!0,tooltip:!0}),d(F,{title:`预约类型`,width:100},{cell:i(({record:e})=>[a(c(u(v)(e.booking_type,e.ticket_count)),1)]),_:1}),d(F,{title:`预约票数`,width:100},{cell:i(({record:e})=>[a(c(e.ticket_count??1),1)]),_:1}),d(F,{title:`预约入馆日期`,width:140,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(e.activity_day?.activity_date?u(h)(e.activity_day.activity_date):`-`),1)]),_:1}),d(F,{title:`状态`,width:100},{cell:i(({record:e})=>[d(I,{color:e.status===`verified`?`green`:e.status===`pending`?`arcoblue`:`gray`},{default:i(()=>[a(c(u(y)(e.status)),1)]),_:2},1032,[`color`])]),_:1}),d(F,{title:`预约时间`,width:175,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(u(g)(e.created_at)),1)]),_:1}),d(F,{title:`核销时间`,width:175,ellipsis:!0,tooltip:!0},{cell:i(({record:e})=>[a(c(u(g)(e.verified_at)),1)]),_:1}),d(F,{title:`二维码 token`,"data-index":`qr_token`,width:220,"min-width":180,ellipsis:!0,tooltip:!0,fixed:`right`,align:`left`})]),_:1},8,[`scroll`,`data`,`loading`])]),_:1})]),_:1})}}}),[[`__scopeId`,`data-v-a0bd6091`]]);export{x as default};

@ -1 +1 @@
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-DhmwAFpV.js";import{n as m,t as h}from"./h5Http-BaUTUr0i.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-a78462a1`]]);export{v as default};
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-BLul_Qh8.js";import{n as m,t as h}from"./h5Http-BaUTUr0i.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-a78462a1`]]);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

@ -1 +0,0 @@
var e=2400;export{e as t};

@ -0,0 +1 @@
var e=2760;export{e as t};

@ -5,10 +5,10 @@
<link rel="icon" type="image/svg+xml" href="/admin/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>苏州科普管理后台</title>
<script type="module" crossorigin src="/admin/assets/index-DhmwAFpV.js"></script>
<script type="module" crossorigin src="/admin/assets/index-BLul_Qh8.js"></script>
<link rel="modulepreload" crossorigin href="/admin/assets/runtime-core.esm-bundler-CnFWH3R5.js">
<link rel="modulepreload" crossorigin href="/admin/assets/axios-CiYFffbI.js">
<link rel="stylesheet" crossorigin href="/admin/assets/index-Dn_JHm42.css">
<link rel="stylesheet" crossorigin href="/admin/assets/index-CpG9zv6u.css">
</head>
<body>
<div id="app"></div>

@ -6,6 +6,7 @@ use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\AdminUserController;
use App\Http\Controllers\Api\ReservationVerifyController;
use App\Http\Controllers\Api\VenueController;
use App\Http\Controllers\Api\VenueImportController;
use App\Http\Controllers\Api\AdminMenuController;
use App\Http\Controllers\Api\ActivityController;
use App\Http\Controllers\Api\ActivityBookingController;
@ -93,6 +94,9 @@ Route::middleware(['auth:sanctum', 'audit.log'])->group(function () {
Route::get('/venues', [VenueController::class, 'index']);
Route::post('/venues', [VenueController::class, 'store']);
Route::get('/venues/import/template', [VenueImportController::class, 'template']);
Route::post('/venues/import/preview', [VenueImportController::class, 'preview']);
Route::post('/venues/import/confirm', [VenueImportController::class, 'confirm']);
Route::put('/venues/{id}', [VenueController::class, 'update'])->whereNumber('id');
Route::post('/venues/{id}/audit/approve', [VenueController::class, 'approve'])->whereNumber('id');
Route::post('/venues/{id}/audit/reject', [VenueController::class, 'reject'])->whereNumber('id');

Loading…
Cancel
Save