fix: mkradiosを動的slotsに対応させる

This commit is contained in:
kakkokari-gtyih 2025-12-24 14:05:51 +09:00
parent 7e00a6794c
commit 19eae3ade0
6 changed files with 40 additions and 123 deletions

View File

@ -14,6 +14,10 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { UserEntityService } from './UserEntityService.js';
function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
return ['random', '1', '2'].includes(bw);
}
@Injectable()
export class ReversiGameEntityService {
constructor(
@ -58,7 +62,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
bw: game.bw,
bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
@ -116,7 +120,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
bw: game.bw,
bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,

View File

@ -81,6 +81,7 @@ export const packedReversiGameLiteSchema = {
bw: {
type: 'string',
optional: false, nullable: false,
enum: ['random', '1', '2'],
},
noIrregularRules: {
type: 'boolean',
@ -199,6 +200,7 @@ export const packedReversiGameDetailedSchema = {
bw: {
type: 'string',
optional: false, nullable: false,
enum: ['random', '1', '2'],
},
noIrregularRules: {
type: 'boolean',

View File

@ -19,8 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.optionContent">
<i v-if="option.icon" :class="[$style.optionIcon, option.icon]" :style="option.iconStyle"></i>
<div>
<div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
<div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
<slot v-if="option.slotId != null" :name="`option-${option.slotId}`"></slot>
<template v-else>
<div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
<div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
</template>
</div>
</div>
</MkRadio>
@ -35,8 +38,9 @@ SPDX-License-Identifier: AGPL-3.0-only
import type { StyleValue } from 'vue';
import type { OptionValue } from '@/types/option-value.js';
export type RadioOption<T = OptionValue> = {
export type RadioOption<T = OptionValue, S = string> = {
value: T;
slotId?: S;
label?: string;
labelStyle?: StyleValue;
icon?: string;
@ -54,6 +58,13 @@ defineProps<{
vertical?: boolean;
}>();
defineSlots<{
label?: () => any;
caption?: () => any
} & {
[K in `option-${NonNullable<T['slotId']>}`]: () => any;
}>();
const model = defineModel<T['value']>({ required: true });
function getKey(value: OptionValue): PropertyKey {

View File

@ -1,107 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<!--
古いMkRadiosの実装特殊な使い方をしていたため移行できなかった game.setting.vue でのみ使用
TODO: 移行して廃止
-->
<script lang="ts">
import { defineComponent, h, ref, watch } from 'vue';
import MkRadio from './MkRadio.vue';
import type { VNode } from 'vue';
import type { OptionValue } from '@/types/option-value';
export default defineComponent({
props: {
modelValue: {
required: false,
},
vertical: {
type: Boolean,
default: false,
},
},
setup(props, context) {
const value = ref(props.modelValue);
watch(value, () => {
context.emit('update:modelValue', value.value);
});
watch(() => props.modelValue, v => {
value.value = v;
});
if (!context.slots.default) return null;
let options = context.slots.default();
const label = context.slots.label && context.slots.label();
const caption = context.slots.caption && context.slots.caption();
// Fragment
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
// vnodev-if=false(trueoptiontype)
options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if'));
return () => h('div', {
class: [
'novjtcto',
...(props.vertical ? ['vertical'] : []),
],
}, [
...(label ? [h('div', {
class: 'label',
}, label)] : []),
h('div', {
class: 'body',
}, options.map(option => h(MkRadio, {
key: option.key as string,
value: option.props?.value,
disabled: option.props?.disabled,
modelValue: value.value as OptionValue,
'onUpdate:modelValue': _v => value.value = _v,
}, () => option.children)),
),
...(caption ? [h('div', {
class: 'caption',
}, caption)] : []),
]);
},
});
</script>
<style lang="scss">
.novjtcto {
> .label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
&:empty {
display: none;
}
}
> .body {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
&:empty {
display: none;
}
}
&.vertical {
> .body {
flex-direction: column;
}
}
}
</style>

View File

@ -36,23 +36,29 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
<!-- TODO: 移行 -->
<MkRadios2 v-model="game.bw">
<option value="random">{{ i18n.ts.random }}</option>
<option :value="'1'">
<MkRadios
:options="[
{ value: 'random', label: i18n.ts.random },
{ value: '1', slotId: 'user1' },
{ value: '2', slotId: 'user2' },
]"
v-model="game.bw"
>
<template #option-user1>
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
<template #name>
<b><MkUserName :user="game.user1"/></b>
</template>
</I18n>
</option>
<option :value="'2'">
</template>
<template #option-user2>
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
<template #name>
<b><MkUserName :user="game.user2"/></b>
</template>
</I18n>
</option>
</MkRadios2>
</template>
</MkRadios>
</MkFolder>
<MkFolder :defaultOpen="true">
@ -106,7 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
import { computed, watch, ref, onUnmounted } from 'vue';
import * as Misskey from 'misskey-js';
import * as Reversi from 'misskey-reversi';
import { i18n } from '@/i18n.js';
@ -114,7 +120,6 @@ import { ensureSignin } from '@/i.js';
import { deepClone } from '@/utility/clone.js';
import MkButton from '@/components/MkButton.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkRadios2 from '@/components/MkRadios2.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';

View File

@ -5337,7 +5337,8 @@ export type components = {
/** Format: id */
timeoutUserId: string | null;
black: number | null;
bw: string;
/** @enum {string} */
bw: 'random' | '1' | '2';
noIrregularRules: boolean;
isLlotheo: boolean;
canPutEverywhere: boolean;
@ -5373,7 +5374,8 @@ export type components = {
/** Format: id */
timeoutUserId: string | null;
black: number | null;
bw: string;
/** @enum {string} */
bw: 'random' | '1' | '2';
noIrregularRules: boolean;
isLlotheo: boolean;
canPutEverywhere: boolean;