Files
UserbaseFront/src/views/app/AppList.vue
T
chenxing 3b0f29b0d1
build / build (push) Successful in 1m58s
feat(接入规范验收修订轮): 操作列收口text形态+通道撑满高度+主题色批量落地
- 操作列:group-user/操作日志/开放接口日志/推送日志四页手写 h('a') 迁 actionColumn({mode:"text"}),颜色归通道 var(--brand);推送日志「重试」改 disabledWhen 置灰保位
- group-user 状态列 h('Tag')/h('Badge') 字符串组件名改 import 真组件(Vue3 坑)
- 高度:日志三页根节点改 page-wrapper yh-fill;TablePageAlovaNew 对齐 flow-center 同构版(.table-region + useElementHeight 实测 + 页长记忆);RoleAuthzList 转 tree-wrapper 骨架(外边界/间隙 16)
- 主题色:当前学校/当前组/当前角色、应用管理左类型面板、角色管理卡片操作位硬编码蓝改 var(--brand) 系令牌
- 顶栏房子图标钮删除(§4:首页入口收敛面包屑「首页」)
- 含 2026-10-10 试点未提交件:tableActions.ts 七函数、main.scss .op-cell、AppList/ModuleManage/RoleUserManage 迁移
2026-10-10 23:39:32 +08:00

