473 lines
15 KiB
Vue
473 lines
15 KiB
Vue
<template>
|
||
<el-dialog
|
||
v-model="visible"
|
||
:title="dialogType === 'add' ? $t('page.form.dialogTitleAdd') : $t('page.form.dialogTitleEdit')"
|
||
width="600px"
|
||
align-center
|
||
:close-on-click-modal="false"
|
||
@close="handleClose"
|
||
>
|
||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||
<el-form-item :label="$t('page.form.username')" prop="username">
|
||
<el-input v-model="formData.username" :placeholder="$t('page.form.placeholderUsername')" />
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.nickname')" prop="name">
|
||
<el-input v-model="formData.name" :placeholder="$t('page.form.placeholderNickname')" />
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.phone')" prop="phone">
|
||
<el-input
|
||
v-model="formData.phone"
|
||
:placeholder="$t('page.form.placeholderPhone')"
|
||
clearable
|
||
maxlength="20"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.password')" prop="password" :rules="passwordRules">
|
||
<el-input
|
||
v-model="formData.password"
|
||
type="password"
|
||
:placeholder="$t('page.form.placeholderPasswordEdit')"
|
||
show-password
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.status')" prop="status">
|
||
<sa-switch v-model="formData.status" />
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.adminId')" prop="admin_id">
|
||
<el-select
|
||
v-model="formData.admin_id"
|
||
:placeholder="$t('page.form.placeholderAdmin')"
|
||
clearable
|
||
filterable
|
||
style="width: 100%"
|
||
:loading="systemUserOptionsLoading"
|
||
>
|
||
<el-option
|
||
v-for="item in systemUserOptions"
|
||
:key="item.id"
|
||
:label="item.label || item.username || `#${item.id}`"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.coin')" prop="coin">
|
||
<el-input-number
|
||
v-model="formData.coin"
|
||
:min="0"
|
||
:precision="2"
|
||
:disabled="dialogType === 'add'"
|
||
:placeholder="$t('page.form.placeholderCoinAdd')"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<!-- lottery_config_id:空 = 自定义权重,否则 = DiceLotteryConfig.id;选择后该配置的五个 weight 会写入下方 player.*_weight -->
|
||
<el-form-item :label="$t('page.form.lotteryPoolConfig')" prop="lottery_config_id">
|
||
<el-select
|
||
v-model="formData.lottery_config_id"
|
||
:placeholder="$t('page.form.placeholderLotteryPool')"
|
||
clearable
|
||
filterable
|
||
style="width: 100%"
|
||
:loading="lotteryConfigLoading"
|
||
@change="onLotteryConfigChange"
|
||
>
|
||
<el-option
|
||
v-for="item in lotteryConfigOptions"
|
||
:key="item.id"
|
||
:label="(item.name && String(item.name).trim()) || `#${item.id}`"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<!-- 当前选中的 DiceLotteryConfig 数据展示 -->
|
||
<el-form-item v-if="currentLotteryConfig" :label="$t('page.form.currentConfig')" class="current-config-block">
|
||
<div class="current-lottery-config">
|
||
<div class="config-row">
|
||
<span class="config-label">{{ $t('page.form.configLabelName') }}:</span>
|
||
<span>{{ currentLotteryConfig.name ?? '-' }}</span>
|
||
</div>
|
||
<div class="config-row">
|
||
<span class="config-label">{{ $t('page.form.configLabelType') }}:</span>
|
||
<span>{{ lotteryConfigTypeText(currentLotteryConfig.name) }}</span>
|
||
</div>
|
||
<div class="config-row">
|
||
<span class="config-label">{{ $t('page.form.configLabelWeights') }}:</span>
|
||
<span>{{ currentLotteryConfigWeightsText }}</span>
|
||
</div>
|
||
<div v-if="currentLotteryConfig.remark" class="config-row">
|
||
<span class="config-label">{{ $t('page.form.configLabelRemark') }}:</span>
|
||
<span>{{ currentLotteryConfig.remark }}</span>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
<!-- lottery_config_id 为空时自定义权重可编辑;有值时来自所选 DiceLotteryConfig,仅展示不可编辑 -->
|
||
<el-form-item :label="$t('page.form.t1Weight')" prop="t1_weight">
|
||
<el-slider
|
||
v-model="formData.t1_weight"
|
||
:min="0"
|
||
:max="100"
|
||
:step="0.01"
|
||
show-input
|
||
:disabled="!isLotteryConfigEmpty()"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.t2Weight')" prop="t2_weight">
|
||
<el-slider
|
||
v-model="formData.t2_weight"
|
||
:min="0"
|
||
:max="100"
|
||
:step="0.01"
|
||
show-input
|
||
:disabled="!isLotteryConfigEmpty()"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.t3Weight')" prop="t3_weight">
|
||
<el-slider
|
||
v-model="formData.t3_weight"
|
||
:min="0"
|
||
:max="100"
|
||
:step="0.01"
|
||
show-input
|
||
:disabled="!isLotteryConfigEmpty()"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.t4Weight')" prop="t4_weight">
|
||
<el-slider
|
||
v-model="formData.t4_weight"
|
||
:min="0"
|
||
:max="100"
|
||
:step="0.01"
|
||
show-input
|
||
:disabled="!isLotteryConfigEmpty()"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('page.form.t5Weight')" prop="t5_weight">
|
||
<el-slider
|
||
v-model="formData.t5_weight"
|
||
:min="0"
|
||
:max="100"
|
||
:step="0.01"
|
||
show-input
|
||
:disabled="!isLotteryConfigEmpty()"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="isLotteryConfigEmpty()">
|
||
<div class="text-gray-500 text-sm">
|
||
{{ $t('page.form.weightsSumHint') }}<span :class="Math.abs(weightsSum - 100) > 0.01 ? 'text-red-500' : ''">{{
|
||
weightsSum
|
||
}}</span
|
||
>{{ $t('page.form.weightsSumUnit') }}
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import api from '../../../api/player/index'
|
||
import lotteryConfigApi from '../../../api/lottery_pool_config/index'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { ElMessage } from 'element-plus'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
|
||
const { t } = useI18n()
|
||
const WEIGHT_FIELDS = ['t1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight'] as const
|
||
|
||
interface Props {
|
||
modelValue: boolean
|
||
dialogType: string
|
||
data?: Record<string, any>
|
||
}
|
||
|
||
interface Emits {
|
||
(e: 'update:modelValue', value: boolean): void
|
||
(e: 'success'): void
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
modelValue: false,
|
||
dialogType: 'add',
|
||
data: undefined
|
||
})
|
||
|
||
const emit = defineEmits<Emits>()
|
||
|
||
const formRef = ref<FormInstance>()
|
||
|
||
const visible = computed({
|
||
get: () => props.modelValue,
|
||
set: (value) => emit('update:modelValue', value)
|
||
})
|
||
|
||
const weightsSum = computed(() => {
|
||
return WEIGHT_FIELDS.reduce((sum, key) => sum + Number(formData[key] ?? 0), 0)
|
||
})
|
||
|
||
/** 当前彩金池配置的 T1~T5 权重展示文案 */
|
||
const currentLotteryConfigWeightsText = computed(() => {
|
||
const c = currentLotteryConfig.value
|
||
if (!c) return '-'
|
||
const t1 = c.t1_weight ?? 0
|
||
const t2 = c.t2_weight ?? 0
|
||
const t3 = c.t3_weight ?? 0
|
||
const t4 = c.t4_weight ?? 0
|
||
const t5 = c.t5_weight ?? 0
|
||
return `${t1}% / ${t2}% / ${t3}% / ${t4}% / ${t5}%`
|
||
})
|
||
|
||
/** 新增时密码必填,编辑时选填 */
|
||
const passwordRules = computed(() =>
|
||
props.dialogType === 'add' ? [{ required: true, message: '密码必需填写', trigger: 'blur' }] : []
|
||
)
|
||
|
||
const rules = reactive<FormRules>({
|
||
username: [{ required: true, message: '用户名必需填写', trigger: 'blur' }],
|
||
name: [{ required: true, message: '昵称必需填写', trigger: 'blur' }],
|
||
phone: [{ required: true, message: '手机号必需填写', trigger: 'blur' }],
|
||
status: [{ required: true, message: '状态必需填写', trigger: 'blur' }],
|
||
coin: [{ required: true, message: '平台币必需填写', trigger: 'blur' }]
|
||
})
|
||
|
||
const initialFormData = {
|
||
id: null as number | null,
|
||
username: '',
|
||
name: '',
|
||
phone: '',
|
||
password: '',
|
||
status: 1 as number,
|
||
/** 所属后台管理员 ID(SystemUser.id) */
|
||
admin_id: null as number | null,
|
||
coin: 0 as number,
|
||
/** 彩金池配置 ID:空 = 自定义权重,否则 = DiceLotteryConfig.id */
|
||
lottery_config_id: null as number | null,
|
||
t1_weight: 0 as number,
|
||
t2_weight: 0 as number,
|
||
t3_weight: 0 as number,
|
||
t4_weight: 0 as number,
|
||
t5_weight: 0 as number
|
||
}
|
||
|
||
const formData = reactive({ ...initialFormData })
|
||
|
||
/** 彩金池配置下拉选项(DiceLotteryConfig id、name) */
|
||
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
|
||
/** 彩金池选项加载中 */
|
||
const lotteryConfigLoading = ref(false)
|
||
/** 后台管理员下拉选项(SystemUser) */
|
||
const systemUserOptions = ref<
|
||
Array<{ id: number; username: string; realname: string; label: string }>
|
||
>([])
|
||
/** 管理员选项加载中 */
|
||
const systemUserOptionsLoading = ref(false)
|
||
/** 当前选中的 DiceLotteryConfig 完整数据(用于展示) */
|
||
const currentLotteryConfig = ref<Record<string, any> | null>(null)
|
||
|
||
function lotteryConfigTypeText(name: unknown): string {
|
||
const n = String(name ?? '')
|
||
if (n === 'default') return '默认'
|
||
if (n === 'killScore') return '杀分'
|
||
if (n === 'up') return '上分'
|
||
return n || '-'
|
||
}
|
||
|
||
/** 是否为空/自定义权重(未选彩金池或选 0) */
|
||
function isLotteryConfigEmpty(): boolean {
|
||
const v = formData.lottery_config_id
|
||
return v == null || v === 0
|
||
}
|
||
|
||
/** 根据当前 lottery_config_id 加载 DiceLotteryConfig,并将五个权重写入当前 player.*_weight */
|
||
async function loadCurrentLotteryConfig() {
|
||
const id = formData.lottery_config_id
|
||
if (id == null || id === 0) {
|
||
currentLotteryConfig.value = null
|
||
return
|
||
}
|
||
try {
|
||
const res = await lotteryConfigApi.read(id)
|
||
const row = (res as any)?.data ?? (res as any)
|
||
if (row && typeof row === 'object') {
|
||
currentLotteryConfig.value = row
|
||
WEIGHT_FIELDS.forEach((key) => {
|
||
;(formData as any)[key] = Number(row[key] ?? 0)
|
||
})
|
||
} else {
|
||
currentLotteryConfig.value = null
|
||
}
|
||
} catch {
|
||
currentLotteryConfig.value = null
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
(newVal) => {
|
||
if (newVal) initPage()
|
||
}
|
||
)
|
||
|
||
/** 选择彩金池后,拉取该配置的五个权重并写入当前 player.*_weight,并更新当前配置展示 */
|
||
async function onLotteryConfigChange(lotteryConfigId: number | null | undefined) {
|
||
if (lotteryConfigId == null || lotteryConfigId === 0) {
|
||
currentLotteryConfig.value = null
|
||
return
|
||
}
|
||
try {
|
||
const res = await lotteryConfigApi.read(lotteryConfigId)
|
||
const row = (res as any)?.data ?? (res as any)
|
||
if (row && typeof row === 'object') {
|
||
WEIGHT_FIELDS.forEach((key) => {
|
||
;(formData as any)[key] = Number(row[key] ?? 0)
|
||
})
|
||
currentLotteryConfig.value = row
|
||
} else {
|
||
currentLotteryConfig.value = null
|
||
}
|
||
} catch (err) {
|
||
console.warn('拉取彩金池配置失败', err)
|
||
currentLotteryConfig.value = null
|
||
}
|
||
}
|
||
|
||
/** 加载后台管理员选项 */
|
||
async function loadSystemUserOptions() {
|
||
systemUserOptionsLoading.value = true
|
||
try {
|
||
systemUserOptions.value = await api.getSystemUserOptions()
|
||
} catch {
|
||
systemUserOptions.value = []
|
||
} finally {
|
||
systemUserOptionsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const initPage = async () => {
|
||
currentLotteryConfig.value = null
|
||
Object.assign(formData, initialFormData)
|
||
await Promise.all([loadLotteryConfigOptions(), loadSystemUserOptions()])
|
||
if (props.data) {
|
||
await nextTick()
|
||
initForm()
|
||
if (!isLotteryConfigEmpty()) {
|
||
await loadCurrentLotteryConfig()
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 从玩家控制器获取 DiceLotteryConfig id/name 列表,供 lottery_config_id 下拉使用 */
|
||
async function loadLotteryConfigOptions() {
|
||
lotteryConfigLoading.value = true
|
||
try {
|
||
lotteryConfigOptions.value = await api.getLotteryConfigOptions()
|
||
} catch {
|
||
lotteryConfigOptions.value = []
|
||
} finally {
|
||
lotteryConfigLoading.value = false
|
||
}
|
||
}
|
||
|
||
const numKeys = [
|
||
'id',
|
||
'status',
|
||
'coin',
|
||
'lottery_config_id',
|
||
't1_weight',
|
||
't2_weight',
|
||
't3_weight',
|
||
't4_weight',
|
||
't5_weight'
|
||
]
|
||
|
||
const initForm = () => {
|
||
if (!props.data) return
|
||
for (const key of Object.keys(formData)) {
|
||
if (!(key in props.data)) continue
|
||
if (key === 'password') {
|
||
;(formData as any).password = ''
|
||
continue
|
||
}
|
||
const val = props.data[key]
|
||
if (numKeys.includes(key)) {
|
||
if (key === 'id') {
|
||
;(formData as any)[key] = val != null ? Number(val) || null : null
|
||
} else if (key === 'lottery_config_id' || key === 'admin_id') {
|
||
const num = Number(val)
|
||
;(formData as any)[key] = val != null && !Number.isNaN(num) && num !== 0 ? num : null
|
||
} else {
|
||
;(formData as any)[key] = Number(val) || 0
|
||
}
|
||
} else {
|
||
;(formData as any)[key] = val ?? ''
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleClose = () => {
|
||
visible.value = false
|
||
formRef.value?.resetFields()
|
||
}
|
||
|
||
const handleSubmit = async () => {
|
||
if (!formRef.value) return
|
||
try {
|
||
await formRef.value.validate()
|
||
const useCustomWeights = isLotteryConfigEmpty()
|
||
if (useCustomWeights && Math.abs(weightsSum.value - 100) > 0.01) {
|
||
ElMessage.warning(t('page.form.ruleWeightsSumMustBe100'))
|
||
return
|
||
}
|
||
const payload = { ...formData }
|
||
if (isLotteryConfigEmpty()) {
|
||
;(payload as any).lottery_config_id = null
|
||
}
|
||
if (props.dialogType === 'edit' && !payload.password) {
|
||
delete (payload as any).password
|
||
}
|
||
if (props.dialogType === 'add') {
|
||
await api.save(payload)
|
||
ElMessage.success(t('page.form.addSuccess'))
|
||
} else {
|
||
await api.update(payload)
|
||
ElMessage.success(t('page.form.editSuccess'))
|
||
}
|
||
emit('success')
|
||
handleClose()
|
||
} catch (error) {
|
||
console.log('表单验证失败:', error)
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.current-config-block {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.current-lottery-config {
|
||
padding: 10px 12px;
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-regular);
|
||
|
||
.config-row {
|
||
margin-bottom: 6px;
|
||
line-height: 1.5;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.config-label {
|
||
color: var(--el-text-color-secondary);
|
||
margin-right: 4px;
|
||
}
|
||
}
|
||
</style>
|