build / build (push) Successful in 1m58s
- 操作列: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 迁移
350 lines
12 KiB
Vue
350 lines
12 KiB
Vue
<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>
|