更改校端权限
deploy-userbase-front / build-and-deploy (push) Successful in 1m57s

This commit is contained in:
chenxing
2026-09-07 09:32:38 +08:00
parent 9200fa2135
commit 7e37f2ada6
15 changed files with 461 additions and 120 deletions
+33
View File
@@ -0,0 +1,33 @@
<template>
<div class="no-auth-page">
<h3>暂无权限</h3>
<h6>如需查看,请联络管理员为您分配权限</h6>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: "NoAuth" });
</script>
<style lang="scss" scoped>
.no-auth-page {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #fff;
h3 {
font-size: 20px;
color: #333;
}
h6 {
margin-top: 12px;
font-size: 14px;
font-weight: normal;
color: #999;
}
}
</style>
+45 -15
View File
@@ -2,7 +2,13 @@
<div class="main-wrapper">
<div class="main-container">
<Alert show-icon>设置说明:拖动排序,被使用的分类不能删除</Alert>
<Button class="mb-5" type="primary" @click="addModuleShow">新增应用类型</Button>
<!-- 多校管理员:学校切换(改名/排序仅对所选学校生效) -->
<Select v-if="multiSchool" v-model="currentSchoolId" class="mb-5 school-select" transfer
placeholder="全部学校(并集)" @on-change="fetchTypeList">
<Option v-for="s in schoolList" :key="s.id" :value="s.id">{{ s.name }}</Option>
</Select>
<Button v-if="!isSchoolMode" class="mb-5" :class="{ 'ml-8': multiSchool }" type="primary"
@click="addModuleShow">新增应用类型</Button>
<draggable class="type_set-wrap fix" tag="ul" v-model="typeList" :group="'appType'" @change="typeSortSave"
item-key="id">
<template #item="{ element }">
@@ -16,7 +22,7 @@
<span>编辑</span>
</div>
<div class="action_delete" @click="removeSave(element)">
<div class="action_delete" v-if="!isSchoolMode" @click="removeSave(element)">
<span>删除</span>
</div>
</div>
@@ -49,16 +55,25 @@
import draggable from 'vuedraggable'
import { debounce } from "lodash";
import MyConfirm from "@/components/MyConfirm";
import { useAppSchoolScope } from "@/utils/hook/useAppSchoolScope";
const { data: typeList, send: sendTypeList } = useRequest(
Apis.AppSettings.appTypeList({
transform: (res: any) => {
return res ?? [];
},
cacheFor: null,
}),
{ initialData: [], immediate: true }
)
// 学校管理员上下文:仅改名/排序(写学校覆盖表);新增/删除类型仅超管(按钮隐藏 + 接口 requireSuper 双挡)
const { isSchoolMode, multiSchool, schoolList, currentSchoolId, requireSchool } = useAppSchoolScope();
const typeList = ref<any[]>([])
async function fetchTypeList() {
try {
const res: any = await Apis.AppSettings.appTypeList({
params: { schoolId: currentSchoolId.value ?? undefined },
cacheFor: null,
});
typeList.value = (res ?? []).slice().sort((a: any, b: any) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
} catch (e: any) {
console.warn(e?.message || "获取应用类型失败");
}
}
fetchTypeList()
const refFormData = useTemplateRef('formData');
const editModal = ref(false);
@@ -80,13 +95,22 @@ async function addModuleShow() {
editModal.value = true;
}
//拖动排序:提交当前顺序的 id 数组
//拖动排序:提交当前顺序的 id 数组(SCHOOL 模式写学校覆盖表快照;多校须先选定学校)
async function typeSortSave() {
if (!requireSchool()) {
// 未选学校:拒绝提交并回拉列表,撤销拖拽造成的本地顺序变化
await fetchTypeList();
return;
}
const ids = (typeList.value ?? []).map((s: any) => s.id);
await Apis.AppSettings.appTypeSort({ data: ids });
await Apis.AppSettings.appTypeSort({
data: ids,
params: currentSchoolId.value != null ? { schoolId: currentSchoolId.value } : undefined,
});
}
async function showSetting(element: any) {
if (!requireSchool()) return;
currentId.value = element.id;
form.value = {
id: element.id,
@@ -106,7 +130,7 @@ async function removeSave(element: any) {
try {
await Apis.AppSettings.deleteType({ pathParams: { id: element.id } });
Message.success("操作成功!");
await sendTypeList();
await fetchTypeList();
} catch (error) {
console.error(error);
@@ -138,11 +162,13 @@ async function onRawEditSave() {
id: currentId.value,
name: form.value.name,
},
// SCHOOL 模式:改名写学校覆盖表(仅该校生效)
params: currentSchoolId.value != null ? { schoolId: currentSchoolId.value } : undefined,
});
}
Message.success("操作成功!");
editModal.value = false;
await sendTypeList();
await fetchTypeList();
} catch (error) {
console.error(error);
} finally {
@@ -163,6 +189,10 @@ function onReset() {
display: flex;
}
.school-select {
width: 180px;
}
.type_set-wrap {
margin: 12px 0 16px;
}
+28 -16
View File
@@ -2,14 +2,14 @@
<Modal v-model="show" :mask-closable="false" width="800" scrollable :title="title"
@on-visible-change="onVisibleChange">
<Form ref="formRef" :model="form" :rules="rules" :label-width="130" class="app-form width-95">
<!-- 第一行:应用名称 / 应用类型 -->
<!-- 第一行:应用名称 / 应用类型(学校模式隐藏类型——类型归属平台主数据) -->
<Row>
<Col span="12">
<FormItem label="应用名称:" prop="name">
<Input v-model="form.name" placeholder="请输入应用名称" />
</FormItem>
</Col>
<Col span="12">
<Col span="12" v-if="!schoolMode">
<FormItem label="应用类型:" prop="categoryId">
<Select v-model="form.categoryId" transfer placeholder="请选择应用类型">
<Option v-for="item in categoryList" :key="item.id" :value="item.id" :label="item.name" />
@@ -26,8 +26,15 @@
</Col>
</Row>
<!-- 第三行:appKey / appSecret(编辑模式才显示) -->
<Row v-if="isEdit">
<!-- 学校模式说明:仅 名称/描述/图标 可编辑,且仅对本校生效 -->
<Row v-if="schoolMode">
<Col span="24">
<Alert show-icon>学校模式:此处修改仅对本校生效(应用别名),平台主数据与密钥/推送等配置不可修改。</Alert>
</Col>
</Row>
<!-- 第三行:appKey / appSecret(编辑模式且非学校模式才显示) -->
<Row v-if="isEdit && !schoolMode">
<Col span="12">
<FormItem label="appKey:">
<Input :model-value="form.appKey?.toString()" readonly />
@@ -43,8 +50,8 @@
</Col>
</Row>
<!-- 第四行:应用前端类型(tag 多选) -->
<Row>
<!-- 第四行:应用前端类型(tag 多选;学校模式隐藏) -->
<Row v-if="!schoolMode">
<Col span="24">
<FormItem label="应用前端类型:">
<Select v-model="form.clients" multiple transfer placeholder="请选择前端类型" class="client-select">
@@ -54,9 +61,9 @@
</Col>
</Row>
<!-- 第五行:Pc应用地址 / Pc应用图标 -->
<!-- 第五行:Pc应用地址 / Pc应用图标(学校模式仅保留 Pc应用图标) -->
<Row>
<Col span="12">
<Col span="12" v-if="!schoolMode">
<FormItem label="Pc应用地址:">
<Input v-model="form.pcUrl" placeholder="请输入Pc应用地址" />
</FormItem>
@@ -79,8 +86,8 @@
</Col>
</Row>
<!-- 第六行:App接口地址 / App应用图标 -->
<Row>
<!-- 第六行:App接口地址 / App应用图标(学校模式隐藏) -->
<Row v-if="!schoolMode">
<Col span="12">
<FormItem label="App接口地址:">
<Input v-model="form.appUrl" placeholder="请输入App接口地址" />
@@ -104,8 +111,8 @@
</Col>
</Row>
<!-- 第七行:公众号应用地址 / 钉钉应用地址 -->
<Row>
<!-- 第七行:公众号应用地址 / 钉钉应用地址(学校模式隐藏) -->
<Row v-if="!schoolMode">
<Col span="12">
<FormItem label="公众号应用地址:">
<Input v-model="form.officialAccountUrl" placeholder="请输入公众号应用地址" />
@@ -139,11 +146,14 @@ const props = withDefaults(defineProps<{
categoryList?: any[];
isEdit?: boolean;
loading?: boolean;
/** 学校管理员模式:仅 名称/描述/Pc图标 三字段可编辑(写学校覆盖表) */
schoolMode?: boolean;
}>(), {
initForm: () => ({}),
categoryList: () => [],
isEdit: false,
loading: false,
schoolMode: false,
});
const emit = defineEmits(['update:modelValue', 'submit', 'cancel', 'reset-secret']);
@@ -211,11 +221,13 @@ const clientOptions = [
{ value: 32, label: '钉钉' },
];
/* ---------- 校验规则 ---------- */
const rules = {
/* ---------- 校验规则(学校模式不校验 categoryId——类型字段不展示且不可改) ---------- */
const rules = computed(() => ({
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
categoryId: [{ required: true, type: 'number' as const, message: '请选择应用类型', trigger: 'change' }],
};
...(props.schoolMode ? {} : {
categoryId: [{ required: true, type: 'number' as const, message: '请选择应用类型', trigger: 'change' }],
}),
}));
/* ---------- 提交 ---------- */
const formRef = ref();
+55 -21
View File
@@ -18,9 +18,15 @@
</div>
<div class="main-container">
<div class="search-wrap fix">
<Button class="fl" type="primary" @click="handleAdd">新增应用</Button>
<!-- 多校管理员:学校切换(覆盖仅对所选学校生效) -->
<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="sortModalVisible = true">排序管理</Button>
@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>
@@ -30,11 +36,11 @@
<!-- 容器组件:新增 / 编辑 -->
<AddAppContainer ref="addRef" />
<EditAppContainer ref="editRef" />
<EditAppContainer ref="editRef" :schoolId="currentSchoolId" />
<!-- 排序管理 -->
<AppSort v-model="sortModalVisible" :categoryId="dataParams.categoryId" :categoryName="currentCategoryName"
@saved="onSortSaved" />
:schoolId="currentSchoolId" @saved="onSortSaved" />
<!-- 推送设置 -->
<PushSettingModal v-model="pushSettingVisible" :appId="currentApp.id" :appName="currentApp.name" />
@@ -53,10 +59,14 @@ 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);
@@ -79,29 +89,39 @@ function onCategoryClick(id: number | null) {
tableRef.value?.refreshData();
}
/* ---------------- 多校切换:刷新类型与列表 ---------------- */
function onSchoolChange() {
fetchTypeList();
tableRef.value?.refreshData();
}
/* ---------------- 输入框搜索:仅在触发搜索时提交 ---------------- */
function onSearch() {
dataParams.value.name = keyword.value.trim() || undefined;
tableRef.value?.refreshData();
}
/* ---------------- 左侧应用类型 ---------------- */
const { data: appTypeList, send: fetchTypeList } = useRequest(
Apis.AppSettings.appTypeList({
transform: (res: any) => {
const list: any[] = res ?? [];
return list.slice().sort((a, b) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
},
cacheFor: null,
}),
{ initialData: [] }
);
/* ---------------- 左侧应用类型(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,
},
@@ -154,12 +174,14 @@ const columns = [
align: 'center',
width: 280,
render: (h: any, { row }: any) => {
return [
$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row)),
$utils.operatingTextHandle(h, '模块管理', '', () => goModuleManage(row)),
$utils.operatingTextHandle(h, '推送设置', '', () => openPushSetting(row)),
$utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)),
];
// 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;
}
},
];
@@ -196,11 +218,19 @@ function handleAdd() {
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({
@@ -246,6 +276,10 @@ const goModuleManage = (row: any) => {
width: 180px;
}
.school-select {
width: 180px;
}
.tree-scroll {
flex: 1;
overflow: auto;
+11 -2
View File
@@ -40,9 +40,12 @@ const props = withDefaults(
modelValue: boolean;
categoryId: number | null;
categoryName: string;
/** 多校管理员当前所选学校:排序快照仅对该校生效 */
schoolId?: number | null;
}>(),
{
categoryName: "",
schoolId: null,
}
);
@@ -83,7 +86,10 @@ async function fetchSortList() {
loading.value = true;
try {
const res = await Apis.AppSettings.appSortList({
params: { categoryId: props.categoryId },
params: {
categoryId: props.categoryId,
schoolId: props.schoolId ?? undefined,
},
cacheFor: null,
});
sortList.value = (res ?? []).slice().sort((a: any, b: any) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
@@ -109,7 +115,10 @@ async function handleSave() {
saving.value = true;
try {
const ids = sortList.value.map((item) => item.id);
await Apis.AppSettings.appSort({ data: ids });
await Apis.AppSettings.appSort({
data: ids,
params: props.schoolId != null ? { schoolId: props.schoolId } : undefined,
});
Message.success("排序保存成功!");
hasChanged.value = false;
modalVisible.value = false;
+40 -18
View File
@@ -1,18 +1,26 @@
<template>
<AppForm v-model="visible" :initForm="detailData" :categoryList="categoryList" :isEdit="true" :loading="submitting"
@submit="handleSubmit" @cancel="visible = false" @reset-secret="handleResetSecret" />
:schoolMode="isSchoolMode" @submit="handleSubmit" @cancel="visible = false" @reset-secret="handleResetSecret" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import AppForm from "./AppForm.vue";
import MyConfirm from '@/components/MyConfirm'
import { UserRoleEnum } from "@/types";
import { useDefaultStore } from "@/store";
/* ---------- 对外暴露 ---------- */
const visible = ref(false);
const categoryList = ref<any[]>([]);
// 编辑模式:从 appList 中根据 id 取出详情回填(无独立详情接口)
const store = useDefaultStore();
const isSchoolMode = store.userRole === UserRoleEnum.School;
// 多校管理员当前所选学校(单校/超管为 undefined,后端按 scope 自动解析)
const props = defineProps<{ schoolId?: number | null }>();
// 编辑模式:从 appList 中根据 id 取出详情回填(无独立详情接口;SCHOOL 模式列表已合并覆盖值)
const detailData = ref<Record<string, any>>({});
const currentId = ref<number | null>(null);
@@ -21,12 +29,16 @@ let onSuccessCallback: (() => void) | null = null;
// useForm 托管提交——currentId 是 ref,闭包内 .value 在 sendForm 时取当前值
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
(dto) => Apis.AppSettings.editApp({ pathParams: { id: currentId.value }, data: dto }),
(dto) => Apis.AppSettings.editApp({
pathParams: { id: currentId.value },
data: dto,
params: props.schoolId != null ? { schoolId: props.schoolId } : undefined,
}),
{ initialForm: { id: null, name: '', categoryId: undefined, desc: '', clients: [], pcUrl: '', pcIcon: '', appUrl: '', appIcon: '', officialAccountUrl: '', dingtalkUrl: '' }, resetAfterSubmiting: true }
);
onSuccess(() => {
Message.success('编辑应用成功!');
Message.success(isSchoolMode ? '保存成功(仅对本校生效)!' : '编辑应用成功!');
visible.value = false;
onSuccessCallback?.();
});
@@ -54,23 +66,33 @@ function open(id: number, row: any, categories: any[], onSuccess?: () => void) {
/* ---------- 提交:由 useForm 接管 loading 与异常处理 ---------- */
async function handleSubmit(formData: Record<string, any>) {
if (!currentId.value) return;
form.value = {
id: currentId.value,
name: formData.name,
categoryId: formData.categoryId,
desc: formData.desc,
clients: formData.clients,
pcUrl: formData.pcUrl,
pcIcon: formData.pcIcon,
appUrl: formData.appUrl,
appIcon: formData.appIcon,
officialAccountUrl: formData.officialAccountUrl,
dingtalkUrl: formData.dingtalkUrl,
};
if (isSchoolMode) {
// 学校模式:仅 名称/描述/Pc图标 三字段写学校覆盖表,其余字段一律不提交
form.value = {
id: currentId.value,
name: formData.name,
desc: formData.desc,
pcIcon: formData.pcIcon,
};
} else {
form.value = {
id: currentId.value,
name: formData.name,
categoryId: formData.categoryId,
desc: formData.desc,
clients: formData.clients,
pcUrl: formData.pcUrl,
pcIcon: formData.pcIcon,
appUrl: formData.appUrl,
appIcon: formData.appIcon,
officialAccountUrl: formData.officialAccountUrl,
dingtalkUrl: formData.dingtalkUrl,
};
}
await sendForm();
}
/* ---------- 重置密钥(非表单提交,直接 MyConfirm + await) ---------- */
/* ---------- 重置密钥(非表单提交,直接 MyConfirm + await;学校模式入口已隐藏) ---------- */
async function handleResetSecret() {
if (!currentId.value) return;
try {