You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
8.7 KiB
202 lines
8.7 KiB
<template>
|
|
<div>
|
|
<!-- 顶部 -->
|
|
<div class="flex items-center justify-between mb-5 flex-wrap gap-3">
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-slate-800">游客显隐控制</h2>
|
|
<p class="text-xs text-slate-400 mt-1">
|
|
控制「游客登录」入口与「实名认证」是否对客户端可见——<b>两者同一个开关</b>。
|
|
<b>整个应用、全部版本一套,不分渠道、不分 Android / iOS。</b>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm text-slate-500">应用</span>
|
|
<select v-model="scopeApp" class="select select-bordered select-sm" @change="load">
|
|
<option value="">全局默认(所有应用兜底)</option>
|
|
<option v-for="a in scopeApps" :key="a" :value="a">{{ a }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!scopeApp" class="scope-tip">
|
|
当前是<b>全局默认</b>作用域:这里配的对所有应用生效;某个应用单独配了,则以应用的为准。
|
|
</div>
|
|
|
|
<div class="guest-wrap">
|
|
<div class="card">
|
|
<section class="ed-section">
|
|
<div class="ed-section-title">游客登录入口</div>
|
|
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
|
<input v-model="form.tourists" type="checkbox" class="checkbox checkbox-sm checkbox-primary" />
|
|
<span>允许游客登录(同时开启实名认证)</span>
|
|
</label>
|
|
<p class="ed-hint mt-2">
|
|
开则<b>全部版本</b>都显示游客登录并要求实名;关则全部隐藏、功能不受实名限制。
|
|
不能只给送审包开——商店会按「审核时隐藏功能」拒审。
|
|
</p>
|
|
</section>
|
|
|
|
<section class="ed-section">
|
|
<div class="ed-section-title">当前效果</div>
|
|
<div class="effect" :class="effect.tone">
|
|
<span class="effect-icon">{{ effect.icon }}</span>
|
|
<div>
|
|
<div class="effect-title">{{ effect.title }}</div>
|
|
<div class="effect-desc">{{ effect.desc }}</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="ed-footer">
|
|
<button class="btn btn-ghost btn-sm" :disabled="submitting" @click="load">重置</button>
|
|
<button class="btn btn-primary btn-sm text-white" :disabled="submitting" @click="save">保存</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 语义说明单独立一块:这是白名单不是黑名单,配反了直接影响过审 -->
|
|
<div class="card note">
|
|
<div class="ed-section-title">这个开关怎么判定</div>
|
|
<p class="note-p">
|
|
<b>全版本一套:开就全部可见,关就全部隐藏。审核员和线上用户必须看到同一套入口。</b>
|
|
</p>
|
|
<table class="note-table">
|
|
<thead>
|
|
<tr><th>开关</th><th>游客登录 + 实名认证</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td>关</td><td>全部版本隐藏,功能不受实名限制</td></tr>
|
|
<tr><td>开</td><td>全部版本可见,并要求实名才能用功能</td></tr>
|
|
</tbody>
|
|
</table>
|
|
<p class="note-p">
|
|
<b>实名认证跟着这个开关走</b>。已有的实名记录不受影响。
|
|
本规则之前发布的老版本 App 不认识它,仍一律要求实名。
|
|
</p>
|
|
<p class="note-p muted">
|
|
判定在服务端做(客户端只拿结论,不拿规则)。
|
|
不要再按版本只给送审包开入口——那正是商店 Guideline 5.6 会拒的做法。
|
|
</p>
|
|
<p class="note-p muted">
|
|
客户端拿不到配置(无网/接口失败)时同样按隐藏处理,所以断网启动后短时间内入口不出现是预期行为;
|
|
实名认证此时也隐藏,功能放行。
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { useApi } from '~/composables/useApi'
|
|
import { useToast } from '~/composables/useToast'
|
|
|
|
definePageMeta({ title: '游客显隐控制' })
|
|
|
|
interface GuestForm {
|
|
tourists: boolean
|
|
}
|
|
|
|
const { webApi, consoleApi } = useApi()
|
|
const { success, error } = useToast()
|
|
|
|
const scopeApp = ref('')
|
|
const scopeApps = ref<string[]>([])
|
|
const submitting = ref(false)
|
|
|
|
function emptyForm(): GuestForm {
|
|
return { tourists: false }
|
|
}
|
|
const form = reactive<GuestForm>(emptyForm())
|
|
|
|
const effect = computed(() => {
|
|
if (!form.tourists) {
|
|
return { tone: 'off', icon: '🚫', title: '全部版本隐藏', desc: '所有客户端都不显示游客登录入口与实名认证,功能不受实名限制。' }
|
|
}
|
|
return { tone: 'on', icon: '✅', title: '全部版本可见', desc: '所有客户端都显示游客登录入口与实名认证,并要求实名才能用功能。' }
|
|
})
|
|
|
|
// 应用名来源:应用中心的应用表 + 部署注册表(兜底老数据);无权读这两张表时改走 api_getmyapps。
|
|
async function loadScopeApps() {
|
|
const names = new Set<string>()
|
|
const [products, deploys] = await Promise.all([
|
|
consoleApi<Array<{ name: string }>>('products/list', {}).catch(() => [] as Array<{ name: string }>),
|
|
consoleApi<Array<{ app_name?: string; name: string }>>('apps/list', {}).catch(() => [] as Array<{ app_name?: string; name: string }>),
|
|
])
|
|
;(products ?? []).forEach((p) => p.name && names.add(p.name))
|
|
;(deploys ?? []).forEach((d) => (d.app_name || d.name) && names.add(d.app_name || d.name))
|
|
if (!names.size) {
|
|
try {
|
|
const d = await webApi<{ apps: Array<{ name: string; app_name?: string }> }>('getmyapps', {})
|
|
;(d?.apps ?? []).forEach((a) => {
|
|
const n = a.app_name || a.name
|
|
if (n) names.add(n)
|
|
})
|
|
} catch {
|
|
// 拿不到就只剩「全局默认」,不阻断页面
|
|
}
|
|
}
|
|
scopeApps.value = [...names].sort()
|
|
}
|
|
|
|
async function load() {
|
|
try {
|
|
const d = await webApi<{ item: GuestForm & { version?: string } }>('getapprelease', { app_name: scopeApp.value })
|
|
Object.assign(form, emptyForm(), {
|
|
tourists: d?.item?.tourists ?? false,
|
|
})
|
|
} catch (e: any) {
|
|
error(e?.message ?? '加载失败')
|
|
Object.assign(form, emptyForm())
|
|
}
|
|
}
|
|
|
|
async function save() {
|
|
submitting.value = true
|
|
try {
|
|
// 只提交游客这一组字段:版本控制和它同一行,整行提交会把对方清空。
|
|
await webApi('saveapptourists', {
|
|
app_name: scopeApp.value,
|
|
tourists: form.tourists,
|
|
})
|
|
success('保存成功')
|
|
await load()
|
|
} catch (e: any) {
|
|
error(e?.message ?? '保存失败')
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await loadScopeApps()
|
|
await load()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.scope-tip { background: #f8fafc; border: 1px solid #e2e8f0; color: #475569; font-size: 12px; padding: 8px 12px; border-radius: 10px; margin-bottom: 16px; }
|
|
.guest-wrap { display: grid; gap: 16px; grid-template-columns: minmax(0, 1fr) minmax(300px, 1fr); align-items: start; }
|
|
@media (max-width: 900px) { .guest-wrap { grid-template-columns: 1fr; } }
|
|
.card { background: #fff; border: 1px solid #eef2f7; border-radius: 14px; padding: 18px 20px; }
|
|
.ed-section { padding-bottom: 16px; margin-bottom: 16px; border-bottom: 1px solid #f1f5f9; }
|
|
.ed-section:last-of-type { border-bottom: 0; margin-bottom: 0; }
|
|
.ed-section-title { font-size: 13px; font-weight: 700; color: #334155; margin-bottom: 10px; }
|
|
.ed-label { display: block; font-size: 12px; color: #64748b; margin-bottom: 4px; }
|
|
.ed-hint { font-size: 11px; color: #94a3b8; margin-top: 4px; line-height: 1.6; }
|
|
.ed-warn { font-size: 12px; color: #b45309; background: #fffbeb; border: 1px solid #fde68a; border-radius: 8px; padding: 6px 10px; margin-top: 10px; }
|
|
.ed-footer { display: flex; justify-content: flex-end; gap: 8px; padding-top: 14px; border-top: 1px solid #f1f5f9; }
|
|
.effect { display: flex; gap: 10px; align-items: flex-start; border-radius: 10px; padding: 10px 12px; }
|
|
.effect.on { background: #dcfce7; }
|
|
.effect.off { background: #f1f5f9; }
|
|
.effect.warn { background: #fffbeb; }
|
|
.effect-icon { font-size: 16px; line-height: 1.3; }
|
|
.effect-title { font-size: 13px; font-weight: 700; color: #334155; }
|
|
.effect-desc { font-size: 11px; color: #64748b; margin-top: 2px; line-height: 1.6; }
|
|
.note { background: #fcfcfd; }
|
|
.note-p { font-size: 12px; color: #475569; line-height: 1.8; margin-bottom: 10px; }
|
|
.note-p.muted { color: #94a3b8; font-size: 11px; }
|
|
.note-table { width: 100%; font-size: 11px; border-collapse: collapse; margin-bottom: 12px; }
|
|
.note-table th, .note-table td { border: 1px solid #e2e8f0; padding: 5px 8px; text-align: left; color: #475569; }
|
|
.note-table th { background: #f8fafc; color: #64748b; font-weight: 600; }
|
|
code { background: #f1f5f9; padding: 0 4px; border-radius: 4px; }
|
|
</style>
|
|
|