任务书 P95 W4/W5/W6。填报入口收回中心后,前端不再从应用模块树里挑 /f/{code} 行算清单。
- My.vue「可填写」:数据源 store.modules → GET /api/form/my/fillable,只画 canFill=true;
空态文案改「表单发布后按面向人群自动出现在这里」,不再引导去 UserBase 配模块行
- AppContext.vue 整页改版:清单吃 fillable?appCode=,三档按 myTier 渲染
(填报恒显 + 单表单直落保留 / 数据管理 MANAGE∪ADMIN / 统计 ADMIN);
GetUserMod?appCode 仍拉一次,唯一用途=交给内嵌 Data.vue 判导出锚点(P76 机制原样),
空树整页告警与「树里没有 /f/ 行」分支退役——取树失败只等于不画导出按钮(fail-closed)
- fillLink 新增 fillRouteOf(与 buildFillLink 同一条 appCode 规则,点进去与分享出去同址);
modulePerms 删 fillCodesOfModules(唯一消费者已退役),canExport/anchorRows/hasPage 保留勿伤;
menuRoute 的 /f/ 解析与门户导航未动(分享链接、P87 探针照旧)
- mock:快应用树改四行终态(删填报行),新增 /api/form/my/fillable 桩覆盖 FILL/MANAGE/ADMIN
- vue-tsc --noEmit 与 vite build 双绿;p87 探针由当前源码重打包后 15/15 复跑绿
This commit is contained in:
+141
-113
@@ -1,68 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 快应用上下文 /app/:appCode/(T8 落地,替换 T5 占位)。
|
||||
* 快应用上下文 /app/:appCode/(T8 落地,P95 W5 改版:导航壳按身份写死)。
|
||||
*
|
||||
* 形态(方案 §6.2 纯 A 形态 + 任务书 T8"快应用上下文"):
|
||||
* 形态(方案 §6.2 纯 A 形态 + P95 三层模型):
|
||||
* 快应用 = UserBase 真实应用行 + 无自有后端 + pc_url → 本路由。
|
||||
* - **模块树**:经 `/api/base/GetUserMod?appCode=<目标应用>` 取**那个应用**的树。H4 未上线时
|
||||
* 后端返回空树 + warn(T1 已验收的兜底,P3 裁决)——本页照此显示空态,**绝不回落本应用菜单**。
|
||||
* - **范围矩阵**:受限身份可见集**以后端为准**,本页不自行按角色过滤(任务书 T8 红线)——
|
||||
* 树里有什么就是什么,页面只做"单表单直落 vs 应用聚合轻导航"的形态分派。
|
||||
* - **单表单直落无导航**:树里只有一个可跳转填报行(`/f/{code}`)→ 直接渲染填报页,无轻导航。
|
||||
* - **应用聚合带轻导航**:树里有多个可跳转行 → 顶部一排轻导航(按 path 分派),内容区渲染对应视图。
|
||||
*
|
||||
* V26 三半句的前端侧(模块把关):
|
||||
* ① 无导出授权者不渲染导出按钮 → `canExport(tree, "app")`,其中 **tree 是本页拉到的那棵应用树**,
|
||||
* 经 `modules` prop 下传给内嵌的 Data.vue(P76:中心会话树里没有快应用的"导出"锚点行,
|
||||
* 拿它判锚点永远判不中——fail-closed 方向,有授权也不画按钮);
|
||||
* ② 导航不渲染 path 空节点 → 树构建走 `buildNavTree`(menuNav 已跳过锚点行,本页复验);
|
||||
* ③ 学生身份只见填报路由 → 树里只有 `/f/{code}` 行时,导航自然只有填报项(后端授权的直接结果,
|
||||
* 前端不再自己按角色过滤)。
|
||||
*
|
||||
* path → 视图分派(快应用模块行的 path 形态,方案 §6.2 表):
|
||||
* `/f/{code}` → 填报(渲染单表单,或跳中心填报页)
|
||||
* `/app/{appCode}/data` → 数据管理(复用 Data.vue,codes 来自树里的填报行)
|
||||
* `/app/{appCode}/stats`→ 统计(复用 Stats.vue)
|
||||
* 其余(含导入这类 path 空锚点)→ 不渲染为可跳转项(menuNav 已过滤 path 空)
|
||||
* - **清单来源**:`GET /api/form/my/fillable?appCode=<目标应用>`——中心按
|
||||
* `audience × 身份 × 管理员两表` 解析出的填报入口。**不再吃该应用的模块树**,
|
||||
* 发一张表单不需要超管加一条 `/f/{code}` 模块行(P95 §0 病根第 1 条就此拆掉)。
|
||||
* - **导航壳按身份渲染**(三档写死,不再由"树里有什么"决定):
|
||||
* · 填报 tab <b>恒显</b>:`canFill=true` 的行;0 行=空态;恰 1 行且地址没带子视图段=
|
||||
* <b>单表单直落</b>形态保留(内容区直接给这张表的填报入口,不叠导航);
|
||||
* · 数据管理 tab:存在 `myTier∈{MANAGE, ADMIN}` 的行时显,`:codes` 喂接口结果;
|
||||
* · 统计 tab:存在 `myTier=ADMIN` 的行时显,codes 同上。
|
||||
* - **"范围以后端为准"这条红线不动**:tab 只决定画不画;进去能看哪些行仍由后端
|
||||
* (`DataScopeService` + `FormAdminGuard.requireDataView`)把关,前端不按角色再筛一遍(任务书 T8 红线)。
|
||||
* - **模块树仍然拉一次**,唯一用途=把原始树经 `modules` prop 下传给内嵌 Data.vue 判"导出"锚点
|
||||
* (P76:中心会话树里没有快应用的锚点行,拿错树永远判不中)。空树/取树失败=fail-closed 不画导出按钮,
|
||||
* <b>不再整页报"这个应用进不来"</b>——原"空树"告警与"树里没有 /f/ 行"的分支随这条依赖收敛而退役。
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { baseApi } from "@/api/module/base";
|
||||
import { buildNavTree, flattenNav, type NavNode } from "@/utils/menuNav";
|
||||
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||
import { fillCodesOfModules } from "@/utils/modulePerms";
|
||||
import { listFillableForms } from "@/api/module/fill";
|
||||
import { fillRouteOf } from "@/utils/fillLink";
|
||||
import Data from "@/views/admin/Data.vue";
|
||||
import Stats from "@/views/admin/Stats.vue";
|
||||
import type { FillableForm } from "@/types/data";
|
||||
import type { IModule } from "@/types/Basics";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const appCode = computed(() => String(route.params.appCode ?? ""));
|
||||
/** 子视图段(/app/:appCode/:view):空=由树决定单表单直落或落默认聚合视图 */
|
||||
/** 子视图段(/app/:appCode/:view):空=填报(单表单直落或多表单列表) */
|
||||
const viewParam = computed(() => String(route.params.view ?? ""));
|
||||
|
||||
const fillRows = ref<FillableForm[]>([]);
|
||||
/** 目标应用的原始模块树:本页只把它交给内嵌 Data.vue 判导出锚点(P76),不再拿它算导航 */
|
||||
const rawTree = ref<IModule[]>([]);
|
||||
const navTree = ref<NavNode[]>([]);
|
||||
const loading = ref(false);
|
||||
const errorText = ref("");
|
||||
const emptyTree = ref(false);
|
||||
|
||||
/** 树里的填报目标 code(数据/统计视图靠它 resolve 出 uuid——快应用不授权定义管理行,§3.4) */
|
||||
const fillCodes = computed(() => fillCodesOfModules(rawTree.value));
|
||||
/** 填报 tab 的行:后端 canFill 判定(层②"只能代填"的行 canFill=false,不进这张列表) */
|
||||
const fillCards = computed(() => fillRows.value.filter((r) => r.canFill));
|
||||
/** 数据管理候选=层②∪层③;统计候选=层③(与后端 FillAccessService 的三个谓词一一对应) */
|
||||
const manageRows = computed(() => fillRows.value.filter((r) => r.myTier === "MANAGE" || r.myTier === "ADMIN"));
|
||||
const adminRows = computed(() => fillRows.value.filter((r) => r.myTier === "ADMIN"));
|
||||
|
||||
/** 可跳转导航项(path 空锚点已被 menuNav 跳过——V26 第二半句的复验点) */
|
||||
const navItems = computed(() => flattenNav(navTree.value));
|
||||
const isSingleForm = computed(() => navItems.value.length === 1 && /^\/f\//.test(navItems.value[0]?.page || ""));
|
||||
function codesOf(rows: FillableForm[]): string[] {
|
||||
const codes: string[] = [];
|
||||
for (const row of rows) if (!codes.includes(row.code)) codes.push(row.code);
|
||||
return codes;
|
||||
}
|
||||
|
||||
const dataCodes = computed(() => codesOf(manageRows.value));
|
||||
const statsCodes = computed(() => codesOf(adminRows.value));
|
||||
const showDataTab = computed(() => dataCodes.value.length > 0);
|
||||
const showStatsTab = computed(() => statsCodes.value.length > 0);
|
||||
|
||||
/** 单表单直落:可填项恰好一个且地址没带子视图段(P95 保留 T8 的这条形态,只换数据源) */
|
||||
const isSingleForm = computed(() => !viewParam.value.trim() && fillCards.value.length === 1);
|
||||
|
||||
/** 当前内容视图:'fill' | 'data' | 'stats' | 'unknown' */
|
||||
const activeView = computed<"fill" | "data" | "stats" | "unknown">(() => {
|
||||
const v = viewParam.value.trim();
|
||||
if (!v) return isSingleForm.value ? "fill" : navItems.value.length ? "data" : "unknown";
|
||||
if (v === "data") return "data";
|
||||
if (v === "stats") return "stats";
|
||||
if (v === "fill") return "fill";
|
||||
// 模块行里配置的完整 path 段(如 /app/honor/data 的 "data" 已覆盖;未知段按 unknown 呈现)
|
||||
if (!v || v === "fill") return "fill";
|
||||
if (v === "data") return showDataTab.value ? "data" : "unknown";
|
||||
if (v === "stats") return showStatsTab.value ? "stats" : "unknown";
|
||||
return "unknown";
|
||||
});
|
||||
|
||||
@@ -73,58 +76,46 @@ const currentTitle = computed(() => {
|
||||
return "应用表单";
|
||||
});
|
||||
|
||||
async function loadTree() {
|
||||
async function loadAll() {
|
||||
if (!appCode.value) {
|
||||
errorText.value = "地址里缺少 appCode";
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
errorText.value = "";
|
||||
emptyTree.value = false;
|
||||
try {
|
||||
// 目标应用的模块树:H4 未上线时后端返回空树(P3 口径),这里如实显示空态
|
||||
const tree: IModule[] = (await baseApi.getUserMod(appCode.value).send()) || [];
|
||||
rawTree.value = tree;
|
||||
// 导航树:path 空节点(权限锚点)跳过(menuNav 规则①,V26 第二半句)
|
||||
navTree.value = buildNavTree(tree, resolveMenuPage);
|
||||
emptyTree.value = navTree.value.length === 0;
|
||||
fillRows.value = (await listFillableForms(appCode.value)) || [];
|
||||
} catch (e: any) {
|
||||
rawTree.value = [];
|
||||
navTree.value = [];
|
||||
errorText.value = e?.message || "目标应用的模块树取不到";
|
||||
emptyTree.value = true;
|
||||
fillRows.value = [];
|
||||
errorText.value = e?.message || "可填清单取不到";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 轻导航点击:按模块行的 path 分派到本页的内容视图段 */
|
||||
function gotoNav(item: { page: string }) {
|
||||
const p = item.page;
|
||||
if (/^\/f\//.test(p)) {
|
||||
router.push(p);
|
||||
return;
|
||||
// 导出锚点那棵树:失败或空都按"判不出锚点=不给导出按钮"处理(fail-closed),
|
||||
// 刻意不并进 errorText——它取不到不影响填报/数据/统计三档壳的成立
|
||||
try {
|
||||
rawTree.value = (await baseApi.getUserMod(appCode.value).send()) || [];
|
||||
} catch {
|
||||
rawTree.value = [];
|
||||
}
|
||||
const m = /^\/app\/[^/]+\/(.+)$/.exec(p);
|
||||
const seg = m ? m[1] : p.replace(/^\//, "");
|
||||
router.push(`/app/${appCode.value}/${seg}`);
|
||||
}
|
||||
|
||||
/** 树里第一个可跳转项(单表单直落/默认落点用) */
|
||||
function firstNavPath(): string {
|
||||
return navItems.value[0]?.page || "";
|
||||
/** tab 点击:填报段回到应用入口(保留单表单直落判定),其余段按名分派 */
|
||||
function gotoView(seg: "" | "data" | "stats") {
|
||||
router.push(seg ? `/app/${appCode.value}/${seg}` : `/app/${appCode.value}/`);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
// 单表单直落:树里只有一个填报行且当前没带子视图段 → 直接把内容区渲染成该表单的填报
|
||||
// (不跳转,避免把用户从 /app/{appCode}/ 弹到别处;用户看着地址栏仍是应用入口)
|
||||
if (!viewParam.value && isSingleForm.value) {
|
||||
// 保持在本页渲染填报(activeView 已算成 fill),无需额外动作
|
||||
}
|
||||
});
|
||||
function goFill(row: FillableForm) {
|
||||
router.push(fillRouteOf(row));
|
||||
}
|
||||
|
||||
watch(appCode, () => loadTree());
|
||||
function fillPathText(row: FillableForm): string {
|
||||
const target = fillRouteOf(row);
|
||||
return `#${target.path}${target.query.appCode ? `?appCode=${target.query.appCode}` : ""}`;
|
||||
}
|
||||
|
||||
onMounted(loadAll);
|
||||
watch(appCode, () => loadAll());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -133,64 +124,75 @@ watch(appCode, () => loadTree());
|
||||
<div class="app-head">
|
||||
<span class="app-tag">快应用</span>
|
||||
<code class="app-code">{{ appCode || "(空)" }}</code>
|
||||
<span class="toolbar-note">模块树按目标应用取(GetUserMod?appCode=),范围以后端为准</span>
|
||||
<Button icon="md-refresh" size="small" @click="loadTree">刷新</Button>
|
||||
<span class="toolbar-note">清单按目标应用取(GET /api/form/my/fillable?appCode=),导航壳按身份渲染,范围以后端为准</span>
|
||||
<Button icon="md-refresh" size="small" :loading="loading" @click="loadAll">刷新</Button>
|
||||
</div>
|
||||
|
||||
<Alert v-if="errorText" show-icon type="error" class="region-tip">{{ errorText }}</Alert>
|
||||
|
||||
<Alert v-else-if="emptyTree" show-icon type="warning" class="region-tip">
|
||||
「{{ appCode }}」这个应用当前没有可进入的模块行。
|
||||
<br />两种可能:① 你的身份没有被授权该应用的任何模块行(受限身份未见集由后端决定);
|
||||
② 平台侧尚未上线目标应用的模块树查询(UserBase 的 appCode 参数,H4)——届时本接口返回空树,
|
||||
<b>本页刻意不回落表单中心自己的菜单</b>(回落会把「要 A 应用的菜单」伪装成「A 应用就这些」)。
|
||||
</Alert>
|
||||
<!-- 导航壳:三档由身份决定(不是由模块树里有什么决定) -->
|
||||
<div class="light-nav">
|
||||
<span class="nav-live">
|
||||
<a :class="['nav-item', { active: activeView === 'fill' }]" href="javascript:;"
|
||||
@click="gotoView('')">填报({{ fillCards.length }})</a>
|
||||
<a v-if="showDataTab" :class="['nav-item', { active: activeView === 'data' }]" href="javascript:;"
|
||||
@click="gotoView('data')">数据管理</a>
|
||||
<a v-if="showStatsTab" :class="['nav-item', { active: activeView === 'stats' }]" href="javascript:;"
|
||||
@click="gotoView('stats')">统计</a>
|
||||
</span>
|
||||
<span class="toolbar-note">数据管理/统计两档按你在这些表单上的层级出现;导出按钮仍按该应用的模块树判</span>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 应用聚合:多于一个可跳转行时给轻导航(单表单直落时不出导航) -->
|
||||
<div v-if="!isSingleForm && navItems.length" class="light-nav">
|
||||
<span class="nav-live">
|
||||
<a v-for="item in navItems" :key="item.id" href="javascript:;"
|
||||
:class="['nav-item', { active: route.path === item.page }]" @click="gotoNav(item)">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="toolbar-note">path 空的权限锚点行(如"导出")不进导航</span>
|
||||
</div>
|
||||
<h5 class="section-title">{{ currentTitle }}</h5>
|
||||
|
||||
<h5 class="section-title">{{ currentTitle }}</h5>
|
||||
|
||||
<!-- 数据/统计视图复用中心管理端组件:定义管理权不在快应用,codes 只来自模块树的填报行;
|
||||
embedded 让它只渲染本体,避免与本页的白卡/内边距叠成两层;
|
||||
modules 把**本页刚拉到的那棵应用树**传进去——导出按钮的锚点判定吃它,不吃中心会话树(P76) -->
|
||||
<Data v-if="activeView === 'data'" context="app" :app-code="appCode" :codes="fillCodes"
|
||||
:modules="rawTree" embedded />
|
||||
<Stats v-else-if="activeView === 'stats'" context="app" :app-code="appCode" :codes="fillCodes" embedded />
|
||||
<template v-else-if="activeView === 'fill'">
|
||||
<!-- 数据/统计视图复用中心管理端组件:定义管理权不在快应用,codes 来自可填清单(接口驱动,P95);
|
||||
embedded 让它只渲染本体,避免与本页白卡/内边距叠成两层;
|
||||
modules 把**本页拉到的那棵应用树**传进去——导出按钮的锚点判定吃它,不吃中心会话树(P76) -->
|
||||
<Data v-if="activeView === 'data'" context="app" :app-code="appCode" :codes="dataCodes"
|
||||
:modules="rawTree" embedded />
|
||||
<Stats v-else-if="activeView === 'stats'" context="app" :app-code="appCode" :codes="statsCodes" embedded />
|
||||
<template v-else-if="activeView === 'fill'">
|
||||
<!-- 单表单直落:只有一个可填项时不叠列表,内容区直接给这张表的填报入口 -->
|
||||
<template v-if="isSingleForm">
|
||||
<Alert show-icon type="info">
|
||||
单表单直落:该应用只授权了一个填报行,内容区直接渲染填报,不再叠一层导航。
|
||||
<br />填报目标:<code>{{ firstNavPath() || "(树里没有 /f/{code} 行)" }}</code>
|
||||
单表单直落:该应用在你名下只有一个可填入口,内容区直接给它,不再叠一层导航。
|
||||
<br />填报目标:<code>{{ fillPathText(fillCards[0]) }}</code>
|
||||
</Alert>
|
||||
<div class="fill-mount">
|
||||
<div class="fill-hint">
|
||||
直接进入填报页:
|
||||
<a href="javascript:;" @click="firstNavPath() && router.push(firstNavPath())">
|
||||
{{ firstNavPath() || "—" }}
|
||||
</a>
|
||||
<a href="javascript:;" @click="goFill(fillCards[0])">{{ fillCards[0].name }}</a>
|
||||
(填报页本体是 T7 交付的 /f/:code,本页不复制一份)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Alert v-else show-icon type="warning">
|
||||
该应用模块行里的这一段路径没有对应视图(本页只认识 data / stats / fill 三种,
|
||||
以及 path 形如 /f/{code} 的填报行)。请核对平台侧的模块行配置。
|
||||
<div v-else-if="fillCards.length" class="fill-cards">
|
||||
<div v-for="row in fillCards" :key="row.code" class="fill-card">
|
||||
<div class="fill-card-main">
|
||||
<div class="fill-card-name">{{ row.name }}</div>
|
||||
<code class="fill-card-path">{{ fillPathText(row) }}</code>
|
||||
</div>
|
||||
<Button type="primary" @click="goFill(row)">进入填报</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Alert v-else show-icon type="warning" class="region-tip">
|
||||
「{{ appCode }}」这个应用在你名下当前没有可填报的表单。
|
||||
<br />表单发布后会按它的<b>面向人群</b>自动出现在这里(面向学生的表出现在学生账号下,
|
||||
面向教师的表出现在教师账号下)——与基础平台的模块行授权无关;
|
||||
如果你是教师且这里为空,说明该应用没有面向学生的表单可代填。
|
||||
</Alert>
|
||||
</template>
|
||||
<Alert v-else show-icon type="warning">
|
||||
这一段路径对你不可见(本页只有 填报 / 数据管理 / 统计 三档,后两档按你在这些表单上的层级出现)。
|
||||
<br />能直接地址进来但看不到档,说明层级不够——数据面与统计的把关在后端,不在这里。
|
||||
</Alert>
|
||||
|
||||
<p class="toolbar-note foot-note">
|
||||
V26 前端侧三半句:①无导出授权不画导出按钮(快应用按 path 空的"导出"锚点行判)——
|
||||
导出按钮在数据视图内按该规则显隐;②导航不渲染 path 空节点;③学生身份只见填报路由(树里授权的直接结果)。
|
||||
接口侧的拒绝是 T4 已验的另一半,前端藏按钮不等于授权。
|
||||
V26 前端侧三半句(P95 改版):①无导出授权不画导出按钮——快应用按该应用树里 path 空的"导出"
|
||||
锚点行判(本页把那棵树原样交给内嵌 Data.vue,判不中就不画,fail-closed);
|
||||
②导航不再由模块树生成,按身份三档写死,path 空节点自然不进导航;
|
||||
③学生身份只见填报档=层①只给 canFill、不给数据/统计档的直接结果。
|
||||
接口侧的拒绝是 T4 已验的另一半,<b>前端藏按钮/藏档不等于授权</b>。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,6 +266,32 @@ watch(appCode, () => loadTree());
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
}
|
||||
.fill-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.fill-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 18px;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
.fill-card-name {
|
||||
font-size: 14px;
|
||||
color: #17233d;
|
||||
font-weight: 600;
|
||||
}
|
||||
.fill-card-path {
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
.foot-note {
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
|
||||
+53
-41
@@ -9,10 +9,11 @@
|
||||
* 两者是**两个端点、两个谓词**,页面按路由分派;顶部 Tab 只是 UI 导航(切路由),不是接口参数。
|
||||
* 管理员代填后:这条答卷在**填表人的"我提交的"**里出现,同时在**被代填人的"关于我的"**里出现(V18)。
|
||||
*
|
||||
* P91 起第三视图「可填写」(/my/fillable):这不是答卷视图,是**填报入口列表**——
|
||||
* 数据源=会话菜单树里 page 形如 /f/{code} 的行(App.vue 已拉过、store.modules 存原件),
|
||||
* 复用 buildNavTree+flattenNav 同一套解析(与左侧导航同口径,天然吃到 menuRoute 的修复)。
|
||||
* **不得在此按角色过滤**:树里有什么就是什么,授权语义在后端(任务书 T8 红线同源)。
|
||||
* P91 起第三视图「可填写」(/my/fillable),P95 W4 换数据源:这不再是答卷视图,而是**填报入口列表**。
|
||||
* 数据源从"会话菜单树里挑 path 形如 /f/{code} 的行"改成 `GET /api/form/my/fillable`——
|
||||
* 谁能填哪张表由中心按三层模型(audience × 身份 × 管理员两表)自解析,
|
||||
* UserBase 侧不再需要逐表单配 `/f/` 模块行(任务书 P95 §0.1/§0.2 红线 6:分享链接与直链解析不动)。
|
||||
* 前端照旧**不做本地角色过滤**:接口下发 canFill=true 的才画卡(canFill 是后端算的,不是我筛的)。
|
||||
*
|
||||
* 三条交互口径沿 T7 的"我的提交"段(不另造轮子):
|
||||
* 1. **编辑入口只在"我提交的"**:编辑=填表人的动作;"关于我的"整列只读
|
||||
@@ -30,14 +31,13 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { actionColumn } from "@/utils/tableActions";
|
||||
import { useElementHeight } from "@/utils/hook/useElementHeight";
|
||||
import { deleteMyAnswer, getMyAnswerDetail, listAnswersAboutMe, listMySubmitted } from "@/api/module/fill";
|
||||
import { deleteMyAnswer, getMyAnswerDetail, listAnswersAboutMe, listFillableForms, listMySubmitted } from "@/api/module/fill";
|
||||
import AnswerEditModal from "@/components/answer/AnswerEditModal.vue";
|
||||
import AnswerValueView from "@/components/answer/AnswerValueView.vue";
|
||||
import { statusDisplay } from "@/utils/dataValue";
|
||||
import { buildNavTree, flattenNav, type FlatNav } from "@/utils/menuNav";
|
||||
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||
import { fillRouteOf } from "@/utils/fillLink";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import type { AnswerDetail, MyAnswerViewItem } from "@/types/data";
|
||||
import type { AnswerDetail, FillableForm, MyAnswerViewItem } from "@/types/data";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -66,9 +66,9 @@ const VIEW_META = {
|
||||
},
|
||||
fillable: {
|
||||
title: "可填写",
|
||||
endpoint: "数据源:会话菜单树(GetUserMod)",
|
||||
predicate: "谓词:模块行 path 形如 /f/{code}(授权在后端)",
|
||||
note: "当前账号被授权的填报入口。列表由会话菜单树直接推导——树里没有的行这里不出现,也不做本地角色过滤。"
|
||||
endpoint: "GET /api/form/my/fillable",
|
||||
predicate: "谓词:后端按三层模型解析出的填报入口(canFill=true 才画卡)",
|
||||
note: "这里是当前账号能填报的表单。表单发布后按面向人群自动出现在这里——不需要有人在基础平台逐张表配模块行。"
|
||||
}
|
||||
} as const;
|
||||
|
||||
@@ -78,10 +78,19 @@ const regionRef = ref<HTMLElement | null>(null);
|
||||
const regionH = useElementHeight(regionRef);
|
||||
|
||||
const rows = ref<MyAnswerViewItem[]>([]);
|
||||
const fillableRows = ref<FillableForm[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadedOnce = ref(false);
|
||||
const errorText = ref("");
|
||||
|
||||
/**
|
||||
* 「可填写」的卡片集=接口里 `canFill=true` 的行。
|
||||
*
|
||||
* <p>过滤条件只有这一条,且判定<b>来自后端</b>:`canFill=false` 的行(教师×面向学生那种"只能代填")
|
||||
* 由后端按三层模型算出,前端只是不把它画成"进入填报"的卡——前端不重新按角色判一遍(T8 红线:范围以后端为准)。</p>
|
||||
*/
|
||||
const fillableCards = computed<FillableForm[]>(() => fillableRows.value.filter((r) => r.canFill));
|
||||
|
||||
/** 会话用户 id:判"这条是不是我提交的"(编辑/删除入口显隐),身份来源仍是会话(不传后端) */
|
||||
const sessionUserId = computed(() => store.sessionUserId);
|
||||
|
||||
@@ -90,14 +99,19 @@ function isMine(row: MyAnswerViewItem): boolean {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (view.value === "fillable") return; // 数据源=本地菜单树,无需请求
|
||||
loading.value = true;
|
||||
errorText.value = "";
|
||||
try {
|
||||
rows.value = view.value === "about" ? await listAnswersAboutMe() : await listMySubmitted();
|
||||
if (view.value === "fillable") {
|
||||
// P95 W4:入口清单来自后端解析(不再是会话菜单树);缺省 appCode=全部应用
|
||||
fillableRows.value = (await listFillableForms()) || [];
|
||||
} else {
|
||||
rows.value = view.value === "about" ? await listAnswersAboutMe() : await listMySubmitted();
|
||||
}
|
||||
loadedOnce.value = true;
|
||||
} catch (e: any) {
|
||||
rows.value = [];
|
||||
fillableRows.value = [];
|
||||
errorText.value = e?.message || "加载失败";
|
||||
Message.error(errorText.value);
|
||||
} finally {
|
||||
@@ -110,25 +124,17 @@ function switchView(target: "submit" | "about" | "fillable") {
|
||||
router.push({ path: `/${target === "submit" ? "my/submit" : "my/" + target}` });
|
||||
}
|
||||
|
||||
// ==================== P91 可填写视图:菜单树 → 填报入口卡片 ====================
|
||||
// ==================== P91 可填写视图:接口入口行 → 填报卡片 ====================
|
||||
|
||||
/**
|
||||
* 与左侧导航同一套解析:buildNavTree 吃 menuRoute(P87 起 /f/{code} 行能解析),
|
||||
* flattenNav 展平后按 page 形态挑填报行;按 page 去重(同名/多行指向同一表单只出一张卡)。
|
||||
*/
|
||||
const fillableItems = computed<FlatNav[]>(() => {
|
||||
if (view.value !== "fillable") return [];
|
||||
const nav = flattenNav(buildNavTree(store.modules, resolveMenuPage));
|
||||
const seen = new Set<string>();
|
||||
return nav.filter((n) => {
|
||||
if (!/^\/f\//.test(n.page) || seen.has(n.page)) return false;
|
||||
seen.add(n.page);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
/** 点卡片=进 /f/{code}(中心表单不带 query,快应用带 ?appCode=,与复制出去的分享链接同一条地址) */
|
||||
function goFill(row: FillableForm) {
|
||||
router.push(fillRouteOf(row));
|
||||
}
|
||||
|
||||
function goFill(page: string) {
|
||||
router.push({ path: page });
|
||||
/** 卡片上那行灰字:把"点进去会到哪个地址"如实写出来(与 fillRouteOf 同一份规则,不另算一遍) */
|
||||
function fillPathText(row: FillableForm): string {
|
||||
const route = fillRouteOf(row);
|
||||
return `#${route.path}${route.query.appCode ? `?appCode=${route.query.appCode}` : ""}`;
|
||||
}
|
||||
|
||||
// ==================== 详情 / 编辑 / 删除 ====================
|
||||
@@ -242,7 +248,7 @@ onMounted(load);
|
||||
<Radio label="submit">我提交的</Radio>
|
||||
<Radio label="about">关于我的</Radio>
|
||||
</RadioGroup>
|
||||
<Button v-if="view !== 'fillable'" icon="md-refresh" @click="load">刷新</Button>
|
||||
<Button icon="md-refresh" @click="load">刷新</Button>
|
||||
<span class="toolbar-note">
|
||||
{{ meta.endpoint }} | {{ meta.predicate }}
|
||||
<template v-if="view !== 'fillable'">(两个端点两个谓词,不做参数分支)</template>
|
||||
@@ -255,27 +261,29 @@ onMounted(load);
|
||||
<br />本视图不渲染编辑/删除入口(填表人才能改自己提交的那条);也不渲染改状态(填表人在接口层即被拒)。
|
||||
</template>
|
||||
<template v-else-if="view === 'fillable'">
|
||||
<br />入口来自基础平台的「填报」模块行(path=/f/{表单code},授权给组后这里才会出现)。
|
||||
<br />入口由表单中心按<b>面向人群 × 你的身份</b>解析(P95 三层模型),与基础平台的模块行无关;
|
||||
教师面向学生表单只能<b>代填本校学生</b>,那种表不在这里出现(在快应用上下文的数据管理里)。
|
||||
</template>
|
||||
</Alert>
|
||||
|
||||
<!-- P91 可填写:卡片列表(菜单名 + 进入填报),数据源=会话菜单树 -->
|
||||
<!-- P95 可填写:卡片列表(名称 + 进入填报),数据源=GET /api/form/my/fillable -->
|
||||
<template v-if="view === 'fillable'">
|
||||
<div v-if="fillableItems.length" class="fill-cards">
|
||||
<div v-for="item in fillableItems" :key="item.id" class="fill-card">
|
||||
<div v-if="fillableCards.length" class="fill-cards">
|
||||
<div v-for="item in fillableCards" :key="`${item.appCode}/${item.code}`" class="fill-card">
|
||||
<div class="fill-card-main">
|
||||
<div class="fill-card-name">
|
||||
<i class="iconfont icon-edit"></i>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<code class="fill-card-path">{{ item.page }}</code>
|
||||
<code class="fill-card-path">{{ fillPathText(item) }}</code>
|
||||
</div>
|
||||
<Button type="primary" @click="goFill(item.page)">进入填报</Button>
|
||||
<Button type="primary" @click="goFill(item)">进入填报</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Alert v-else show-icon type="warning">
|
||||
当前账号没有可填写的表单入口。请找管理员在基础平台为本应用配置「填报」模块行
|
||||
(path 填 /f/{表单code}),并授权给你所在的组织组。
|
||||
<Alert v-else-if="loadedOnce" show-icon type="warning">
|
||||
当前账号没有可填写的表单。表单发布后,会按它的<b>面向人群</b>自动出现在这里
|
||||
(面向学生的表出现在学生账号下,面向教师的表出现在教师账号下)——
|
||||
不需要在基础平台为本应用逐张配置「填报」模块行。
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
@@ -284,10 +292,14 @@ onMounted(load);
|
||||
no-data-text="这里还没有数据" class="link-table" />
|
||||
</div>
|
||||
|
||||
<p class="toolbar-note foot-note">
|
||||
<p v-if="view !== 'fillable'" class="toolbar-note foot-note">
|
||||
共 {{ rows.length }} 条(跨表单,不带值文档——要看值走详情,值文档与钉住版本的 schema 一起下发的,
|
||||
渲染与校验同源)。
|
||||
</p>
|
||||
<p v-else class="toolbar-note foot-note">
|
||||
共 {{ fillableCards.length }} 张可填(接口还包含"只能代填"的那些行,本列表只画 canFill=true 的;
|
||||
排序按截止时间,不限截止的在最后)。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Modal v-model="detailOpen" :title="`答卷详情(${detail?.formName || detail?.code || ''})`" :width="720"
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
* 前端不据此再过滤一遍(任务书 T8 红线)。
|
||||
*
|
||||
* 快应用上下文(AppContext)复用本组件:传 `context="app"` + `app-code` + `codes`
|
||||
* (候选来自该应用模块树的 `/f/{code}` 行,经 resolve 拿 uuid——定义管理权不在快应用上下文里,§3.4)
|
||||
* + `modules`(**同一棵原始模块树**,按钮级权限按它判,P76)。
|
||||
* (候选来自 `GET /api/form/my/fillable?appCode=` 按层级筛出的 code,经 resolve 拿 uuid——
|
||||
* 定义管理权不在快应用上下文里,§3.4;P95 W5 起不再从该应用模块树挑 `/f/` 行)
|
||||
* + `modules`(**同一棵原始模块树**,只给按钮级权限判定用,P76)。
|
||||
*/
|
||||
import { computed, h, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
@@ -51,7 +52,7 @@ const props = withDefaults(
|
||||
context?: "center" | "app";
|
||||
/** app 上下文的归属应用(center 留空=form-center) */
|
||||
appCode?: string;
|
||||
/** app 上下文的候选 code(来自该应用模块树的 /f/{code} 行) */
|
||||
/** app 上下文的候选 code(来自 /api/form/my/fillable 的层级筛取,P95 W5) */
|
||||
codes?: string[];
|
||||
/**
|
||||
* 按钮级权限判定的**那棵树**(P76):app 上下文必须由宿主传入目标应用的模块树
|
||||
|
||||
Reference in New Issue
Block a user