star-cloud/resources/views/admin/basic-settings/apk-versions/create.blade.php
sky121113 93845c990e [FIX] 修正 APK 版本管理 UI 間距、深色模式 Bug 與三語系翻譯對齊
1. 將 APK 列表與新增頁面的自定義 Header 改用系統標準 <x-page-header> 元件,外層容器垂直間距統一調整為 space-y-4,完美對齊麵包屑與後台標準比例。
2. 修正前端 Alpine.js x-text 指令中誤用 PHP 二元運算子 ?: 導致機台 APP 版本號渲染空白的 JavaScript 語法 Bug。
3. 修正 Modal 表格清單的 thead 背景誤用不存在的 slate-850 導致深色模式下顯示突兀純白的問題,調整為更具層次感的 bg-slate-50/50 dark:bg-slate-950/40。
4. 補齊 Version Info、Forced Update 與 Optional Update 等三語系翻譯,並移除舊文案,採用 ksort 自動完成三語系 Perfect Align 字母排序對齊。
5. 優化強制更新與重要注意事項的文案及視覺樣式,調整強制更新啟用時的玫瑰紅 (rose) 警示背景,並放大重要通知的字體以提高可讀性。
2026-05-27 09:54:43 +08:00

286 lines
21 KiB
PHP

@extends('layouts.admin')
@section('content')
<div class="space-y-4 pb-20">
<!-- Header -->
<x-page-header
:title="__('Upload New APK')"
:subtitle="__('Register a new firmware version for OTA deployment')"
>
<x-slot name="back">
<a href="{{ route('admin.basic-settings.apk-versions.index') }}" class="p-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 text-slate-500 hover:text-cyan-500 transition-all shadow-sm flex items-center justify-center">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
</a>
</x-slot>
<button type="submit" form="create-apk-form" class="btn-luxury-primary px-8 flex items-center gap-2">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
<span>{{ __('Save Version') }}</span>
</button>
</x-page-header>
<form id="create-apk-form"
x-data="{
uploadMode: 'file',
fileName: '',
isDragging: false,
isForced: false,
submitForm() {
const form = this.$el;
const versionName = form.querySelector('[name=version_name]').value.trim();
const versionCode = form.querySelector('[name=version_code]').value.trim();
if (!versionName) {
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: '{{ __('Please enter version name') }}', type: 'error' }
}));
return;
}
if (!versionCode || isNaN(versionCode) || parseInt(versionCode) <= 0) {
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: '{{ __('Version code must be a positive integer') }}', type: 'error' }
}));
return;
}
if (this.uploadMode === 'file') {
const fileInput = form.querySelector('[name=apk_file]');
if (!fileInput.files.length) {
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: '{{ __('Please select an APK file to upload') }}', type: 'error' }
}));
return;
}
} else {
const urlInput = form.querySelector('[name=download_url]').value.trim();
if (!urlInput) {
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: '{{ __('Please provide a download URL') }}', type: 'error' }
}));
return;
}
}
form.submit();
},
handleFileDrop(event) {
this.isDragging = false;
const files = event.dataTransfer.files;
if (files.length > 0) {
const fileInput = this.$refs.apkFileInput;
const dt = new DataTransfer();
dt.items.add(files[0]);
fileInput.files = dt.files;
this.fileName = files[0].name;
}
},
handleFileSelect(event) {
if (event.target.files.length > 0) {
this.fileName = event.target.files[0].name;
}
}
}"
@submit.prevent="submitForm"
action="{{ route('admin.basic-settings.apk-versions.store') }}"
method="POST"
enctype="multipart/form-data"
class="space-y-6"
novalidate>
@csrf
{{-- 錯誤提示 --}}
@if ($errors->any())
<div class="p-5 bg-rose-500/10 border border-rose-500/20 text-rose-600 dark:text-rose-400 rounded-2xl flex items-start gap-4 animate-luxury-in font-bold">
<div class="w-8 h-8 bg-rose-500/20 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
</div>
<div>
<p class="text-sm tracking-wide">{{ __('Please check the following errors:') }}</p>
<ul class="mt-1 text-xs list-disc list-inside opacity-80">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
{{-- 左欄:版本基本資訊 --}}
<div class="lg:col-span-7 space-y-6">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in group/card">
<div class="flex items-center gap-5 mb-8">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-cyan-500/10 to-teal-500/10 flex items-center justify-center text-cyan-500 border border-cyan-500/20 shadow-lg shadow-cyan-500/5 group-hover/card:scale-110 transition-transform duration-500">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25"/></svg>
</div>
<div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight font-display uppercase">{{ __('Version Information') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Firmware identification details') }}</p>
</div>
</div>
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{{-- 版本名稱 --}}
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Version Name') }} <span class="text-rose-500">*</span></label>
<input type="text" name="version_name" value="{{ old('version_name') }}" required class="luxury-input w-full" placeholder="{{ __('e.g., 10_08_6_R') }}">
<p class="mt-1.5 text-[10px] font-bold text-slate-400/70 dark:text-slate-500/70 tracking-wide">{{ __('Format: Major_Minor_Patch_R') }}</p>
</div>
{{-- 版本代碼 --}}
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Version Code') }} <span class="text-rose-500">*</span></label>
<input type="number" name="version_code" value="{{ old('version_code') }}" required min="1" class="luxury-input w-full" placeholder="{{ __('e.g., 100806') }}">
<p class="mt-1.5 text-[10px] font-bold text-slate-400/70 dark:text-slate-500/70 tracking-wide">{{ __('Must be strictly greater than previous version') }}</p>
</div>
</div>
{{-- 機台風味 --}}
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Machine Flavor') }} <span class="text-rose-500">*</span></label>
<select name="flavor" required class="luxury-input w-full">
<option value="" disabled {{ old('flavor') ? '' : 'selected' }}>{{ __('Select flavor...') }}</option>
<option value="S_12_XY_U_Standard_" {{ old('flavor') == 'S_12_XY_U_Standard_' ? 'selected' : '' }}>S_12_XY_U_Standard_ (S號 / Android 12 / XY主板)</option>
</select>
<p class="mt-1.5 text-[10px] font-bold text-slate-400/70 dark:text-slate-500/70 tracking-wide">{{ __('Target hardware flavor for this APK') }}</p>
</div>
{{-- 更新說明 --}}
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Release Notes') }}</label>
<textarea name="release_notes" rows="4" class="luxury-input w-full resize-none" placeholder="{{ __('Describe the changes in this version...') }}">{{ old('release_notes') }}</textarea>
</div>
</div>
</div>
</div>
{{-- 右欄:檔案上傳與更新模式 --}}
<div class="lg:col-span-5 space-y-6">
{{-- 上傳模式卡片 --}}
<div class="luxury-card rounded-3xl p-8 animate-luxury-in group/card" style="animation-delay: 50ms">
<div class="flex items-center gap-5 mb-8">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500/10 to-indigo-500/10 flex items-center justify-center text-violet-500 border border-violet-500/20 shadow-lg shadow-violet-500/5 group-hover/card:scale-110 transition-transform duration-500">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/></svg>
</div>
<div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight font-display uppercase">{{ __('APK Source') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Upload file or provide URL') }}</p>
</div>
</div>
{{-- 上傳模式切換 --}}
<div class="flex rounded-xl bg-slate-100 dark:bg-slate-800/80 p-1 mb-6">
<button type="button"
@click="uploadMode = 'file'"
:class="uploadMode === 'file' ? 'bg-white dark:bg-slate-700 text-cyan-600 dark:text-cyan-400 shadow-sm' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'"
class="flex-1 py-2.5 text-xs font-black uppercase tracking-widest rounded-lg transition-all duration-300 flex items-center justify-center gap-2">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/></svg>
{{ __('File Upload') }}
</button>
<button type="button"
@click="uploadMode = 'url'"
:class="uploadMode === 'url' ? 'bg-white dark:bg-slate-700 text-cyan-600 dark:text-cyan-400 shadow-sm' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'"
class="flex-1 py-2.5 text-xs font-black uppercase tracking-widest rounded-lg transition-all duration-300 flex items-center justify-center gap-2">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-1.637a4.5 4.5 0 00-1.242-7.244l-4.5-4.5a4.5 4.5 0 00-6.364 6.364L4.343 8.25"/></svg>
{{ __('External URL') }}
</button>
</div>
{{-- 檔案上傳區域 --}}
<div x-show="uploadMode === 'file'" x-transition>
<div class="relative border-2 border-dashed rounded-2xl p-8 text-center transition-all duration-300 cursor-pointer"
:class="isDragging ? 'border-cyan-500 bg-cyan-500/5 dark:bg-cyan-500/10 scale-[1.02]' : 'border-slate-200 dark:border-slate-700 hover:border-cyan-400 dark:hover:border-cyan-600 hover:bg-slate-50/50 dark:hover:bg-slate-800/50'"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="handleFileDrop($event)"
@click="$refs.apkFileInput.click()">
<input type="file"
name="apk_file"
x-ref="apkFileInput"
accept=".apk"
class="hidden"
@change="handleFileSelect($event)">
<div class="flex flex-col items-center gap-4">
<div class="w-16 h-16 rounded-2xl flex items-center justify-center transition-all duration-300"
:class="isDragging ? 'bg-cyan-500 text-white scale-110' : fileName ? 'bg-emerald-500 text-white' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<svg x-show="!fileName" class="w-8 h-8 stroke-[1.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5h10.5a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0017.25 4.5H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25z"/></svg>
<svg x-show="fileName" class="w-8 h-8 stroke-[1.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<div>
<p x-show="!fileName" class="text-sm font-extrabold text-slate-600 dark:text-slate-300">
{{ __('Drag & drop your APK here') }}
</p>
<p x-show="fileName" class="text-sm font-extrabold text-emerald-600 dark:text-emerald-400" x-text="fileName"></p>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-2">
{{ __('Maximum file size: 100MB') }} · .apk
</p>
</div>
</div>
</div>
</div>
{{-- 外連 URL 輸入區域 --}}
<div x-show="uploadMode === 'url'" x-transition>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Download URL') }}</label>
<input type="url" name="download_url" value="{{ old('download_url') }}" class="luxury-input w-full" placeholder="https://example.com/release/app-v10.8.6.apk">
<p class="mt-1.5 text-[10px] font-bold text-slate-400/70 dark:text-slate-500/70 tracking-wide">{{ __('Provide a publicly accessible direct download link') }}</p>
</div>
</div>
{{-- 更新模式卡片 --}}
<div class="luxury-card rounded-3xl p-8 animate-luxury-in group/card" style="animation-delay: 100ms">
<div class="flex items-center gap-5 mb-8">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/10 to-orange-500/10 flex items-center justify-center text-amber-500 border border-amber-500/20 shadow-lg shadow-amber-500/5 group-hover/card:scale-110 transition-transform duration-500">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>
</div>
<div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight font-display uppercase">{{ __('Update Mode') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Control deployment behavior') }}</p>
</div>
</div>
{{-- 強制更新開關 --}}
<div class="flex items-center justify-between p-5 rounded-2xl border transition-all duration-300"
:class="isForced
? 'bg-rose-500/10 dark:bg-rose-500/10 border-rose-500/20 dark:border-rose-500/30 text-rose-600 dark:text-rose-400 shadow-sm shadow-rose-500/5'
: 'bg-slate-50/80 dark:bg-slate-800/40 border-slate-100 dark:border-slate-800/80 text-slate-700 dark:text-slate-200'">
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300"
:class="isForced ? 'bg-rose-500/10 text-rose-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"/></svg>
</div>
<div>
<p class="text-sm font-extrabold transition-colors"
:class="isForced ? 'text-rose-600 dark:text-rose-400' : 'text-slate-700 dark:text-slate-200'">{{ __('Forced Update') }}</p>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 tracking-wide mt-0.5">{{ __('During OTA deployment, the device will install directly in the background without user confirmation.') }}</p>
</div>
</div>
<label class="relative inline-flex items-center cursor-pointer shrink-0">
<input type="hidden" name="is_forced" value="0">
<input type="checkbox" name="is_forced" value="1" x-model="isForced" class="sr-only peer" {{ old('is_forced') ? 'checked' : '' }}>
<div class="w-11 h-6 bg-slate-200 dark:bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-slate-600 peer-checked:bg-rose-500"></div>
</label>
</div>
{{-- 注意事項 --}}
<div class="mt-6 p-5 rounded-2xl bg-amber-500/10 border border-amber-500/20 shadow-sm shadow-amber-500/5">
<div class="flex items-start gap-4">
<svg class="w-5 h-5 stroke-[2.5] text-amber-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
<div class="text-xs font-bold text-amber-800 dark:text-amber-300 tracking-wide leading-relaxed">
<p class="text-sm font-black uppercase tracking-widest mb-1.5 flex items-center gap-1">{{ __('Important Notice') }}</p>
<p class="font-bold opacity-90 leading-relaxed">{{ __('Version Code must be strictly greater than the previous release, otherwise the OTA silent installation on the device will fail with INSTALL_FAILED_ALREADY_EXISTS.') }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
@endsection