Files
wordpress-theme-aksrt/assets/admin/src/pages/users/UsersLevel.vue
T
kate522 9375b1c30c refactor(assets): vendor 资产迁入标准目录并全仓去除 zibll 标识
- assets/vendor 全量迁移: fontawesome5→assets/fontawesome5, css→assets/css,
  js→assets/js/runtime(避开 14 个同名自有文件), img 合并去重→assets/img,
  fonts→assets/fonts; 删除 assets/vendor
- libs 五个与 widgets/third-party 字节相同的库不重复入库, loader paths 改指向
- 运行时切换: _win.uri=assets 根, loader baseUrl=/js/runtime, enlighter 注入
  路径同步; PHP 7 个引用文件切换并改名 $zibll_*→$runtime_*
- 去品牌: Qinver 头注释/品牌 console.log/演示链接清除, 默认文案中性化
- 功能标识符改名: zib__→aksrt__, zib_ajax→aksrt_ajax, window.zib→
  window.aksrtLegacy, zibllblock/*→aksrtblock/*, .zib-widget→.aksrt-panel,
  .zib-slider→.aksrt-slider, zib_menu_options meta 键等
- 自有 CSS 改名: legacy-utilities/compat-vars/footer shell
- admin SPA 重建; 契约测试同步(路径/断言/文件更名)
- 例外保留: tests/visual 对照环境与 fixtures 前缀(外部 Zibll 产品引用)

验证: php -l 133 个改动文件通过; runtime 86 个 JS node --check 通过;
契约测试 68/70(2 个失败经 stash 对照确认为存量问题)
2026-08-22 16:16:30 +08:00

291 lines
9.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="aksrt-page-container">
<PageHeader
title="用户等级"
desc="配置等级开关、最高等级、升级经验阈值与经验获取矩阵(对齐 legacy 用户等级 / 经验值)。"
>
<template #actions>
<n-space>
<n-button quaternary @click="go('/users/credits')">用户积分</n-button>
<n-button quaternary @click="go('/users/badge')">用户徽章</n-button>
</n-space>
</template>
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>当前摘要</h3></div>
<n-space>
<n-tag round :type="form.user_level_s ? 'success' : 'default'">
等级系统:{{ form.user_level_s ? '已启用' : '已关闭' }}
</n-tag>
<n-tag round type="info">最高等级:{{ form.user_level_max }}</n-tag>
<n-tag round type="info">日经验上限:{{ form.user_integral_opt.day_max }}</n-tag>
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>等级显示</h3></div>
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
<n-form-item label="启用用户等级">
<n-switch v-model:value="form.user_level_s" />
</n-form-item>
<n-form-item v-if="form.user_level_s" label="公开页显示等级">
<n-switch v-model:value="form.user_level_public_display" />
</n-form-item>
<n-form-item v-if="form.user_level_s" label="最高等级">
<n-input-number v-model:value="form.user_level_max" :min="3" :max="30" style="width: 180px;" @update:value="onMaxChange" />
</n-form-item>
<n-form-item v-if="form.user_level_s" label="升级通知">
<n-switch v-model:value="form.user_level_notify" />
</n-form-item>
</n-form>
</n-card>
<n-card v-if="form.user_level_s" class="aksrt-section-card">
<div class="aksrt-section-header"><h3>升级所需经验(累计)</h3></div>
<n-alert type="info" :show-icon="false" style="margin-bottom: 12px;">
Lv.1 通常为 0。数值为升到该级所需的累计经验。修改最高等级后会自动补齐/截断行。
</n-alert>
<n-data-table :columns="levelColumns" :data="levelRows" :bordered="false" size="small" />
</n-card>
<n-card v-if="form.user_level_s" class="aksrt-section-card">
<div class="aksrt-section-header"><h3>经验值得分</h3></div>
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
<n-form-item label="每日上限">
<n-input-number v-model:value="form.user_integral_opt.day_max" :min="0" :max="10000" style="width: 180px;" />
</n-form-item>
</n-form>
<n-divider />
<div v-for="group in integralGroups" :key="group" style="margin-bottom: 16px;">
<h4 style="margin: 0 0 8px;">{{ group }}</h4>
<n-grid cols="1 s:2 m:3" :x-gap="12" :y-gap="8" responsive="screen">
<n-grid-item v-for="row in INTEGRAL_CATALOG.filter((r) => r.group === group)" :key="row.key">
<div class="integral-field">
<div class="integral-field__label">{{ row.label }}</div>
<n-input-number v-model:value="form.user_integral_opt[row.key]" :min="0" :max="1000" style="width: 100%;" />
</div>
</n-grid-item>
</n-grid>
</div>
</n-card>
<div class="aksrt-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
</div>
</n-spin>
</div>
</template>
<script setup>
import { computed, h, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { NInput, NInputNumber, useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import {
INTEGRAL_CATALOG,
defaultIntegralOpt,
defaultLevelOpt,
} from '@/schema/sections/users/level.js'
const router = useRouter()
const message = useMessage()
const loading = ref(true)
const saving = ref(false)
const form = reactive({
user_level_s: true,
user_level_public_display: true,
user_level_max: 10,
user_level_opt: defaultLevelOpt(10),
user_integral_opt: defaultIntegralOpt(),
user_level_notify: true,
})
const integralGroups = computed(() => [...new Set(INTEGRAL_CATALOG.map((r) => r.group))])
const levelRows = computed(() => {
const max = Math.max(3, Math.min(30, Number(form.user_level_max) || 10))
const rows = []
for (let i = 1; i <= max; i++) {
rows.push({
level: i,
name: form.user_level_opt['name_' + i] ?? 'LV' + i,
upgrade: Number(form.user_level_opt['upgrade_integral_' + i] ?? 0),
})
}
return rows
})
const levelColumns = [
{ title: '等级', key: 'level', width: 80 },
{
title: '名称',
key: 'name',
render: (row) =>
h(NInput, {
value: form.user_level_opt['name_' + row.level] ?? '',
size: 'small',
onUpdateValue: (v) => {
form.user_level_opt['name_' + row.level] = v
},
}),
},
{
title: '等级图标 URL',
key: 'icon',
render: (row) =>
h(NInput, {
value: form.user_level_opt['icon_img_' + row.level] ?? '',
size: 'small',
placeholder: row.level <= 10 ? '留空使用主题内置图标' : '请输入图片 URL',
onUpdateValue: (v) => {
form.user_level_opt['icon_img_' + row.level] = v
},
}),
},
{
title: '升级所需累计经验',
key: 'upgrade',
width: 200,
render: (row) =>
h(NInputNumber, {
value: Number(form.user_level_opt['upgrade_integral_' + row.level] ?? 0),
min: 0,
max: 9999999,
size: 'small',
style: 'width: 160px;',
onUpdateValue: (v) => {
form.user_level_opt['upgrade_integral_' + row.level] = Number(v || 0)
},
}),
},
]
function go(path) {
router.push(path)
}
function extractData(response) {
return response?.data ?? response ?? {}
}
function onMaxChange(val) {
const max = Math.max(3, Math.min(30, Number(val) || 10))
form.user_level_max = max
const next = { ...form.user_level_opt }
const filled = defaultLevelOpt(max)
for (let i = 1; i <= max; i++) {
if (next['upgrade_integral_' + i] === undefined) {
next['upgrade_integral_' + i] = filled['upgrade_integral_' + i]
}
if (next['name_' + i] === undefined) {
next['name_' + i] = 'LV' + i
}
if (next['icon_img_' + i] === undefined) {
next['icon_img_' + i] = ''
}
}
// drop extras
for (const k of Object.keys(next)) {
const m = k.match(/_(\d+)$/)
if (m && Number(m[1]) > max) {
delete next[k]
}
}
form.user_level_opt = next
}
function normalizeIntegral(raw) {
const base = defaultIntegralOpt()
if (!raw || typeof raw !== 'object') {
return base
}
for (const row of INTEGRAL_CATALOG) {
if (raw[row.key] !== undefined) {
base[row.key] = Math.max(0, Number(raw[row.key]) || 0)
}
}
if (raw.day_max !== undefined) {
base.day_max = Math.max(0, Number(raw.day_max) || 0)
}
return base
}
function normalizeLevelOpt(raw, max) {
const base = defaultLevelOpt(max)
if (!raw || typeof raw !== 'object') {
return base
}
for (let i = 1; i <= max; i++) {
if (raw['upgrade_integral_' + i] !== undefined) {
base['upgrade_integral_' + i] = Math.max(0, Number(raw['upgrade_integral_' + i]) || 0)
}
if (raw['name_' + i] !== undefined) {
base['name_' + i] = String(raw['name_' + i] || '')
}
if (raw['icon_img_' + i] !== undefined) {
base['icon_img_' + i] = String(raw['icon_img_' + i] || '')
}
}
return base
}
async function load() {
loading.value = true
try {
const schemaRes = await api.getSchemaPage('users', 'level')
const d = extractData(schemaRes)
const enabled = d.user_level_s !== false
form.user_level_s = enabled
form.user_level_public_display = d.user_level_public_display !== false
form.user_level_max = Math.max(3, Math.min(30, Number(d.user_level_max || 10)))
form.user_level_notify = d.user_level_notify !== false
form.user_level_opt = normalizeLevelOpt(d.user_level_opt, form.user_level_max)
form.user_integral_opt = normalizeIntegral(d.user_integral_opt)
} catch (error) {
message.error(error.message || '加载用户等级配置失败')
} finally {
loading.value = false
}
}
async function handleSave() {
saving.value = true
try {
const max = Math.max(3, Math.min(30, Number(form.user_level_max) || 10))
const payload = {
user_level_s: !!form.user_level_s,
user_level_public_display: !!form.user_level_public_display,
user_level_max: max,
user_level_notify: !!form.user_level_notify,
user_level_opt: normalizeLevelOpt(form.user_level_opt, max),
user_integral_opt: normalizeIntegral(form.user_integral_opt),
}
await api.saveSchemaPage('users', 'level', payload)
Object.assign(form, {
user_level_max: max,
user_level_opt: payload.user_level_opt,
user_integral_opt: payload.user_integral_opt,
})
message.success('用户等级配置已保存')
} catch (error) {
message.error(error.message || '保存失败')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<style scoped>
.integral-field__label {
font-size: 12px;
margin-bottom: 4px;
opacity: 0.8;
}
</style>