feat: 皮肤包上传管理功能,持久化数据目录
- 新增皮肤包上传/列表/删除 API(基于 upload_key) - 构建时自动解压 ZIP 皮肤包应用到项目 - 配置和皮肤包迁移到 backend/data/ 目录,切分支不会丢失 - 旧式目录皮肤自动迁移为 ZIP 格式 - 前端皮肤包管理 UI(上传、删除、下拉选择) - .gitignore 排除运行时数据目录
This commit is contained in:
@@ -484,12 +484,43 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>主题目录</label>
|
||||
<input v-model="appForm.certificates[certType].theme" type="text" placeholder="AutoPacking/ymh">
|
||||
<select v-if="appSkins.length"
|
||||
:value="appSkins.some(s => s.name.replace('.zip', '') === appForm.certificates[certType].theme) ? appForm.certificates[certType].theme : ''"
|
||||
@change="appForm.certificates[certType].theme = $event.target.value"
|
||||
class="theme-select">
|
||||
<option value="">无</option>
|
||||
<option v-for="skin in appSkins" :key="skin.name" :value="skin.name.replace('.zip', '')">
|
||||
{{ skin.name.replace('.zip', '') }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-if="!appSkins.length || !appSkins.some(s => s.name.replace('.zip', '') === appForm.certificates[certType].theme)" style="margin-top: 4px;">
|
||||
<input v-model="appForm.certificates[certType].theme" type="text" placeholder="AutoPacking/ymh">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 皮肤包管理 -->
|
||||
<h4 class="section-title" style="margin-top: 20px;">皮肤包管理</h4>
|
||||
<div class="skins-section">
|
||||
<div v-if="!appForm.upload_key" class="skins-hint">请先保存 APP 后再上传皮肤包</div>
|
||||
<div v-else>
|
||||
<div class="skins-list" v-if="appSkins.length">
|
||||
<div v-for="skin in appSkins" :key="skin.name" class="skin-item">
|
||||
<span class="skin-name">{{ skin.name }}</span>
|
||||
<span class="skin-size">{{ formatSize(skin.size) }}</span>
|
||||
<button class="btn-icon btn-danger-icon" @click="deleteSkin(skin.name)" title="删除">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="skins-hint">暂无皮肤包</div>
|
||||
<label class="btn btn-outline skin-upload-btn" :class="{ disabled: skinUploading }">
|
||||
{{ skinUploading ? '上传中...' : '上传皮肤包 (.zip)' }}
|
||||
<input type="file" accept=".zip" @change="uploadSkin" :disabled="skinUploading" hidden>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end;">
|
||||
<button class="action-btn" style="padding: 8px 16px;" @click="showAppModal = false">取消</button>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 8px 24px;" @click="saveApp">保存</button>
|
||||
@@ -567,6 +598,8 @@ const serverForm = ref({ name: '', api: '', assDom: '', universalLink: '' })
|
||||
const showAppModal = ref(false)
|
||||
const editingAppId = ref(null)
|
||||
const appForm = ref({})
|
||||
const appSkins = ref([])
|
||||
const skinUploading = ref(false)
|
||||
|
||||
const showSchemeModal = ref(false)
|
||||
const editingSchemeId = ref(null)
|
||||
@@ -760,6 +793,7 @@ const openAppModal = (id = null, app = null) => {
|
||||
}
|
||||
}
|
||||
showAppModal.value = true
|
||||
loadSkins()
|
||||
}
|
||||
|
||||
const onServerChange = () => {
|
||||
@@ -786,6 +820,60 @@ const toggleCert = (certType) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 皮肤包管理
|
||||
const loadSkins = async () => {
|
||||
const uploadKey = appForm.value.upload_key
|
||||
if (!uploadKey) { appSkins.value = []; return }
|
||||
try {
|
||||
const res = await authFetch(`/api/config/skins/${uploadKey}`)
|
||||
if (res.ok) appSkins.value = await res.json()
|
||||
} catch { appSkins.value = [] }
|
||||
}
|
||||
|
||||
const uploadSkin = async (event) => {
|
||||
const file = event.target.files[0]
|
||||
if (!file) return
|
||||
const uploadKey = appForm.value.upload_key
|
||||
if (!uploadKey) { alert('请先保存 APP 后再上传皮肤包'); return }
|
||||
skinUploading.value = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const res = await authFetch(`/api/config/skins/${uploadKey}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
throw new Error(err.detail || '上传失败')
|
||||
}
|
||||
await loadSkins()
|
||||
event.target.value = ''
|
||||
} catch (e) {
|
||||
alert('上传失败: ' + e.message)
|
||||
} finally {
|
||||
skinUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSkin = async (skinName) => {
|
||||
if (!confirm(`确定删除皮肤包 ${skinName}?`)) return
|
||||
const uploadKey = appForm.value.upload_key
|
||||
try {
|
||||
const res = await authFetch(`/api/config/skins/${uploadKey}/${skinName}`, { method: 'DELETE' })
|
||||
if (!res.ok) throw new Error('删除失败')
|
||||
await loadSkins()
|
||||
} catch (e) {
|
||||
alert('删除失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||
}
|
||||
|
||||
const saveApp = async () => {
|
||||
if (!appForm.value.name || !appForm.value.AppGuid) {
|
||||
alert('请填写必填字段:App 名称、AppGuid')
|
||||
@@ -1030,4 +1118,19 @@ const formatJson = () => {
|
||||
.checkbox-label { display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: 500; }
|
||||
.checkbox-label input { width: 16px; height: 16px; }
|
||||
.cert-fields { padding-top: 8px; }
|
||||
.skins-section { padding: 12px; background: #fafafa; border-radius: 8px; }
|
||||
.skins-hint { color: #999; font-size: 13px; margin-bottom: 8px; }
|
||||
.skins-list { margin-bottom: 12px; }
|
||||
.skin-item { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.skin-item:last-child { border-bottom: none; }
|
||||
.skin-name { font-size: 14px; color: #333; flex: 1; }
|
||||
.skin-size { font-size: 12px; color: #999; }
|
||||
.btn-icon { width: 24px; height: 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; line-height: 1; display: flex; align-items: center; justify-content: center; }
|
||||
.btn-danger-icon { background: #fff1f0; color: #ff4d4f; }
|
||||
.btn-danger-icon:hover { background: #ff4d4f; color: white; }
|
||||
.btn-outline { background: white; border: 1px solid #1890ff; color: #1890ff; cursor: pointer; padding: 6px 16px; border-radius: 6px; font-size: 13px; display: inline-block; }
|
||||
.btn-outline:hover { background: #e6f7ff; }
|
||||
.btn-outline.disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.skin-upload-btn { margin-top: 8px; }
|
||||
.theme-select { width: 100%; padding: 10px 12px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 14px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user