fix(admin): unify settings save controls

This commit is contained in:
kate522 committed 2026-11-10 21:34:00 +08:00
1 parent 7fe26d3fff
commit f3dd1ec3b1
8 files changed
+113 -68

No files matched your search

+43 -24
View File
@@ -103,57 +103,76 @@
</n-form>
</n-card>
<div class="jio-form-footer">
<n-button type="primary" :loading="saving" @click="saveCfg">保存配置</n-button>
</div>
<JioSaveBar :dirty="dirty" :saving="saving" @save="saveCfg" @reload="loadCfg" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { computed, ref, reactive, onMounted } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api'
import JioSaveBar from '@/components/form/JioSaveBar.vue'
const message = useMessage()
// ---------- 配置 ----------
const cfg = reactive({
enabled: true,
currency_name: '',
register_bonus: 10,
daily_signin: { enabled: true, base: 1, streak: { 3: 2, 7: 5, 30: 20 } },
comment_reward: { enabled: true, amount: 1, daily_cap: 5, min_length: 10, on_approved: true },
referral_reward: {
function defaultCfg() {
return {
enabled: true,
promoter_amount: 5,
referred_amount: 2,
level_points: 0,
require_verify: true,
ip_daily_cap: 3,
},
download_default_cost: 0,
})
currency_name: '',
register_bonus: 10,
daily_signin: { enabled: true, base: 1, streak: { 3: 2, 7: 5, 30: 20 } },
comment_reward: { enabled: true, amount: 1, daily_cap: 5, min_length: 10, on_approved: true },
referral_reward: {
enabled: true,
promoter_amount: 5,
referred_amount: 2,
level_points: 0,
require_verify: true,
ip_daily_cap: 3,
},
download_default_cost: 0,
}
}
const cfg = reactive(defaultCfg())
const streakJson = ref('{}')
const loading = ref(true)
const saving = ref(false)
const initial = ref('')
const snapshot = computed(() => JSON.stringify({ cfg, streakJson: streakJson.value }))
const dirty = computed(() => !loading.value && initial.value !== '' && snapshot.value !== initial.value)
async function loadCfg() {
loading.value = true
try {
const response = await api.getCreditSettings()
const data = response?.data || response || {}
Object.assign(cfg, data)
Object.assign(cfg, defaultCfg(), data)
streakJson.value = JSON.stringify(cfg.daily_signin?.streak || {}, null, 0)
initial.value = snapshot.value
} catch (e) {
message.error(e.message || '加载失败')
} finally {
loading.value = false
}
}
async function saveCfg() {
let streak
try {
streak = JSON.parse(streakJson.value || '{}')
} catch (error) {
message.error('连续奖励 JSON 格式错误')
return
}
if (!streak || typeof streak !== 'object' || Array.isArray(streak)) {
message.error('连续奖励必须是 JSON 对象')
return
}
saving.value = true
try {
let streak = {}
try { streak = JSON.parse(streakJson.value || '{}') } catch (_) { /* ignore */ }
await api.saveCreditSettings({ ...cfg, daily_signin: { ...cfg.daily_signin, streak } })
cfg.daily_signin.streak = streak
await api.saveCreditSettings(cfg)
initial.value = snapshot.value
message.success('已保存')
} catch (e) {
message.error(e.message || '保存失败')
@@ -1,10 +1,6 @@
<template>
<div class="jio-page-container content-page">
<PageHeader title="底部Tab配置" desc="自定义移动端底部导航栏的按钮,支持排序、图标配置和返回顶部功能。">
<template #actions>
<n-button type="primary" :loading="saving" @click="handleSave">保存</n-button>
</template>
</PageHeader>
<PageHeader title="底部Tab配置" desc="自定义移动端底部导航栏的按钮,支持排序、图标配置和返回顶部功能。" />
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<!-- 基础设置 -->
@@ -99,12 +95,10 @@
</draggable>
<n-empty v-else description="暂无Tab按钮,请点击上方按钮添加" />
</n-card>
<div class="jio-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存底部Tab配置</n-button>
</div>
</n-spin>
<JioSaveBar :dirty="dirty" :saving="saving" @save="handleSave" @reload="loadSettings" />
<!-- 编辑弹窗 -->
<n-modal
v-model:show="showModal"
@@ -260,6 +254,7 @@ import { NButton, NTag, useDialog, useMessage } from 'naive-ui'
import draggable from 'vuedraggable'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import JioSaveBar from '@/components/form/JioSaveBar.vue'
import FaIconPicker from '@/components/form/FaIconPicker.vue'
import IconValue from '@/components/form/IconValue.vue'
import JioSortHandle from '@/components/form/JioSortHandle.vue'
@@ -280,6 +275,12 @@ const form = reactive({
footer_tabbar_single: 'default',
items: [],
})
const snapshot = ref('')
const serialize = () => JSON.stringify(form)
const dirty = computed(() => !loading.value && snapshot.value !== '' && serialize() !== snapshot.value)
function markClean() {
snapshot.value = serialize()
}
const editingItem = reactive({
id: '',
@@ -552,6 +553,7 @@ async function loadSettings() {
add_items: Array.isArray(item.add_items) ? item.add_items : [],
sort_order: item.sort_order ?? idx,
}))
markClean()
}
} catch (e) {
console.error('[FooterTabbar] load failed:', e)
@@ -581,6 +583,7 @@ async function handleSave() {
}
const res = await api.saveFooterTabbarSettings(payload)
if (res.code === 'success') {
markClean()
message.success(res.message || '保存成功')
} else {
message.error(res.message || '保存失败')
@@ -47,20 +47,19 @@
:scroll-x="1200"
/>
</n-card>
<div class="jio-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存能力矩阵</n-button>
</div>
</n-spin>
<JioSaveBar :dirty="dirty" :saving="saving" @save="handleSave" @reload="load" />
</div>
</template>
<script setup>
import { h, onMounted, reactive, ref } from 'vue'
import { computed, h, onMounted, reactive, ref } from 'vue'
import { NInputNumber, NSwitch, NTag } from 'naive-ui'
import { useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import JioSaveBar from '@/components/form/JioSaveBar.vue'
const message = useMessage()
const loading = ref(true)
@@ -74,6 +73,7 @@ const summary = reactive({
})
const userCaps = reactive({})
const defaultsSnapshot = ref({})
const dirty = computed(() => !loading.value && JSON.stringify(userCaps) !== JSON.stringify(defaultsSnapshot.value))
onMounted(load)
@@ -186,18 +186,11 @@ function resetDefaults() {
message.info('已恢复默认矩阵')
}
function expandAll() {
message.info('当前矩阵为平铺展示,无需展开')
}
function collapseAll() {
message.info('当前矩阵为平铺展示,无需折叠')
}
async function handleSave() {
saving.value = true
try {
await api.saveCapabilities(JSON.parse(JSON.stringify(userCaps)))
const res = await api.saveCapabilities(JSON.parse(JSON.stringify(userCaps)))
if (res?.data?.user_caps) replaceReactive(userCaps, res.data.user_caps)
defaultsSnapshot.value = JSON.parse(JSON.stringify(userCaps))
message.success('能力矩阵已保存')
} catch (error) {
@@ -241,10 +234,4 @@ async function handleSave() {
.jio-cap-matrix__empty {
color: #c0c4cc;
}
.jio-form-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>
+11 -4
View File
@@ -70,11 +70,8 @@
</n-grid>
</div>
</n-card>
<div class="jio-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
</div>
</n-spin>
<JioSaveBar :dirty="dirty" :saving="saving" @save="handleSave" @reload="load" />
</div>
</template>
@@ -84,6 +81,7 @@ import { useRouter } from 'vue-router'
import { NInput, NInputNumber, useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import JioSaveBar from '@/components/form/JioSaveBar.vue'
import {
INTEGRAL_CATALOG,
defaultIntegralOpt,
@@ -104,6 +102,13 @@ const form = reactive({
user_level_notify: true,
})
const snapshot = ref('')
const serialize = () => JSON.stringify(form)
const dirty = computed(() => !loading.value && snapshot.value !== '' && serialize() !== snapshot.value)
function markClean() {
snapshot.value = serialize()
}
const integralGroups = computed(() => [...new Set(INTEGRAL_CATALOG.map((r) => r.group))])
const levelRows = computed(() => {
@@ -245,6 +250,7 @@ async function load() {
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)
markClean()
} catch (error) {
message.error(error.message || '加载用户等级配置失败')
} finally {
@@ -270,6 +276,7 @@ async function handleSave() {
user_level_opt: payload.user_level_opt,
user_integral_opt: payload.user_integral_opt,
})
markClean()
message.success('用户等级配置已保存')
} catch (error) {
message.error(error.message || '保存失败')
+3 -5
View File
@@ -18,11 +18,8 @@
</n-form-item>
</n-form>
</n-card>
<div class="jio-form-actions">
<n-button :disabled="!dirty || saving" @click="load">放弃更改</n-button>
<n-button type="primary" :disabled="!dirty" :loading="saving" @click="save">保存设置</n-button>
</div>
</n-spin>
<JioSaveBar :dirty="dirty" :saving="saving" @save="save" @reload="load" />
</div>
</template>
@@ -30,6 +27,7 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import JioSaveBar from '@/components/form/JioSaveBar.vue'
import api from '@/api'
const defaults = {
@@ -45,7 +43,7 @@ const initial = ref('')
const loading = ref(true)
const saving = ref(false)
const snapshot = computed(() => JSON.stringify(form))
const dirty = computed(() => initial.value !== '' && snapshot.value !== initial.value)
const dirty = computed(() => !loading.value && initial.value !== '' && snapshot.value !== initial.value)
function extractData(response) {
return response?.data ?? response ?? {}
@@ -53,11 +53,6 @@
</div>
</section>
<div class="jio-form-actions">
<n-button :disabled="!dirty || saving" @click="load">放弃更改</n-button>
<n-button type="primary" :disabled="!dirty || loading" :loading="saving" @click="save">保存设置</n-button>
</div>
<section class="wechat-section wechat-test">
<h3>发送测试</h3>
<p class="wechat-muted">先保存配置,再选择已启用的模板。测试消息会发送到当前管理员绑定的公众号账号。</p>
@@ -68,6 +63,7 @@
</section>
</template>
</n-spin>
<JioSaveBar :dirty="dirty" :saving="saving" @save="save" @reload="load" />
</div>
</template>
@@ -75,6 +71,7 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import JioSaveBar from '@/components/form/JioSaveBar.vue'
import api from '@/api'
const message = useMessage()
@@ -90,7 +87,7 @@ const form = reactive({ enabled: false, templates: {} })
const initial = ref('')
const testType = ref(null)
const snapshot = computed(() => JSON.stringify(form))
const dirty = computed(() => initial.value !== '' && initial.value !== snapshot.value)
const dirty = computed(() => !loading.value && initial.value !== '' && initial.value !== snapshot.value)
const groups = computed(() => {
const grouped = new Map()