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

<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>