350 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page-wrapper tree-wrapper">
<div class="tree-container app-type-tree">
<div class="tree-scroll">
<ul class="grade-ul">
<li :class="dataParams.categoryId == null ? 'active' : ''" @click="onCategoryClick(null)">
全部
</li>
<li v-for="item in appTypeList" :key="item.id"
:class="item.id == dataParams.categoryId ? 'active' : ''" @click="onCategoryClick(item.id)">
{{ item.name }}
</li>
</ul>
</div>
<div class="tree-footer">
<Button type="text" @click="goAppType">管理应用类型</Button>
</div>
</div>
<div class="main-container">
<div class="search-wrap fix">
<!-- 多校管理员:学校切换(覆盖仅对所选学校生效) -->
<Select v-if="multiSchool" v-model="currentSchoolId" class="fl school-select" transfer
placeholder="全部学校(并集)" @on-change="onSchoolChange">
<Option v-for="s in schoolList" :key="s.id" :value="s.id">{{ s.name }}</Option>
</Select>
<Button v-if="!isSchoolMode" class="fl" :class="{ 'ml-8': multiSchool }" type="primary"
@click="handleAdd">新增应用</Button>
<Button class="fl ml-8" type="default" v-if="dataParams.categoryId != null"
@click="openSortModal">排序管理</Button>
<Input class="fr" v-model="keyword" placeholder="输入应用名称或appKey筛选" search clearable suffix="ios-search"
style="width:220px" @on-search="onSearch" @on-clear="onSearch" />
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columnsComp" />
</div>
<!-- 容器组件:新增 / 编辑 -->
<AddAppContainer ref="addRef" />
<EditAppContainer ref="editRef" :schoolId="currentSchoolId" />
<!-- 排序管理 -->
<AppSort v-model="sortModalVisible" :categoryId="dataParams.categoryId" :categoryName="currentCategoryName"
:schoolId="currentSchoolId" @saved="onSortSaved" />
<!-- 推送设置 -->
<PushSettingModal v-model="pushSettingVisible" :appId="currentApp.id" :appName="currentApp.name" />
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
defineOptions({ name: "AppList" });
import { useRouter } from "vue-router";
import MyConfirm from "@/components/MyConfirm";
import AddAppContainer from "./AddAppContainer.vue";
import EditAppContainer from "./EditAppContainer.vue";
import PushSettingModal from "./PushSettingModal.vue";
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
import { OperatorEnum } from '@/types';
import AppSort from './AppSort.vue';
import { useAppSchoolScope } from "@/utils/hook/useAppSchoolScope";
import { actionColumn } from "@/utils/tableActions";
const router = useRouter();
// 学校管理员上下文:多校显示切换下拉,覆盖仅对所选学校生效(默认并集,写操作须先选定学校)
const { isSchoolMode, multiSchool, schoolList, currentSchoolId, requireSchool } = useAppSchoolScope();
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const sortModalVisible = ref(false);
const pushSettingVisible = ref(false);
const currentApp = ref<{ id: number; name: string }>({ id: 0, name: "" });
const keyword = ref("");
const dataParams = ref<{
categoryId: number | null;
name?: string;
}>({
categoryId: null,
name: "",
});
/* ---------------- 左侧分类切换:立即刷新 ---------------- */
function onCategoryClick(id: number | null) {
dataParams.value.categoryId = id;
tableRef.value?.refreshData();
}
/* ---------------- 多校切换:刷新类型与列表 ---------------- */
function onSchoolChange() {
fetchTypeList();
tableRef.value?.refreshData();
}
/* ---------------- 输入框搜索:仅在触发搜索时提交 ---------------- */
function onSearch() {
dataParams.value.name = keyword.value.trim() || undefined;
tableRef.value?.refreshData();
}
/* ---------------- 左侧应用类型(SCHOOL 模式合并学校私有改名/排序;随学校切换刷新) ---------------- */
const appTypeList = ref<any[]>([]);
async function fetchTypeList() {
try {
const res: any = await Apis.AppSettings.appTypeList({
params: { schoolId: currentSchoolId.value ?? undefined },
cacheFor: null,
});
appTypeList.value = ((res ?? []) as any[]).slice().sort((a, b) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
} catch (e: any) {
console.warn(e?.message || "获取应用类型失败");
}
}
fetchTypeList();
const tableApi = (params: { pageIndex: number; pageSize: number }) => {
return Apis.AppSettings.appList({
params: {
categoryId: dataParams.value.categoryId ?? undefined,
name: dataParams.value.name || undefined,
schoolId: currentSchoolId.value ?? undefined,
pageIndex: params.pageIndex,
pageSize: params.pageSize,
},
cacheFor: null,
});
};
const typeNameMap = computed(() => {
const map: Record<number, string> = {};
(appTypeList.value ?? []).forEach((t: any) => (map[t.id] = t.name));
return map;
});
const currentCategoryName = computed(() => {
if (dataParams.value.categoryId == null) return "";
return typeNameMap.value[dataParams.value.categoryId] ?? "";
});
/* ---------------- 排序保存后刷新列表 ---------------- */
function onSortSaved() {
tableRef.value?.reloadData(OperatorEnum.sel);
}
/* ---------------- 表格固定列(不含“应用类型”列) ---------------- */
const columns = [
{ type: "index", title: "序号", width: 70 },
{ title: "应用名称", key: "name", width: 160 },
{ title: "描述", key: "desc", ellipsis: true, tooltip: true, minWidth: 140 },
{
title: "前端",
key: "clients",
width: 180,
render: (h: any, { row }: any) => {
// value 对齐平台 ClientEnum:1=Pc浏览器 2=移动App 8=微信公众号 32=钉钉
const map: Record<number, string> = { 1: "Pc浏览器", 2: "移动App", 8: "微信公众号", 32: "钉钉" };
const arr: number[] = Array.isArray(row.clients) ? row.clients : [];
if (!arr.length) return h("span", "-");
return h("span", arr.map((v) => map[v] ?? v).join("、"));
},
},
{
title: "性质",
key: "appNature",
width: 80,
render: (h: any, { row }: any) => h("span", row.appNature === "BASE" ? "基石" : "业务"),
},
actionColumn(
[
// SCHOOL 模式:仅编辑(名称/描述/图标,写学校覆盖表);模块管理/推送设置/删除全部隐藏。
// 基石应用(app_nature=BASE)只做身份注册:模块/推送置灰;删除允许——基石也可下线,
// 但走「输名确认」删除模(危险确认,与 projman 应用删除同款)。
{ key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => handleEdit(r) },
{
key: "module",
label: "模块管理",
icon: "icon-apps",
on: (r: any) => goModuleManage(r),
when: () => !isSchoolMode.value,
disabledWhen: (r: any) => r.appNature === "BASE",
},
{
key: "push",
label: "推送设置",
icon: "icon-fasong",
on: (r: any) => openPushSetting(r),
when: () => !isSchoolMode.value,
disabledWhen: (r: any) => r.appNature === "BASE",
},
{
key: "del",
label: "删除",
icon: "icon-ashbin",
on: (r: any) => handleDelete(r),
when: () => !isSchoolMode.value,
danger: true,
},
],
// UserBase 恒文字链(工程风格显式声明,不随动作数漂移);
// icon 字段仅留作未来切 mode:"icon" 的备选。4 动作 → 2 直出 + 推送设置/删除进「更多」
{ mode: "text", width: 180 },
),
];
// 条件列:仅在「未筛选应用类型」时插入到“应用名称”之后
const typeColumn = {
title: "应用类型",
key: "categoryId",
width: 110,
render: (h: any, { row }: any) => h("span", typeNameMap.value[row.categoryId] ?? "-"),
};
/* ---------------- 表格最终数据源(动态列) ---------------- */
const columnsComp = computed(() => {
const cols = [...columns];
// 未筛选应用类型时,展示“应用类型”列
if (!dataParams.value.categoryId) {
const idx = cols.findIndex((c) => c.key === "name");
cols.splice(idx + 1, 0, typeColumn);
}
return cols;
});
/* ---------------- 新增应用 ---------------- */
const addRef = ref();
function handleAdd() {
addRef.value?.open(appTypeList.value, dataParams.value.categoryId, () => {
tableRef.value?.reloadData(OperatorEnum.add);
});
}
/* ---------------- 编辑应用 ---------------- */
const editRef = ref();
function handleEdit(row: any) {
// 多校管理员:编辑写学校覆盖表,必须先选定学校(后端对模糊写目标直接拒绝)
if (!requireSchool()) return;
editRef.value?.open(row.id, row, appTypeList.value, () => {
tableRef.value?.reloadData(OperatorEnum.edit);
});
}
/* ---------------- 排序管理入口(多校须先选定学校) ---------------- */
function openSortModal() {
if (!requireSchool()) return;
sortModalVisible.value = true;
}
/* ---------------- 删除应用(删除模:输名确认 + 红色确认键,对齐 projman;基石应用同样可删) ---------------- */
async function handleDelete(row: any) {
const name = row.name || String(row.id);
await MyConfirm({
title: "删除应用",
msg: `确定删除应用「${name}」吗?`,
info: "删除后不可恢复,关联就绪资源需手动清理!"
+ (row.appNature === "BASE" ? "(基石应用:请先确认无平台组件仍依赖其 appKey 签名身份)" : ""),
confirmName: name,
danger: true,
confirmText: "删除",
async onOk() {
try {
await Apis.AppSettings.deleteApp({ pathParams: { id: row.id } });
Message.success('删除成功!');
tableRef.value?.reloadData(OperatorEnum.del);
} catch (e: any) {
Message.error(e?.message || '删除失败');
}
},
});
}
/* ---------------- 推送设置 ---------------- */
function openPushSetting(row: any) {
currentApp.value = { id: row.id, name: row.name };
pushSettingVisible.value = true;
}
/* ---------------- 导航 ---------------- */
const goAppType = () => {
router.push("/app-manage/category");
};
const goModuleManage = (row: any) => {
router.push({
path: "/app-manage/module",
query: { id: row.id, name: row.name },
});
};
</script>
<style lang="scss" scoped>
.app-type-tree {
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
width: 180px;
}
.school-select {
width: 180px;
}
.tree-scroll {
flex: 1;
overflow: auto;
min-height: 0;
padding: 10px 12px;
}
.tree-footer {
flex-shrink: 0;
padding-top: 8px;
text-align: center;
background: #ffffff;
border-top: 1px solid #eeeeee;
.ivu-btn-text {
color: var(--brand);
}
}
.grade-ul {
margin: 0;
display: flex;
flex-direction: column;
gap: 12px;
li {
cursor: pointer;
background-color: #fafafa;
border: 1px solid #ebeef5;
border-radius: 4px;
height: 40px;
line-height: 40px;
text-align: center;
}
li.active {
color: var(--brand);
border-color: var(--brand);
}
}
</style>