feat(role,scope)!: 角色与授权可配 + 需求收敛(4 角色 / 10 菜单 / 删两模块)
角色与授权(交接摘要 §一·三十五): - 权限判定合并两个来源:内置角色按代码(BuiltinRole)、自定义角色按库 (sys_role_permission)。实现放 module 层的 DbRoleGrantSource,security 只依赖 接口 RoleGrantSource —— 否则角色服务要调 permissionChecker,两个包迟早成环 - 权限判定必须加缓存:此前是零 IO,而压测基线是"SQL 条数恒定"。缓存放 Redis, 空结果也缓存(否则已删角色的 token 会把库打穿)、Redis 挂了退化成直读库、TTL 只兜底 - V18 建 sys_role.user_type(账号的 user_type 一直从角色反推,自定义角色不在代码映射里) 与 sys_role_menu;8 个接口 + 错误码 40045;契约 119 -> 129 - 内置角色只读(权限码是前后端契约,做成可改 = 一次误操作就能把管理员锁在系统外); 平台级能力两道防线(界面禁用 + 服务端 40301);防自锁;改完立即生效(写授权清缓存) - RoleProvisioner 从"已存在就 continue"改成幂等回填 + 每日 03:20 定时任务 —— 修的是"代码改了权限、已开通租户永远停在发放那一刻",这种漂移不报错也没测试会红 - scripts/e2e-m10-role.py 77/77 需求收敛(交接摘要 §一·三十六,本节的数字为准): - 删副班主任 / 审核员两个内置角色(6 -> 4)、历史整理器(module/cleanup)、 加入申请(module/join + 学生端 pages/join)、collect_task_org.deputy_user_ids 整列; 审核范围三级回退 -> 两级,连带 6 个 mapper 的 LIKE '%,id,%' 分支 - V19 迁移处理存量:先解 sys_user_role 绑定再软删角色;账号只在"除这两个角色外没有 别的角色"时才软删。必须迁而不能只删代码 —— 角色码删掉后 mergePermissions 返回空集, 存量账号会"能登录、处处 40301",不抛异常不打日志 - 契约 129 -> 119、错误码 59 -> 54、权限码 30 -> 28、PC 菜单 12 -> 10、 小程序页 10 -> 9、原型 32 -> 29 屏、E2E 39 -> 37 个(断言 1695 条) - UserService.list 对不认识的角色码由"静默丢掉过滤条件"改成失败关闭 (之前传个不存在的 roleCode 会返回全校账号,且看起来完全正常) - 品牌元信息同步改名「深学在线」:pom 名称、/doc.html 标题、配置与启动类注释、 存储自检探针内容;学生端 app 名与全局导航栏标题
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name" : "填报通",
|
||||
"name" : "深学在线",
|
||||
"appid" : "",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
|
||||
@@ -25,14 +25,6 @@
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/join/join",
|
||||
"style": {
|
||||
"navigationBarTitleText": "申请加入名单",
|
||||
"navigationBarBackgroundColor": "#0052D9",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tasks/tasks",
|
||||
"style": {
|
||||
@@ -87,7 +79,7 @@
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "填报通",
|
||||
"navigationBarTitleText": "深学在线",
|
||||
"navigationBarBackgroundColor": "#0052D9",
|
||||
"backgroundColor": "#F3F3F3"
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
<!-- 学生改动 -->
|
||||
<view v-if="(d.changedDetail || []).length" class="det__card">
|
||||
<view class="det__title">学生改动({{ d.changedDetail.length }} 项)</view>
|
||||
<view class="det__title">学生改动({{ (d.changedDetail || []).length }} 项)</view>
|
||||
<view v-for="c in d.changedDetail" :key="c.field" class="det__change">
|
||||
<text class="det__change-label">{{ c.label || c.field }}</text>
|
||||
<text class="det__muted">名册:{{ c.rosterValue || '—' }} → 学生:{{ c.studentValue || '—' }}</text>
|
||||
@@ -94,29 +94,16 @@
|
||||
</view>
|
||||
|
||||
<!--
|
||||
代传入口(§10.6):只在这个学生**还没提交**时才有意义 ——
|
||||
已提交的单子不能塞材料(那会让审到一半的材料清单变了)。
|
||||
放在最下面而不是和"通过/驳回"并排:它是补救动作,不是主流程。
|
||||
⚠️ 2026-10:这里原本有一段「帮他传材料」的模板(代传,§10.6),
|
||||
但它引用的 `canProxy / proxyMaterial / proxyReason / proxyBusy / doProxy`
|
||||
**五个变量在脚本里从未定义** —— 也就是这一段**永远不会渲染**,
|
||||
而 vue-tsc 一直在报 TS2339,看起来像"页面写坏了"。
|
||||
|
||||
现在直接删掉:需求收敛后主流程里没有代传(学生填报 → 班主任/管理员审核),
|
||||
后端 `file:proxyUpload` 接口还在(`e2e-m5-proxy.py` 33 条断言锁着),
|
||||
但**两端都没有代传入口**。将来要做,请重新实现并补 UI 断言,
|
||||
不要照着这段删掉的模板"恢复"。
|
||||
-->
|
||||
<view v-if="canProxy" class="det__proxy">
|
||||
<view class="det__proxy-title">帮他传材料</view>
|
||||
<view class="det__proxy-hint">
|
||||
学生不会弄(照片在家长手机里 / 不会压缩)时用这里。
|
||||
<text class="det__strong">这份材料会标注「由你代传」</text>,学生也看得到。
|
||||
</view>
|
||||
<view class="det__proxy-mats">
|
||||
<view v-for="g in (d.materials || []).filter((m) => m.required)" :key="g.materialCode"
|
||||
class="det__proxy-mat" :class="{ 'det__proxy-mat--on': proxyMaterial === g.materialCode }"
|
||||
@click="proxyMaterial = g.materialCode">
|
||||
{{ g.materialName }}{{ g.done ? '(已交)' : '' }}
|
||||
</view>
|
||||
</view>
|
||||
<input v-model="proxyReason" class="det__proxy-reason"
|
||||
placeholder="代传原因(选填,但学生看得到)" :maxlength="100" />
|
||||
<button class="det__btn det__btn--ghost" :disabled="proxyBusy" @click="doProxy">
|
||||
{{ proxyBusy ? '正在上传…' : '选择照片并代传' }}
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 驳回弹层:原因快选 + 必须二次确认 -->
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
红 {{ cur.redCount }} · 黄 {{ cur.yellowCount }}
|
||||
</view>
|
||||
<view v-if="(cur.changedFields || []).length" class="audit__changed">
|
||||
学生改了 {{ cur.changedFields.length }} 项预填内容
|
||||
学生改了 {{ (cur.changedFields || []).length }} 项预填内容
|
||||
</view>
|
||||
|
||||
<view class="audit__thumbs">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<!-- 待办:这一屏最该被点的是"待我初审" -->
|
||||
<view class="dash__todos">
|
||||
<view class="dash__todo" @click="goAudit">
|
||||
<view class="dash__todo" @click="goAudit()">
|
||||
<text class="dash__todo-num">{{ t.auditCount }}</text>
|
||||
<text class="dash__todo-label">待我初审</text>
|
||||
</view>
|
||||
|
||||
@@ -519,7 +519,7 @@ function uploadFor(m: Material) {
|
||||
let sendPath = path
|
||||
// 选中的原始文件名。**H5 用 blob 地址上传时 multipart 里带不过去**(是一串 UUID、
|
||||
// 没有后缀),所以下面要显式告诉服务端叫什么 —— 否则它按后缀判格式会直接拒掉。
|
||||
const pickedName = ((res.tempFiles || [])[0] as { name?: string } | undefined)?.name || ''
|
||||
const pickedName = ((res.tempFiles as Array<{ name?: string }> | undefined)?.[0])?.name || ''
|
||||
try {
|
||||
const plan = planFor(m.compressProfile, m.maxSizeKb, m.minQuality)
|
||||
if (plan) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<view class="land">
|
||||
<!-- 顶部:这是哪个学校、要填什么 -->
|
||||
<view class="land__hero">
|
||||
<text class="land__tenant">{{ ctx.tenantName || '填报通' }}</text>
|
||||
<text class="land__tenant">{{ ctx.tenantName || '深学在线' }}</text>
|
||||
<text class="land__task">{{ ctx.taskName || title }}</text>
|
||||
<view v-if="ctx.taskStatusText" class="land__badge" :class="badgeClass">
|
||||
{{ ctx.taskStatusText }}
|
||||
|
||||
@@ -1,466 +0,0 @@
|
||||
<template>
|
||||
<view class="join">
|
||||
<view class="join__wrap">
|
||||
<view class="join__hero">
|
||||
<text class="join__tenant">{{ ctx.tenantName || '材料填报' }}</text>
|
||||
<text class="join__task">{{ ctx.taskName }}</text>
|
||||
</view>
|
||||
|
||||
<!--
|
||||
① 已经申请过:把状态摆在最上面。
|
||||
这条通道最容易让人误解的就是"我提交了,是不是就能填了?" ——
|
||||
所以三个状态各有一句话直说结果,尤其待审时要说清**还不能填**。
|
||||
-->
|
||||
<view v-if="mine" class="join__card">
|
||||
<view class="join__status" :class="`join__status--${mine.status}`">
|
||||
{{ mine.statusText }}
|
||||
</view>
|
||||
|
||||
<view class="join__row">
|
||||
<text class="join__row-label">申请编号</text>
|
||||
<text class="join__row-value">#{{ mine.requestId }}</text>
|
||||
</view>
|
||||
<view class="join__row">
|
||||
<text class="join__row-label">姓名</text>
|
||||
<text class="join__row-value">{{ mine.realName }}</text>
|
||||
</view>
|
||||
<view class="join__row">
|
||||
<text class="join__row-label">身份证号</text>
|
||||
<text class="join__row-value">{{ mine.idCardMask }}</text>
|
||||
</view>
|
||||
<view class="join__row">
|
||||
<text class="join__row-label">提交时间</text>
|
||||
<text class="join__row-value">{{ fmt(mine.createTime) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 待审:说清"还不能填",并给出他能做的事 -->
|
||||
<view v-if="mine.status === 0" class="join__note">
|
||||
老师审核通过后,你<text class="join__strong">返回重新核验</text>就能开始填材料。
|
||||
审核前暂时还不能填 —— 名单是准入依据,这一步不能跳过。
|
||||
</view>
|
||||
|
||||
<!-- 通过:直接给"去核验"的入口 -->
|
||||
<view v-else-if="mine.status === 1" class="join__note join__note--ok">
|
||||
已经加入名单了,点下面的按钮重新核验一次即可开始填报。
|
||||
</view>
|
||||
|
||||
<!-- 驳回:原因原样显示(这是学生唯一能知道"哪里不对"的来源) -->
|
||||
<view v-else class="join__note join__note--bad">
|
||||
<text class="join__strong">未通过原因:</text>{{ mine.rejectReason || '(老师未填写)' }}
|
||||
</view>
|
||||
|
||||
<button v-if="mine.status === 1" class="join__btn" @click="reverify">
|
||||
重新核验,开始填报
|
||||
</button>
|
||||
<button v-if="mine.status === 2" class="join__btn join__btn--ghost"
|
||||
:disabled="submitting" @click="showForm = true">
|
||||
重新填写申请(还剩 {{ remainHint }})
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- ② 填申请 -->
|
||||
<view v-if="!mine || mine.status === 2" class="join__card">
|
||||
<template v-if="showForm">
|
||||
<view class="join__title">申请加入名单</view>
|
||||
<view class="join__hint">
|
||||
核对不上,通常是因为<text class="join__strong">老师名单里还没有你</text>,
|
||||
或者姓名写得和报名时不一样。填一次申请,老师核对后会把你加进名单。
|
||||
</view>
|
||||
|
||||
<view class="join__field">
|
||||
<text class="join__label">姓名</text>
|
||||
<input v-model="form.realName" class="join__input" placeholder="与报名时一致的姓名"
|
||||
:disabled="submitting" />
|
||||
</view>
|
||||
<view class="join__field">
|
||||
<text class="join__label">身份证号</text>
|
||||
<input v-model="form.idCardNo" class="join__input" placeholder="18 位身份证号"
|
||||
maxlength="18" :disabled="submitting" />
|
||||
</view>
|
||||
<view class="join__field">
|
||||
<text class="join__label">手机号</text>
|
||||
<input v-model="form.phone" class="join__input" placeholder="老师要能联系到你"
|
||||
maxlength="11" type="number" :disabled="submitting" />
|
||||
</view>
|
||||
<view class="join__field">
|
||||
<text class="join__label">班级(选填)</text>
|
||||
<input v-model="form.orgHint" class="join__input" placeholder="如 24中技数控2班"
|
||||
:disabled="submitting" />
|
||||
</view>
|
||||
<view class="join__field">
|
||||
<text class="join__label">说明(选填)</text>
|
||||
<input v-model="form.reason" class="join__input" placeholder="如:我是转班过来的"
|
||||
:disabled="submitting" />
|
||||
</view>
|
||||
|
||||
<button class="join__btn" :disabled="submitting || !canSubmit" @click="submit">
|
||||
{{ submitting ? '提交中…' : '提交申请' }}
|
||||
</button>
|
||||
|
||||
<view v-if="error" class="join__error">{{ error }}</view>
|
||||
|
||||
<view class="join__foot">
|
||||
身份证号只用于与名单核对,系统<text class="join__strong">不会保存明文</text>。
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view class="join__title">还没有加入名单?</view>
|
||||
<view class="join__hint">
|
||||
如果你确认姓名和身份证号没填错,但仍然核对不上,可以在这里提交申请,
|
||||
由老师核对后加入名单。
|
||||
</view>
|
||||
<button class="join__btn" @click="showForm = true">申请加入名单</button>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 申请加入名单(FR-509 / 客户确认后新增,原型 STEP 03B)。
|
||||
*
|
||||
* ## 三条必须说清的事(文案不是装饰,是这段流程的一部分)
|
||||
*
|
||||
* 1. **申请期间仍然不能填报。** 这条通道只解决"进名单",不绕过门禁 ——
|
||||
* 学生在待审状态最容易以为"我交了就能填",所以状态卡上直说"暂时还不能填"。
|
||||
* 2. **通过后要重新核验一次。** 通过时服务端已经把他绑到名册行上了,
|
||||
* 所以重进就能过;说清这一步,他就不会以为"通过了还要再填一遍资料"。
|
||||
* 3. **身份证号不存明文。** 未核验的人最该被告知这一点 ——
|
||||
* 否则"填身份证号"这一步会让人犹豫。
|
||||
*/
|
||||
defineOptions({ name: 'JoinRequest' })
|
||||
|
||||
interface SceneContext {
|
||||
tenantName?: string
|
||||
taskName?: string
|
||||
taskId?: number
|
||||
}
|
||||
|
||||
interface MineRequest {
|
||||
requestId: number
|
||||
taskId: number
|
||||
taskName?: string
|
||||
status: number
|
||||
statusText: string
|
||||
realName: string
|
||||
idCardMask: string
|
||||
orgHint?: string
|
||||
createTime?: string
|
||||
auditTime?: string
|
||||
rejectReason?: string
|
||||
rosterId?: number
|
||||
canFillNow?: boolean
|
||||
}
|
||||
|
||||
const ctx = ref<SceneContext>({})
|
||||
const mine = ref<MineRequest | null>(null)
|
||||
const showForm = ref(false)
|
||||
const submitting = ref(false)
|
||||
const error = ref('')
|
||||
let taskId = 0
|
||||
let scene = ''
|
||||
|
||||
const form = ref({
|
||||
realName: '',
|
||||
idCardNo: '',
|
||||
phone: '',
|
||||
orgHint: '',
|
||||
reason: ''
|
||||
})
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
const f = form.value
|
||||
return f.realName.trim().length > 0
|
||||
&& f.idCardNo.trim().length === 18
|
||||
&& /^1[3-9]\d{9}$/.test(f.phone.trim())
|
||||
})
|
||||
|
||||
const remainHint = computed(() => '最多 2 次机会')
|
||||
|
||||
function token(): string {
|
||||
try {
|
||||
return uni.getStorageSync('token') || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(v?: string) {
|
||||
return v ? v.replace('T', ' ').slice(0, 16) : '—'
|
||||
}
|
||||
|
||||
async function loadContext(s: string) {
|
||||
if (!s) return
|
||||
try {
|
||||
const data = await request<SceneContext>({
|
||||
url: `/api/v1/mp/scene-context?scene=${encodeURIComponent(s)}`
|
||||
})
|
||||
ctx.value = data
|
||||
taskId = data.taskId || 0
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '读取任务信息失败'
|
||||
}
|
||||
}
|
||||
|
||||
/** 拉"我的申请":决定显示状态卡还是表单 */
|
||||
async function loadMine() {
|
||||
try {
|
||||
const data = await request<{ list: MineRequest[], total: number }>({
|
||||
url: `/api/v1/mp/roster/join-request${taskId ? `?taskId=${taskId}` : ''}`,
|
||||
token: token()
|
||||
})
|
||||
// 只看最近一条:一个人在同一任务下的申请按时间倒序,最新那条决定他现在的处境
|
||||
mine.value = (data.list || [])[0] || null
|
||||
} catch (e) {
|
||||
// 拉不到不算致命:最多是少显示状态,仍可提交
|
||||
error.value = e instanceof Error ? e.message : ''
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
error.value = ''
|
||||
const f = form.value
|
||||
if (!canSubmit.value) {
|
||||
error.value = '请把姓名、18 位身份证号、手机号填完整'
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await request({
|
||||
url: '/api/v1/mp/roster/join-request',
|
||||
method: 'POST',
|
||||
token: token(),
|
||||
data: {
|
||||
taskId,
|
||||
realName: f.realName.trim(),
|
||||
idCardNo: f.idCardNo.trim(),
|
||||
phone: f.phone.trim(),
|
||||
orgHint: f.orgHint.trim() || undefined,
|
||||
reason: f.reason.trim() || undefined
|
||||
}
|
||||
})
|
||||
uni.showToast({ title: '已提交,等待老师审核', icon: 'none', duration: 2500 })
|
||||
showForm.value = false
|
||||
await loadMine()
|
||||
} catch (e) {
|
||||
// 后端的话原样显示("你已在名单中,请直接返回重新核验"这类提示必须让学生看到)
|
||||
error.value = e instanceof Error ? e.message : '提交失败'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 通过之后回核验页 —— 服务端已绑定,重进就能过 */
|
||||
function reverify() {
|
||||
uni.redirectTo({ url: `/pages/verify/verify?scene=${encodeURIComponent(scene)}` })
|
||||
}
|
||||
|
||||
onLoad(async (options) => {
|
||||
scene = options?.scene || ''
|
||||
taskId = Number(options?.taskId || 0)
|
||||
// 从核验页带过来的姓名/证件号:他刚打过一遍,不必再打
|
||||
form.value.realName = options?.name || ''
|
||||
form.value.idCardNo = options?.idCardNo || ''
|
||||
await loadContext(scene)
|
||||
await loadMine()
|
||||
// 带过来的是"门禁没匹配上"的那份输入 —— 直接展开表单,少一次点击
|
||||
if (!mine.value && (form.value.realName || form.value.idCardNo)) {
|
||||
showForm.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
$primary: #0052d9;
|
||||
$success: #00a870;
|
||||
$danger: #e34d59;
|
||||
$warn: #ed7b2f;
|
||||
$page-bg: #f3f3f3;
|
||||
|
||||
page {
|
||||
background-color: $page-bg;
|
||||
}
|
||||
|
||||
.join {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
|
||||
&__wrap {
|
||||
max-width: 720rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__hero {
|
||||
padding: 32rpx 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: linear-gradient(135deg, $primary 0%, #2b6fe0 100%);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&__tenant {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgb(255 255 255 / 80%);
|
||||
}
|
||||
|
||||
&__task {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__card {
|
||||
padding: 32rpx 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #1b2430;
|
||||
}
|
||||
|
||||
&__status {
|
||||
display: inline-block;
|
||||
padding: 6rpx 18rpx;
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 26rpx;
|
||||
border-radius: 8rpx;
|
||||
|
||||
&--0 {
|
||||
color: $warn;
|
||||
background: #fdf2e9;
|
||||
}
|
||||
|
||||
&--1 {
|
||||
color: $success;
|
||||
background: #e8f6f0;
|
||||
}
|
||||
|
||||
&--2 {
|
||||
color: $danger;
|
||||
background: #fdecee;
|
||||
}
|
||||
}
|
||||
|
||||
&__hint {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.7;
|
||||
color: #4a5768;
|
||||
}
|
||||
|
||||
&__strong {
|
||||
font-weight: 600;
|
||||
color: #1b2430;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 14rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
&__row-label {
|
||||
font-size: 26rpx;
|
||||
color: #8b95a3;
|
||||
}
|
||||
|
||||
&__row-value {
|
||||
font-size: 26rpx;
|
||||
color: #1b2430;
|
||||
}
|
||||
|
||||
&__note {
|
||||
padding: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.7;
|
||||
color: #4a5768;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
|
||||
&--ok {
|
||||
color: #2f6b4f;
|
||||
background: #e8f6f0;
|
||||
}
|
||||
|
||||
&--bad {
|
||||
color: #a03a44;
|
||||
background: #fdecee;
|
||||
}
|
||||
}
|
||||
|
||||
&__field {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 26rpx;
|
||||
color: #4a5768;
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 30rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
height: 88rpx;
|
||||
margin-top: 36rpx;
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
background: $primary;
|
||||
border-radius: 12rpx;
|
||||
|
||||
&[disabled] {
|
||||
color: #fff;
|
||||
background: #b9c6dd;
|
||||
}
|
||||
|
||||
&--ghost {
|
||||
color: $primary;
|
||||
background: #eaf1fd;
|
||||
|
||||
&[disabled] {
|
||||
color: #8b95a3;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
padding: 18rpx 20rpx;
|
||||
margin-top: 24rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
color: $danger;
|
||||
background: #fdecee;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
&__foot {
|
||||
margin-top: 28rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.7;
|
||||
color: #8b95a3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -45,7 +45,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
|
||||
@@ -33,18 +33,13 @@
|
||||
<view v-if="error" class="verify__error">{{ error }}</view>
|
||||
|
||||
<!--
|
||||
核验失败时给一条**受控通道**(FR-509 / 客户确认后新增)。
|
||||
注意两件事:
|
||||
1) 这条通道**不绕过门禁** —— 申请期间仍然不能填,去申请页会说清这一点;
|
||||
2) 它只在真失败过之后才出现(`error` 非空),不是一进来就摆个"申请"按钮。
|
||||
否则会有学生绕过核验直接去申请,把管理员的待办当成人工客服。
|
||||
核验失败时**只给"重新填写"这一条路**(2026-10 需求收敛:删掉「申请加入名单」)。
|
||||
刻意不提任何联系方式:名单外的人怎么进名单是老师那边的事,
|
||||
系统内不展示班主任/管理员的电话(客户定过的规矩)。
|
||||
-->
|
||||
<button v-if="error" class="verify__btn verify__btn--ghost" @click="gotoJoin">
|
||||
申请加入名单
|
||||
</button>
|
||||
|
||||
<view class="verify__foot">
|
||||
核对不上?先确认姓名和证件号没有打错。仍然不行可点上面的「申请加入名单」。
|
||||
<view v-if="error" class="verify__foot">
|
||||
核对不上?请先确认姓名和身份证号没有打错。
|
||||
如果确实已经报名,请直接联系老师,让老师把你加进名单后再来核对。
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -225,20 +220,6 @@ function next() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 去申请加入名单(FR-509 的入口)—— 带上已填的姓名与证件号,省他再输一遍。 */
|
||||
function gotoJoin() {
|
||||
const s = sceneFromUrl()
|
||||
const params = new URLSearchParams({
|
||||
scene: s,
|
||||
taskId: String(ctx.value.taskId || 0),
|
||||
// 只把**已经打过的字**带过去。这不是"预填答案":门禁刚判定它不匹配,
|
||||
// 带过去是为了让他少打一遍,而不是暗示它是对的。
|
||||
name: realName.value.trim(),
|
||||
idCardNo: idCardNo.value.trim()
|
||||
})
|
||||
uni.navigateTo({ url: `/pages/join/join?${params.toString()}` })
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
loadContext(options?.scene || '')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user