Files
userbase/src/views/app/AppList.vue
T
2026-09-07 09:32:38 +08:00

353 lines
11 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" :tableHeight="tableHeight" />
</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 useCurrentInstance from "@/utils/hook/useCurrentInstance";
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";
const router = useRouter();
const { $utils } = useCurrentInstance();
// 学校管理员上下文:多校显示切换下拉,覆盖仅对所选学校生效(默认并集,写操作须先选定学校)
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 tableHeight = ref(
$utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true })
);
/* ---------------- 表格固定列(不含“应用类型”列) ---------------- */
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: "action",
align: 'center',
width: 280,
render: (h: any, { row }: any) => {
// SCHOOL 模式:仅编辑(名称/描述/图标,写学校覆盖表);模块管理/推送设置/删除全部隐藏
const actions = [$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row))];
if (!isSchoolMode.value) {
actions.push($utils.operatingTextHandle(h, '模块管理', '', () => goModuleManage(row)));
actions.push($utils.operatingTextHandle(h, '推送设置', '', () => openPushSetting(row)));
actions.push($utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)));
}
return actions;
}
},
];
// 条件列:仅在「未筛选应用类型」时插入到“应用名称”之后
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;
}
/* ---------------- 删除应用 ---------------- */
async function handleDelete(row: any) {
await MyConfirm({
msg: `确定删除应用「${row.name || row.id}」吗?`,
info: "删除后不可恢复!",
async onOk() {
try {
await Apis.AppSettings.deleteApp({ pathParams: { id: row.id } });
Message.success('删除成功!');
tableRef.value?.reloadData(OperatorEnum.del);
} catch (e: any) {
console.warn(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: #577afb;
}
}
.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: #577afb;
border-color: #577afb;
}
}
.action-cell {
display: flex;
align-items: center;
justify-content: center;
}
.action-link {
cursor: pointer;
color: #577afb;
font-size: 13px;
&:hover {
color: #3d5afe;
}
&.danger {
color: #ed4014;
&:hover {
color: #c51d04;
}
}
}
.action-divider {
margin: 0 8px;
color: #dcdee2;
}
</style>