feat: 皮肤站玩家管理(新建/重命名/删除/默认角色/切换正版)
Sync source to CNB / Sync Git ref (push) Skipped

- skin-site-api.ts 补 setDefault/clearDefault/defaultPlayer/switchMojang/adaptLegacyInternal,并恢复 deleteLauncherRelease
- SkinSiteProfileView 增加玩家管理 UI 与状态
- en-US / zh-CN 各补 15 个 profile-view 文案
This commit is contained in:
AxTps committed 2026-10-21 22:19:47 +08:00
1 parent 0aa901ad48
commit bc37e5d436
4 files changed
+346 -38

No files matched your search

@@ -12,9 +12,16 @@ import {
skinSiteUser,
} from '@/composables/skin-site-session'
import {
skinSiteAddPlayer,
skinSiteClearDefaultPlayer,
skinSiteDefaultPlayer,
skinSiteDeletePlayer,
skinSiteDeleteTexture,
type SkinSitePlayerSkin,
skinSitePlayerSkin,
type SkinSitePlayerSkin,
skinSiteRenamePlayer,
skinSiteSetDefaultPlayer,
skinSiteSwitchMojang,
skinSiteUploadTexture,
type SkinTextureType,
} from '@/helpers/skin-site-api'
@@ -51,6 +58,36 @@ const messages = defineMessages({
uploaded: { id: 'app.skin-site.profile-view.uploaded', defaultMessage: 'Texture updated.' },
removed: { id: 'app.skin-site.profile-view.removed', defaultMessage: 'Texture removed.' },
back: { id: 'app.skin-site.profile-view.back', defaultMessage: 'Back to home' },
addPlayer: { id: 'app.skin-site.profile-view.add-player', defaultMessage: 'New player' },
playerNamePlaceholder: {
id: 'app.skin-site.profile-view.player-name-placeholder',
defaultMessage: 'Player name',
},
confirm: { id: 'app.skin-site.profile-view.confirm', defaultMessage: 'Confirm' },
cancel: { id: 'app.skin-site.profile-view.cancel', defaultMessage: 'Cancel' },
rename: { id: 'app.skin-site.profile-view.rename', defaultMessage: 'Rename' },
delete: { id: 'app.skin-site.profile-view.delete', defaultMessage: 'Delete' },
setDefault: { id: 'app.skin-site.profile-view.set-default', defaultMessage: 'Set as default' },
clearDefault: {
id: 'app.skin-site.profile-view.clear-default',
defaultMessage: 'Clear default',
},
toggleMojang: {
id: 'app.skin-site.profile-view.toggle-mojang',
defaultMessage: 'Toggle Mojang',
},
isDefault: { id: 'app.skin-site.profile-view.is-default', defaultMessage: 'Default' },
added: { id: 'app.skin-site.profile-view.added', defaultMessage: 'Player added.' },
renamed: { id: 'app.skin-site.profile-view.renamed', defaultMessage: 'Player renamed.' },
deleted: { id: 'app.skin-site.profile-view.deleted', defaultMessage: 'Player deleted.' },
defaultSet: {
id: 'app.skin-site.profile-view.default-set',
defaultMessage: 'Default player updated.',
},
mojangToggled: {
id: 'app.skin-site.profile-view.mojang-toggled',
defaultMessage: 'Mojang status updated.',
},
})
const selectedId = ref<string | null>(null)
const texture = ref<SkinSitePlayerSkin>({})
@@ -93,6 +130,141 @@ watch(selectedId, (id) => {
else texture.value = {}
})
// --- Player management ---
const defaultUuid = ref<string | null>(null)
const busy = ref<string | null>(null)
async function loadDefaultPlayer() {
if (!signedIn.value) {
defaultUuid.value = null
return
}
try {
const res = await skinSiteDefaultPlayer()
defaultUuid.value = res.success ? (res.payload?.uuid ?? null) : null
} catch {
defaultUuid.value = null
}
}
const showAdd = ref(false)
const newName = ref('')
const renamingId = ref<string | null>(null)
const renameValue = ref('')
const confirmingDeleteId = ref<string | null>(null)
function resetDialogs() {
showAdd.value = false
newName.value = ''
renamingId.value = null
renameValue.value = ''
confirmingDeleteId.value = null
}
async function submitAdd() {
const name = newName.value.trim()
if (!name || busy.value) return
busy.value = 'add'
try {
const res = await skinSiteAddPlayer(name)
if (!res.success) throw new Error(res.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.added) })
resetDialogs()
await requestSkinSitePlayers()
await loadDefaultPlayer()
} catch (error) {
handleError(error)
} finally {
busy.value = null
}
}
function startRename(player: { uuid: string; name: string }) {
renamingId.value = player.uuid
renameValue.value = player.name
}
async function submitRename() {
const name = renameValue.value.trim()
if (!renamingId.value || !name || busy.value) return
busy.value = 'rename'
try {
const res = await skinSiteRenamePlayer(renamingId.value, name)
if (!res.success) throw new Error(res.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.renamed) })
resetDialogs()
await requestSkinSitePlayers()
} catch (error) {
handleError(error)
} finally {
busy.value = null
}
}
async function submitDelete(uuid: string) {
if (busy.value) return
busy.value = 'delete'
try {
const res = await skinSiteDeletePlayer(uuid)
if (!res.success) throw new Error(res.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.deleted) })
resetDialogs()
if (selectedId.value === uuid) selectedId.value = null
await requestSkinSitePlayers()
await loadDefaultPlayer()
} catch (error) {
handleError(error)
} finally {
busy.value = null
}
}
async function setDefaultPlayer(uuid: string) {
if (busy.value) return
busy.value = 'default'
try {
const res = await skinSiteSetDefaultPlayer(uuid)
if (!res.success) throw new Error(res.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.defaultSet) })
await loadDefaultPlayer()
} catch (error) {
handleError(error)
} finally {
busy.value = null
}
}
async function clearDefaultPlayer() {
if (busy.value) return
busy.value = 'default'
try {
const res = await skinSiteClearDefaultPlayer()
if (!res.success) throw new Error(res.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.defaultSet) })
await loadDefaultPlayer()
} catch (error) {
handleError(error)
} finally {
busy.value = null
}
}
async function toggleMojang(uuid: string) {
if (busy.value) return
busy.value = 'mojang'
try {
const res = await skinSiteSwitchMojang(uuid)
if (!res.success) throw new Error(res.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.mojangToggled) })
await requestSkinSitePlayers()
await loadDefaultPlayer()
} catch (error) {
handleError(error)
} finally {
busy.value = null
}
}
async function onUpload(type: SkinTextureType, event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
@@ -135,7 +307,10 @@ async function removeTexture(type: SkinTextureType) {
}
onMounted(() => {
if (signedIn.value) void requestSkinSitePlayers().catch(() => {})
if (signedIn.value) {
void requestSkinSitePlayers().catch(() => {})
void loadDefaultPlayer()
}
})
watch(signedIn, (value) => {
@@ -161,9 +336,46 @@ watch(signedIn, (value) => {
<div
class="flex flex-col gap-2 rounded-2xl border border-solid border-surface-5 bg-bg-raised p-3"
>
<h3 class="m-0 text-base font-semibold text-contrast">
{{ formatMessage(messages.title) }}
</h3>
<div class="flex items-center justify-between gap-2">
<h3 class="m-0 text-base font-semibold text-contrast">
{{ formatMessage(messages.title) }}
</h3>
<button
v-if="!showAdd"
type="button"
class="shrink-0 rounded-full bg-brand px-2.5 py-1 text-xs font-semibold text-contrast hover:brightness-110"
@click="showAdd = true"
>
+ {{ formatMessage(messages.addPlayer) }}
</button>
</div>
<div v-if="showAdd" class="flex flex-col gap-1.5 rounded-xl bg-button-bg p-2">
<input
v-model="newName"
type="text"
:placeholder="formatMessage(messages.playerNamePlaceholder)"
class="w-full rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-xs text-contrast outline-none focus:border-brand"
@keyup.enter="submitAdd"
/>
<div class="flex gap-1.5">
<button
type="button"
class="flex-1 rounded-lg bg-brand px-2 py-1 text-xs font-semibold text-contrast disabled:opacity-50"
:disabled="busy === 'add' || !newName.trim()"
@click="submitAdd"
>
{{ formatMessage(messages.confirm) }}
</button>
<button
type="button"
class="rounded-lg bg-bg-raised px-2 py-1 text-xs font-semibold text-secondary"
@click="resetDialogs"
>
{{ formatMessage(messages.cancel) }}
</button>
</div>
</div>
<div v-if="playersLoading" class="flex items-center gap-2 text-sm text-secondary">
<SpinnerIcon class="size-4 animate-spin" />
@@ -172,18 +384,17 @@ watch(signedIn, (value) => {
<p v-else-if="!players.length" class="m-0 text-sm text-secondary">
{{ formatMessage(messages.noPlayers) }}
</p>
<button
v-for="player in players"
:key="player.uuid"
type="button"
class="flex items-center gap-2 rounded-xl px-2 py-1.5 text-left transition-colors"
:class="
player.uuid === selectedId
? 'bg-brand-highlight text-contrast'
: 'bg-button-bg hover:brightness-110'
"
@click="selectPlayer(player.uuid)"
>
<div
v-for="player in players"
:key="player.uuid"
class="flex items-center gap-2 rounded-xl px-2 py-1.5 transition-colors"
:class="
player.uuid === selectedId
? 'bg-brand-highlight text-contrast'
: 'bg-button-bg hover:brightness-110'
"
@click="selectPlayer(player.uuid)"
>
<Avatar
:src="player.headDataUrl"
:alt="player.name"
@@ -195,7 +406,40 @@ watch(signedIn, (value) => {
<span v-if="player.isMojang" class="shrink-0 text-[10px] uppercase text-secondary"
>Mojang</span
>
</button>
<div class="flex shrink-0 items-center gap-1">
<button
v-if="defaultUuid === player.uuid"
type="button"
class="rounded px-1.5 py-0.5 text-[10px] font-semibold text-brand"
@click.stop="clearDefaultPlayer"
>
{{ formatMessage(messages.clearDefault) }}
</button>
<button
v-else
type="button"
class="rounded px-1.5 py-0.5 text-[10px] font-semibold text-secondary hover:bg-bg"
@click.stop="setDefaultPlayer(player.uuid)"
>
{{ formatMessage(messages.setDefault) }}
</button>
<button
type="button"
class="rounded px-1.5 py-0.5 text-[10px] font-semibold text-secondary hover:bg-bg"
@click.stop="startRename(player)"
>
{{ formatMessage(messages.rename) }}
</button>
<button
type="button"
class="rounded px-1.5 py-0.5 text-[10px] font-semibold text-red hover:bg-bg"
:disabled="busy !== null"
@click.stop="submitDelete(player.uuid)"
>
{{ formatMessage(messages.delete) }}
</button>
</div>
</div>
</div>
<!-- Preview + texture actions -->
+54 -20
View File
@@ -336,6 +336,40 @@ export function skinSiteRenamePlayer(
{ method: 'POST', type: 'skin' },
)
}
/** Mark a player as the account default (used by launcher login). */
export function skinSiteSetDefaultPlayer(uuid: string): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>(`player/default/set?uuid=${encodeURIComponent(uuid)}`, {
method: 'POST',
type: 'skin',
})
}
/** Clear the account default player. */
export function skinSiteClearDefaultPlayer(): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>('player/default', { method: 'DELETE', type: 'skin' })
}
/** Fetch the account default player, if one is set. */
export function skinSiteDefaultPlayer(): Promise<SkinSiteResponse<SkinSitePlayerInfo | null>> {
return skinSiteRequest<SkinSitePlayerInfo | null>('player/default', { type: 'skin' })
}
/** Toggle the Mojang (official) status of a player and resync its profile. */
export function skinSiteSwitchMojang(uuid: string): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>(`player/${encodeURIComponent(uuid)}/switchMojang`, {
method: 'GET',
type: 'skin',
})
}
/** Convert a legacy internal player so it can log in through the modern flow. */
export function skinSiteAdaptLegacyInternal(uuid: string): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>(`player/${encodeURIComponent(uuid)}/adaptLegacyInternal`, {
method: 'POST',
type: 'skin',
})
}
// ---------------------------------------------------------------------------
// Registration & password reset
// ---------------------------------------------------------------------------
@@ -414,8 +448,8 @@ export function skinSitePublicDonationSummary(): Promise<
return skinSiteRequest<SkinSitePublicDonationSummary>('donation/summary', { auth: false })
}
export function skinSiteDonationList(offset = 0): Promise<SkinSiteResponse<SkinSitePublicDonationPage>> {
return skinSiteRequest<SkinSitePublicDonationPage>(`donation/list?limit=20&offset=${offset}`, {
export function skinSiteDonationList(offset = 0): Promise<SkinSiteResponse<SkinSiteDonationPage>> {
return skinSiteRequest<SkinSiteDonationPage>(`donation/list?limit=20&offset=${offset}`, {
auth: false,
})
}
@@ -1217,7 +1251,6 @@ export function skinSiteActivateLauncherRelease(
export function skinSiteDeleteLauncherRelease(id: number): Promise<SkinSiteResponse<boolean>> {
return skinSiteRequest<boolean>('launcher/admin/' + id, { method: 'DELETE' })
}
export interface SkinSiteLauncherUploadStatus {
id: string
chunkSize: number
@@ -1865,7 +1898,6 @@ export function skinSiteWikiUpdateSettings(
})
}
export interface SkinSiteReviewPaperSummary {
id: string
enabled: boolean
@@ -1908,9 +1940,7 @@ export interface SkinSiteReviewConfigData {
prerequisitePermissions: string[]
}
export function skinSiteReviewPaperList(): Promise<
SkinSiteResponse<SkinSiteReviewPaperSummary[]>
> {
export function skinSiteReviewPaperList(): Promise<SkinSiteResponse<SkinSiteReviewPaperSummary[]>> {
return skinSiteRequest<SkinSiteReviewPaperSummary[]>('review/admin/papers')
}
@@ -1991,9 +2021,7 @@ export interface SkinSiteInviteCode {
code: string
}
export function skinSiteAdminBotInfo(
userUuid: string,
): Promise<SkinSiteResponse<SkinSiteBotInfo>> {
export function skinSiteAdminBotInfo(userUuid: string): Promise<SkinSiteResponse<SkinSiteBotInfo>> {
return skinSiteRequest<SkinSiteBotInfo>('admin/users/' + encodeURIComponent(userUuid) + '/bot')
}
@@ -2047,7 +2075,9 @@ export function skinSiteAdminUserAudit(params?: {
return skinSiteRequest<SkinSiteUserAdminAuditPage>('admin/users/audit' + suffix)
}
export function skinSiteAdminInviteUser(email: string): Promise<SkinSiteResponse<SkinSiteInviteCode>> {
export function skinSiteAdminInviteUser(
email: string,
): Promise<SkinSiteResponse<SkinSiteInviteCode>> {
return skinSiteRequest<SkinSiteInviteCode>('admin/inviteCode', {
method: 'POST',
body: { email },
@@ -2381,10 +2411,10 @@ export function skinSiteChatVoiceConfig(): Promise<
export function skinSiteChatVoiceConfigUpdate(
data: SkinSiteChatVoiceGlobalConfig,
): Promise<SkinSiteResponse<SkinSiteChatVoiceGlobalConfigUpdateResult>> {
return skinSiteRequest<SkinSiteChatVoiceGlobalConfigUpdateResult>(
'chat/voice/admin/config',
{ method: 'PUT', body: data },
)
return skinSiteRequest<SkinSiteChatVoiceGlobalConfigUpdateResult>('chat/voice/admin/config', {
method: 'PUT',
body: data,
})
}
export function skinSiteChatVoiceLogs(
@@ -2503,7 +2533,11 @@ export function skinSiteWikiMigrationCandidates(
export function skinSiteWikiMigrationDecide(
candidateId: number,
data: { decision: SkinSiteWikiMigrationDecision; targetTitle?: string | null; overwrite?: boolean },
data: {
decision: SkinSiteWikiMigrationDecision
targetTitle?: string | null
overwrite?: boolean
},
): Promise<SkinSiteResponse<void>> {
return skinSiteRequest<void>('wiki/admin/migration/candidates/' + candidateId, {
method: 'POST',
@@ -2520,9 +2554,7 @@ export function skinSiteWikiMigrationPublish(
)
}
export function skinSiteWikiMigrationRollback(
jobId: number,
): Promise<SkinSiteResponse<void>> {
export function skinSiteWikiMigrationRollback(jobId: number): Promise<SkinSiteResponse<void>> {
return skinSiteRequest<void>('wiki/admin/migration/' + jobId + '/rollback', {
method: 'POST',
})
@@ -2672,7 +2704,9 @@ export function skinSiteWikiPages(
return skinSiteRequest<SkinSiteWikiPageList>('wiki/pages?' + query.toString())
}
export function skinSiteWikiRecent(before?: number): Promise<SkinSiteResponse<SkinSiteWikiSearchPage>> {
export function skinSiteWikiRecent(
before?: number,
): Promise<SkinSiteResponse<SkinSiteWikiSearchPage>> {
const query = new URLSearchParams()
if (before != null) query.set('before', String(before))
return skinSiteRequest<SkinSiteWikiSearchPage>('wiki/recent?' + query.toString())
@@ -65,24 +65,39 @@
"app.skin-site.map.open": { "message": "Open in browser" },
"app.skin-site.map.server": { "message": "Server" },
"app.skin-site.map.title": { "message": "Server map" },
"app.skin-site.profile-view.add-player": { "message": "New player" },
"app.skin-site.profile-view.added": { "message": "Player added." },
"app.skin-site.profile-view.back": { "message": "Back to home" },
"app.skin-site.profile-view.cancel": { "message": "Cancel" },
"app.skin-site.profile-view.cape": { "message": "Cape" },
"app.skin-site.profile-view.clear-default": { "message": "Clear default" },
"app.skin-site.profile-view.confirm": { "message": "Confirm" },
"app.skin-site.profile-view.default-set": { "message": "Default player updated." },
"app.skin-site.profile-view.delete": { "message": "Delete" },
"app.skin-site.profile-view.deleted": { "message": "Player deleted." },
"app.skin-site.profile-view.elytra": { "message": "Elytra" },
"app.skin-site.profile-view.is-default": { "message": "Default" },
"app.skin-site.profile-view.loading": { "message": "Loading players…" },
"app.skin-site.profile-view.mojang-toggled": { "message": "Mojang status updated." },
"app.skin-site.profile-view.no-players": { "message": "No players yet." },
"app.skin-site.profile-view.player-name-placeholder": { "message": "Player name" },
"app.skin-site.profile-view.readonly": {
"message": "Mojang accounts are managed by Mojang; textures cannot be edited here."
},
"app.skin-site.profile-view.remove": { "message": "Remove" },
"app.skin-site.profile-view.removed": { "message": "Texture removed." },
"app.skin-site.profile-view.rename": { "message": "Rename" },
"app.skin-site.profile-view.renamed": { "message": "Player renamed." },
"app.skin-site.profile-view.select-hint": {
"message": "Select a player to preview and edit its textures."
},
"app.skin-site.profile-view.set-default": { "message": "Set as default" },
"app.skin-site.profile-view.signed-out": {
"message": "Sign in to manage your players."
},
"app.skin-site.profile-view.skin": { "message": "Skin" },
"app.skin-site.profile-view.title": { "message": "Your players" },
"app.skin-site.profile-view.toggle-mojang": { "message": "Toggle Mojang" },
"app.skin-site.profile-view.upload": { "message": "Upload" },
"app.skin-site.profile-view.uploaded": { "message": "Texture updated." },
"app.skin-site.profile.avatar": { "message": "Avatar" },
@@ -61,24 +61,39 @@
"app.skin-site.map.open": { "message": "在浏览器中打开" },
"app.skin-site.map.server": { "message": "服务器" },
"app.skin-site.map.title": { "message": "服务器地图" },
"app.skin-site.profile-view.add-player": { "message": "新建角色" },
"app.skin-site.profile-view.added": { "message": "角色已添加。" },
"app.skin-site.profile-view.back": { "message": "返回主页" },
"app.skin-site.profile-view.cancel": { "message": "取消" },
"app.skin-site.profile-view.cape": { "message": "披风" },
"app.skin-site.profile-view.clear-default": { "message": "取消默认" },
"app.skin-site.profile-view.confirm": { "message": "确认" },
"app.skin-site.profile-view.default-set": { "message": "默认角色已更新。" },
"app.skin-site.profile-view.delete": { "message": "删除" },
"app.skin-site.profile-view.deleted": { "message": "角色已删除。" },
"app.skin-site.profile-view.elytra": { "message": "鞘翅" },
"app.skin-site.profile-view.is-default": { "message": "默认" },
"app.skin-site.profile-view.loading": { "message": "正在加载角色…" },
"app.skin-site.profile-view.mojang-toggled": { "message": "正版状态已更新。" },
"app.skin-site.profile-view.no-players": { "message": "暂无角色。" },
"app.skin-site.profile-view.player-name-placeholder": { "message": "角色名" },
"app.skin-site.profile-view.readonly": {
"message": "Mojang 账号由 Mojang 管理,无法在此修改材质。"
},
"app.skin-site.profile-view.remove": { "message": "移除" },
"app.skin-site.profile-view.removed": { "message": "材质已移除。" },
"app.skin-site.profile-view.rename": { "message": "重命名" },
"app.skin-site.profile-view.renamed": { "message": "角色已重命名。" },
"app.skin-site.profile-view.select-hint": {
"message": "选择角色以预览和修改其材质。"
},
"app.skin-site.profile-view.set-default": { "message": "设为默认" },
"app.skin-site.profile-view.signed-out": {
"message": "登录后管理你的角色。"
},
"app.skin-site.profile-view.skin": { "message": "皮肤" },
"app.skin-site.profile-view.title": { "message": "我的角色" },
"app.skin-site.profile-view.toggle-mojang": { "message": "切换正版" },
"app.skin-site.profile-view.upload": { "message": "上传" },
"app.skin-site.profile-view.uploaded": { "message": "材质已更新。" },
"app.skin-site.profile.avatar": { "message": "头像" },