[FEAT] 新增 APK 上傳檔名自動解析與優化中國醫風味選項
1. 新增 APK 上傳時的檔名自動解析功能(可從 app-[flavor]-[version_name] 格式自動解析並填入版本名稱與機台風味)。 2. 新增版本代碼的自動數學推導,將 Major_Minor_Patch 轉換為整數(例如 21_01_6_R 轉換為 210106)並自動填入。 3. 將原本「中國醫客製」風味選項由 S_12_XY_U_Cmuh 修正為 S_9_XY_U_Cmuh(S9 主板)。 4. 重構 create.blade.php 表單,將原本位於 HTML x-data 屬性內部的複雜 JS 邏輯抽離至獨立的 <script> 區塊(apkVersionForm 函數),解決 HTML 屬性與 JS 引號/括號衝突導致的渲染問題。 5. 引入 Alpine.js 的 x-model 機制對版本名稱(versionName)與版本代碼進行雙向綁定,確保自動解析賦值的強健性。
This commit is contained in:
parent
dd5e78a9ff
commit
b0fc0e6520
@ -19,76 +19,7 @@
|
||||
</x-page-header>
|
||||
|
||||
<form id="create-apk-form"
|
||||
x-data="{
|
||||
uploadMode: 'file',
|
||||
fileName: '',
|
||||
isDragging: false,
|
||||
isForced: false,
|
||||
versionCode: '{{ old('version_code', '') }}',
|
||||
|
||||
adjustVersionCode(delta) {
|
||||
const current = parseInt(this.versionCode) || 0;
|
||||
const next = Math.max(1, current + delta);
|
||||
this.versionCode = next.toString();
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}"
|
||||
x-data="apkVersionForm()"
|
||||
@submit.prevent="submitForm"
|
||||
action="{{ route('admin.basic-settings.apk-versions.store') }}"
|
||||
method="POST"
|
||||
@ -116,7 +47,7 @@
|
||||
{{-- 版本名稱 --}}
|
||||
<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') }}">
|
||||
<input type="text" name="version_name" x-model="versionName" 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>
|
||||
{{-- 版本代碼 --}}
|
||||
@ -152,7 +83,7 @@
|
||||
{{-- S號 / Android 12 / XY主板 × 客戶專案 --}}
|
||||
<option value="S_12_XY_U_Chengwai" data-title="S_12_XY_U_Chengwai (晟崴客製 / S12 / XY主板)">S_12_XY_U_Chengwai (晟崴客製 / S12 / XY主板)</option>
|
||||
<option value="S_12_XY_U_General" data-title="S_12_XY_U_General (通用主幹 / S12 / XY主板)">S_12_XY_U_General (通用主幹 / S12 / XY主板)</option>
|
||||
<option value="S_12_XY_U_Cmuh" data-title="S_12_XY_U_Cmuh (中國醫客製 / S12 / XY主板)">S_12_XY_U_Cmuh (中國醫客製 / S12 / XY主板)</option>
|
||||
<option value="S_9_XY_U_Cmuh" data-title="S_9_XY_U_Cmuh (中國醫客製 / S9 / XY主板)">S_9_XY_U_Cmuh (中國醫客製 / S9 / XY主板)</option>
|
||||
</x-searchable-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>
|
||||
@ -292,4 +223,121 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function apkVersionForm() {
|
||||
return {
|
||||
uploadMode: 'file',
|
||||
fileName: '',
|
||||
isDragging: false,
|
||||
isForced: false,
|
||||
versionName: '{{ old('version_name', '') }}',
|
||||
versionCode: '{{ old('version_code', '') }}',
|
||||
|
||||
adjustVersionCode(delta) {
|
||||
const current = parseInt(this.versionCode) || 0;
|
||||
const next = Math.max(1, current + delta);
|
||||
this.versionCode = next.toString();
|
||||
},
|
||||
|
||||
submitForm() {
|
||||
const form = this.$el;
|
||||
const versionName = this.versionName.trim();
|
||||
const versionCode = this.versionCode.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();
|
||||
},
|
||||
|
||||
parseApkName(name) {
|
||||
const baseName = name.replace(/\.apk$/i, '');
|
||||
|
||||
// Match "app-{flavor}-{versionName}" pattern
|
||||
const pattern = /^app-([^-]+)-(\d+_\d+_\d+_[a-zA-Z0-9]+)/i;
|
||||
const match = baseName.match(pattern);
|
||||
|
||||
if (match) {
|
||||
const flavor = match[1];
|
||||
const versionName = match[2];
|
||||
|
||||
// 1. Fill Version Name (Alpine.js model binding)
|
||||
this.versionName = versionName;
|
||||
|
||||
// 2. Parse & Calculate Version Code (e.g. 10_01_6_R -> 10 * 10000 + 1 * 100 + 6 = 100106)
|
||||
const parts = versionName.split('_');
|
||||
if (parts.length >= 3) {
|
||||
const major = parseInt(parts[0]) || 0;
|
||||
const minor = parseInt(parts[1]) || 0;
|
||||
const patch = parseInt(parts[2]) || 0;
|
||||
this.versionCode = (major * 10000 + minor * 100 + patch).toString();
|
||||
}
|
||||
|
||||
// 3. Select Flavor (Preline HSSelect integration)
|
||||
const selectEl = document.getElementById('apk-flavor-select');
|
||||
if (selectEl) {
|
||||
selectEl.value = flavor;
|
||||
const instance = window.HSSelect?.getInstance(selectEl);
|
||||
if (instance) {
|
||||
instance.setValue(flavor);
|
||||
}
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: '{{ __('已自動解析 APK 檔名並填入欄位!') }}', type: 'success' }
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
this.parseApkName(this.fileName);
|
||||
}
|
||||
},
|
||||
|
||||
handleFileSelect(event) {
|
||||
if (event.target.files.length > 0) {
|
||||
this.fileName = event.target.files[0].name;
|
||||
this.parseApkName(this.fileName);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
Loading…
Reference in New Issue
Block a user