- 同步仓库 CI 配置 .gitea/workflows/build.yml - 纳入 src/public/config/scripts 等工程文件 - .gitignore 增加 .scratch/.backup/.trash,排除本地临时产物
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import layout from "./components/layout.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { baseApi } from "@/api/module/notice";
|
||||
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||
import { Message } from "view-ui-plus";
|
||||
|
||||
const router = useRouter();
|
||||
const isLogin = ref(false);
|
||||
const layoutData = reactive({
|
||||
appName: "校园公告",
|
||||
baseHost: config.baseHost,
|
||||
ssoHost: config.ssoHost,
|
||||
appList: [] as any[],
|
||||
userInfo: null as any,
|
||||
modList: [] as any[],
|
||||
isAlone: false,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 1. 获取当前用户信息
|
||||
const user: any = await baseApi.getUserInfo().send();
|
||||
const { id, name, profilePicture } = user || {};
|
||||
|
||||
// 2. 获取当前用户已授权的模块列表
|
||||
const modList: any[] = await baseApi.getUserMod().send();
|
||||
|
||||
layoutData.userInfo = { id, name: name || "管理员", avatar: profilePicture || "" };
|
||||
layoutData.modList = modList || [];
|
||||
|
||||
// 开发环境打印平台原始模块配置,便于排查菜单路径解析问题
|
||||
if (import.meta.env.DEV) {
|
||||
console.table(
|
||||
(modList || []).map((m: any) => ({
|
||||
name: m.name,
|
||||
id: m.id,
|
||||
modId: m.modId,
|
||||
path: m.page,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// 有子菜单时不算「单模块独占」模式
|
||||
const hasChildren = (modList || []).some((m: any) => m.children && m.children.length > 0);
|
||||
layoutData.isAlone = (modList || []).length <= 1 && !hasChildren;
|
||||
|
||||
isLogin.value = true;
|
||||
|
||||
// 登录后跳转第一个可用模块("/" 本身即已收到页,无需跳转)
|
||||
if (router.currentRoute.value.path === "/") {
|
||||
const firstModule = (modList || [])[0];
|
||||
const firstRaw = firstModule?.children?.length
|
||||
? firstModule.children[0]
|
||||
: firstModule;
|
||||
const first = resolveMenuPage(firstRaw?.page, firstRaw?.modId ?? firstRaw?.id, firstRaw?.name);
|
||||
if (!first.external && first.path && first.path !== "/") {
|
||||
router.replace(first.path);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("加载用户信息失败", error);
|
||||
if (error?.message !== "redirect") {
|
||||
Message.error("载入用户信息失败,请检查后端服务是否正常!");
|
||||
}
|
||||
// 降级:使用默认值("/" 即已收到页,无需跳转)
|
||||
layoutData.userInfo = { name: "管理员", avatar: "" };
|
||||
layoutData.modList = [];
|
||||
layoutData.isAlone = true;
|
||||
isLogin.value = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<layout v-if="isLogin" :layoutData="layoutData" />
|
||||
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
||||
<div class="main-wrapper">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
/* 页面级滚动容器:main-wrapper 只有 min-height 时内容超一屏会被 body 的
|
||||
overflow:hidden 直接裁掉(如写通知页),无任何滚动条。定高 + 纵向滚动,
|
||||
放得下的列表页(表格高度由 Utils.useTableHeight 算好)不受影响 */
|
||||
.base-wrap-main .main-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* 表格操作列(operatingHandle 渲染的 Tooltip 包裹层)横排且随单元格居中 */
|
||||
.ivu-table-cell > .ivu-tooltip {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
/// <reference types='./globals.d.ts' />
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 校园公告接口文档 - version 1.0
|
||||
*
|
||||
* 校园公告子应用:发公告、列表、删除、编辑、管理员查看、统计(springdoc-openapi)
|
||||
*
|
||||
* OpenAPI version: 3.0.1
|
||||
*
|
||||
*
|
||||
* NOTE: This file is auto generated by the alova's vscode plugin.
|
||||
*
|
||||
* https://alova.js.org/devtools/vscode
|
||||
*
|
||||
* **Do not edit the file manually.**
|
||||
*/
|
||||
export default {
|
||||
'templateController.typeEdit': ['POST', '/api/template/TypeEdit'],
|
||||
'templateController.typeDelete': ['POST', '/api/template/TypeDelete'],
|
||||
'templateController.templateEdit': ['POST', '/api/template/TemplateEdit'],
|
||||
'templateController.templateDelete': ['POST', '/api/template/TemplateDelete'],
|
||||
'smsController.setSchoolSms': ['POST', '/api/sms/SetSchoolSms'],
|
||||
'smsController.setSchoolModuleSms': ['POST', '/api/sms/SetSchoolModuleSms'],
|
||||
'smsController.setAllSchoolSms': ['POST', '/api/sms/SetAllSchoolSms'],
|
||||
'noticeController.userPull': ['POST', '/api/notice/UserPull'],
|
||||
'noticeController.setTopmost': ['POST', '/api/notice/SetTopmost'],
|
||||
'noticeController.receiveUsers': ['POST', '/api/notice/ReceiveUsers'],
|
||||
'noticeController.initializeRed': ['POST', '/api/notice/InitializeRed'],
|
||||
'noticeController.forward': ['POST', '/api/notice/Forward'],
|
||||
'noticeController.expedite': ['POST', '/api/notice/Expedite'],
|
||||
'noticeController.edit': ['POST', '/api/notice/Edit'],
|
||||
'noticeController.delete_': ['POST', '/api/notice/Delete'],
|
||||
'noticeController.create': ['POST', '/api/notice/Create'],
|
||||
'fileController.upload': ['POST', '/api/file/Upload'],
|
||||
'fileController.uploadImage': ['POST', '/api/file/UploadImage'],
|
||||
'fileController.delete_1': ['POST', '/api/file/Delete'],
|
||||
'fileController.bind': ['POST', '/api/file/Bind'],
|
||||
'dictController.update': ['POST', '/api/dict/Update'],
|
||||
'dictController.sort': ['POST', '/api/dict/Sort'],
|
||||
'dictController.delete_2': ['POST', '/api/dict/Delete'],
|
||||
'dictController.add': ['POST', '/api/dict/Add'],
|
||||
'contactsController.setContactsTea': ['POST', '/api/contacts/SetContactsTea'],
|
||||
'contactsController.editGroup': ['POST', '/api/contacts/EditGroup'],
|
||||
'contactsController.delGroup': ['POST', '/api/contacts/DelGroup'],
|
||||
'contactsController.addGroup': ['POST', '/api/contacts/AddGroup'],
|
||||
'baseApiController.logout': ['POST', '/api/base/Logout'],
|
||||
'baseApiController.fileUpload': ['POST', '/api/base/FileUpload'],
|
||||
'templateController.getTypelist': ['GET', '/api/template/Typelist'],
|
||||
'templateController.templateFirst': ['GET', '/api/template/TemplateFirst'],
|
||||
'templateController.list': ['GET', '/api/template/List'],
|
||||
'smsController.getSchool': ['GET', '/api/sms/GetSchool'],
|
||||
'smsController.getSchoolSms': ['GET', '/api/sms/GetSchoolSms'],
|
||||
'smsController.getModule': ['GET', '/api/sms/GetModule'],
|
||||
'smsController.getGroup': ['GET', '/api/sms/GetGroup'],
|
||||
'noticeController.statis': ['GET', '/api/notice/Statis'],
|
||||
'noticeController.sendList': ['GET', '/api/notice/SendList'],
|
||||
'noticeController.readDetail': ['GET', '/api/notice/ReadDetail'],
|
||||
'noticeController.list_1': ['GET', '/api/notice/List'],
|
||||
'noticeController.getNextPrev': ['GET', '/api/notice/GetNextPrev'],
|
||||
'noticeController.getDetail': ['GET', '/api/notice/GetDetail'],
|
||||
'noticeController.detail': ['GET', '/api/notice/Detail'],
|
||||
'noticeController.allFileDown': ['GET', '/api/notice/AllFileDown'],
|
||||
'fileController.preview': ['GET', '/api/file/Preview'],
|
||||
'fileController.list_2': ['GET', '/api/file/List'],
|
||||
'fileController.down': ['GET', '/api/file/Down'],
|
||||
'dictController.sync': ['GET', '/api/dict/Sync'],
|
||||
'dictController.manageList': ['GET', '/api/dict/ManageList'],
|
||||
'dictController.list_3': ['GET', '/api/dict/List'],
|
||||
'contactsController.getGroup_1': ['GET', '/api/contacts/GetGroup'],
|
||||
'contactsController.getGroupTree': ['GET', '/api/contacts/GetGroupTree'],
|
||||
'contactsController.getDeptRoleGroupTree': ['GET', '/api/contacts/GetDeptRoleGroupTree'],
|
||||
'contactsController.getContactsTea': ['GET', '/api/contacts/GetContactsTea'],
|
||||
'baseApiController.getUserMod': ['GET', '/api/base/GetUserMod'],
|
||||
'baseApiController.getUserInfo': ['GET', '/api/base/GetUserInfo'],
|
||||
'baseApiController.getSchoolAll': ['GET', '/api/base/GetSchoolAll'],
|
||||
'baseApiController.getGroupUser': ['GET', '/api/base/GetGroupUser'],
|
||||
'envConfigController.get': ['GET', '/api/env-config']
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 校园公告接口文档 - version 1.0
|
||||
*
|
||||
* 校园公告子应用:发公告、列表、删除、编辑、管理员查看、统计(springdoc-openapi)
|
||||
*
|
||||
* OpenAPI version: 3.0.1
|
||||
*
|
||||
*
|
||||
* NOTE: This file is auto generated by the alova's vscode plugin.
|
||||
*
|
||||
* https://alova.js.org/devtools/vscode
|
||||
*
|
||||
* **Do not edit the file manually.**
|
||||
*/
|
||||
import { Method } from 'alova';
|
||||
import apiDefinitions from './apiDefinitions';
|
||||
|
||||
const cache = Object.create(null);
|
||||
const createFunctionalProxy = (
|
||||
array: (string | symbol)[],
|
||||
alovaInstance: Alova<any, any, any, any, any>,
|
||||
configMap: any
|
||||
) => {
|
||||
const apiPathKey = array.join('.') as keyof typeof apiDefinitions;
|
||||
if (cache[apiPathKey]) {
|
||||
return cache[apiPathKey];
|
||||
}
|
||||
// create a new proxy instance
|
||||
const proxy = new Proxy(function () {}, {
|
||||
get(_, property) {
|
||||
// record the target property, so that it can get the completed accessing paths
|
||||
const newArray = [...array, property];
|
||||
// always return a new proxy to continue recording accessing paths.
|
||||
return createFunctionalProxy(newArray, alovaInstance, configMap);
|
||||
},
|
||||
apply(_, __, [config]) {
|
||||
const apiItem = apiDefinitions[apiPathKey];
|
||||
if (!apiItem) {
|
||||
throw new Error(`the api path of \`${apiPathKey}\` is not found`);
|
||||
}
|
||||
const mergedConfig = {
|
||||
...configMap[apiPathKey],
|
||||
...config
|
||||
};
|
||||
const [method, url] = apiItem;
|
||||
const pathParams = mergedConfig.pathParams;
|
||||
const urlReplaced = url!.replace(/\{([^}]+)\}/g, (_, key) => {
|
||||
const pathParam = pathParams[key];
|
||||
return pathParam;
|
||||
});
|
||||
delete mergedConfig.pathParams;
|
||||
let data = mergedConfig.data;
|
||||
if (Object.prototype.toString.call(data) === '[object Object]' && typeof FormData !== 'undefined') {
|
||||
let hasBlobData = false;
|
||||
const formData = new FormData();
|
||||
for (const key in data) {
|
||||
if (Array.isArray(data[key])) {
|
||||
for (const ele of data[key]) {
|
||||
formData.append(key, ele);
|
||||
if (ele instanceof Blob) {
|
||||
hasBlobData = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
formData.append(key, data[key]);
|
||||
if (data[key] instanceof Blob) {
|
||||
hasBlobData = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
data = hasBlobData ? formData : data;
|
||||
}
|
||||
return new Method(method!.toUpperCase() as MethodType, alovaInstance, urlReplaced, mergedConfig, data);
|
||||
}
|
||||
});
|
||||
cache[apiPathKey] = proxy;
|
||||
return proxy;
|
||||
};
|
||||
|
||||
export const createApis = (alovaInstance: Alova<any, any, any, any, any>, configMap: any) => {
|
||||
const Apis = new Proxy({} as Apis, {
|
||||
get(_, property) {
|
||||
return createFunctionalProxy([property], alovaInstance, configMap);
|
||||
}
|
||||
});
|
||||
return Apis;
|
||||
};
|
||||
|
||||
export const mountApis = (Apis: Apis) => {
|
||||
// define global variable `Apis`
|
||||
(globalThis as any).Apis = Apis;
|
||||
};
|
||||
|
||||
export const withConfigType = <Config extends MethodsConfigMap>(config: Config) => config;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createAlova } from 'alova';
|
||||
import { createApis, withConfigType, mountApis } from './createApis';
|
||||
|
||||
export const alovaInstance = createAlova({
|
||||
baseURL: 'http://base.yunhaixingtong.com/notice',
|
||||
requestAdapter: GlobalFetch(),
|
||||
beforeRequest: method => {},
|
||||
responded: res => {
|
||||
return res.json();
|
||||
}
|
||||
});
|
||||
|
||||
export const $$userConfigMap = withConfigType({});
|
||||
|
||||
const Apis = createApis(alovaInstance, $$userConfigMap);
|
||||
|
||||
mountApis(Apis);
|
||||
|
||||
export default Apis;
|
||||
@@ -0,0 +1,351 @@
|
||||
import { Method, type AlovaGenerics } from "alova";
|
||||
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||
import conf from "@/assets/config/host.config";
|
||||
import type {
|
||||
NoticeVo,
|
||||
NoticeDetailVo,
|
||||
NoticePageResult,
|
||||
BasePageResult,
|
||||
InitializeRedVo,
|
||||
NextPrevVo,
|
||||
ReadGroup,
|
||||
ReadReceiptVo,
|
||||
ReadTabCountVo,
|
||||
NoticeStatisticsVo,
|
||||
NoticeRequest,
|
||||
NoticeQuery,
|
||||
DictOption,
|
||||
DeptManageItem,
|
||||
ContactGroupPage,
|
||||
ContactUserPage,
|
||||
SchoolTreeNode,
|
||||
SchTreeNode,
|
||||
ClassTreeNode,
|
||||
ApprovalQuery,
|
||||
ApprovalTabCountVo,
|
||||
PushScopeItem,
|
||||
ScopePreviewVo,
|
||||
SubmitRequest,
|
||||
} from "@/types/notice";
|
||||
|
||||
/**
|
||||
* 校园通知接口封装(P0 标准化后新契约,P1-7 TS 契约化,校园通知改造扩展)。
|
||||
*
|
||||
* 前缀说明:conf.serviceHost 本地为空(走 vite 代理),线上为基础平台域名;
|
||||
* platform 固定使用 "api",与后端 Config.API_PATH 中的 api 匹配。
|
||||
* 统一响应由 alova-iview 解包为 data(code==0),页面直接使用返回值。
|
||||
*
|
||||
* 变更要点(P0):
|
||||
* 1. 通知域去掉 Notice_list 二级段(/api/notice/Notice_list/List → /api/notice/List)
|
||||
* 2. 文件域从 base/ 迁移到 file/(/api/base/FileDown → /api/file/Down)
|
||||
* 3. 字典域从 base/ 迁移到 dict/,GET 写操作改 POST(/api/base/DelDept → /api/dict/Delete)
|
||||
* 4. postJSON 用于 @RequestBody 接口;postForm 用于 @RequestParam 接口(表单提交)
|
||||
* 5. 参数更名:chaoSend→fileIds、topmost→smsFlag
|
||||
* 6. Delete 入参从 ids 逗号串改为 List<Integer> JSON 数组
|
||||
*
|
||||
* 变更要点(P1-7 / 校园通知改造):
|
||||
* 7. 全部方法加返回值泛型标注,替换 Record<string, any>
|
||||
* 8. 列表分页 content 字段对齐后端 BasePageResult(非旧 list)
|
||||
* 9. 新增:ScopeTree/ScopePreview 推送范围、SaveDraft/DraftList 草稿闭环、
|
||||
* Submit 提交审批、ReadReceipt 阅读回执、Approve 带 opinion
|
||||
*/
|
||||
|
||||
const NOTICE = "/api/notice";
|
||||
const BASE = "/api/base";
|
||||
const DICT = "/api/dict";
|
||||
const FILE = "/api/file";
|
||||
const CONTACTS = "/api/contacts";
|
||||
|
||||
/** 已收/已发序列可选筛选(重要程度必须用 noticeType:GetNextPrev 的 `type` 是序列类型,二者不能同名) */
|
||||
export type NextPrevFilter = {
|
||||
keyword?: string;
|
||||
/** 公告重要程度 1 普通 / 2 重要(后端映射为 NoticeQuery.type) */
|
||||
noticeType?: number;
|
||||
deptId?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
};
|
||||
|
||||
/** GET 请求,T 为返回数据类型 */
|
||||
function get<T = any>(url: string, params?: Record<string, any>): Method<AlovaGenerics<T>> {
|
||||
return new Method<AlovaGenerics<T>>("GET", alovaInstance, url, { params });
|
||||
}
|
||||
|
||||
/** POST JSON body(与后端 @RequestBody 对齐),T 为返回数据类型 */
|
||||
function postJSON<T = any>(url: string, data?: Record<string, any> | any[]): Method<AlovaGenerics<T>> {
|
||||
return new Method<AlovaGenerics<T>>(
|
||||
"POST",
|
||||
alovaInstance,
|
||||
url,
|
||||
{ headers: { "Content-Type": "application/json;charset=UTF-8" } },
|
||||
data ? JSON.stringify(data) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
/** POST 表单(与后端 @RequestParam 对齐,application/x-www-form-urlencoded),T 为返回数据类型 */
|
||||
function postForm<T = any>(url: string, data?: Record<string, any>): Method<AlovaGenerics<T>> {
|
||||
const pairs = data
|
||||
? Object.entries(data).filter(([, v]) => v !== undefined && v !== null)
|
||||
: [];
|
||||
const form = pairs.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
||||
// 同时把参数拼到 URL query:后端 @RequestParam 表单/查询参数都能接收,
|
||||
// 且本地 vite-plugin-mock 不解析 urlencoded body,靠 query 才能拿到参数(不影响线上)。
|
||||
const fullUrl = form ? `${url}${url.includes("?") ? "&" : "?"}${form}` : url;
|
||||
return new Method<AlovaGenerics<T>>(
|
||||
"POST",
|
||||
alovaInstance,
|
||||
fullUrl,
|
||||
{ headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" } },
|
||||
form || undefined
|
||||
);
|
||||
}
|
||||
|
||||
/** 附件下载直链(浏览器 location.href 跳转) */
|
||||
function fileDownUrl(fileId: number): string {
|
||||
return `${conf.serviceHost}${FILE}/Down?id=${fileId}`;
|
||||
}
|
||||
|
||||
/** 通知全部附件打包下载直链 */
|
||||
function allFileDownUrl(dataid: number): string {
|
||||
return `${conf.serviceHost}${NOTICE}/AllFileDown?dataid=${dataid}`;
|
||||
}
|
||||
|
||||
class BaseApi {
|
||||
// ========== 用户信息 & 模块权限 ==========
|
||||
/** 获取当前登录用户信息 */
|
||||
getUserInfo() {
|
||||
return get<SchoolTreeNode>(`${BASE}/GetUserInfo`);
|
||||
}
|
||||
/** 获取当前用户已授权的模块列表 */
|
||||
getUserMod() {
|
||||
return get<SchoolTreeNode[]>(`${BASE}/GetUserMod`);
|
||||
}
|
||||
/** 当前学校组织树(部门→教师,用于选择用户) */
|
||||
getSchoolAll() {
|
||||
return get<SchoolTreeNode[]>(`${BASE}/GetSchoolAll`);
|
||||
}
|
||||
/** 年级→班级树(学生场景:选择通知对象到班级一级,旧链路) */
|
||||
getClassTree() {
|
||||
return get<ClassTreeNode[]>(`${BASE}/GetClassTree`);
|
||||
}
|
||||
/** 学员组织树(学校→学段→年级→班级;toUser=false 仅到班级,不含学生叶子)——推送范围数据源 */
|
||||
getStuTree(toUser = false) {
|
||||
return get<SchTreeNode[]>(`${BASE}/getStuTree`, { toUser });
|
||||
}
|
||||
/** 教师组织树(组→教师;toUser=true 组下挂教师叶子,叶子 id=teaId 与审批教师ID同一空间)——审批人选人数据源 */
|
||||
getTeaTree(toUser = true) {
|
||||
return get<SchTreeNode[]>(`${BASE}/getTeaTree`, { toUser });
|
||||
}
|
||||
}
|
||||
|
||||
class NoticeApi {
|
||||
// ========== 收件箱 ==========
|
||||
/** 我的通知列表(readType 0 全部/1 未读/2 已读,type 1 普通/2 重要) */
|
||||
list(params: NoticeQuery) {
|
||||
return get<NoticePageResult<NoticeVo>>(`${NOTICE}/List`, params);
|
||||
}
|
||||
/**
|
||||
* 「已收到」三 tab 统计(v17):{all, unread, read}。
|
||||
* 入参同 list(keyword/type/deptId/时间范围生效;分页与 readType 字段被后端忽略),
|
||||
* 因此数字与对应 tab 下列表的 total 一致,且 all = unread + read。
|
||||
*/
|
||||
readTabCount(params: NoticeQuery) {
|
||||
return get<ReadTabCountVo>(`${NOTICE}/ReadTabCount`, params);
|
||||
}
|
||||
/** 清单:一键全部已读 */
|
||||
initializeRed() {
|
||||
return postForm<InitializeRedVo>(`${NOTICE}/InitializeRed`);
|
||||
}
|
||||
|
||||
// ========== 已发/全部通知 ==========
|
||||
/** 已发/全部通知列表(scope="user" 只看自己,不传=全部;status 0草稿/1已发布/2待审批/3已驳回) */
|
||||
sendList(params: NoticeQuery) {
|
||||
return get<BasePageResult<NoticeVo>>(`${NOTICE}/SendList`, params);
|
||||
}
|
||||
|
||||
// ========== 详情 ==========
|
||||
/** 查看详情(type=1 标读;type=0 编辑不标读) */
|
||||
modify(serid: number, type = 1) {
|
||||
return get<NoticeDetailVo>(`${NOTICE}/Detail`, { serid, type });
|
||||
}
|
||||
/** 编辑回显(不标读) */
|
||||
getNoticeDetails(serid: number) {
|
||||
return get<NoticeDetailVo>(`${NOTICE}/GetDetail`, { serid });
|
||||
}
|
||||
/**
|
||||
* 上一篇/下一篇。
|
||||
* @param type "0" 已发序列 / "1" 已收序列
|
||||
* @param filters 可选:已收序列沿用列表筛选(keyword / noticeType / deptId / 时间范围)。
|
||||
* 后端刻意忽略 readType(未读公告点开即被标读,带未读过滤会把当前条挤出序列),不要传。
|
||||
*/
|
||||
getNextPrevNotice(serid: number, type: string, filters?: NextPrevFilter) {
|
||||
return get<NextPrevVo>(`${NOTICE}/GetNextPrev`, { serid, type, ...(filters || {}) });
|
||||
}
|
||||
/** 阅读情况(已读/未读分组,教师维度) */
|
||||
orgizeModify(serid: number) {
|
||||
return get<ReadGroup[]>(`${NOTICE}/ReadDetail`, { serid });
|
||||
}
|
||||
/** 阅读回执(按 年级→班级→学生 分组 + 教师组,学员身份快照) */
|
||||
readReceipt(serid: number) {
|
||||
return get<ReadReceiptVo>(`${NOTICE}/ReadReceipt`, { serid });
|
||||
}
|
||||
/** 追加接收人 */
|
||||
receiveUsers(serid: number, users: string) {
|
||||
return postForm<void>(`${NOTICE}/ReceiveUsers`, { serid, users });
|
||||
}
|
||||
|
||||
// ========== 推送范围(改造方案 3.1 · v3 换源) ==========
|
||||
/** 范围树数据源已改 base/getStuTree(见 baseApi.getStuTree);此处仅按已选范围预估学员数 */
|
||||
scopePreview(scopes: PushScopeItem[], receiveList?: string) {
|
||||
return get<ScopePreviewVo>(`${NOTICE}/ScopePreview`, {
|
||||
scopes: JSON.stringify(scopes || []),
|
||||
receiveList: receiveList || "",
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 发布/编辑/删除/草稿 ==========
|
||||
/** 新建通知(JSON body,fileIds 为附件ID逗号串,smsFlag 为短信标记;带 flow 即送审) */
|
||||
create(data: NoticeRequest) {
|
||||
return postJSON<number>(`${NOTICE}/Create`, data);
|
||||
}
|
||||
/** 编辑通知(JSON body,含 id) */
|
||||
editNotice(data: NoticeRequest) {
|
||||
return postJSON<void>(`${NOTICE}/Edit`, data);
|
||||
}
|
||||
/** 存为草稿(status=0:仅存内容 + 范围快照 + 附件 + 流程定义,不圈人) */
|
||||
saveDraft(data: NoticeRequest) {
|
||||
return postJSON<number>(`${NOTICE}/SaveDraft`, data);
|
||||
}
|
||||
/** 草稿箱列表(status=0 本人草稿) */
|
||||
draftList(params: NoticeQuery) {
|
||||
return get<BasePageResult<NoticeVo>>(`${NOTICE}/DraftList`, params);
|
||||
}
|
||||
/** 提交审批(status 0/3→2;steps 多步流程,空=复用草稿已存流程定义) */
|
||||
submit(data: SubmitRequest) {
|
||||
return postJSON<void>(`${NOTICE}/Submit`, data);
|
||||
}
|
||||
/** 删除通知(ids 数组,支持批量) */
|
||||
deleteNotice(ids: number | string) {
|
||||
// 兼容页面传单个 id 或逗号串,统一转成数组
|
||||
const idArr = String(ids).split(",").map(Number).filter(Boolean);
|
||||
return postJSON<void>(`${NOTICE}/Delete`, idArr);
|
||||
}
|
||||
/** 撤回 */
|
||||
userPull(serid: number) {
|
||||
return postForm<void>(`${NOTICE}/UserPull`, { serid });
|
||||
}
|
||||
/** 置顶/取消置顶 */
|
||||
setTopmost(serid: number, topmost = 1) {
|
||||
return postForm<void>(`${NOTICE}/SetTopmost`, { serid, topmost });
|
||||
}
|
||||
/** 催收未读 */
|
||||
expedite(serid: number) {
|
||||
return postForm<void>(`${NOTICE}/Expedite`, { serid });
|
||||
}
|
||||
|
||||
// ========== 附件 ==========
|
||||
fileDownUrl = fileDownUrl;
|
||||
allFileDownUrl = allFileDownUrl;
|
||||
|
||||
// ========== 部门字典 ==========
|
||||
/** 发文部门下拉 */
|
||||
getSysDic() {
|
||||
return get<DictOption[]>(`${DICT}/List`, { type: 1 });
|
||||
}
|
||||
/** 是否开通短信(迁移到 dict/Sync 查询,保持旧调用名不变) */
|
||||
getSchModuleIsSms() {
|
||||
return get<boolean>(`${DICT}/Sync`);
|
||||
}
|
||||
/** 通知统计 */
|
||||
statistics(params?: { starttime?: string; endtime?: string }) {
|
||||
return get<NoticeStatisticsVo>(`${NOTICE}/Statis`, params);
|
||||
}
|
||||
|
||||
// ========== 审批(多步流程:任一步驳回整单退回,末步通过真实发布) ==========
|
||||
/** 审批列表(status: pending/approved/rejected/all,缺省 pending) */
|
||||
approvalList(params: ApprovalQuery) {
|
||||
return get<BasePageResult<NoticeVo>>(`${NOTICE}/ApprovalList`, params);
|
||||
}
|
||||
/** 待我审批数量(菜单角标) */
|
||||
approvalCount() {
|
||||
return get<number>(`${NOTICE}/ApprovalCount`);
|
||||
}
|
||||
/** 审批中心四 tab 统计数字(v13:口径与各 tab 默认过滤一致,不含 keyword) */
|
||||
approvalTabCount() {
|
||||
return get<ApprovalTabCountVo>(`${NOTICE}/ApprovalTabCount`);
|
||||
}
|
||||
/** 审批通过(当前步;或签任一通过/会签全部通过后流转下一步,末步通过即发布) */
|
||||
approve(serid: number, opinion?: string) {
|
||||
return postForm<void>(`${NOTICE}/Approve`, { serid, opinion: opinion || "" });
|
||||
}
|
||||
/** 审批驳回(当前步,意见必填,通知退回发布人可编辑重提) */
|
||||
reject(serid: number, remark: string) {
|
||||
return postForm<void>(`${NOTICE}/Reject`, { serid, remark });
|
||||
}
|
||||
/** 审批人记忆回显:当前发布人最近一次所选审批人教师ID列表(教师树路径,无记忆返回空) */
|
||||
approverSuggest() {
|
||||
return get<{ teaIds: number[] }>(`${NOTICE}/ApproverSuggest`);
|
||||
}
|
||||
/** 审批教师绑定:教师树叶子ID → 姓名(未命中 name 为 null,前端降级显示 ID) */
|
||||
approverResolve(ids: number[]) {
|
||||
return get<{ id: number; name: string | null }[]>(`${NOTICE}/ApproverResolve`, { ids: ids.join(",") });
|
||||
}
|
||||
}
|
||||
|
||||
class SettingApi {
|
||||
// ========== 发文部门管理 ==========
|
||||
/** 获取发文部门管理列表 */
|
||||
getDictMangeList() {
|
||||
return get<DeptManageItem[]>(`${DICT}/ManageList`, { type: "dept" });
|
||||
}
|
||||
/** 新增发文部门 */
|
||||
addDeptDic(name: string) {
|
||||
return postForm<void>(`${DICT}/Add`, { name });
|
||||
}
|
||||
/** 更新发文部门(名称、启用状态) */
|
||||
updateDeptDic(SerID: number, Name: string, Enable = 1) {
|
||||
return postForm<void>(`${DICT}/Update`, { SerID, Name, Enable });
|
||||
}
|
||||
/** 删除发文部门(原 GET DelDept,已 POST 化) */
|
||||
delDept(serid: number) {
|
||||
return postForm<void>(`${DICT}/Delete`, { serid });
|
||||
}
|
||||
/** 发文部门排序(原 GET OrdbyDicIds,已 POST 化) */
|
||||
sortDeptDic(ids: string) {
|
||||
return postForm<void>(`${DICT}/Sort`, { ids });
|
||||
}
|
||||
// ========== 自定义组管理 ==========
|
||||
/** 获取联系组列表 */
|
||||
getGroup(page: number, count: number) {
|
||||
return get<ContactGroupPage>(`${CONTACTS}/GetGroup`, { page, count });
|
||||
}
|
||||
/** 新增联系组 */
|
||||
addGroup(name: string) {
|
||||
return postForm<void>(`${CONTACTS}/AddGroup`, { name });
|
||||
}
|
||||
/** 编辑联系组 */
|
||||
editGroup(id: number, name: string) {
|
||||
return postForm<void>(`${CONTACTS}/EditGroup`, { name, id });
|
||||
}
|
||||
/** 删除联系组 */
|
||||
delGroup(ids: string) {
|
||||
return postForm<void>(`${CONTACTS}/DelGroup`, { ids });
|
||||
}
|
||||
/** 获取联系组用户 */
|
||||
getContactsTea(gid: number, page = 0, count = 0) {
|
||||
return get<ContactUserPage>(`${CONTACTS}/GetContactsTea`, { gid, page, count });
|
||||
}
|
||||
/** 设置联系组用户 */
|
||||
setContactsTea(gid: number, ids: string) {
|
||||
return postForm<void>(`${CONTACTS}/SetContactsTea`, { gid, ids });
|
||||
}
|
||||
/** 联系组用户树(选择用户-自定义组 tab) */
|
||||
getGroupTree() {
|
||||
return get<SchoolTreeNode[]>(`${CONTACTS}/GetGroupTree`, { type: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
export const baseApi = new BaseApi();
|
||||
export const noticeApi = new NoticeApi();
|
||||
export const settingApi = new SettingApi();
|
||||
export default noticeApi;
|
||||
@@ -0,0 +1,43 @@
|
||||
import config from "./host.config";
|
||||
import { AxiosRequestConfig } from "axios";
|
||||
import { PcDebugUser } from "@/types/Basics";
|
||||
|
||||
const serviceUrl = config.isMock ? "" : config.serviceHost,
|
||||
timeout: number = 15000;
|
||||
|
||||
let baseConfig: AxiosRequestConfig = {
|
||||
baseURL: serviceUrl + "/",
|
||||
headers: {
|
||||
// "Content-Type": "application/x-www-form-urlencoded",
|
||||
"Content-Type": "application/json;charset=urf-8",
|
||||
yschapirequest: "1",
|
||||
"Access-Control-Allow-Headers": "Set-Cookie",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
params: {},
|
||||
data: {},
|
||||
timeout, // 接口超时限制(ms)
|
||||
withCredentials: true,
|
||||
responseType: "json",
|
||||
// transformRequest: [
|
||||
// function (data) {
|
||||
// if (typeof data == "string") return data;
|
||||
// let ret = "";
|
||||
// for (let it in data) {
|
||||
// ret += encodeURIComponent(it) + "=" + encodeURIComponent(data[it]) + "&";
|
||||
// }
|
||||
// return ret;
|
||||
// },
|
||||
// ],
|
||||
// maxContentLength: 2000,
|
||||
validateStatus(status) {
|
||||
return status >= 200 && status < 300; // default
|
||||
},
|
||||
};
|
||||
|
||||
let debugUser: PcDebugUser = {
|
||||
DebugUser: "zhd",
|
||||
AppType: "ChildOptCourse",
|
||||
};
|
||||
|
||||
export { baseConfig, debugUser };
|
||||
@@ -0,0 +1,83 @@
|
||||
import { BaseConf } from "@/types/Basics";
|
||||
|
||||
/**
|
||||
* 配置优先级(高 → 低):
|
||||
* ① window.__APP_CONFIG__:线上运行时配置,index.html 引导脚本在应用模块加载前同步拉取
|
||||
* /<contextPath>/api/env-config?target=<部署目录名>(本应用部署于 /{contextPath}/front/,
|
||||
* 故为 front,对应后端键 yh.env-config.source.front,projman 配置管理维护)写入
|
||||
* ② import.meta.env.VITE_*:仅本地开发/演示(vite dev --mode dev 读 .env.dev、--mode mock 读 .env.mock);
|
||||
* build 产物不读任何 .env.*(.env.base/.env.ld/.env.prod 已删),VITE_* 恒为 undefined,
|
||||
* 本层自动跳过,产物不含任何环境值
|
||||
* ③ 同域推导兜底:env-config 未配置/拉取失败时框架至少能渲染并调通同源 API;
|
||||
* ssoHost 为空 → 登出跳转不可用——与后端未就绪状态一致,可接受。
|
||||
*/
|
||||
const env = import.meta.env;
|
||||
|
||||
// ③ 同域推导兜底:context 段取当前路径首段(/{contextPath}/front/ → contextPath,即后端上下文),
|
||||
// 与服务端 context-path 对齐(本地无、base/prod /notice);本地 dev/mock 在根路径运行(无段),
|
||||
// serviceHost 留空走 vite 同源代理(规避 CAS 过滤器不回 CORS 头)
|
||||
const seg = window.location.pathname.split("/").filter(Boolean);
|
||||
const origin = window.location.origin;
|
||||
|
||||
let conf: BaseConf = {
|
||||
baseHost: origin,
|
||||
ssoHost: "",
|
||||
serviceHost: seg.length >= 2 ? `${origin}/${seg[0]}` : "",
|
||||
relayConf: {
|
||||
ziyuankuHost: "",
|
||||
zhuanxiangHost: "",
|
||||
},
|
||||
isMock: false,
|
||||
isDebug: env.DEV,
|
||||
};
|
||||
|
||||
// 逐键覆盖:只覆盖来源里实际出现且非空的键,未出现的键沿用底层值
|
||||
const applyConf = (target: any, source: any) => {
|
||||
if (!source || typeof source !== "object") return;
|
||||
Object.keys(source).forEach(key => {
|
||||
if (source[key] !== undefined && source[key] !== null && source[key] !== "") {
|
||||
(target as any)[key] = source[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ② 本地开发/演示(build 产物中恒 undefined → 自动跳过)
|
||||
applyConf(conf, {
|
||||
baseHost: env.VITE_BASE_HOST,
|
||||
ssoHost: env.VITE_SSO_HOST,
|
||||
serviceHost: env.VITE_IS_MOCK === "true" ? "" : env.VITE_SERVICE_HOST,
|
||||
relayConf: {
|
||||
ziyuankuHost: env.VITE_ZIYUANKU_HOST,
|
||||
zhuanxiangHost: env.VITE_ZHUANXIANG_HOST,
|
||||
},
|
||||
isMock: env.VITE_IS_MOCK === "true",
|
||||
});
|
||||
|
||||
// ① 线上运行时配置(最高优先,覆盖前面所有);relayConf 为嵌套对象,按子键合并,
|
||||
// 运行时 JSON 只配部分资源库地址时未配的键沿用兜底值
|
||||
const runtime = (window as any).__APP_CONFIG__;
|
||||
applyConf(conf, runtime);
|
||||
if (runtime && typeof runtime === "object" && runtime.relayConf) {
|
||||
applyConf(conf.relayConf, runtime.relayConf);
|
||||
}
|
||||
|
||||
// HTTPS 自动升级:当前页面是 https 时,将所有 http:// 配置项升级为 https://
|
||||
var ishttps = "https:" == document.location.protocol ? true : false;
|
||||
if (ishttps) {
|
||||
Object.keys(conf).forEach(element => {
|
||||
if (typeof conf[element] === "string" && conf[element].indexOf("http://") > -1) {
|
||||
conf[element] = conf[element].replace("http://", "https://");
|
||||
}
|
||||
});
|
||||
// relayConf 内部也需要处理
|
||||
if (conf.relayConf) {
|
||||
Object.keys(conf.relayConf).forEach(key => {
|
||||
const val = (conf.relayConf as any)[key];
|
||||
if (typeof val === "string" && val.indexOf("http://") > -1) {
|
||||
(conf.relayConf as any)[key] = val.replace("http://", "https://");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default conf;
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { IModule } from "@/store";
|
||||
|
||||
/**
|
||||
* 校园通知左侧菜单数据(对照后端 GetUserMod 现行模块)——历史本地兜底,现已无引用。
|
||||
* 真实菜单管线见 `utils/menuRoute.normalizeModList`(v10/v11):栏目名用接口 `name`、
|
||||
* 顺序按接口 `orderNumber` 升序,前端不改名;草稿箱由该管线注入(平台无对应 modId 31)。
|
||||
*/
|
||||
export function getMenuList(): Promise<IModule[]> {
|
||||
const menu: IModule[] = [
|
||||
{ id: 1, modId: 1, name: "我的通知", page: "/" },
|
||||
{ id: 2, modId: 2, name: "已发管理", page: "/list" },
|
||||
{ id: 31, modId: 31, name: "草稿箱", page: "/drafts" },
|
||||
{ id: 3, modId: 3, name: "全部通知", page: "/noticelist" },
|
||||
{ id: 5, modId: 5, name: "审批中心", page: "/approval" },
|
||||
{ id: 4, modId: 4, name: "通知统计", page: "/stats" },
|
||||
{ id: 21, modId: 21, name: "应用设置", page: "/setting" },
|
||||
];
|
||||
return Promise.resolve(menu);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M41.65 64.5995C42.942 65.8915 45.05 66.2995 46.75 65.4495C49.742 64.1575 52.292 62.8995 54.842 60.7575C56.134 59.4655 56.984 57.7995 56.542 55.6575C56.1 53.1075 56.542 50.1155 57.834 47.5655C59.126 45.0155 61.642 43.3155 64.226 42.4655C65.926 42.0235 67.184 40.3235 67.626 38.6575C68.068 35.6655 68.068 32.2655 67.626 29.3075C67.184 27.6075 65.926 25.9075 64.226 25.4655C61.676 24.6155 59.126 22.9155 57.834 20.3655C56.542 17.8155 56.134 14.8235 56.542 12.2735C56.984 10.5735 56.1 8.43153 54.842 7.17353C52.292 5.06553 49.742 3.77353 46.75 2.48153C45.05 1.63153 42.942 2.03953 41.65 3.33153C37.4 7.24153 30.6 7.24153 26.35 3.39953C25.058 2.10753 22.95 1.69953 21.25 2.54953C18.292 3.80753 15.708 5.09953 13.158 7.24153C11.458 8.53353 11.016 10.2335 11.458 12.3415C12.75 17.8495 9.35 23.7995 3.842 25.4995C1.7 25.9415 0.442 27.6415 0.442 29.3415C0 31.0415 0 32.2995 0 33.9995C0 35.6995 0 36.9915 0.442 38.6915C0.442 40.3915 1.734 42.0915 3.842 42.4995C6.392 43.3495 8.942 45.0495 10.234 47.5995C11.526 50.1495 11.934 53.1415 11.492 55.6915C11.05 57.3915 11.934 59.4995 13.192 60.7915C15.742 62.9335 18.292 64.1915 21.284 65.4835C22.984 66.3335 25.126 65.9255 26.384 64.6335C30.6 60.3495 37.4 60.3495 41.65 64.5995Z" fill="#919BF2"/>
|
||||
<path d="M24.208 34C24.208 35.2859 24.4613 36.5592 24.9534 37.7472C25.4455 38.9353 26.1667 40.0147 27.076 40.924C27.9853 41.8333 29.0647 42.5545 30.2528 43.0466C31.4408 43.5387 32.7141 43.792 34 43.792C35.2859 43.792 36.5592 43.5387 37.7472 43.0466C38.9353 42.5545 40.0147 41.8333 40.924 40.924C41.8333 40.0147 42.5545 38.9353 43.0466 37.7472C43.5387 36.5592 43.792 35.2859 43.792 34C43.792 32.7141 43.5387 31.4408 43.0466 30.2528C42.5545 29.0648 41.8333 27.9853 40.924 27.076C40.0147 26.1667 38.9353 25.4455 37.7472 24.9534C36.5592 24.4613 35.2859 24.208 34 24.208C32.7141 24.208 31.4408 24.4613 30.2528 24.9534C29.0647 25.4455 27.9853 26.1667 27.076 27.076C26.1667 27.9853 25.4455 29.0648 24.9534 30.2528C24.4613 31.4408 24.208 32.7141 24.208 34Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97095)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M44.1242 40.672C45.2522 41.032 46.2482 41.452 47.1122 41.932L46.4282 42.976C45.4922 42.424 44.5142 41.98 43.4942 41.644L44.1242 40.672ZM51.1982 40.402C51.0062 40.93 50.8502 41.314 50.7302 41.554H57.9842V42.328C57.7682 43.228 57.4802 44.104 57.1202 44.956L55.8782 44.578C56.1662 44.05 56.4242 43.42 56.6522 42.688H53.5922C53.5202 43.12 53.4122 43.516 53.2682 43.876C54.2042 45.256 56.0582 46.21 58.8302 46.738L58.3622 47.89C55.7222 47.35 53.8502 46.366 52.7462 44.938C52.5182 45.286 52.2782 45.592 52.0262 45.856C51.1262 46.696 49.6802 47.35 47.6882 47.818L47.2022 46.666C48.9782 46.282 50.2562 45.754 51.0362 45.082C51.6962 44.47 52.1342 43.672 52.3502 42.688H50.0462C49.5422 43.336 48.9482 43.93 48.2642 44.47L47.5082 43.462C48.6722 42.61 49.4762 41.536 49.9202 40.24L51.1982 40.402ZM56.7602 48.304V53.65H55.4642V49.51H46.4822V53.794H45.1862V48.304H56.7602ZM51.7562 50.302C51.5042 52.582 50.9582 54.118 50.1182 54.91C49.1702 55.894 47.0822 56.512 43.8542 56.764L43.3502 55.612C46.2182 55.444 48.1082 54.952 49.0202 54.136C49.8362 53.356 50.3162 52.042 50.4602 50.194L51.7562 50.302ZM47.3822 44.938C46.4822 45.874 45.3662 46.756 44.0342 47.584L43.1702 46.576C44.5262 45.808 45.6782 44.968 46.6262 44.056L47.3822 44.938ZM52.1702 53.596C54.4982 54.208 56.6282 54.946 58.5602 55.81L57.8582 56.89C55.7822 55.906 53.6642 55.138 51.5042 54.586L52.1702 53.596Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97095">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97102)">
|
||||
<path d="M52.5096 4.53333H43.4429V2.26667C43.4429 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2941 0 23.0429 0C21.7917 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70481 4.53333 2.64294 8.5952 2.64294 13.6V54.4C2.64294 59.4048 6.70481 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.5189 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.2429 48.8512 16.2429 47.6C16.2429 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9653 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.2429 35.2512 16.2429 34C16.2429 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9653 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.2429 21.6512 16.2429 20.4C16.2429 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9653 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M46.2482 45.172H47.4902V47.962H49.3082V49.15H47.4902V51.67C48.1742 51.526 48.8222 51.382 49.4342 51.238V52.444L47.4902 52.894V56.818H46.2482V53.128C45.2402 53.308 44.1362 53.47 42.9362 53.614L42.7742 52.354C44.2382 52.186 45.3962 52.03 46.2482 51.886V49.15H43.2602L42.9902 48.016C43.5062 46.888 43.9862 45.484 44.4302 43.804H42.8282V42.562H44.7362C44.9402 41.65 45.0962 40.894 45.2042 40.294L46.4282 40.492L45.9962 42.562H49.2542V43.804H45.6902C45.2702 45.424 44.8142 46.81 44.3222 47.962H46.2482V45.172ZM49.8302 42.58H53.6822V40.366H54.9062V42.58H57.3002C56.9042 42.04 56.4182 41.464 55.8422 40.852L56.7062 40.312C57.3662 41.008 57.8762 41.608 58.2362 42.112L57.5702 42.58H59.2082V43.804H54.9062V45.46H58.5962V55.234C58.5962 56.218 58.1462 56.71 57.2462 56.71H56.0042L55.6802 55.486L56.8322 55.54C57.2042 55.54 57.3902 55.354 57.3902 54.982V52.84H54.9062V56.674H53.6822V52.84H51.3242V56.782H50.1182V45.46H53.6822V43.804H49.8302V42.58ZM53.6822 46.612H51.3242V48.646H53.6822V46.612ZM51.3242 49.762V51.724H53.6822V49.762H51.3242ZM57.3902 51.724V49.762H54.9062V51.724H57.3902ZM57.3902 48.646V46.612H54.9062V48.646H57.3902Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97102">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97108)">
|
||||
<path d="M52.5096 4.53333H43.4429V2.26667C43.4429 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2941 0 23.0429 0C21.7917 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70481 4.53333 2.64294 8.5952 2.64294 13.6V54.4C2.64294 59.4048 6.70481 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.5189 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.2429 48.8512 16.2429 47.6C16.2429 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9653 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.2429 35.2512 16.2429 34C16.2429 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9653 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.2429 21.6512 16.2429 20.4C16.2429 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9653 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M55.2302 55.288C56.7302 55.288 58.0802 55.264 59.2802 55.216L59.0642 56.512H55.5182C51.2102 56.512 48.5162 56.278 47.4362 55.81C46.5002 55.414 45.6542 54.688 44.8982 53.632C44.5742 54.784 44.1242 55.846 43.5482 56.818L42.6122 55.99C43.5722 54.358 44.1002 52.516 44.1962 50.464L45.3482 50.608C45.3122 51.256 45.2522 51.826 45.1682 52.318C45.6362 53.05 46.1522 53.626 46.7162 54.046V49.672H43.1522V48.484H50.7842V49.672H47.8682V51.58H50.3342V52.732H47.8682V54.694C47.9762 54.73 48.0662 54.766 48.1382 54.802C49.1822 55.126 51.5462 55.288 55.2302 55.288ZM43.9622 40.996H50.1002V47.278H43.9622V40.996ZM48.8402 46.198V44.668H45.2222V46.198H48.8402ZM45.2222 43.66H48.8402V42.076H45.2222V43.66ZM51.0362 41.014H58.8122V42.202H55.5722C55.5122 42.73 55.4342 43.21 55.3382 43.642H58.3082V50.914H57.1022V44.758H52.7462V50.914H51.5942V43.642H54.1682C54.2642 43.234 54.3422 42.754 54.4022 42.202H51.0362V41.014ZM56.2202 50.968C57.3362 51.832 58.2722 52.696 59.0282 53.56L58.1102 54.46C57.4142 53.596 56.4962 52.708 55.3562 51.796L56.2202 50.968ZM54.3302 45.604H55.5182V48.142C55.4822 49.834 55.1522 51.202 54.5282 52.246C53.9282 53.182 52.9322 53.926 51.5402 54.478L50.8382 53.362C52.1222 52.894 53.0282 52.252 53.5562 51.436C54.0362 50.572 54.2942 49.474 54.3302 48.142V45.604Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97108">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97114)">
|
||||
<path d="M52.5096 4.53333H43.4429V2.26667C43.4429 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2941 0 23.0429 0C21.7917 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70481 4.53333 2.64294 8.5952 2.64294 13.6V54.4C2.64294 59.4048 6.70481 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.5189 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.2429 48.8512 16.2429 47.6C16.2429 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9653 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.2429 35.2512 16.2429 34C16.2429 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9653 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.2429 21.6512 16.2429 20.4C16.2429 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9653 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M50.4602 48.826H48.7682V47.62H53.4302V48.826H51.7382V53.704C52.5902 53.512 53.3702 53.314 54.0782 53.11V54.316C52.5182 54.76 50.8322 55.138 49.0202 55.45L48.6962 54.226C49.3082 54.154 49.8962 54.064 50.4602 53.956V48.826ZM57.2822 56.53C56.6582 56.53 56.1122 56.062 55.6442 55.126C55.1042 54.046 54.7082 52.576 54.4562 50.716C54.2642 49.192 54.1322 47.218 54.0602 44.794H48.3542V43.534H54.0242C54.0002 42.838 53.9882 41.74 53.9882 40.24H55.2482C55.2482 41.752 55.2602 42.85 55.2842 43.534H58.8662V44.794H55.3022C55.3862 47.83 55.5782 50.218 55.8782 51.958C56.2742 53.998 56.7422 55.054 57.2822 55.126C57.6542 55.054 58.0202 53.95 58.3802 51.814L59.4062 52.642C58.8182 55.234 58.1102 56.53 57.2822 56.53ZM56.7962 40.222C57.5522 41.062 58.1522 41.818 58.5962 42.49L57.6962 43.12C57.2762 42.472 56.6702 41.692 55.8782 40.78L56.7962 40.222ZM44.9162 40.474C46.0442 41.374 47.0102 42.298 47.8142 43.246L46.9322 44.146C46.2002 43.246 45.2282 42.316 44.0162 41.356L44.9162 40.474ZM42.7562 45.802H46.5362V53.65C46.9682 53.23 47.5202 52.654 48.1922 51.922L48.5342 53.29C47.5262 54.382 46.4762 55.336 45.3842 56.152L44.8802 55C45.1442 54.772 45.2762 54.526 45.2762 54.262V47.044H42.7562V45.802Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97114">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97120)">
|
||||
<path d="M52.5096 4.53333H43.4429V2.26667C43.4429 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2941 0 23.0429 0C21.7917 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70481 4.53333 2.64294 8.5952 2.64294 13.6V54.4C2.64294 59.4048 6.70481 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.5189 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.2429 48.8512 16.2429 47.6C16.2429 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9653 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.2429 35.2512 16.2429 34C16.2429 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9653 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.2429 21.6512 16.2429 20.4C16.2429 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9653 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M42.9181 45.784H48.0661C48.2221 45.196 48.4081 44.452 48.6241 43.552H44.1421V42.346H48.8941C49.0021 41.854 49.1521 41.128 49.3441 40.168L50.6761 40.276L50.2621 42.346H58.0201V43.552H49.9921C49.8241 44.296 49.6441 45.04 49.4521 45.784H59.0821V47.044H49.1101C48.9421 47.62 48.6841 48.412 48.3361 49.42H56.9041V50.536C56.0161 51.904 54.6601 53.242 52.8361 54.55C53.8321 54.91 54.8701 55.348 55.9501 55.864L55.2301 56.998C52.5661 55.678 49.7821 54.628 46.8781 53.848L47.5621 52.84C48.7621 53.164 50.0581 53.566 51.4501 54.046C53.0941 52.966 54.4021 51.826 55.3741 50.626H46.4821C46.9501 49.366 47.3581 48.172 47.7061 47.044H42.9181V45.784Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97120">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
t="1615860353141"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="6624"
|
||||
width="14"
|
||||
height="14"
|
||||
>
|
||||
<path
|
||||
d="M0 102.4h307.2v307.2H0zM0 614.4h307.2v307.2H0zM409.6 102.4h614.4v102.4H409.6zM409.6 307.2h614.4v102.4H409.6zM409.6 614.4h614.4v102.4H409.6zM409.6 819.2h614.4v102.4H409.6z"
|
||||
p-id="6625"
|
||||
fill="#656667"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 914 B |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<path d="M832 192H544l-64-64H192c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64V256c0-35.2-28.8-64-64-64z m0 576H192V192h266.7l64 64H832v512z" fill="#999999"/>
|
||||
<path d="M256 320h512v64H256z m0 128h384v64H256z m0 128h256v64H256z" fill="#cccccc"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 380 B |
@@ -0,0 +1,13 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="30" height="30" rx="3" fill="white" stroke="#DCDEE0" stroke-width="2" stroke-dasharray="4 4"/>
|
||||
<g clip-path="url(#clip0_125_1092)">
|
||||
<path d="M24 8H8V24H24V8Z" fill="white" fill-opacity="0.01"/>
|
||||
<path d="M16.0209 11.3333L16.0087 20.6666" stroke="#666666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.3333 16H20.6667" stroke="#666666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_125_1092">
|
||||
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 695 B |
@@ -0,0 +1,15 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="30" height="30" rx="3" fill="white" stroke="#DCDEE0" stroke-width="2"/>
|
||||
<g clip-path="url(#clip0_125_1099)">
|
||||
<path d="M24 8H8V24H24V8Z" fill="white" fill-opacity="0.01"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.6667 13H21.3333L20.3333 22.6667H11.6667L10.6667 13Z" stroke="#F15B43" stroke-width="1.125" stroke-linejoin="round"/>
|
||||
<path d="M14.6667 16.3333V19.6667" stroke="#F15B43" stroke-width="1.125" stroke-linecap="round"/>
|
||||
<path d="M17.3333 16.3333V19.6658" stroke="#F15B43" stroke-width="1.125" stroke-linecap="round"/>
|
||||
<path d="M12 13L17.4414 9L20 13" stroke="#F15B43" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_125_1099">
|
||||
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 919 B |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<path d="M482.214544 543.934407l-0.099952-59.970717 59.970718-0.099952 0.099951 59.970718-59.970717 0.099951z m119.941435-0.199902l-0.099952-59.970718 59.970718-0.099951 0.099951 59.970717-59.970717 0.099952z m119.941434-0.199903l-0.099951-59.970717 59.970718-0.099951 0.099951 59.970717-59.970718 0.099951z m119.941435-0.299853l-0.099951-59.970718 59.970717-0.099951 0.099952 59.970717-59.970718 0.099952z m119.941435-0.199903l-0.099951-59.970717 59.970717-0.099951 0.099951 59.970717-59.970717 0.099951z" />
|
||||
<path d="M482.914202 1024l-0.099951-59.970717 59.970717-0.099952 0.099951 59.970718-59.970717 0.099951z m-0.199902-119.941435l-0.099952-59.970717 59.970718-0.099951 0.099951 59.970717-59.970717 0.099951z m-0.199903-119.941435l-0.099951-59.970717 59.970717-0.099951 0.099952 59.970717-59.970718 0.099951z m-0.199902-119.941435l-0.099951-59.970717 59.970717-0.099951 0.099951 59.970717-59.970717 0.099951z m-0.099951-119.941434l-0.099952-59.970718 59.970718-0.099951 0.099951 59.970717-59.970717 0.099952z m-0.199903-119.941435l-0.099951-59.970718 59.970718-0.099951 0.099951 59.970718-59.970718 0.099951z m-0.199902-119.941435l-0.099951-59.970718 59.970717-0.099951 0.099951 59.970718-59.970717 0.099951z m-0.199902-119.941435l-0.099952-59.970717 59.970718-0.099952 0.099951 59.970718-59.970717 0.099951z m-0.199903-119.941435l-0.099951-59.970717 59.970717-0.099951 0.099952 59.970717-59.970718 0.099951z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7225_67996)">
|
||||
<path d="M18 0H0V18H18V0Z" fill="white" fill-opacity="0.01"/>
|
||||
<path d="M14.625 2.25H3.375C2.75368 2.25 2.25 2.75368 2.25 3.375V14.625C2.25 15.2463 2.75368 15.75 3.375 15.75H14.625C15.2463 15.75 15.75 15.2463 15.75 14.625V3.375C15.75 2.75368 15.2463 2.25 14.625 2.25Z" stroke="#5B6871" stroke-width="1.125" stroke-linejoin="round"/>
|
||||
<path d="M9 6V12" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 9H12" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7225_67996">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 793 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 0H0V18H18V0Z" fill="white" fill-opacity="0.01"/>
|
||||
<path d="M2.625 15.75H16.125" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.125 10.02V12.75H6.86895L14.625 4.99054L11.8857 2.25L4.125 10.02Z" stroke="#5B6871" stroke-width="1.125" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 424 B |
@@ -0,0 +1,14 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7225_67984)">
|
||||
<path d="M18 0H0V18H18V0Z" fill="white" fill-opacity="0.01"/>
|
||||
<path d="M7 3V15" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 7L7 3" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 15V3" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 15L15 11" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7225_67984">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 740 B |
@@ -0,0 +1 @@
|
||||
<svg t="1784779423851" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6101" width="24" height="24"><path d="M1016.32 494.08l-143.872-143.36c-9.728-9.728-26.112-9.728-35.84 0-9.728 9.728-9.728 26.112 0 35.84L936.96 486.4h-399.36V87.04l99.84 100.352c9.728 9.728 26.112 9.728 35.84 0 9.728-9.728 9.728-26.112 0-35.84l-143.36-143.872c-9.728-9.728-26.112-9.728-35.84 0l-143.36 143.872c-9.728 9.728-9.728 26.112 0 35.84 9.728 9.728 26.112 9.728 35.84 0L486.4 87.04v399.36H87.04l100.352-99.84c9.728-9.728 9.728-26.112 0-35.84-9.728-9.728-26.112-9.728-35.84 0l-143.872 143.36c-9.728 9.728-9.728 26.112 0 35.84l143.872 143.36c9.728 9.728 26.112 9.728 35.84 0 9.728-9.728 9.728-26.112 0-35.84L87.04 537.6h399.36v399.36l-99.84-100.352c-9.728-9.728-26.112-9.728-35.84 0-9.728 9.728-9.728 26.112 0 35.84l143.36 143.872c9.728 9.728 26.112 9.728 35.84 0l143.36-143.872c9.728-9.728 9.728-26.112 0-35.84-9.728-9.728-26.112-9.728-35.84 0L537.6 936.96v-399.36h399.36l-100.352 99.84c-9.728 9.728-9.728 26.112 0 35.84 9.728 9.728 26.112 9.728 35.84 0l143.872-143.36c10.24-9.728 10.24-26.112 0-35.84z" p-id="6102"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7225_67990)">
|
||||
<path d="M18 0H0V18H18V0Z" fill="white" fill-opacity="0.01"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 5.625H15L13.875 16.5H4.125L3 5.625Z" stroke="#5B6871" stroke-width="1.125" stroke-linejoin="round"/>
|
||||
<path d="M7.50073 9.37598V13.1261" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round"/>
|
||||
<path d="M10.5009 9.375V13.1241" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round"/>
|
||||
<path d="M4.5 5.62496L10.6216 1.125L13.5 5.625" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7225_67990">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 795 B |
@@ -0,0 +1 @@
|
||||
<svg t="1784812882139" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3484" width="24" height="24"><path d="M446.112323 177.545051c137.567677 0.219798 252.612525 104.59798 266.162424 241.493333 13.562828 136.895354-78.778182 261.818182-213.617777 289.008485-134.852525 27.203232-268.386263-52.156768-308.945455-183.608889s25.018182-272.252121 151.738182-325.779394A267.235556 267.235556 0 0 1 446.112323 177.545051m0-62.060607c-182.794343 0-330.989899 148.195556-330.989899 330.989899s148.195556 330.989899 330.989899 330.989899 330.989899-148.195556 330.989899-330.989899-148.195556-330.989899-330.989899-330.989899z m431.321212 793.341415a30.849293 30.849293 0 0 1-21.94101-9.102223l-157.220202-157.220202c-11.752727-12.179394-11.584646-31.534545 0.37495-43.50707 11.972525-11.972525 31.327677-12.140606 43.494141-0.37495l157.220202 157.220202a31.036768 31.036768 0 0 1 6.723232 33.810101 31.004444 31.004444 0 0 1-28.651313 19.174142z m0 0" p-id="3485"></path></svg>
|
||||
|
After Width: | Height: | Size: 1014 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_161_3866" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 0H10.6667V10.6667H0V13.3333H10.6667V24H13.3333V13.3333H24V10.6667H13.3333V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_161_3866)">
|
||||
<rect x="-4" y="-4" width="32" height="32" fill="#646566"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 476 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_161_2681" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="3" width="12" height="12">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7203 3.21967L14.7809 4.28033L10.061 9.00025L14.7809 13.7202L13.7203 14.7809L9.00034 10.0609L4.28039 14.7809L3.21973 13.7202L7.93968 9.00025L3.21975 4.28033L4.28041 3.21967L9.00034 7.93959L13.7203 3.21967Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_161_2681)">
|
||||
<rect width="18" height="18" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 254 B |
|
After Width: | Height: | Size: 524 B |
@@ -0,0 +1,6 @@
|
||||
import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";
|
||||
import NoticeMock from "./noticeapi";
|
||||
|
||||
export function setupProdMockServer() {
|
||||
createProdMockServer([...NoticeMock]);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
html,body,#app{
|
||||
height: 100%;
|
||||
}
|
||||
body{
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
min-width: 1200px;
|
||||
}
|
||||
a{
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain)*/
|
||||
div, span, applet, object, iframe,h1, h2, h3, h4, h5, h6, p, blockquote, pre,a, abbr, acronym, address, big, cite, code,del, dfn, em, img, ins, kbd, q, s, samp,small, strike, strong, sub, sup, tt, var,b, u, i, center,dl, dt, dd, ol, ul, li,fieldset, form, label, legend,table, caption, tbody, tfoot, thead, tr, th, td,article, aside, canvas, details, embed,figure, figcaption, footer, header, hgroup,menu, nav, output, ruby, section, summary,time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; vertical-align: baseline;}/* HTML5 display-role reset for older browsers */article, aside, details, figcaption, figure,footer, header, hgroup, menu, nav, section { display: block;}ol, ul { list-style: none;}blockquote, q { quotes: none;}blockquote:before, blockquote:after,q:before, q:after { content: ''; content: none;}table { border-collapse: collapse; border-spacing: 0;}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*滚动条样式 */
|
||||
|
||||
/*第一种样式,所有的滚动条,都默认这个样子*/
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/*正常情况下滑块的样式*/
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, .05);
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在该类指向的控件上时滑块的样式*/
|
||||
*:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, .2);
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在滑块上时滑块的样式*/
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, .3);
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*正常时候的主干部分*/
|
||||
*::-webkit-scrollbar-track {
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/*鼠标悬浮在滚动条上的主干部分*/
|
||||
*::-webkit-scrollbar-track:hover {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .4);
|
||||
background-color: rgba(0, 0, 0, .01);
|
||||
}
|
||||
|
||||
/*第一种样式结束*/
|
||||
|
||||
|
||||
/*第二种样式*/
|
||||
|
||||
.scroll2::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/*正常时候的主干部分*/
|
||||
.scroll2::-webkit-scrollbar-track {
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/*正常情况下滑块的样式*/
|
||||
.scroll2::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, .05);
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在该类指向的控件上时滑块的样式*/
|
||||
.scroll2:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, .2);
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在滚动条上的主干部分*/
|
||||
.scroll2::-webkit-scrollbar-track:hover {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在滑块上时滑块的样式*/
|
||||
.scroll2::-webkit-scrollbar-thumb:hover {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
/*第二种样式结束*/
|
||||
@@ -0,0 +1,35 @@
|
||||
.my-table {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #c7c7c7;
|
||||
border-right: 1px solid #c7c7c7;
|
||||
border-collapse: separate;
|
||||
|
||||
.td-title {
|
||||
background: #fafafa;
|
||||
vertical-align: middle;
|
||||
color: #111;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #fafafa;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
td {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border-top: 1px solid #c7c7c7;
|
||||
border-left: 1px solid #c7c7c7;
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
.rule-tree {
|
||||
padding: 0 !important;
|
||||
width: 280px !important;
|
||||
.head-ul {
|
||||
background: #f6f8f9;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
li {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
box-sizing: content-box;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.ivu-tooltip,
|
||||
.ivu-tooltip-rel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #eaeaea;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.disabled {
|
||||
background-color: #fff;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
button {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
color: #515a6e;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body-box {
|
||||
padding: 11px 0;
|
||||
}
|
||||
|
||||
.ztree li span.button {
|
||||
background-repeat: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
table.small-table {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 5px;
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
td {
|
||||
padding: 5px 20px;
|
||||
}
|
||||
tr:first-child td {
|
||||
padding-top: 10px;
|
||||
}
|
||||
tr:last-child td {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.input-box {
|
||||
width: 100px;
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
}
|
||||
.ivu-input-wrapper {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
$liColor: #969799, #e6a23c, #45b972, #eb0404;
|
||||
|
||||
@each $c in $liColor {
|
||||
$i: index($liColor, $c);
|
||||
.status-color-#{$i} {
|
||||
color: $c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
@import "base/base";
|
||||
@import "base/bscroll.css";
|
||||
@import "base/reset";
|
||||
|
||||
@import "utils/mixins";
|
||||
@import "utils/utils";
|
||||
|
||||
@import "componets/status";
|
||||
@import "componets/small_table";
|
||||
@import "componets/my_table";
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.base-subtitle {
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
line-height: 24px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 3px;
|
||||
background: #5979ea;
|
||||
height: 15px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h4.second-title {
|
||||
@extend .base-subtitle;
|
||||
&::before {
|
||||
background: #5979ea;
|
||||
}
|
||||
}
|
||||
|
||||
h4.subtitle {
|
||||
@extend .second-title;
|
||||
&::before {
|
||||
background: #bda366;
|
||||
}
|
||||
}
|
||||
|
||||
.details-wrap {
|
||||
display: flex;
|
||||
padding-top: 10px;
|
||||
.left-box {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
|
||||
.user-list-ul {
|
||||
height: calc(100% - 50px);
|
||||
overflow-y: auto;
|
||||
li {
|
||||
height: 42px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-top: none;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: border 200ms;
|
||||
img {
|
||||
object-fit: cover;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
span.stu-name {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
}
|
||||
span.stu-card {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
li.active {
|
||||
// background: #f3f3f3;
|
||||
color: #7892ed;
|
||||
border-right: 3px solid #5979ea;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right-box {
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-spin-load {
|
||||
width: 60px;
|
||||
height: 90px;
|
||||
padding-top: 60px;
|
||||
background-image: url(../image/loading.gif);
|
||||
background-size: 60px 60px;
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
border: none;
|
||||
color: #577afb;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.ivu-table-cell-tree {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@mixin tag_mixin($bgColor,$textColor) {
|
||||
width:64px;
|
||||
height:20px ;
|
||||
padding: 0 5px;
|
||||
background: $bgColor;
|
||||
color: $textColor;
|
||||
font-size: 12px;
|
||||
border-radius: 2px 2px 2px 2px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// utils 工具// 颜色
|
||||
|
||||
@import "./varible";
|
||||
.bg-error {
|
||||
background-color: $color-error !important;
|
||||
}
|
||||
.text-error {
|
||||
color: $color-error !important;
|
||||
}
|
||||
.text-success {
|
||||
color: $color-success !important;
|
||||
}
|
||||
.text-warning {
|
||||
color: $color-warning !important;
|
||||
}
|
||||
.text-primary {
|
||||
color: $color-primary !important;
|
||||
}
|
||||
.text-normal {
|
||||
color: #404042 !important;
|
||||
}
|
||||
.text-black {
|
||||
color: #000;
|
||||
}
|
||||
.text-dark {
|
||||
color: #333;
|
||||
}
|
||||
.text-deep {
|
||||
color: #555;
|
||||
}
|
||||
.text-weak {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
.text-white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.text-error.disabled {
|
||||
color: #aaa !important;
|
||||
}
|
||||
|
||||
// 文字对齐
|
||||
.tl {
|
||||
text-align: left;
|
||||
}
|
||||
.tc {
|
||||
text-align: center;
|
||||
}
|
||||
.tr {
|
||||
text-align: right;
|
||||
}
|
||||
// 浮动与清除浮动
|
||||
.fl {
|
||||
float: left;
|
||||
}
|
||||
.fr {
|
||||
float: right;
|
||||
}
|
||||
.fix {
|
||||
*zoom: 1;
|
||||
}
|
||||
.fix:after {
|
||||
display: table;
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
// 显示
|
||||
.dn {
|
||||
display: none;
|
||||
}
|
||||
.di {
|
||||
display: inline;
|
||||
}
|
||||
.db {
|
||||
display: block;
|
||||
}
|
||||
.dib {
|
||||
display: inline-block;
|
||||
}
|
||||
.dt {
|
||||
display: table;
|
||||
}
|
||||
div.dib {
|
||||
*display: inline;
|
||||
*zoom: 1;
|
||||
}
|
||||
.vm {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.vib {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ovh {
|
||||
overflow: hidden;
|
||||
}
|
||||
// 定位
|
||||
.pr {
|
||||
position: relative;
|
||||
}
|
||||
.pa {
|
||||
position: absolute;
|
||||
}
|
||||
.pf {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.align-middle {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.align-middle-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@for $i from 1 through 100 {
|
||||
.width-#{$i} {
|
||||
width: $i * 1%;
|
||||
}
|
||||
.height-#{$i} {
|
||||
height: $i * 1px;
|
||||
}
|
||||
// @if $i<50 and $i%2==0 {
|
||||
// @if $i<50 {
|
||||
.ml-#{$i} {
|
||||
margin-left: $i * 1px !important;
|
||||
}
|
||||
.mr-#{$i} {
|
||||
margin-right: $i * 1px !important;
|
||||
}
|
||||
.mt-#{$i} {
|
||||
margin-top: $i * 1px !important;
|
||||
}
|
||||
.mb-#{$i} {
|
||||
margin-bottom: $i * 1px !important;
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
// 字体
|
||||
@for $i from 10 through 28 {
|
||||
.f#{$i} {
|
||||
font-size: $i * 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// .chart-box {
|
||||
// height: 50px;
|
||||
// width: 100%;
|
||||
// border-bottom: 50px solid;
|
||||
// border-image: linear-gradient(90deg, #f55c65 0%, #fcced0 48%, #d4dffc 52%, #7194f5 100%) 50 50 50 50;
|
||||
// }
|
||||
// @for $i from 0 through 50 {
|
||||
// .blue-#{$i} {
|
||||
// background-color: rgba(#7194f5, 0.1 * $i * 0.2);
|
||||
// }
|
||||
// }
|
||||
// @for $i from 0 through 50 {
|
||||
// .red-#{$i} {
|
||||
// background-color: rgba(#f55c65, 0.1 * $i * 0.2);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,22 @@
|
||||
/*scss变量*/
|
||||
|
||||
// 主题色
|
||||
$color-error: #e8684a;
|
||||
$color-success: #45b972;
|
||||
$color-warning: #e9b954;
|
||||
$color-primary: #5979ea;
|
||||
|
||||
// 文字色
|
||||
$color-black: #404042;
|
||||
$color-dark: #333;
|
||||
$color-deep: #555;
|
||||
$color-pl: #999999;
|
||||
$color-weak: #b3b3b3;
|
||||
$color-white: #fff;
|
||||
|
||||
// 背景色
|
||||
$bg-bar: #f9f9f9;
|
||||
$bg-page: #f3f3f3;
|
||||
$bg-page-light: #f9f9f9;
|
||||
|
||||
$border-color: #e9e9e9;
|
||||
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const $$userConfigMap: typeof import('./api/index')['$$userConfigMap']
|
||||
const Button: typeof import('view-ui-plus')['Button']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const Form: typeof import('view-ui-plus')['Form']
|
||||
const FormItem: typeof import('view-ui-plus')['FormItem']
|
||||
const Input: typeof import('view-ui-plus')['Input']
|
||||
const Message: typeof import('view-ui-plus')['Message']
|
||||
const Modal: typeof import('view-ui-plus')['Modal']
|
||||
const Select: typeof import('view-ui-plus')['Select']
|
||||
const Table: typeof import('view-ui-plus')['Table']
|
||||
const alovaInstance: typeof import('./api/index')['alovaInstance']
|
||||
const api: typeof import('./api/index')['default']
|
||||
const apiDefinitions: typeof import('./api/apiDefinitions')['default']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApis: typeof import('./api/createApis')['createApis']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const mountApis: typeof import('./api/createApis')['mountApis']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useForm: typeof import('alova/client')['useForm']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const usePagination: typeof import('alova/client')['usePagination']
|
||||
const useRequest: typeof import('alova/client')['useRequest']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useWatcher: typeof import('alova/client')['useWatcher']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const withConfigType: typeof import('./api/createApis')['withConfigType']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<div class="flow-editor">
|
||||
<Checkbox v-model="enabled" @on-change="emitChange">选择审批人后走审批流程{{ uncheckedText || "(不勾选=直接发布)" }}</Checkbox>
|
||||
|
||||
<div v-if="enabled" class="steps">
|
||||
<div class="step" v-for="(st, i) in steps" :key="i">
|
||||
<div class="step-head">
|
||||
<span class="step-no">{{ i + 1 }}</span>
|
||||
<span class="step-tag">第 {{ i + 1 }} 步</span>
|
||||
<Select v-model="st.vote" size="small" class="vote-sel" @on-change="emitChange">
|
||||
<Option value="any">或签(任一通过即可)</Option>
|
||||
<Option value="all">会签(全部通过)</Option>
|
||||
</Select>
|
||||
<span class="step-ops">
|
||||
<a v-if="steps.length > 1 && i > 0" @click="move(i, -1)">上移</a>
|
||||
<a v-if="steps.length > 1 && i < steps.length - 1" @click="move(i, 1)">下移</a>
|
||||
<a v-if="steps.length > 1" class="danger" @click="removeStep(i)">删除</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="step-body">
|
||||
<span v-if="st.teaIds.length" class="chips">
|
||||
<span class="chip" v-for="tid in st.teaIds" :key="tid">
|
||||
{{ nameOf(tid) }}<i @click="dropApprover(i, tid)">×</i>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="ph">未选择审批人</span>
|
||||
<Button size="small" icon="md-add" @click="openPicker(i)">选择审批人</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-step">
|
||||
<a @click="addStep">+ 再加一步(多步依次审批)</a>
|
||||
<a class="last" @click="loadSuggest(true)">🔄 带入上次所选审批人(第 1 步)</a>
|
||||
</div>
|
||||
<div class="tip">按顺序流转:上一步通过后进入下一步;任一步驳回则退回发布人。每步可多选审批人:或签任一人通过即过,会签需全部通过。</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批人选择:与「指定班级」同款穿梭式弹窗(UserCheckModal),数据源=教师树 base/getTeaTree?toUser=true(前端归一化后喂入),叶子=教师 teaId -->
|
||||
<UserCheckModal
|
||||
ref="pickerRef"
|
||||
:sources="pickerSources"
|
||||
:userids="editingTeaIds"
|
||||
@on-submit="onPickerSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/*
|
||||
* 多步审批流编辑器(v8:审批人选人弹窗对齐「指定班级」口径——教师树前端归一化后喂 UserCheckModal 穿梭式弹窗)。
|
||||
* 对外 v-model = FlowModel:{ enabled, flow:[{ name?, vote, approverTeaIds:number[] }] };
|
||||
* 后端 FlowStepItem.approverTeaIds 与 approvers 二选一,教师ID由服务端按本校教师名单绑定姓名。
|
||||
* 记忆带入:notice/ApproverSuggest 返回 teaIds → notice/ApproverResolve 解析姓名(未命中降级 ID)。
|
||||
* 编辑回显:setFlow(mv, names) —— names 为按步对齐的 approverNames(池外/教师变动时兜底展示);
|
||||
* 教师树加载后按叶子回填 teaNames,回显 chip 直接显示姓名。
|
||||
*/
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { baseApi, noticeApi } from "@/api/module/notice";
|
||||
import type { SchTreeNode } from "@/types/notice";
|
||||
|
||||
type Step = { vote: "all" | "any"; teaIds: number[] };
|
||||
export type FlowStepModel = { name?: string; vote: "all" | "any"; approverTeaIds: number[] };
|
||||
export type FlowModel = { enabled: boolean; flow: FlowStepModel[] };
|
||||
|
||||
const props = defineProps<{ modelValue: FlowModel; uncheckedText?: string }>();
|
||||
const emits = defineEmits<{ (e: "update:modelValue", v: FlowModel): void }>();
|
||||
|
||||
const enabled = ref(false);
|
||||
const newStep = (): Step => ({ vote: "any", teaIds: [] });
|
||||
const steps = ref<Step[]>([newStep()]);
|
||||
|
||||
/** teaId → 姓名(教师树叶子 + ApproverResolve/回显兜底) */
|
||||
const teaNames = ref<Record<number, string>>({});
|
||||
const nameOf = (tid: number) => teaNames.value[tid] || `ID ${tid}`;
|
||||
|
||||
/* ============ 审批人选人弹窗(v8:与「指定班级」同款——教师树前端归一化 + 穿梭式 UserCheckModal) ============ */
|
||||
const pickerRef = ref<any>(null);
|
||||
const editingStep = ref(0);
|
||||
/** 归一化后的教师树缓存(弹窗 loader 与姓名回填共用一次请求,失败可重试) */
|
||||
const pickerTree = ref<any[]>([]);
|
||||
let pickerPromise: Promise<any[]> | null = null;
|
||||
|
||||
/**
|
||||
* getTeaTree(toUser=true) → 弹窗安全树(对齐 ClassScopeInput.toGradeTree 口径):
|
||||
* 教师叶子重建为 `{id,name,isParent:false}`(原始 children:null 会在建树时抛错,v8-R1)、
|
||||
* 组节点显式 isParent,无任何教师的空组分支剪除(避免树里出现点不开的空文件夹)。
|
||||
* 顺带用叶子姓名回填 teaNames,供编辑回显 chip 直接显示姓名。
|
||||
*/
|
||||
function toPickerTree(nodes: SchTreeNode[]): any[] {
|
||||
const out: any[] = [];
|
||||
(nodes || []).forEach((n) => {
|
||||
if (!n) return;
|
||||
const kids = (n.children || []).filter((c) => c && c.type !== 2);
|
||||
const teas = (n.children || []).filter((c) => c && c.type === 2);
|
||||
teas.forEach((t) => {
|
||||
if (t.name) teaNames.value[Number(t.id)] = t.name;
|
||||
});
|
||||
const sub = toPickerTree(kids);
|
||||
if (!teas.length && !sub.length) return;
|
||||
out.push({
|
||||
id: n.id,
|
||||
name: n.name,
|
||||
isParent: true,
|
||||
children: [...sub, ...teas.map((t) => ({ id: t.id, name: t.name, isParent: false }))],
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
async function loadTeaTree(): Promise<any[]> {
|
||||
if (pickerTree.value.length) return pickerTree.value;
|
||||
if (!pickerPromise) {
|
||||
pickerPromise = (baseApi.getTeaTree(true).send() as unknown as Promise<SchTreeNode[]>)
|
||||
.then((raw) => {
|
||||
pickerTree.value = toPickerTree(raw || []);
|
||||
return pickerTree.value;
|
||||
})
|
||||
.catch((e) => {
|
||||
pickerPromise = null; // 失败后允许重开弹窗重试
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return pickerPromise;
|
||||
}
|
||||
|
||||
/** 单数据源(教师树)→ UserCheckModal 不渲染 tab;叶子 id=teaId。
|
||||
* v18:审核设置功能下线,选人树直显 base/getTeaTree 全量教师树(含教师叶子),不再按审批人池过滤。 */
|
||||
const pickerSources = [{ name: "审批人", loader: () => loadTeaTree() }];
|
||||
|
||||
/** 有已选审批人时预热教师树:仅用于回填姓名给 chips,失败静默(详情 names 已兜底) */
|
||||
function warmTree() {
|
||||
if (steps.value.some((s) => s.teaIds.length)) loadTeaTree().catch(() => {});
|
||||
}
|
||||
|
||||
/** 打开弹窗时回显当前步已选教师(UserCheckModal 在树加载后读取该 prop 打勾) */
|
||||
const editingTeaIds = computed<number[]>(() => steps.value[editingStep.value]?.teaIds || []);
|
||||
|
||||
function openPicker(i: number) {
|
||||
editingStep.value = i;
|
||||
pickerRef.value?.show();
|
||||
}
|
||||
|
||||
/** 提交:UserCheckModal 返回 [{id,name,group}],id=真实 teaId;回填当前步并记录姓名供 chip 展示 */
|
||||
function onPickerSubmit(arr: any[]) {
|
||||
const st = steps.value[editingStep.value];
|
||||
if (!st) return;
|
||||
st.teaIds = arr.map((x) => x.id);
|
||||
arr.forEach((x) => {
|
||||
if (x.name) teaNames.value[x.id] = x.name;
|
||||
});
|
||||
emitChange();
|
||||
}
|
||||
|
||||
/* ============ 步骤操作 ============ */
|
||||
function addStep() {
|
||||
steps.value.push(newStep());
|
||||
emitChange();
|
||||
}
|
||||
function removeStep(i: number) {
|
||||
steps.value.splice(i, 1);
|
||||
emitChange();
|
||||
}
|
||||
function move(i: number, dir: number) {
|
||||
const j = i + dir;
|
||||
if (j < 0 || j >= steps.value.length) return;
|
||||
const a = steps.value;
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
emitChange();
|
||||
}
|
||||
function dropApprover(i: number, tid: number) {
|
||||
steps.value[i].teaIds = steps.value[i].teaIds.filter((x) => x !== tid);
|
||||
emitChange();
|
||||
}
|
||||
|
||||
function collectFlow(): FlowStepModel[] {
|
||||
return steps.value
|
||||
.map((s, i) => ({ name: `第${i + 1}步`, vote: s.vote, approverTeaIds: [...s.teaIds] }))
|
||||
.filter((s) => s.approverTeaIds.length);
|
||||
}
|
||||
function emitChange() {
|
||||
const flow = enabled.value ? collectFlow() : [];
|
||||
emits("update:modelValue", { enabled: enabled.value && flow.length > 0, flow });
|
||||
}
|
||||
|
||||
function validFlow(): FlowStepModel[] {
|
||||
return enabled.value ? collectFlow() : [];
|
||||
}
|
||||
function validate(): boolean {
|
||||
return !enabled.value || collectFlow().length > 0;
|
||||
}
|
||||
|
||||
/** 编辑回显:mv.flow 携 approverTeaIds;names 为按步对齐的 approverNames(教师树外/改名兜底) */
|
||||
function setFlow(mv: FlowModel | null, names?: string[][]) {
|
||||
if (mv && mv.enabled && mv.flow?.length) {
|
||||
enabled.value = true;
|
||||
steps.value = mv.flow.map((f) => ({ vote: f.vote === "all" ? "all" : "any", teaIds: [...(f.approverTeaIds || [])] }));
|
||||
(names || []).forEach((nameList, i) => {
|
||||
const ids = steps.value[i]?.teaIds || [];
|
||||
if (nameList && nameList.length === ids.length) {
|
||||
ids.forEach((tid, j) => {
|
||||
if (nameList[j]) teaNames.value[tid] = nameList[j];
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
enabled.value = false;
|
||||
steps.value = [newStep()];
|
||||
}
|
||||
emitChange();
|
||||
// 回显可能不带 names(如 onMounted 路径):预热教师树补齐 chip 姓名
|
||||
if (enabled.value) warmTree();
|
||||
}
|
||||
|
||||
defineExpose({ validFlow, setFlow, validate, loadSuggest });
|
||||
|
||||
/* ============ 记忆带入(ApproverSuggest → ApproverResolve → 第 1 步) ============ */
|
||||
async function bindStepByTeaIds(stepIdx: number, teaIds: number[]): Promise<number[]> {
|
||||
const missed: number[] = [];
|
||||
if (!teaIds.length) return missed;
|
||||
const st = steps.value[stepIdx];
|
||||
if (!st) return missed;
|
||||
st.teaIds = [];
|
||||
let byId: Record<number, string | null> = {};
|
||||
try {
|
||||
const resolved = (await noticeApi.approverResolve(teaIds).send()) || [];
|
||||
resolved.forEach((r) => (byId[r.id] = r.name));
|
||||
} catch {
|
||||
/* 解析失败不阻断,姓名降级 ID */
|
||||
}
|
||||
// v18:审核设置下线,不再按池标注「不在审批人名单」,直接按解析结果带入
|
||||
for (const tid of teaIds) {
|
||||
const nm = byId[tid];
|
||||
if (!nm) missed.push(tid);
|
||||
else if (nm) teaNames.value[tid] = nm;
|
||||
st.teaIds.push(tid);
|
||||
}
|
||||
return missed;
|
||||
}
|
||||
|
||||
async function loadSuggest(byUser: boolean) {
|
||||
try {
|
||||
const v = await noticeApi.approverSuggest().send();
|
||||
const teaIds = ((v && v.teaIds) || []).map(Number).filter(Boolean);
|
||||
if (!teaIds.length) {
|
||||
if (byUser) Message.info("暂无审批人记忆,请在第 1 步选择审批人");
|
||||
return;
|
||||
}
|
||||
const missed = await bindStepByTeaIds(0, teaIds);
|
||||
emitChange();
|
||||
Message.success(`已带入上次所选 ${steps.value[0].teaIds.length} 名审批人(第 1 步)${missed.length ? `(ID ${missed.join("、")} 未找到姓名)` : ""}`);
|
||||
} catch (e: any) {
|
||||
if (byUser) Message.error(e?.message || "获取审批人记忆失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 勾选启用时静默带入一次记忆(对齐 api-test onApprovalToggle 自动加载) */
|
||||
let suggestTried = false;
|
||||
watch(enabled, (v) => {
|
||||
if (v && !suggestTried) {
|
||||
suggestTried = true;
|
||||
if (!steps.value.some((s) => s.teaIds.length)) loadSuggest(false);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue && props.modelValue.enabled && props.modelValue.flow?.length) {
|
||||
setFlow(props.modelValue);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flow-editor {
|
||||
.steps {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.step {
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
background: #fbfcfd;
|
||||
}
|
||||
.step-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.step-no {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background: #2d8cf0;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
.step-tag {
|
||||
font-size: 13px;
|
||||
color: #17233d;
|
||||
}
|
||||
.vote-sel {
|
||||
width: 190px;
|
||||
}
|
||||
.step-ops {
|
||||
margin-left: auto;
|
||||
a {
|
||||
color: #2d8cf0;
|
||||
font-size: 13px;
|
||||
margin-left: 12px;
|
||||
cursor: pointer;
|
||||
&.danger {
|
||||
color: #ed4014;
|
||||
}
|
||||
}
|
||||
}
|
||||
.step-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.chip {
|
||||
background: #eaf3ff;
|
||||
color: #2d8cf0;
|
||||
border-radius: 3px;
|
||||
padding: 2px 8px;
|
||||
font-size: 13px;
|
||||
i {
|
||||
font-style: normal;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
color: #9bbfe8;
|
||||
}
|
||||
}
|
||||
.ph {
|
||||
color: #bbb;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
.add-step a {
|
||||
color: #2d8cf0;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
margin-right: 14px;
|
||||
&.last {
|
||||
color: #19be6b;
|
||||
}
|
||||
}
|
||||
.tip {
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="improt-wrap">
|
||||
<div class="form-item">
|
||||
<label>案例下载:</label>
|
||||
<a @click="downTempFile" href="javascript:;">点击下载模板.xls</a>
|
||||
</div>
|
||||
<div class="upload-panel">
|
||||
<Upload type="drag" :on-success="onSuccess" :on-error="onError" :data="uploadData" :headers="uploadHeader"
|
||||
:with-credentials="true" :action="uploadUrl"
|
||||
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel">
|
||||
<div class="upload-box">
|
||||
<Icon type="ios-paper" size="52" style="color: #6281eb"></Icon>
|
||||
<h4>点击或将文件拖拽到这里上传</h4>
|
||||
<div class="info">文件大小不超过50M 支持扩展名: xls,xlsx</div>
|
||||
</div>
|
||||
</Upload>
|
||||
</div>
|
||||
<Alert v-if="!!errorMsg" type="error" show-icon><span v-html="errorMsg"></span></Alert>
|
||||
<Alert v-else-if="!!successMsg" type="success" show-icon>{{ successMsg }}</Alert>
|
||||
<Alert v-else style="margin-bottom: 0" type="warning" show-icon>
|
||||
请按照案例模板内容进行编辑,否则将无法导入。
|
||||
</Alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Utils from "@/utils";
|
||||
export default {
|
||||
props: {
|
||||
tempFileUrl: String,
|
||||
uploadUrl: String,
|
||||
uploadData: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMsg: null,
|
||||
successMsg: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
uploadHeader() {
|
||||
const roleInfo = Utils.getCurrentRoleInfo();
|
||||
roleInfo.schoolId = roleInfo.schId;
|
||||
roleInfo.yschapirequest = 1;
|
||||
// roleInfo.userType = 2;
|
||||
return roleInfo;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downTempFile() {
|
||||
window.top.location.href = this.tempFileUrl;
|
||||
},
|
||||
onSuccess(res, file) {
|
||||
if (res.code == 0) {
|
||||
this.errorMsg = null;
|
||||
this.successMsg = "操作成功!";
|
||||
this.$emit("on-success");
|
||||
} else {
|
||||
this.errorMsg = res.msg;
|
||||
}
|
||||
},
|
||||
onError(err, file) {
|
||||
this.errorMsg = JSON.stringify(err);
|
||||
console.log(err, file);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.improt-wrap {
|
||||
padding: 16px;
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-panel {
|
||||
.upload-box {
|
||||
background: #f7f8fc;
|
||||
padding: 20px 0;
|
||||
|
||||
h4 {
|
||||
color: #505c7e;
|
||||
margin: 3px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<Select v-model="innerId" @on-change="handleChange" v-bind="$attrs">
|
||||
<Option v-for="item in stageSubjects" :key="item.k" :value="item.k">
|
||||
{{ item.v }}
|
||||
</Option>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDefaultStore } from '@/store'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'on-change': [stage: number, subject: string]
|
||||
}>()
|
||||
|
||||
const store = useDefaultStore()
|
||||
const stageSubjects = computed(() => store.getStageSubjects)
|
||||
|
||||
const innerId = ref<number | null>(null)
|
||||
|
||||
const cacheKey = 'bsch.learnmanage.cur_subject_id';
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
if (!stageSubjects.value.length) return
|
||||
const cached = sessionStorage.getItem(cacheKey)
|
||||
const id = cached ? Number(cached) : stageSubjects.value[0]?.k;
|
||||
if (id != null) innerId.value = id
|
||||
handleChange(id)
|
||||
})
|
||||
|
||||
function handleChange(val: number) {
|
||||
const find = stageSubjects.value.find((i: any) => i.k === val)
|
||||
if (!find) return
|
||||
sessionStorage.setItem(cacheKey, String(val))
|
||||
emit('on-change', find.stage, find.code)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<!-- 通知对象选择(旧链路):全部班级 / 指定班级(学生场景,复用 UserCheckModal 树选弹窗) -->
|
||||
<div class="check-user-wrap">
|
||||
<div class="check-user-box">
|
||||
<RadioGroup v-model="isAllClass" class="radio" @on-change="onScopeChange">
|
||||
<Radio :disabled="editDisabled" :label="0">{{ isEdit ? "现有班级" : "全部班级" }}</Radio>
|
||||
<Radio :label="1">指定班级</Radio>
|
||||
</RadioGroup>
|
||||
<Input type="text" v-model="classStr" readonly :disabled="!isAllClass" placeholder="选择公告的班级" class="input"></Input>
|
||||
<Button icon="md-add" @click="openPicker" :disabled="!isAllClass"></Button>
|
||||
<Button icon="md-trash" @click="reset" :disabled="!isAllClass"></Button>
|
||||
</div>
|
||||
<UserCheckModal ref="picker" :sources="sources" :userids="tempIds" @on-submit="submit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* 班级选择输入(学生场景)。v-model = "all" 或 班级 id 逗号串。
|
||||
* 数据源为「年级 → 班级」树(base/GetClassTree),叶子为班级,弹窗仅收集叶子 id。
|
||||
*/
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { baseApi } from "@/api/module/notice";
|
||||
|
||||
export default {
|
||||
name: "ClassCheckInput",
|
||||
components: { UserCheckModal },
|
||||
props: {
|
||||
modelValue: { type: String, default: "all" },
|
||||
/** 编辑态:全部班级选项禁用为「现有班级」 */
|
||||
isEdit: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
isAllClass: 0 as number,
|
||||
tempIds: [] as number[],
|
||||
classStr: "",
|
||||
editDisabled: false,
|
||||
sources: [{ name: "班级", loader: () => baseApi.getClassTree().send() as Promise<any[]> }],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.syncFromValue(this.modelValue);
|
||||
},
|
||||
methods: {
|
||||
idsFromVal(val: string): number[] {
|
||||
return String(val || "")
|
||||
.split(",")
|
||||
.map((x) => parseInt(x))
|
||||
.filter((x) => !isNaN(x));
|
||||
},
|
||||
syncFromValue(val: string) {
|
||||
if (val === "all") {
|
||||
this.isAllClass = 0;
|
||||
this.classStr = this.isEdit ? "现有班级" : "全部班级";
|
||||
} else if (val === "") {
|
||||
this.isAllClass = 1;
|
||||
this.classStr = "";
|
||||
this.tempIds = [];
|
||||
} else {
|
||||
this.isAllClass = 1;
|
||||
this.tempIds = this.idsFromVal(val);
|
||||
this.classStr = "";
|
||||
}
|
||||
},
|
||||
onScopeChange(type: number) {
|
||||
const tempIds = this.idsFromVal(String(this.modelValue || ""));
|
||||
if (type === 0) {
|
||||
if (this.isEdit) {
|
||||
this.tempIds = tempIds;
|
||||
this.classStr = "现有班级";
|
||||
this.$emit("update:modelValue", String(tempIds));
|
||||
} else {
|
||||
this.tempIds = [];
|
||||
this.classStr = "全部班级";
|
||||
this.$emit("update:modelValue", "all");
|
||||
}
|
||||
} else {
|
||||
if (this.isEdit) this.editDisabled = true;
|
||||
this.tempIds = tempIds;
|
||||
this.classStr = "";
|
||||
this.openPicker();
|
||||
}
|
||||
},
|
||||
openPicker() {
|
||||
(this.$refs.picker as any).show();
|
||||
},
|
||||
reset() {
|
||||
this.tempIds = [];
|
||||
this.classStr = "";
|
||||
this.$emit("update:modelValue", "");
|
||||
},
|
||||
submit(arr: any[]) {
|
||||
this.classStr = arr.map((x) => x.name).join("、");
|
||||
const ids = arr.map((x) => x.id).join(",");
|
||||
this.$emit("update:modelValue", ids || "all");
|
||||
if (!arr.length) {
|
||||
this.isAllClass = 0;
|
||||
this.classStr = "全部班级";
|
||||
this.$emit("update:modelValue", "all");
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(val: string) {
|
||||
this.syncFromValue(val);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.check-user-wrap {
|
||||
width: 100%;
|
||||
.check-user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.radio {
|
||||
width: 180px;
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
line-height: 32px;
|
||||
}
|
||||
.input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<!-- 接收班级选择(v5):全部班级 / 指定班级(复用 UserCheckModal 树选弹窗,树=base/getStuTree 前端自归类年级→班级) -->
|
||||
<div class="class-scope-wrap">
|
||||
<div class="check-user-box">
|
||||
<RadioGroup v-model="mode" class="radio" @on-change="onScopeChange">
|
||||
<Radio :label="0">全部班级</Radio>
|
||||
<Radio :label="1">指定班级</Radio>
|
||||
</RadioGroup>
|
||||
<Input type="text" v-model="classStr" readonly :disabled="mode === 0" placeholder="选择公告的班级" class="input"></Input>
|
||||
<Button icon="md-add" @click="openPicker" :disabled="mode === 0"></Button>
|
||||
<Button icon="md-trash" @click="reset" :disabled="mode === 0"></Button>
|
||||
</div>
|
||||
<div class="scope-preview" v-if="modelValue.length && preview">已选学员 <b>{{ preview.studentCount }}</b> 人</div>
|
||||
<UserCheckModal ref="picker" :sources="sources" :userids="classIds" @on-submit="submit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* 接收班级选择输入(v5,替代 PushScopePanel 的多级树面板)。v-model = PushScopeItem[]:
|
||||
* - 全部班级 → [{scopeType:1, scopeId:0, scopeName:"全部班级"}](后端 SCHOOL 忽略 scopeId 走全校学生);
|
||||
* - 指定班级 → [{scopeType:4, scopeId:classId, scopeName:className}…](弹窗提交);
|
||||
* - 编辑回显 setScopes():type 2/3(旧面板选的学部/年级)在树中展开为下属班级,
|
||||
* type 5/6 等无法映射项原样保留(extras)随表单提交防丢范围。
|
||||
* 树数据源 base/getStuTree?toUser=false(后端无 GetClassTree),自底向上归出「年级→班级」两层。
|
||||
*/
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { baseApi, noticeApi } from "@/api/module/notice";
|
||||
import type { PushScopeItem, SchTreeNode, ScopePreviewVo } from "@/types/notice";
|
||||
|
||||
/** 全部班级范围项(后端 scopeType=1 不解析 scopeId) */
|
||||
const ALL_SCOPE: PushScopeItem = { scopeType: 1, scopeId: 0, scopeName: "全部班级" };
|
||||
|
||||
export default {
|
||||
name: "ClassScopeInput",
|
||||
components: { UserCheckModal },
|
||||
props: {
|
||||
modelValue: { type: Array as () => PushScopeItem[], default: () => [] },
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
/** 0=全部班级 1=指定班级 */
|
||||
mode: 0 as number,
|
||||
/** 指定班级已选班级(scopeType=4) */
|
||||
classItems: [] as PushScopeItem[],
|
||||
/** 无法映射到班级的回显项(type 5/6 或树中已不存在的 2/3):原样保留提交 */
|
||||
extras: [] as PushScopeItem[],
|
||||
classStr: "",
|
||||
/** 自身 emit 引发的 modelValue 变化不回灌(防环) */
|
||||
suppressWatch: false,
|
||||
/** getStuTree 原始树(供名称解析与 2/3 展开) */
|
||||
rawTree: [] as SchTreeNode[],
|
||||
gradeTree: [] as any[],
|
||||
treeLoading: false,
|
||||
treeFailed: false,
|
||||
/** 树未就绪时的回显缓冲(对齐 PushScopePanel pendingScopes 口径) */
|
||||
pendingScopes: null as PushScopeItem[] | null,
|
||||
preview: null as ScopePreviewVo | null,
|
||||
previewTimer: undefined as number | undefined,
|
||||
sources: [{ name: "班级", loader: () => this.loadTree() }],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classIds(): number[] {
|
||||
return this.classItems.map((x) => x.scopeId);
|
||||
},
|
||||
/** id → 组节点(学部/年级展开用) */
|
||||
nodeById(): Map<number, SchTreeNode> {
|
||||
const map = new Map<number, SchTreeNode>();
|
||||
const walk = (ns?: SchTreeNode[] | null) => {
|
||||
(ns || []).forEach((n) => {
|
||||
map.set(Number(n.id), n);
|
||||
walk(n.children);
|
||||
});
|
||||
};
|
||||
walk(this.rawTree);
|
||||
return map;
|
||||
},
|
||||
/** classId → 班级名(叶子节点,回显名称解析用) */
|
||||
classById(): Map<number, string> {
|
||||
const map = new Map<number, string>();
|
||||
const walk = (ns?: SchTreeNode[] | null) => {
|
||||
(ns || []).forEach((n) => {
|
||||
if (!(n.children || []).length) map.set(Number(n.id), n.name);
|
||||
else walk(n.children);
|
||||
});
|
||||
};
|
||||
walk(this.rawTree);
|
||||
return map;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.modelValue.length) {
|
||||
this.syncFromValue(this.modelValue);
|
||||
this.runPreview();
|
||||
} else {
|
||||
this.applyAll(); // 新建默认全部班级(emit 后由 watch 触发预估)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadTree().catch(() => {});
|
||||
},
|
||||
methods: {
|
||||
/** 拉取 getStuTree 并缓存;弹窗 loader 与名称解析共用(失败弹窗侧另行报错) */
|
||||
async loadTree(): Promise<any[]> {
|
||||
if (this.gradeTree.length) return this.gradeTree;
|
||||
if (this.treeLoading) {
|
||||
// 并发调用(mounted 与弹窗同时触发):等首次结果
|
||||
await new Promise<void>((r) => window.setTimeout(r, 200));
|
||||
if (this.gradeTree.length) return this.gradeTree;
|
||||
}
|
||||
this.treeLoading = true;
|
||||
this.treeFailed = false;
|
||||
try {
|
||||
const raw = (await baseApi.getStuTree(false).send()) || [];
|
||||
this.rawTree = raw;
|
||||
this.gradeTree = this.toGradeTree(raw);
|
||||
return this.gradeTree;
|
||||
} catch (e) {
|
||||
this.treeFailed = true;
|
||||
throw e;
|
||||
} finally {
|
||||
this.treeLoading = false;
|
||||
// 回显缓冲:详情先于树就绪时,树加载完成后再应用
|
||||
if (this.pendingScopes) {
|
||||
const scoped = this.pendingScopes;
|
||||
this.pendingScopes = null;
|
||||
this.applyScopes(scoped);
|
||||
}
|
||||
}
|
||||
},
|
||||
/** getStuTree →「年级→班级」两层树(ztree 结构):孩子全为叶子的组节点=年级 */
|
||||
toGradeTree(nodes: SchTreeNode[]): any[] {
|
||||
const out: any[] = [];
|
||||
const walk = (ns?: SchTreeNode[] | null) => {
|
||||
(ns || []).forEach((n) => {
|
||||
const kids = (n.children || []).filter((c) => c && c.type !== 2);
|
||||
if (kids.length && kids.every((c) => !(c.children || []).length)) {
|
||||
out.push({
|
||||
id: n.id,
|
||||
name: n.name,
|
||||
isParent: true,
|
||||
children: kids.map((c) => ({ id: c.id, name: c.name, isParent: false })),
|
||||
});
|
||||
} else {
|
||||
walk(kids);
|
||||
}
|
||||
});
|
||||
};
|
||||
walk(nodes);
|
||||
return out;
|
||||
},
|
||||
/** 组节点子树内的全部叶子(班级)id */
|
||||
leafIdsOf(node: SchTreeNode): number[] {
|
||||
const out: number[] = [];
|
||||
const walk = (n: SchTreeNode) => {
|
||||
const kids = (n.children || []).filter((c) => c && c.type !== 2);
|
||||
if (!kids.length) out.push(Number(n.id));
|
||||
else kids.forEach(walk);
|
||||
};
|
||||
walk(node);
|
||||
return out;
|
||||
},
|
||||
/** 全部班级态 */
|
||||
applyAll() {
|
||||
this.mode = 0;
|
||||
this.classItems = [];
|
||||
this.extras = [];
|
||||
this.classStr = "全部班级";
|
||||
this.emitValue();
|
||||
},
|
||||
emitValue() {
|
||||
const val: PushScopeItem[] = this.mode === 0 ? [{ ...ALL_SCOPE }] : [...this.extras, ...this.classItems];
|
||||
this.suppressWatch = true;
|
||||
this.$emit("update:modelValue", val);
|
||||
this.$nextTick(() => (this.suppressWatch = false));
|
||||
},
|
||||
onScopeChange(type: number) {
|
||||
if (type === 0) {
|
||||
this.applyAll();
|
||||
} else {
|
||||
this.classStr = [...this.extras, ...this.classItems].map((x) => x.scopeName).join("、");
|
||||
this.openPicker();
|
||||
}
|
||||
},
|
||||
openPicker() {
|
||||
(this.$refs.picker as any).show();
|
||||
},
|
||||
reset() {
|
||||
this.classItems = [];
|
||||
this.classStr = "";
|
||||
this.emitValue();
|
||||
},
|
||||
submit(arr: any[]) {
|
||||
if (!arr.length) {
|
||||
// 空选回落全部班级(对齐旧链路 ClassCheckInput 口径)
|
||||
this.applyAll();
|
||||
return;
|
||||
}
|
||||
this.classItems = arr.map((x) => ({ scopeType: 4, scopeId: Number(x.id), scopeName: x.name }));
|
||||
this.classStr = this.classItems.map((x) => x.scopeName).join("、");
|
||||
this.emitValue();
|
||||
},
|
||||
/** 编辑回显:树未就绪时缓冲,就绪后应用(名称解析/学部年级展开依赖树) */
|
||||
setScopes(scopes?: PushScopeItem[] | null) {
|
||||
const list = scopes || [];
|
||||
if (!this.rawTree.length && !this.treeFailed) {
|
||||
this.pendingScopes = list;
|
||||
if (!this.treeLoading) this.loadTree().catch(() => {});
|
||||
return;
|
||||
}
|
||||
this.applyScopes(list);
|
||||
},
|
||||
applyScopes(list: PushScopeItem[]) {
|
||||
const classes: PushScopeItem[] = [];
|
||||
const extras: PushScopeItem[] = [];
|
||||
let school = false;
|
||||
for (const s of list) {
|
||||
if (s.scopeType === 1) {
|
||||
school = true;
|
||||
} else if (s.scopeType === 4) {
|
||||
const id = Number(s.scopeId);
|
||||
classes.push({ scopeType: 4, scopeId: id, scopeName: this.classById.get(id) || s.scopeName || `班级${s.scopeId}` });
|
||||
} else if (s.scopeType === 2 || s.scopeType === 3) {
|
||||
// 旧多级面板选的学部/年级:树中展开为下属班级
|
||||
const node = this.nodeById.get(Number(s.scopeId));
|
||||
const leafIds = node ? this.leafIdsOf(node) : [];
|
||||
if (leafIds.length) {
|
||||
for (const id of leafIds) {
|
||||
if (!classes.some((c) => c.scopeId === id)) {
|
||||
classes.push({ scopeType: 4, scopeId: id, scopeName: this.classById.get(id) || `班级${id}` });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
extras.push(s); // 树中已不存在:原样保留防丢范围
|
||||
}
|
||||
} else {
|
||||
extras.push(s); // type 5/6 等:原样保留
|
||||
}
|
||||
}
|
||||
if (school && !classes.length && !extras.length) {
|
||||
this.applyAll();
|
||||
} else if (classes.length || extras.length) {
|
||||
this.mode = 1;
|
||||
this.classItems = classes;
|
||||
this.extras = extras;
|
||||
this.classStr = [...extras, ...classes].map((x) => x.scopeName).join("、");
|
||||
this.emitValue();
|
||||
} else {
|
||||
this.applyAll(); // 空范围默认全部班级
|
||||
}
|
||||
},
|
||||
/** 外部赋值同步(不依赖树):按 scopeType 还原 radio 态与名称 */
|
||||
syncFromValue(list: PushScopeItem[]) {
|
||||
const school = list.some((s) => s.scopeType === 1);
|
||||
const classes = list.filter((s) => s.scopeType === 4);
|
||||
const extras = list.filter((s) => s.scopeType !== 1 && s.scopeType !== 4);
|
||||
if (school && !classes.length && !extras.length) {
|
||||
this.mode = 0;
|
||||
this.classItems = [];
|
||||
this.extras = [];
|
||||
this.classStr = "全部班级";
|
||||
} else if (list.length) {
|
||||
this.mode = 1;
|
||||
this.classItems = classes;
|
||||
this.extras = extras;
|
||||
this.classStr = [...extras, ...classes].map((x) => x.scopeName).join("、");
|
||||
}
|
||||
},
|
||||
async runPreview() {
|
||||
if (!this.modelValue.length) {
|
||||
this.preview = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.preview = await noticeApi.scopePreview(this.modelValue as PushScopeItem[]).send();
|
||||
} catch {
|
||||
this.preview = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(val: PushScopeItem[]) {
|
||||
// 自身 emit 的变化只跳过状态回灌,预估仍按最新值刷新
|
||||
if (!this.suppressWatch) this.syncFromValue(val || []);
|
||||
this.runPreview();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.class-scope-wrap {
|
||||
width: 100%;
|
||||
.check-user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.radio {
|
||||
width: 180px;
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
line-height: 32px;
|
||||
}
|
||||
.input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.scope-preview {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
b {
|
||||
color: #2d8cf0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 附件预览弹窗(open-file-viewer,参考党建学习 PartyLearn 实现)。
|
||||
* 支持 图片/docx/xlsx/pptx/pdf/txt 在线预览;mp4 用 <video> 播放(走 Preview 内联流);
|
||||
* doc/ppt/xls/rtf/zip 等旧二进制格式不支持,弹窗内给「下载查看」兜底。
|
||||
* 关键点:组件自拉 URL 带不上平台认证,必须先 fetch( credentials: include ) 取 Blob 再传 :file。
|
||||
*/
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { OpenFileViewer } from "@open-file-viewer/vue";
|
||||
import { imagePlugin, pdfPlugin, officePlugin, textPlugin } from "@open-file-viewer/core";
|
||||
import "@open-file-viewer/core/style.css";
|
||||
// pdf worker:以 ?raw 内嵌源码(worker 自包含无 import),运行时生成 Blob URL,
|
||||
// 不依赖部署环境对 .mjs 的 MIME 配置/文件完整性(模块 worker 对 MIME 校验严格,nginx 旧默认不含 mjs)
|
||||
// @ts-ignore
|
||||
import pdfWorkerRaw from "pdfjs-dist/build/pdf.worker.mjs?raw";
|
||||
|
||||
import config from "@/assets/config/host.config";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
fileId: number | null;
|
||||
/** 预览文件名(用于类型判断与标题展示,需带扩展名) */
|
||||
fileName?: string;
|
||||
}>(),
|
||||
{ fileName: "" }
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const previewName = ref("");
|
||||
const fileType = ref("");
|
||||
const blob = ref<Blob | null>(null);
|
||||
|
||||
const fileServiceBase = config.isMock ? "" : config.serviceHost;
|
||||
/** 内联流地址(<video> 等场景用;Down 是 attachment 头,视频流不可用) */
|
||||
const previewUrl = computed(() => (props.fileId ? `${fileServiceBase}/api/file/Preview?id=${props.fileId}` : ""));
|
||||
|
||||
const isVideo = computed(() => ["mp4", "webm", "ogg"].includes(fileType.value));
|
||||
const isUnsupported = computed(
|
||||
() => !isVideo.value && ["doc", "ppt", "xls", "rtf", "zip", "rar", "7z", "wps"].includes(fileType.value)
|
||||
);
|
||||
|
||||
const pdfWorkerSrc = URL.createObjectURL(new Blob([pdfWorkerRaw as unknown as BlobPart], { type: "text/javascript" }));
|
||||
|
||||
const plugins = [
|
||||
imagePlugin(),
|
||||
pdfPlugin({
|
||||
workerSrc: pdfWorkerSrc,
|
||||
// cmaps/标准字体自托管(public 下随构建发布),避免运行时依赖 cdn.jsdelivr.net(国内网络不稳定)
|
||||
cMapUrl: `${import.meta.env.BASE_URL}cmaps/`,
|
||||
standardFontDataUrl: `${import.meta.env.BASE_URL}standard_fonts/`,
|
||||
}),
|
||||
officePlugin(),
|
||||
textPlugin(),
|
||||
];
|
||||
|
||||
const load = async () => {
|
||||
if (!props.fileId) return;
|
||||
loading.value = true;
|
||||
blob.value = null;
|
||||
previewName.value = props.fileName || "文件预览";
|
||||
fileType.value = (previewName.value.split(".").pop() || "").toLowerCase();
|
||||
try {
|
||||
// Down/Preview 走 Cookie 认证(location.href 能下载即证明),同源带 cookie 即可
|
||||
const res = await fetch(`${fileServiceBase}/api/file/Preview?id=${props.fileId}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error(`文件加载失败(${res.status})`);
|
||||
blob.value = await res.blob();
|
||||
} catch (error: any) {
|
||||
Message.error(error?.message || "文件加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const download = () => {
|
||||
if (props.fileId) window.location.href = `${fileServiceBase}/api/file/Down?id=${props.fileId}`;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
if (value) load();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model="show" :title="previewName || '文件预览'" :width="960" :mask-closable="false">
|
||||
<div class="preview-body">
|
||||
<Spin fix v-if="loading" size="large"></Spin>
|
||||
<template v-if="blob && !loading">
|
||||
<video v-if="isVideo" :src="previewUrl" controls autoplay class="video-player"></video>
|
||||
<div v-else-if="isUnsupported" class="unsupported">
|
||||
<Icon type="ios-alert-outline" size="64" />
|
||||
<p>该格式({{ fileType }})暂不支持在线预览</p>
|
||||
<Button type="primary" icon="ios-download-outline" @click="download">下载查看</Button>
|
||||
</div>
|
||||
<OpenFileViewer v-else :file="blob" :file-name="previewName" height="100%" toolbar :plugins="plugins" />
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button @click="show = false">关闭</Button>
|
||||
<Button type="primary" @click="download">下载</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-body {
|
||||
position: relative;
|
||||
height: 72vh;
|
||||
overflow: hidden;
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
}
|
||||
.unsupported {
|
||||
text-align: center;
|
||||
padding-top: 120px;
|
||||
color: #999;
|
||||
p {
|
||||
margin: 12px 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<Select
|
||||
class="search-wrap_sel"
|
||||
v-model="currentGradeId"
|
||||
:placeholder="isGradeAllCheck ? '全部年级' : '请选择年级'"
|
||||
:clearable="isGradeAllCheck"
|
||||
@on-change="onGradeChange"
|
||||
>
|
||||
<Option v-for="item in gradeList" :value="item.k">{{ item.v }}</Option>
|
||||
</Select>
|
||||
<Select
|
||||
ref="refClassSel"
|
||||
class="search-wrap_sel ml-12"
|
||||
v-model="currentClassId"
|
||||
:placeholder="isClassAllCheck ? '全部班级' : '请选择班级'"
|
||||
:clearable="isClassAllCheck"
|
||||
@on-change="onClassChange"
|
||||
>
|
||||
<Option v-for="item in classList" :value="item.k">{{ item.v }}</Option>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onBeforeMount, computed } from "vue";
|
||||
import { useDefaultStore } from "@/store/index.ts";
|
||||
import { GradeClassInfo } from "@/types/index";
|
||||
import { BaseDict } from "@/types/Basics";
|
||||
import { storeToRefs } from "pinia";
|
||||
interface GradeClassSel {
|
||||
isGradeAllCheck?: boolean;
|
||||
isClassAllCheck?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<GradeClassSel>(), {
|
||||
isGradeAllCheck: false,
|
||||
isClassAllCheck: true,
|
||||
});
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: "on-change", info: GradeClassInfo): void;
|
||||
}>();
|
||||
const store = useDefaultStore();
|
||||
|
||||
const refClassSel = ref();
|
||||
const gradeClassList = ref<any[]>([]);
|
||||
const { gradeId: currentGradeId, classId: currentClassId } = storeToRefs(store);
|
||||
|
||||
onBeforeMount(() => {
|
||||
gradeClassList.value = store.$state.gradeClassList;
|
||||
if (!props.isGradeAllCheck && currentGradeId.value == null)
|
||||
currentGradeId.value = _.get(gradeList.value, "[0].k", null);
|
||||
});
|
||||
|
||||
const gradeList = computed<BaseDict[]>(() => {
|
||||
let arr = gradeClassList.value.map((element: GradeClassInfo) => {
|
||||
return { k: element.gradeId, v: element.gradeName };
|
||||
});
|
||||
return _.uniqBy(arr, "k");
|
||||
});
|
||||
|
||||
const classList = computed<BaseDict[]>(() => {
|
||||
let arr = gradeClassList.value
|
||||
.filter((x: GradeClassInfo) => x.gradeId == currentGradeId.value)
|
||||
.map((element: GradeClassInfo) => {
|
||||
return { k: element.classId, v: element.className };
|
||||
});
|
||||
return arr;
|
||||
});
|
||||
|
||||
function onGradeChange() {
|
||||
setTimeout(() => {
|
||||
refClassSel.value.focusIndex = 0;
|
||||
}, 200);
|
||||
currentClassId.value = !props.isClassAllCheck ? _.get(classList.value, "[0].k", null) : null;
|
||||
let info = getCurrentGradeInfo();
|
||||
emits("on-change", info);
|
||||
}
|
||||
|
||||
function onClassChange() {
|
||||
let info = getCurrentGradeInfo();
|
||||
emits("on-change", info);
|
||||
}
|
||||
|
||||
function getCurrentGradeInfo() {
|
||||
return store.findGradeClassInfo(currentGradeId.value, currentClassId.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div class="group-tree-search-panel">
|
||||
<div v-if="searchVisible" class="search-bar mt-8 mb-8">
|
||||
<Input v-model="searchKeyword" :placeholder="searchPlaceholder" suffix="ios-search" search clearable
|
||||
class="search-input" @on-search="onSearch" @on-clear="onClear" />
|
||||
<Button v-if="showCloseButton" icon="md-close" @click="closeSearch" />
|
||||
</div>
|
||||
|
||||
<div class="tree-body">
|
||||
<MyTree v-if="!treeLoading" ref="refMyTree" :autoClick="autoClick" :treeData="treeData"
|
||||
:setting="treeSetting" @on-tree-click="treeLink" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick, useTemplateRef } from "vue";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { Message } from "view-ui-plus";
|
||||
import MyTree from '@/components/MyTree.vue'
|
||||
|
||||
const GROUP_TYPE = {
|
||||
NORMAL_GROUP: 1,
|
||||
SCHOOL_GROUP: 2,
|
||||
GRADE_GROUP: 3,
|
||||
CLASS_GROUP: 4,
|
||||
AREA_GROUP: 5,
|
||||
STAGE_GROUP: 6,
|
||||
TEACHER_GROUP: 7,
|
||||
STUDENT_GROUP: 8,
|
||||
} as const
|
||||
|
||||
const DEFAULT_CACHE_KEY = 'bsch.group-tree.selected-node'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
treeApi: () => Promise<any[]>;
|
||||
autoClick?: boolean;
|
||||
defaultSearchVisible?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
cacheKey?: string;
|
||||
}>(), {
|
||||
autoClick: false,
|
||||
defaultSearchVisible: true,
|
||||
showCloseButton: false,
|
||||
searchPlaceholder: '输入组名称搜索',
|
||||
cacheKey: DEFAULT_CACHE_KEY,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'on-change': [treeNode: any]
|
||||
'on-tree-data': [tree: any[]]
|
||||
}>()
|
||||
|
||||
const curTreeNode = defineModel<any>('curTreeNode', { default: {} })
|
||||
|
||||
const refMyTree = useTemplateRef('refMyTree')
|
||||
const treeLoading = ref(false)
|
||||
const treeData = ref<any[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const searchVisible = ref(props.defaultSearchVisible)
|
||||
|
||||
const treeSetting = ref({
|
||||
view: { dblClickExpand: false, showIcon: false },
|
||||
edit: { enable: false, showRemoveBtn: false, showRenameBtn: false, drag: false },
|
||||
check: { enable: false },
|
||||
callback: {},
|
||||
})
|
||||
|
||||
function formatTree(list: any[], depth = 1): any[] {
|
||||
return (list || []).map((node) => {
|
||||
const children = node.children;
|
||||
const hasChildren = Array.isArray(children) && children.length > 0;
|
||||
const formatted: any = { ...node, isParent: hasChildren };
|
||||
if (depth === 1) formatted.open = true;
|
||||
if (hasChildren) {
|
||||
formatted.children = formatTree(children, depth + 1);
|
||||
} else {
|
||||
delete formatted.children;
|
||||
}
|
||||
return formatted;
|
||||
});
|
||||
}
|
||||
|
||||
function findNodeById(list: any[], id?: number | null): any {
|
||||
if (id == null) return null;
|
||||
for (const node of list || []) {
|
||||
if (Number(node.id) === Number(id)) return node;
|
||||
const child = findNodeById(node.children || [], id);
|
||||
if (child) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findFirstNodeByType(list: any[], type: number): any {
|
||||
for (const node of list || []) {
|
||||
if (Number(node.type) === type) return node;
|
||||
const child = findFirstNodeByType(node.children || [], type);
|
||||
if (child) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCachedSelectedNodeId() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(props.cacheKey);
|
||||
if (!raw) return null;
|
||||
const cached = JSON.parse(raw);
|
||||
const id = Number(cached?.id);
|
||||
return Number.isFinite(id) ? id : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cacheSelectedNode(treeNode: any) {
|
||||
if (!treeNode?.id) return;
|
||||
sessionStorage.setItem(props.cacheKey, JSON.stringify({
|
||||
id: Number(treeNode.id),
|
||||
name: treeNode.name,
|
||||
type: treeNode.type,
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveInitialSelectId(list: any[], preferredId?: number | null) {
|
||||
const preferred = findNodeById(list, preferredId);
|
||||
if (preferred) return preferred.id;
|
||||
|
||||
const cached = findNodeById(list, getCachedSelectedNodeId());
|
||||
if (cached) return cached.id;
|
||||
|
||||
const schoolNode = findFirstNodeByType(list, GROUP_TYPE.SCHOOL_GROUP);
|
||||
if (schoolNode) return schoolNode.id;
|
||||
|
||||
if (props.autoClick && list.length) return list[0].id;
|
||||
return list[0]?.id ?? null;
|
||||
}
|
||||
|
||||
async function loadTree(preferredId?: number | null) {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
const tree = await props.treeApi();
|
||||
treeData.value = formatTree(tree || []);
|
||||
emit('on-tree-data', cloneDeep(treeData.value));
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '加载组织树失败');
|
||||
} finally {
|
||||
treeLoading.value = false;
|
||||
}
|
||||
|
||||
if (!treeData.value.length) return;
|
||||
|
||||
await nextTick();
|
||||
const selectId = resolveInitialSelectId(treeData.value, preferredId);
|
||||
if (selectId != null) {
|
||||
refMyTree.value?.reloadTree(selectId);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTree(selectId?: number) {
|
||||
await loadTree(selectId);
|
||||
}
|
||||
|
||||
function treeLink(treeNode: any) {
|
||||
const copyNode = cloneDeep(treeNode)
|
||||
copyNode.children = [];
|
||||
curTreeNode.value = copyNode;
|
||||
cacheSelectedNode(copyNode);
|
||||
emit('on-change', copyNode)
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
refMyTree.value?.search(searchKeyword.value);
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
searchKeyword.value = '';
|
||||
refMyTree.value?.search('');
|
||||
}
|
||||
|
||||
function openSearch() {
|
||||
searchVisible.value = true;
|
||||
}
|
||||
|
||||
function closeSearch() {
|
||||
searchVisible.value = false;
|
||||
onClear();
|
||||
}
|
||||
|
||||
function reloadTree(id?: number) {
|
||||
refMyTree.value?.reloadTree(id);
|
||||
}
|
||||
|
||||
function selectNode(id: number, isClick = true) {
|
||||
refMyTree.value?.selectNode(id, isClick);
|
||||
}
|
||||
|
||||
function removeNode(id: number) {
|
||||
refMyTree.value?.removeNode(id);
|
||||
}
|
||||
|
||||
function renameNode(id: number, name: string) {
|
||||
refMyTree.value?.renameNode(id, name);
|
||||
}
|
||||
|
||||
function getNodeById(id: number) {
|
||||
return refMyTree.value?.getNodeById(id);
|
||||
}
|
||||
|
||||
function addNote(pid: number, id: number, name: string, isParent?: boolean, children?: any[]) {
|
||||
refMyTree.value?.addNote(pid, id, name, isParent, children);
|
||||
}
|
||||
|
||||
function openTree(open: boolean) {
|
||||
refMyTree.value?.openTree(open);
|
||||
}
|
||||
|
||||
loadTree()
|
||||
|
||||
defineExpose({
|
||||
openSearch,
|
||||
closeSearch,
|
||||
onSearch,
|
||||
onClear,
|
||||
loadTree,
|
||||
refreshTree,
|
||||
reloadTree,
|
||||
selectNode,
|
||||
removeNode,
|
||||
renameNode,
|
||||
getNodeById,
|
||||
addNote,
|
||||
openTree,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-tree-search-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tree-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tree-body :deep(.ztree) {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<Modal v-model="isModal" @on-visible-change="showChange" :title="title">
|
||||
<Alert type="warning" class="mb-1" show-icon>
|
||||
<template #icon>
|
||||
<Icon type="ios-bulb-outline" :class="{ 'mt-5': !info }" slot="icon" :size="28"></Icon>
|
||||
</template>
|
||||
<div v-if="msg" :class="['message-box', info ? '' : 'mt-10 mb-10 ml-10']">{{ msg }}</div>
|
||||
<template #desc v-if="info" slot="desc">{{ info }}</template>
|
||||
</Alert>
|
||||
<template #footer>
|
||||
<Button type="primary" :loading="okLoading" @click="onOkClick">{{ confirmText }}</Button>
|
||||
<Button :loading="cancleLoading" @click="onCancleClick">{{ closeText }}</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { Modal, Button, Alert, Icon } from "view-ui-plus";
|
||||
|
||||
const isModal = ref(false),
|
||||
okLoading = ref(false),
|
||||
cancleLoading = ref(false);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
msg: string;
|
||||
info?: string;
|
||||
isLoading?: boolean;
|
||||
confirmText?: string;
|
||||
closeText?: string;
|
||||
confirmFn: any;
|
||||
closeFn: any;
|
||||
destroy: Function;
|
||||
}>(),
|
||||
{
|
||||
title: "提示",
|
||||
isLoading: true,
|
||||
confirmText: "确认",
|
||||
closeText: "取消",
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(function () {
|
||||
setTimeout(() => {
|
||||
isModal.value = true;
|
||||
}, 0);
|
||||
});
|
||||
|
||||
const onOkClick = async () => {
|
||||
if (!!props.isLoading) okLoading.value = true;
|
||||
const { onOk, reslove } = props.confirmFn;
|
||||
await Promise.resolve(onOk());
|
||||
isModal.value = false;
|
||||
okLoading.value = false;
|
||||
reslove(true);
|
||||
};
|
||||
|
||||
const onCancleClick = async () => {
|
||||
if (!!props.isLoading) cancleLoading.value = true;
|
||||
const { onCancel, reslove } = props.closeFn;
|
||||
if (typeof onCancel !== "undefined") await Promise.resolve(onCancel());
|
||||
cancleLoading.value = false;
|
||||
isModal.value = false;
|
||||
reslove(false);
|
||||
};
|
||||
|
||||
const showChange = (state: boolean) => {
|
||||
if (!state) {
|
||||
props.destroy();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { MyConfirm } from "@/types/Basics";
|
||||
import confirm from "./Confirm.vue";
|
||||
|
||||
import { createApp } from "vue";
|
||||
export default ({ title, msg, info, confirmText, closeText, isLoading, onOk, onCancel }: MyConfirm) => {
|
||||
return new Promise<boolean>((reslove, reject) => {
|
||||
const instance = createApp(confirm, {
|
||||
title,
|
||||
msg,
|
||||
info,
|
||||
confirmText,
|
||||
closeText,
|
||||
isLoading,
|
||||
confirmFn: {
|
||||
onOk,
|
||||
reslove,
|
||||
},
|
||||
closeFn: {
|
||||
onCancel,
|
||||
reslove,
|
||||
},
|
||||
destroy: () => {
|
||||
closeUnmount();
|
||||
},
|
||||
});
|
||||
|
||||
// 创建一个节点
|
||||
const vnode = document.createElement("div");
|
||||
// 添加到body元素上
|
||||
document.body.appendChild(vnode);
|
||||
// 实例挂载到节点上
|
||||
instance.mount(vnode);
|
||||
// 关闭状态
|
||||
// 注册实例并移除节点
|
||||
function closeUnmount() {
|
||||
setTimeout(() => {
|
||||
instance.unmount();
|
||||
document.body.removeChild(vnode);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :id="domId" :ref="domId" class="ztree"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
treeData: {
|
||||
type: Object,
|
||||
},
|
||||
setting: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
autoClick: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isCheck: {
|
||||
typeof: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domId: "",
|
||||
treeSetting: _.cloneDeep(this.setting),
|
||||
treeObj: null,
|
||||
highlightNodes: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$watch("treeData", val => {
|
||||
if (val) {
|
||||
this.$nextTick(() => {
|
||||
this.initTree(this.treeData, this.domId);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.domId = this.uuid();
|
||||
if (this.treeData) {
|
||||
this.$nextTick(() => {
|
||||
this.initTree(this.treeData, this.domId);
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
uuid() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
var r = (Math.random() * 16) | 0,
|
||||
v = c == "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
},
|
||||
reloadTree(nodeId) {
|
||||
this.initTree(this.treeData, this.domId);
|
||||
if (nodeId != null) {
|
||||
this.$nextTick(() => {
|
||||
this.selectNode(nodeId, true); // 第二个参数 true 会 emit on-tree-click
|
||||
});
|
||||
}
|
||||
},
|
||||
initTree(_data, _domId) {
|
||||
const _this = this;
|
||||
_this.treeSetting.callback.onClick = (event, treeId, treeNode, clickFlag) => {
|
||||
// console.log(event, treeId, treeNode, clickFlag);
|
||||
if (_this.isCheck) {
|
||||
var treeObj = $.fn.zTree.getZTreeObj(treeId);
|
||||
treeObj.checkNode(treeNode, !treeNode.checked, true);
|
||||
}
|
||||
_this.$emit("on-tree-click", treeNode);
|
||||
};
|
||||
|
||||
_this.treeSetting.callback.beforeDrop = (treeId, treeNodes, targetNode, moveType) => {
|
||||
if (moveType == "inner") {
|
||||
// 移除限制,允许叶子节点接受子节点
|
||||
} else {
|
||||
if (targetNode.pid === 0) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
_this.treeSetting.callback.beforeDrag = (treeId, treeNodes) => {
|
||||
return treeNodes[0].drag !== false;
|
||||
};
|
||||
|
||||
_this.treeSetting.callback.beforeClick = (treeId, treeNode, clickFlag) => {
|
||||
return !(treeNode.click === false);
|
||||
};
|
||||
|
||||
_this.treeSetting.callback.onDrop = (event, treeId, treeNodes, targetNode, moveType) => {
|
||||
_this.$emit("on-tree-drag", { treeNodes, targetNode, moveType });
|
||||
};
|
||||
|
||||
_this.treeSetting.callback.onCheck = (event, treeId, treeNode) => {
|
||||
_this.$emit("on-tree-check", treeNode);
|
||||
};
|
||||
|
||||
_this.treeObj = $.fn.zTree.init($("#" + _domId), _this.treeSetting, _data);
|
||||
if (_this.autoClick) {
|
||||
if (Array.isArray(this.treeData)) {
|
||||
let id = this.treeData[0].id;
|
||||
_this.selectNode(id);
|
||||
} else {
|
||||
let id = this.treeData.id;
|
||||
_this.selectNode(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
selectNode(id, isClick = true) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
if (node == null) return false;
|
||||
treeObj.selectNode(node);
|
||||
treeObj.expandNode(node, true, false, false);
|
||||
treeObj.selectNode(node);
|
||||
if (isClick) this.$emit("on-tree-click", node);
|
||||
},
|
||||
openTree(open) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
treeObj.expandAll(open);
|
||||
},
|
||||
removeNode(id) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
var pnode = node.getParentNode();
|
||||
treeObj.removeNode(node);
|
||||
pnode.isParent = true;
|
||||
treeObj.updateNode(pnode);
|
||||
this.selectNode(pnode.id);
|
||||
},
|
||||
getNodeById(id) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
return node;
|
||||
},
|
||||
renameNode(id, name) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
node.name = name;
|
||||
treeObj.updateNode(node);
|
||||
},
|
||||
addNote(pid, id, name, isParent, children) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var pnode = treeObj.getNodesByFilter(node => node.id == pid, true);
|
||||
var newNode = { pid, id, name, isParent };
|
||||
if (children && children.length) newNode.children = children;
|
||||
treeObj.addNodes(pnode, -1, newNode);
|
||||
},
|
||||
setHighlight(node, on = true) {
|
||||
const a = $('#' + node.tId + '_a');
|
||||
if (on) a.addClass('ztree-search-highlight');
|
||||
else a.removeClass('ztree-search-highlight');
|
||||
},
|
||||
clearHighlight() {
|
||||
(this.highlightNodes || []).forEach(n => this.setHighlight(n, false));
|
||||
this.highlightNodes = [];
|
||||
},
|
||||
search(keyword) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
if (!treeObj) return;
|
||||
this.clearHighlight();
|
||||
const kw = (keyword || '').trim().toLowerCase();
|
||||
if (!kw) return;
|
||||
const matchNodes = treeObj.getNodesByFilter(
|
||||
(node) => node.name && node.name.toLowerCase().includes(kw),
|
||||
false
|
||||
) || [];
|
||||
if (!matchNodes.length) {
|
||||
if (this.$Message && this.$Message.info) this.$Message.info('未搜索到匹配组');
|
||||
return;
|
||||
}
|
||||
matchNodes.forEach(n => {
|
||||
let p = n;
|
||||
while (p) {
|
||||
treeObj.expandNode(p, true, false, false);
|
||||
p = p.getParentNode();
|
||||
}
|
||||
});
|
||||
this.highlightNodes = matchNodes;
|
||||
matchNodes.forEach(n => this.setHighlight(n, true));
|
||||
this.selectNode(matchNodes[0].id, true);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ztree-search-highlight {
|
||||
background-color: #ff6600 !important;
|
||||
}
|
||||
|
||||
.ztree li a.curSelectedNode {
|
||||
background-color: #ffe4bb !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div style="height: 100%;">
|
||||
<div class="tree-search-bar">
|
||||
<Input v-model="keyword" placeholder="输入学校,回车查找" suffix="ios-search" clearable @on-enter="onSearch"
|
||||
@on-clear="onSearch" style="width:100%;margin-bottom: 10px;" />
|
||||
</div>
|
||||
<div :id="domId" :ref="domId" class="ztree"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
treeData: {
|
||||
type: Object,
|
||||
},
|
||||
setting: {
|
||||
type: Object,
|
||||
default: {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showIcon: false,
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: false,
|
||||
},
|
||||
check: {
|
||||
enable: false,
|
||||
},
|
||||
callback: {},
|
||||
},
|
||||
},
|
||||
autoClick: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isCheck: {
|
||||
typeof: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domId: "",
|
||||
treeSetting: _.cloneDeep(this.setting),
|
||||
treeObj: null,
|
||||
keyword: '',
|
||||
highlightNodes: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$watch("treeData", val => {
|
||||
console.log(val);
|
||||
if (val) {
|
||||
console.log(val);
|
||||
this.$nextTick(() => {
|
||||
this.initTree(this.treeData, this.domId);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.domId = this.uuid();
|
||||
},
|
||||
methods: {
|
||||
uuid() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
var r = (Math.random() * 16) | 0,
|
||||
v = c == "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
},
|
||||
reloadTree() {
|
||||
this.treeData.open = true;
|
||||
this.initTree(this.treeData, this.domId);
|
||||
},
|
||||
initTree(_data, _domId) {
|
||||
const _this = this;
|
||||
_this.treeSetting.callback.onClick = (event, treeId, treeNode, clickFlag) => {
|
||||
// console.log(event, treeId, treeNode, clickFlag);
|
||||
if (_this.isCheck) {
|
||||
var treeObj = $.fn.zTree.getZTreeObj(treeId);
|
||||
treeObj.checkNode(treeNode, !treeNode.checked, true);
|
||||
}
|
||||
_this.$emit("on-tree-click", treeNode);
|
||||
};
|
||||
|
||||
_this.treeSetting.callback.beforeDrop = (treeId, treeNodes, targetNode, moveType) => {
|
||||
if (moveType == "inner") {
|
||||
if (!targetNode.isParent) return false;
|
||||
} else {
|
||||
if (targetNode.pid === 0) return false;
|
||||
}
|
||||
};
|
||||
|
||||
_this.treeSetting.callback.beforeClick = (treeId, treeNode, clickFlag) => {
|
||||
return !(treeNode.click === false);
|
||||
};
|
||||
|
||||
_this.treeSetting.callback.onDrop = (event, treeId, treeNodes, targetNode, moveType) => {
|
||||
_this.$emit("on-tree-drag", { treeNodes, targetNode, moveType });
|
||||
};
|
||||
|
||||
_this.treeSetting.callback.onCheck = (event, treeId, treeNode) => {
|
||||
_this.$emit("on-tree-check", treeNode);
|
||||
};
|
||||
|
||||
_this.treeObj = $.fn.zTree.init($("#" + _domId), _this.treeSetting, _data);
|
||||
if (_this.autoClick) {
|
||||
if (Array.isArray(this.treeData)) {
|
||||
let id = this.treeData[0].id;
|
||||
_this.selectNode(id);
|
||||
} else {
|
||||
let id = this.treeData.id;
|
||||
_this.selectNode(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
selectNode(id, isClick = true) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
if (node == null) return false;
|
||||
treeObj.selectNode(node);
|
||||
treeObj.expandNode(node, true, false, false);
|
||||
treeObj.selectNode(node);
|
||||
if (isClick) this.$emit("on-tree-click", node);
|
||||
},
|
||||
openTree(open) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
treeObj.expandAll(open);
|
||||
},
|
||||
removeNode(id) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
var pnode = node.getParentNode();
|
||||
treeObj.removeNode(node);
|
||||
pnode.isParent = true;
|
||||
treeObj.updateNode(pnode);
|
||||
this.selectNode(pnode.id);
|
||||
},
|
||||
getNodeById(id) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
return node;
|
||||
},
|
||||
renameNode(id, name) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
node.name = name;
|
||||
treeObj.updateNode(node);
|
||||
},
|
||||
addNote(pid, id, name, isParent) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var pnode = treeObj.getNodesByFilter(node => node.id == pid, true);
|
||||
var newNode = { pid, id, name, isParent };
|
||||
treeObj.addNodes(pnode, -1, newNode);
|
||||
},
|
||||
onSearch() {
|
||||
this.clearHighlight();
|
||||
if (!this.keyword) return;
|
||||
this.doSearch(this.keyword);
|
||||
},
|
||||
doSearch(key) {
|
||||
const treeObj = this.treeObj;
|
||||
// 1. 模糊匹配
|
||||
const matchNodes = treeObj.getNodesByFilter(node =>
|
||||
node.type == 2 &&
|
||||
node.name && node.name.toLowerCase().includes(key.toLowerCase())
|
||||
);
|
||||
if (!matchNodes.length) {
|
||||
this.$Message.info('未搜索到匹配学校')
|
||||
return;
|
||||
}
|
||||
// 2. 展开所有命中节点的父节点
|
||||
matchNodes.forEach(n => {
|
||||
let p = n.getParentNode();
|
||||
while (p) {
|
||||
treeObj.expandNode(p, true, false, false);
|
||||
p = p.getParentNode();
|
||||
}
|
||||
});
|
||||
// 3. 高亮
|
||||
this.highlightNodes = matchNodes;
|
||||
matchNodes.forEach(n => this.setHighlight(n, true));
|
||||
|
||||
// const box = document.getElementById('treeBox');
|
||||
// const nodeEl = document.getElementById(matchNodes[0].tId);
|
||||
// box.scrollTop = nodeEl.offsetTop - box.clientHeight / 2; // 居中
|
||||
|
||||
if (matchNodes.length) {
|
||||
const treeObj = this.treeObj;
|
||||
console.log(matchNodes[0]);
|
||||
this.selectNode(matchNodes[0].id);
|
||||
|
||||
// treeObj.selectNode(matchNodes[0]); // 先选中,会自动滚入可视区
|
||||
//this.$nextTick(() => treeObj.cancelSelectedNode()); // 取消选中,保持纯高亮
|
||||
}
|
||||
|
||||
},
|
||||
setHighlight(node, on = true) {
|
||||
const a = $('#' + node.tId + '_a');
|
||||
if (on) a.addClass('ztree-search-highlight');
|
||||
else a.removeClass('ztree-search-highlight');
|
||||
},
|
||||
clearHighlight() {
|
||||
this.highlightNodes.forEach(n => this.setHighlight(n, false));
|
||||
this.highlightNodes = [];
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ztree {
|
||||
height: calc(100% - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ztree-search-highlight {
|
||||
background-color: #ff6600 !important;
|
||||
}
|
||||
|
||||
.ztree li a.curSelectedNode {
|
||||
background-color: #ffe4bb !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="notice-upload">
|
||||
<!-- 自定义上传附件(自旧前端 oldfront/src/components/uploadFile.vue 适配) -->
|
||||
<div class="upload-row">
|
||||
<Upload
|
||||
:on-success="encloSuccess"
|
||||
:on-remove="encloRemove"
|
||||
:default-file-list="defaultList"
|
||||
:action="uploadFileUrl"
|
||||
:accept="fileType"
|
||||
:multiple="isMultiple"
|
||||
:before-upload="handleUpload"
|
||||
:format="formatArr"
|
||||
:max-size="maxSize"
|
||||
:on-format-error="handleFormatError"
|
||||
:on-exceeded-size="handleExceeded"
|
||||
>
|
||||
<Button><Icon type="ios-cloud-upload-outline"></Icon>选择文件</Button>
|
||||
</Upload>
|
||||
<span class="upload-tip">提示:附件大小不超过{{ maxFileSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* 通知附件上传(自旧前端 oldfront/src/components/uploadFile.vue 适配 Vue3)。
|
||||
* v-model 为已上传附件 fileid 数组;编辑时经 defaultList 回显已有附件。
|
||||
*/
|
||||
import { Message, Notice } from "view-ui-plus";
|
||||
import config from "@/assets/config/host.config";
|
||||
|
||||
export default {
|
||||
name: "NoticeUpload",
|
||||
props: {
|
||||
defaultList: { type: Array as () => any[], default: () => [] },
|
||||
fileids: { type: Array as () => number[], default: () => [] },
|
||||
fileType: { type: String, default: "" },
|
||||
multiple: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["update:fileids"],
|
||||
data() {
|
||||
return {
|
||||
isMultiple: !this.multiple || false,
|
||||
uploadFileUrl: (config.isMock ? "" : config.serviceHost) + "/api/base/FileUpload",
|
||||
formatArr: ["jpg", "gif", "png", "bmp", "xls", "doc", "txt", "docx", "xlsx", "zip", "rar", "pdf", "ppt", "pptx", "wps"],
|
||||
maxFileSize: "200MB",
|
||||
maxSize: 204800,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
encloSuccess(resp: any, file: any) {
|
||||
if (resp.code !== 0) {
|
||||
Message.error(resp.msg || "上传失败");
|
||||
return;
|
||||
}
|
||||
this.fileids.push(resp.data.fileid);
|
||||
this.$emit("update:fileids", this.fileids);
|
||||
},
|
||||
encloRemove(file: any) {
|
||||
//删除文件:默认列表项带 fileid,刚上传的取 response
|
||||
const list = file.fileid ? file : file.response;
|
||||
let fileid = list?.fileid || list?.data?.fileid;
|
||||
const arry = this.fileids.filter((element: number) => element != fileid);
|
||||
this.$emit("update:fileids", arry);
|
||||
},
|
||||
handleUpload() {
|
||||
if (!this.isMultiple && this.fileids.length > 0) {
|
||||
Message.info("仅允许上传一个附件!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handleFormatError() {
|
||||
Notice.info({
|
||||
title: "不支持的上传类型!",
|
||||
desc: "请上传指定的文件类型",
|
||||
});
|
||||
},
|
||||
handleExceeded() {
|
||||
Notice.warning({
|
||||
title: "超出大小限制",
|
||||
desc: "最大文件大小为" + this.maxFileSize + "!",
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notice-upload {
|
||||
display: block;
|
||||
.upload-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
:deep(.ivu-upload) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
/* 文件列表整体另起一行,每项独占一行,不再与按钮挤在一起 */
|
||||
:deep(.ivu-upload-list) {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
:deep(.ivu-upload-list-file) {
|
||||
padding: 4px 0;
|
||||
}
|
||||
.upload-tip {
|
||||
margin-left: 10px;
|
||||
color: #f90;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<div class="scope-panel">
|
||||
<Input
|
||||
v-model="keyword"
|
||||
size="small"
|
||||
clearable
|
||||
search
|
||||
placeholder="输入名称搜索范围"
|
||||
class="scope-search"
|
||||
/>
|
||||
<div class="scope-tree">
|
||||
<div v-if="treeLoading" class="scope-tip">加载中…</div>
|
||||
<div v-else-if="!visibleRows.length" class="scope-tip">暂无班级数据</div>
|
||||
<div
|
||||
v-for="row in visibleRows"
|
||||
:key="row.key"
|
||||
class="tree-row"
|
||||
:style="{ paddingLeft: row.depth * 18 + 'px' }"
|
||||
>
|
||||
<span class="toggle" @click="toggle(row.key)">
|
||||
<i v-if="row.hasChildren" :class="expanded[row.key] ? 'arrow-down' : 'arrow-up'"></i>
|
||||
</span>
|
||||
<Checkbox
|
||||
:model-value="isChecked(row.key)"
|
||||
@on-change="() => onCheck(row)"
|
||||
><span :title="row.node.name">{{ row.node.name }}</span></Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已选范围 chips(最粗粒度) -->
|
||||
<div class="scope-chips" v-if="modelValue.length">
|
||||
<span class="chip" v-for="s in modelValue" :key="s.scopeType + ':' + s.scopeId">
|
||||
{{ s.scopeName }}
|
||||
<i @click="removeScope(s)">×</i>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="scope-empty">未选择推送范围</div>
|
||||
|
||||
<!-- 选中学员数预估(ScopePreview 按范围展开去重) -->
|
||||
<div class="scope-preview" v-if="modelValue.length">
|
||||
<span>已选学员 <b>{{ preview ? preview.studentCount : "…" }}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/*
|
||||
* 推送范围面板(v3:数据源换 base/getStuTree?toUser=false,与 api-test 写通知同口径)。
|
||||
* v-model = PushScopeItem[](最粗粒度勾选:父节点勾选则忽略其子孙)。
|
||||
* getStuTree 组节点不区分学校/学段/年级/班级,toScopeNode 自底向上归类(对齐 PushScopeTypeEnum):
|
||||
* 无组子节点=班级(4),其父=年级(3),再上=学部(2),顶层根强制=全校(1)。
|
||||
* 选中学员数:notice/ScopePreview.studentCount。
|
||||
* 编辑回显:父组件加载详情后调用 setScopes(pushScopes) 勾选对应节点;
|
||||
* 旧链路班级通知(receiveType=class)由父组件映射为 scopeType=4 的班级项。
|
||||
*/
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { baseApi, noticeApi } from "@/api/module/notice";
|
||||
import type { PushScopeItem, SchTreeNode, ScopeTreeNode, ScopePreviewVo } from "@/types/notice";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: PushScopeItem[];
|
||||
}>();
|
||||
const emits = defineEmits<{ (e: "update:modelValue", v: PushScopeItem[]): void }>();
|
||||
|
||||
type Row = { key: string; node: ScopeTreeNode; depth: number; hasChildren: boolean; parentKey: string | null };
|
||||
|
||||
const keyword = ref("");
|
||||
const tree = ref<ScopeTreeNode[]>([]);
|
||||
const treeLoading = ref(false);
|
||||
const expanded = ref<Record<string, boolean>>({});
|
||||
/** 勾选集合(key = scopeType:scopeId),含父子级联的显式勾选 */
|
||||
const checkedKeys = ref<Set<string>>(new Set());
|
||||
|
||||
const keyOf = (n: ScopeTreeNode) => `${n.scopeType}:${n.scopeId}`;
|
||||
|
||||
/** 展平树(保留 parentKey 供级联与折叠计算) */
|
||||
const allRows = computed<Row[]>(() => {
|
||||
const out: Row[] = [];
|
||||
const walk = (nodes: ScopeTreeNode[], depth: number, parentKey: string | null) => {
|
||||
for (const n of nodes || []) {
|
||||
const key = keyOf(n);
|
||||
out.push({ key, node: n, depth, hasChildren: !!(n.children && n.children.length), parentKey });
|
||||
if (n.children && n.children.length) walk(n.children, depth + 1, key);
|
||||
}
|
||||
};
|
||||
walk(tree.value, 0, null);
|
||||
return out;
|
||||
});
|
||||
const rowByKey = computed(() => new Map(allRows.value.map((r) => [r.key, r])));
|
||||
|
||||
/** 折叠过滤:祖先未展开的行隐藏 */
|
||||
const visibleRows = computed(() => {
|
||||
const kw = keyword.value.trim().toLowerCase();
|
||||
const kwHit = (r: Row): boolean => {
|
||||
const self = (r.node.name || "").toLowerCase().includes(kw);
|
||||
const childHit = (r.node.children || []).some((c) => {
|
||||
const cr = rowByKey.value.get(keyOf(c));
|
||||
return cr ? kwHit(cr) : false;
|
||||
});
|
||||
return self || childHit;
|
||||
};
|
||||
return allRows.value.filter((r) => {
|
||||
// 祖先全部展开(搜索时自动放行,由 kwHit 收敛)
|
||||
let p = r.parentKey;
|
||||
while (p) {
|
||||
if (!kw && !expanded.value[p]) return false;
|
||||
p = rowByKey.value.get(p)?.parentKey ?? null;
|
||||
}
|
||||
return !kw || kwHit(r);
|
||||
});
|
||||
});
|
||||
|
||||
function toggle(key: string) {
|
||||
expanded.value[key] = !expanded.value[key];
|
||||
}
|
||||
|
||||
/** 勾选态:自身勾选 或 祖先勾选(级联选中) */
|
||||
function isChecked(key: string): boolean {
|
||||
if (checkedKeys.value.has(key)) return true;
|
||||
let p = rowByKey.value.get(key)?.parentKey ?? null;
|
||||
while (p) {
|
||||
if (checkedKeys.value.has(p)) return true;
|
||||
p = rowByKey.value.get(p)?.parentKey ?? null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 勾选/取消:勾父级联全子孙,取消 likewise;取消父节点仅移除自身及子孙 */
|
||||
function onCheck(row: Row) {
|
||||
const checked = !isChecked(row.key);
|
||||
if (checked) {
|
||||
checkedKeys.value.add(row.key);
|
||||
// 勾选父节点:子孙全选(加入集合,提交时仍取最粗粒度)
|
||||
const walk = (nodes?: ScopeTreeNode[]) => {
|
||||
(nodes || []).forEach((c) => {
|
||||
checkedKeys.value.add(keyOf(c));
|
||||
walk(c.children);
|
||||
});
|
||||
};
|
||||
walk(row.node.children);
|
||||
} else {
|
||||
const remove = (nodes?: ScopeTreeNode[]) => {
|
||||
(nodes || []).forEach((c) => {
|
||||
checkedKeys.value.delete(keyOf(c));
|
||||
remove(c.children);
|
||||
});
|
||||
};
|
||||
checkedKeys.value.delete(row.key);
|
||||
remove(row.node.children);
|
||||
}
|
||||
emitSelection();
|
||||
}
|
||||
|
||||
/** 收集最粗粒度勾选(勾选节点的祖先未勾选时才收集) */
|
||||
function collectCoarsest(): PushScopeItem[] {
|
||||
const items: PushScopeItem[] = [];
|
||||
for (const r of allRows.value) {
|
||||
if (!checkedKeys.value.has(r.key)) continue;
|
||||
let p = r.parentKey;
|
||||
let ancestorChecked = false;
|
||||
while (p) {
|
||||
if (checkedKeys.value.has(p)) {
|
||||
ancestorChecked = true;
|
||||
break;
|
||||
}
|
||||
p = rowByKey.value.get(p)?.parentKey ?? null;
|
||||
}
|
||||
if (!ancestorChecked) {
|
||||
items.push({ scopeType: r.node.scopeType, scopeId: r.node.scopeId, scopeName: r.node.name });
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function emitSelection() {
|
||||
emits("update:modelValue", collectCoarsest());
|
||||
}
|
||||
|
||||
function removeScope(s: PushScopeItem) {
|
||||
const key = `${s.scopeType}:${s.scopeId}`;
|
||||
const row = rowByKey.value.get(key);
|
||||
if (row) {
|
||||
onCheck(row); // onCheck 取反:当前已勾选 → 取消
|
||||
} else {
|
||||
// 树中不存在的回显项(数据已变更):直接从 modelValue 剔除
|
||||
emits(
|
||||
"update:modelValue",
|
||||
props.modelValue.filter((x) => !(x.scopeType === s.scopeType && x.scopeId === s.scopeId))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑回显:按 pushScopes 勾选树节点(树加载完成前调用则缓冲,待树就绪后应用) */
|
||||
let pendingScopes: PushScopeItem[] | null = null;
|
||||
|
||||
function setScopes(scopes?: PushScopeItem[] | null) {
|
||||
if (!tree.value.length && treeLoading.value) {
|
||||
pendingScopes = scopes || [];
|
||||
return;
|
||||
}
|
||||
applyScopes(scopes);
|
||||
}
|
||||
|
||||
function applyScopes(scopes?: PushScopeItem[] | null) {
|
||||
checkedKeys.value = new Set();
|
||||
const keep: PushScopeItem[] = [];
|
||||
for (const s of scopes || []) {
|
||||
const key = `${s.scopeType}:${s.scopeId}`;
|
||||
if (rowByKey.value.has(key)) {
|
||||
checkedKeys.value.add(key);
|
||||
const walk = (nodes?: ScopeTreeNode[]) => {
|
||||
(nodes || []).forEach((c) => {
|
||||
checkedKeys.value.add(keyOf(c));
|
||||
walk(c.children);
|
||||
});
|
||||
};
|
||||
walk(rowByKey.value.get(key)?.node.children);
|
||||
} else {
|
||||
keep.push(s); // 树中无此节点:保留原值随表单提交
|
||||
}
|
||||
}
|
||||
// 展开已选节点的祖先链
|
||||
for (const key of checkedKeys.value) {
|
||||
let p = rowByKey.value.get(key)?.parentKey ?? null;
|
||||
while (p) {
|
||||
expanded.value[p] = true;
|
||||
p = rowByKey.value.get(p)?.parentKey ?? null;
|
||||
}
|
||||
}
|
||||
emits("update:modelValue", [...keep, ...collectCoarsest()]);
|
||||
}
|
||||
|
||||
defineExpose({ setScopes });
|
||||
|
||||
/* ============ 选中学员数预估(ScopePreview,防抖 350ms) ============ */
|
||||
const preview = ref<ScopePreviewVo | null>(null);
|
||||
let previewTimer: number | undefined;
|
||||
|
||||
async function runPreview() {
|
||||
if (!props.modelValue.length) {
|
||||
preview.value = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
preview.value = await noticeApi.scopePreview(props.modelValue).send();
|
||||
} catch {
|
||||
preview.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
window.clearTimeout(previewTimer);
|
||||
previewTimer = window.setTimeout(runPreview, 350);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
/** getStuTree 组节点 → 面板范围节点:自底向上归类 scopeType(对齐 api-test toScopeNode,见头注释) */
|
||||
function toScopeNode(node: SchTreeNode, forcedType?: number): ScopeTreeNode | null {
|
||||
if (!node || node.type !== 1) return null; // toUser=false 仅组节点;出现人员叶子防御性跳过
|
||||
const children = (node.children || []).map((c) => toScopeNode(c)).filter((c): c is ScopeTreeNode => !!c);
|
||||
let scopeType = forcedType;
|
||||
if (!scopeType) {
|
||||
if (!children.length) scopeType = 4;
|
||||
else if (children.some((c) => c.scopeType === 4)) scopeType = 3;
|
||||
else if (children.some((c) => c.scopeType === 3)) scopeType = 2;
|
||||
else scopeType = 1;
|
||||
}
|
||||
return { scopeType, scopeId: node.id, name: node.name, children };
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
const raw = (await baseApi.getStuTree(false).send()) || [];
|
||||
tree.value = raw.map((n) => toScopeNode(n, 1)).filter((n): n is ScopeTreeNode => !!n);
|
||||
// 默认展开前两层(全校/学段)
|
||||
for (const r of allRows.value) {
|
||||
if (r.depth <= 1 && r.hasChildren) expanded.value[r.key] = true;
|
||||
}
|
||||
} catch (e: any) {
|
||||
tree.value = [];
|
||||
} finally {
|
||||
treeLoading.value = false;
|
||||
// 回显缓冲:详情先于树就绪时,待树加载完成再应用
|
||||
if (pendingScopes) {
|
||||
const scoped = pendingScopes;
|
||||
pendingScopes = null;
|
||||
applyScopes(scoped);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scope-panel {
|
||||
.scope-search {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.scope-tree {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 4px;
|
||||
padding: 6px 4px;
|
||||
.scope-tip {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 20px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.tree-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 26px;
|
||||
.toggle {
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
flex: none;
|
||||
i {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 5px solid #999;
|
||||
cursor: pointer;
|
||||
&.arrow-down {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.ivu-checkbox-wrapper) {
|
||||
margin-right: 0;
|
||||
}
|
||||
:deep(.ivu-checkbox+span) {
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
.scope-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
.chip {
|
||||
background: #eaf3ff;
|
||||
color: #2d8cf0;
|
||||
border-radius: 3px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
i {
|
||||
font-style: normal;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
color: #9bbfe8;
|
||||
}
|
||||
}
|
||||
}
|
||||
.scope-empty {
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.scope-preview {
|
||||
margin-top: 8px;
|
||||
background: #f8fafc;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
b {
|
||||
font-size: 15px;
|
||||
color: #2d8cf0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, withDefaults } from "vue";
|
||||
|
||||
interface Props {
|
||||
prefix?: string;
|
||||
name?: string;
|
||||
color?: string;
|
||||
hoverColor?: string;
|
||||
size?: string | number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
prefix: "icon",
|
||||
name: "",
|
||||
color: "#000",
|
||||
});
|
||||
|
||||
const isHover = ref(false);
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
|
||||
const svgStyle = computed(() => {
|
||||
if (!!props.size) return { width: props.size + "px", height: props.size + "px" };
|
||||
else return null;
|
||||
});
|
||||
|
||||
const compColor = computed(() => {
|
||||
if (!props.hoverColor) return props.color;
|
||||
else {
|
||||
return !!isHover.value ? props.hoverColor : props.color;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
@mouseover="isHover = true"
|
||||
@mouseout="isHover = false"
|
||||
aria-hidden="true"
|
||||
:color="compColor"
|
||||
:style="svgStyle"
|
||||
>
|
||||
<use :xlink:href="symbolId" :fill="compColor" :color="compColor" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts" setup>
|
||||
import { OperatorEnum } from "@/types";
|
||||
import { PageData, PageParams } from "@/types/Basics";
|
||||
import Utils from "@/utils";
|
||||
import { Message } from "view-ui-plus";
|
||||
|
||||
/* 表格插件参数类型 */
|
||||
interface TablePageProps {
|
||||
dataApi: (params: PageParams) => Promise<PageData>;
|
||||
columns: object[];
|
||||
tableHeight: number | undefined;
|
||||
pageParams: PageParams | undefined;
|
||||
dataFilter?: ((resp: object) => object[]) | undefined;
|
||||
autoLoad?: boolean | undefined;
|
||||
}
|
||||
import { ref, onBeforeMount, onMounted } from "vue";
|
||||
const props = withDefaults(defineProps<TablePageProps>(), {
|
||||
autoLoad: true,
|
||||
});
|
||||
const list = ref<any[]>([]),
|
||||
selectList = ref<number[]>([]),
|
||||
total = ref(0),
|
||||
tableH = ref(0),
|
||||
tableLoading = ref(true);
|
||||
const params = ref<PageParams>({ page: 1, count: 10 });
|
||||
onBeforeMount(async () => {
|
||||
if (props.tableHeight) tableH.value = props.tableHeight;
|
||||
else tableH.value = Utils.useTableHeight();
|
||||
if (!!props.pageParams) params.value = props.pageParams;
|
||||
// 页长记忆(bsch 规范 §3:customPageCount localStorage)
|
||||
params.value.count = parseInt(localStorage.getItem("customPageCount") || String(params.value.count));
|
||||
if (props.autoLoad) await getData();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// window.onresize = _.debounce(() => {
|
||||
// tableH.value = Utils.useTableHeight();
|
||||
// }, 1000);
|
||||
});
|
||||
|
||||
const getData = async () => {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const tempParams: any = _.cloneDeep(params.value);
|
||||
tempParams.size = tempParams.count;
|
||||
delete tempParams.count;
|
||||
|
||||
tempParams.page -= 1;
|
||||
|
||||
var data = await props.dataApi(tempParams);
|
||||
total.value = data.count;
|
||||
|
||||
if (typeof props.dataFilter != "undefined") list.value = props.dataFilter(data);
|
||||
else list.value = data.list;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
Message.error(error);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
function selectChange(selection: any) {
|
||||
selectList.value = selection;
|
||||
}
|
||||
async function pageUpdate(page: number) {
|
||||
params.value.page = page;
|
||||
await getData();
|
||||
}
|
||||
async function pageSizeUpdate(size: number) {
|
||||
params.value.page = 1;
|
||||
params.value.count = size;
|
||||
localStorage.setItem("customPageCount", String(size));
|
||||
await getData();
|
||||
}
|
||||
|
||||
/** 刷新数据-更新删除用 */
|
||||
// "add" | "del" | "edit" | "sel"
|
||||
async function refreshData() {
|
||||
params.value.page = 1;
|
||||
await getData();
|
||||
}
|
||||
|
||||
/** 重置数据 */
|
||||
// "add" | "del" | "edit" | "sel"
|
||||
async function reloadData(type: OperatorEnum = OperatorEnum.sel) {
|
||||
let { page, count } = params.value;
|
||||
if (type == OperatorEnum.add) {
|
||||
console.log(page, count, total.value);
|
||||
|
||||
if (page * count == total.value) page += 1;
|
||||
} else if (type == OperatorEnum.del) {
|
||||
if ((page - 1) * count + 1 == total.value) page -= 1;
|
||||
if (page < 1) page = 1;
|
||||
} else if (type == OperatorEnum.edit) {
|
||||
//page=page
|
||||
} else if (type == OperatorEnum.sel) page = 1;
|
||||
params.value.page = page;
|
||||
await getData();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
list,
|
||||
refreshData,
|
||||
reloadData,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data="list"
|
||||
@on-selection-change="selectChange"
|
||||
class="link-table table-bottom-box"
|
||||
:height="tableH"
|
||||
:loading="tableLoading"
|
||||
no-data-text="暂无数据"
|
||||
>
|
||||
<template #loading>
|
||||
<div class="table-spin-load">加载中</div>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="page-panel">
|
||||
<div class="fl ml-15 align-middle">
|
||||
<slot :checkList="selectList"></slot>
|
||||
</div>
|
||||
|
||||
<Page
|
||||
class="page-box fr"
|
||||
:total="total"
|
||||
:model-value="params.page"
|
||||
:page-size="params.count"
|
||||
:page-size-opts="[10, 20, 30, 40]"
|
||||
@on-page-size-change="pageSizeUpdate"
|
||||
@on-change="pageUpdate"
|
||||
placement="top"
|
||||
show-total
|
||||
show-sizer
|
||||
></Page>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<!-- 表格 -->
|
||||
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
|
||||
class="link-table table-bottom-box" @on-selection-change="selectChange">
|
||||
<template #loading>
|
||||
<div class="table-spin-load">加载中</div>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 / 插槽 -->
|
||||
<div class="page-panel">
|
||||
<div class="fl ml-15 align-middle">
|
||||
<slot :checkList="selectList" />
|
||||
</div>
|
||||
|
||||
<Page class="page-box fr" :total="total" :model-value="page" :page-size="pageSize"
|
||||
:page-size-opts="[10, 20, 30, 40]" show-total show-sizer placement="top" @on-change="onPageChange"
|
||||
@on-page-size-change="onSizeChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { Message } from 'view-ui-plus'
|
||||
import Utils from '@/utils'
|
||||
import { OperatorEnum } from '@/types'
|
||||
|
||||
interface Props {
|
||||
/** 必填:alova-gen 生成的 selectTextbook 方法(或任意同形方法) */
|
||||
dataApi: any
|
||||
columns: any[]
|
||||
dataParams: any
|
||||
tableHeight?: number
|
||||
autoLoad?: boolean
|
||||
dataFilter?: (resp: any) => any[],
|
||||
formatResponse?: (res: any) => { total: number; list: any[] }
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autoLoad: true,
|
||||
formatResponse: (res: any) => ({ total: res.total, list: res.content })
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'selection-change', list: number[]): void
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
|
||||
/* ---------------- 表格高度 ---------------- */
|
||||
const tableH = ref(props.tableHeight ?? Utils.useTableHeight())
|
||||
|
||||
/* ---------------- 选中行 ---------------- */
|
||||
const selectList = ref<number[]>([])
|
||||
function selectChange(rows: any[]) {
|
||||
selectList.value = rows.map((r: any) => r.id)
|
||||
emit('selection-change', selectList.value)
|
||||
}
|
||||
|
||||
/* ---------------- 分页 + 请求 ---------------- */
|
||||
const {
|
||||
loading,
|
||||
data: resp,
|
||||
send: sendLoad,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
reload
|
||||
} = usePagination(
|
||||
(current: number, size: number) =>
|
||||
props.dataApi({
|
||||
...props.dataParams, // 合并外部条件
|
||||
pageIndex: current,
|
||||
pageSize: size,
|
||||
}),
|
||||
{
|
||||
// total: res => res.total,
|
||||
// data: res => res.list,
|
||||
total: res => props.formatResponse(res).total,
|
||||
data: res => props.formatResponse(res).list,
|
||||
// 默认分页参数
|
||||
initialPage: 1,
|
||||
initialPageSize: 10,
|
||||
immediate: props.autoLoad,
|
||||
watchingStates: [() => props.dataParams]
|
||||
}
|
||||
)
|
||||
/* 列表数据 */
|
||||
const list = computed(() => {
|
||||
if (!resp.value) return []
|
||||
return props.dataFilter ? props.dataFilter(resp.value) : resp.value
|
||||
})
|
||||
|
||||
/* ---------------- 翻页事件 ---------------- */
|
||||
const onPageChange = (p: number) => (page.value = p)
|
||||
const onSizeChange = (s: number) => (pageSize.value = s)
|
||||
|
||||
/* ---------------- 外部可调用 ---------------- */
|
||||
function refreshData() {
|
||||
page.value = 1
|
||||
reload()
|
||||
}
|
||||
function reloadData(type: OperatorEnum = OperatorEnum.sel) {
|
||||
let p = page.value
|
||||
const s = pageSize.value
|
||||
|
||||
if (type === OperatorEnum.add && p * s === total.value) p += 1
|
||||
if (type === OperatorEnum.del && (p - 1) * s + 1 === total.value) p -= 1
|
||||
if (p < 1) p = 1
|
||||
page.value = p
|
||||
sendLoad()
|
||||
}
|
||||
|
||||
defineExpose({ list, refreshData, reloadData })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 沿用旧样式即可 */
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
|
||||
class="link-table table-bottom-box" @on-selection-change="selectChange">
|
||||
<template #loading>
|
||||
<div class="table-spin-load">加载中</div>
|
||||
</template>
|
||||
<template v-for="slotName in tableSlotNames" :key="slotName" v-slot:[slotName]="slotProps">
|
||||
<slot :name="slotName" v-bind="slotProps" />
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<div class="page-panel">
|
||||
<div class="fl ml-15 align-middle">
|
||||
<slot :checkList="selectList" />
|
||||
</div>
|
||||
|
||||
<Page class="page-box fr" :total="total" :model-value="page" :page-size="pageSize"
|
||||
:page-size-opts="[10, 20, 30, 40]" show-total show-sizer placement="top" @on-change="onPageChange"
|
||||
@on-page-size-change="onSizeChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useSlots } from 'vue'
|
||||
import Utils from '@/utils'
|
||||
import { OperatorEnum } from '@/types'
|
||||
|
||||
interface Props {
|
||||
dataApi: any
|
||||
columns: any[]
|
||||
tableHeight?: number
|
||||
autoLoad?: boolean
|
||||
dataFilter?: (resp: any) => any[]
|
||||
formatResponse?: (res: any) => { total: number; list: any[] }
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autoLoad: true,
|
||||
formatResponse: (res: any) => ({ total: res.total, list: res.content })
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'selection-change', list: number[]): void
|
||||
}>()
|
||||
const slots = useSlots()
|
||||
|
||||
const tableH = ref(props.tableHeight ?? Utils.useTableHeight())
|
||||
const tableSlotNames = computed(() => Object.keys(slots).filter(name => !['default', 'loading'].includes(name)))
|
||||
|
||||
const selectList = ref<number[]>([])
|
||||
function selectChange(rows: any[]) {
|
||||
selectList.value = rows.map((r: any) => r.id)
|
||||
emit('selection-change', selectList.value)
|
||||
}
|
||||
|
||||
const {
|
||||
loading,
|
||||
data: resp,
|
||||
send: sendLoad,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
reload
|
||||
} = usePagination(
|
||||
(current: number, size: number) =>
|
||||
props.dataApi({ pageIndex: current, pageSize: size }),
|
||||
{
|
||||
total: res => props.formatResponse(res).total,
|
||||
data: res => props.formatResponse(res).list,
|
||||
initialPage: 1,
|
||||
initialPageSize: 10,
|
||||
immediate: props.autoLoad,
|
||||
}
|
||||
)
|
||||
|
||||
const list = computed(() => {
|
||||
if (!resp.value) return []
|
||||
return props.dataFilter ? props.dataFilter(resp.value) : resp.value
|
||||
})
|
||||
|
||||
const onPageChange = (p: number) => (page.value = p)
|
||||
const onSizeChange = (s: number) => (pageSize.value = s)
|
||||
|
||||
function refreshData() {
|
||||
page.value = 1
|
||||
reload()
|
||||
}
|
||||
|
||||
function reloadData(type: OperatorEnum = OperatorEnum.sel) {
|
||||
let p = page.value
|
||||
const s = pageSize.value
|
||||
|
||||
if (type === OperatorEnum.add && p * s === total.value) p += 1
|
||||
if (type === OperatorEnum.del && (p - 1) * s + 1 === total.value) p -= 1
|
||||
if (type === OperatorEnum.sel) p = 1
|
||||
if (p < 1) p = 1
|
||||
|
||||
page.value = p
|
||||
sendLoad()
|
||||
}
|
||||
|
||||
defineExpose({ list, refreshData, reloadData })
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div>
|
||||
<Select class="mt-8" v-model="curSubJectId" placeholder="请选择学科">
|
||||
<Option v-for="item in subjectComp" :value="item.k">{{ item.v }}</Option>
|
||||
</Select>
|
||||
<MyTree v-if="!treeLoading" ref="refMyTree" :autoClick="false" :treeData="treeData" :setting="treeSetting"
|
||||
@on-tree-click="treeLink" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MyTree from '@/components/MyTree.vue'
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { cloneDeep, isEmpty } from "lodash";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import { get, last } from "lodash";
|
||||
import { requireImg } from "@/utils/hook/useImg";
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
}>()
|
||||
|
||||
const curTreeNode = defineModel<any>('curTreeNode', { default: {} })
|
||||
//学科学段
|
||||
const curSubjectInfo = defineModel<any>('curSubjectInfo', {
|
||||
default: {
|
||||
stage: null,
|
||||
subject: null
|
||||
}
|
||||
})
|
||||
|
||||
const store = useDefaultStore();
|
||||
const { $utils } = useCurrentInstance();
|
||||
|
||||
const refMyTree = useTemplateRef('refMyTree');
|
||||
function showIconForTree(treeId: any, treeNode: any) {
|
||||
return treeNode.level == 0;
|
||||
};
|
||||
const treeSetting = ref({
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
// showIcon: true,
|
||||
showIcon: showIconForTree
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: false,
|
||||
},
|
||||
check: {
|
||||
enable: false,
|
||||
},
|
||||
callback: {},
|
||||
})
|
||||
|
||||
|
||||
const curSubJectId = ref<number | null>(null);
|
||||
|
||||
const subjectComp = computed(() => {
|
||||
return store.getStageSubjects;
|
||||
})
|
||||
|
||||
const paramsComp = computed(() => {
|
||||
const find = subjectComp.value.find((item: any) => item.k == curSubJectId.value)
|
||||
if (!find) return null;
|
||||
else
|
||||
return {
|
||||
stage: find.stage,
|
||||
subject: find.code
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const curNodePath = computed(() => {
|
||||
if (!curTreeNode.value) return [];
|
||||
let arr: any[] = curTreeNode.value.getPath().map((x: any) => x.name)
|
||||
return arr;
|
||||
})
|
||||
|
||||
|
||||
|
||||
const { loading: treeLoading, data: treeData, send: sendReloadTree, onSuccess: onNodeTreeSuccess } = useRequest(
|
||||
() => Apis.NodeTree.getKnowledge({ params: paramsComp.value }),
|
||||
{
|
||||
initialData: [],
|
||||
immediate: false,
|
||||
})
|
||||
|
||||
watch(() => curSubJectId.value, async (val) => {
|
||||
await nextTick();
|
||||
curTreeNode.value = null;
|
||||
curSubjectInfo.value.stage = paramsComp.value?.stage;
|
||||
curSubjectInfo.value.subject = paramsComp.value?.subject;
|
||||
if (!!val)
|
||||
sessionStorage.setItem('bsch.learnmanage.cur_subject_id', val.toString())
|
||||
sendReloadTree(-1);
|
||||
})
|
||||
|
||||
onNodeTreeSuccess(() => reloadTree(-1))
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
console.log(subjectComp.value);
|
||||
|
||||
if (subjectComp.value.length > 0) {
|
||||
|
||||
let cacheStr = sessionStorage.getItem('bsch.learnmanage.cur_subject_id');
|
||||
if (!!cacheStr) {
|
||||
curSubJectId.value = Number(cacheStr);
|
||||
}
|
||||
else
|
||||
curSubJectId.value = subjectComp.value[0].k;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
async function reloadTree(id?: number) {
|
||||
await nextTick();
|
||||
function recursion(tree: any[], level: number): any[] {
|
||||
tree.forEach(element => {
|
||||
if (level == 1) {
|
||||
element.open = true;
|
||||
element.parent = true;
|
||||
element.isParent = true;
|
||||
element.icon = requireImg(`tree-icon-item.png`, "image");
|
||||
element.iconSkin = requireImg(`tree-icon-item.png`, "image");
|
||||
}
|
||||
else {
|
||||
element.icon = '';
|
||||
element.iconSkin = '';
|
||||
}
|
||||
|
||||
// element.icon = requireImg(`tree-icon-${element.isParent ? "group" : "item"}.png`, "image");
|
||||
|
||||
element.level = level;
|
||||
|
||||
if (!!element.children && element.children.length > 0)
|
||||
element.children = recursion(element.children, level + 1);
|
||||
else if (!element.isParent) {
|
||||
delete element.children;
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
treeData.value = recursion(treeData.value, 1);
|
||||
|
||||
|
||||
// treeData.value.forEach((n: any) => {
|
||||
// n.open = true;
|
||||
// n.icon = requireImg(`tree-icon-item.png`, "image");
|
||||
// if (!n.children || n.children.length == 0) {
|
||||
// n.parent = true;
|
||||
// n.children = [];
|
||||
// }
|
||||
// });
|
||||
|
||||
if (!!curTreeNode.value && !!curTreeNode.value.id)
|
||||
refMyTree.value?.reloadTree(curTreeNode.value.id)
|
||||
else
|
||||
refMyTree.value?.reloadTree(id)
|
||||
}
|
||||
|
||||
async function refreshMoveTree() {
|
||||
// const id = selectedNode.value.id;
|
||||
// await Apis.NodeTree.refreshSortTree({ params: { nodeId: id } })
|
||||
// Message.success('刷新成功!');
|
||||
// refTreeNode.value?.reloadTree()
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function treeLink(treeNode: any) {
|
||||
const copyNode = cloneDeep(treeNode)
|
||||
copyNode.children = [];
|
||||
curTreeNode.value = copyNode;
|
||||
console.log('click', treeNode.compositeId);
|
||||
console.log(copyNode.getPath().map((x: any) => x.name));
|
||||
}
|
||||
|
||||
async function onChangeSubject() { }
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.opera-ul {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 10px;
|
||||
|
||||
li {
|
||||
padding: 10px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
li.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#modal-sortTreeNode-id {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
height: 380px;
|
||||
|
||||
.tree-div {
|
||||
z-index: 1001;
|
||||
border: 1px solid #cacaca;
|
||||
border-radius: 5px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.ztree {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="opera-ul">
|
||||
<Tooltip content="新增节点" placement="top" transfer>
|
||||
<li @click="showAddNode" :class="{ disabled: !nodePermissions.canCreate }">
|
||||
<SvgIcon name="tree-icon-1" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip content="编辑节点" placement="top" transfer>
|
||||
<li @click="showEditNode" :class="{ disabled: !nodePermissions.canUpdate }">
|
||||
<SvgIcon name="tree-icon-2" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip content="移动节点" placement="top" transfer>
|
||||
<li @click="showMoveNode" :class="{ disabled: !nodePermissions.canMove }">
|
||||
<SvgIcon name="tree-icon-3" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="删除节点" placement="top" transfer>
|
||||
<li @click="showDeleteNode" :class="{ disabled: !nodePermissions.canDelete }">
|
||||
<SvgIcon name="tree-icon-4" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
<Select class="mt-8" v-model="curSubJectId" placeholder="请选择学科">
|
||||
<Option v-for="item in subjectComp" :value="item.k">{{ item.v }}</Option>
|
||||
</Select>
|
||||
<MyTree v-if="!treeLoading" ref="refMyTree" :autoClick="false" :treeData="treeData" :setting="treeSetting"
|
||||
@on-tree-click="treeLink" />
|
||||
|
||||
<div class="modal-wrap">
|
||||
<Modal v-model="editNodeModal.modal" :title="editNodeModal.title" width="450px" :mask-closable="false">
|
||||
<Form ref="refFormData" :label-width="100" :model="editNodeModal" class="myform mb-20 mr-20">
|
||||
<Form-Item label="当前位置:">
|
||||
{{ curNodePath.join(' > ') }}
|
||||
</Form-Item>
|
||||
<Form-Item label="节点名称:" prop="name"
|
||||
:rules="{ required: true, message: '请输入节点名称', trigger: 'blur' }">
|
||||
<Input ref="refNodeNameInput" maxlength="30" v-model="editNodeModal.name" />
|
||||
</Form-Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="editNodeModal.modal = false">取消</Button>
|
||||
<Button type="primary" :loading="editNodeModal.loading" @click="saveEditNode">确认</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<Modal id="modal-sortTreeNode-id" class-name="up-dowm-components" v-model="showMoveModalStatus" title="移动节点"
|
||||
:closable="false" :mask-closable="false" width="600">
|
||||
<div class="fix">
|
||||
<Alert class="fl">拖拽到目标节点时,确保节点是展开状态,以保证节点数据一致</Alert>
|
||||
<Button class="fr" @click="sendReloadTree()">刷新节点树</Button>
|
||||
</div>
|
||||
<MyTree class="tree-div" ref="refSortMyTree" :autoClick="false" :treeData="moveTreeData"
|
||||
:setting="moveTreeSetting" @on-tree-drag="onTreeDragMove" />
|
||||
|
||||
<template #footer>
|
||||
<Button @click="showMoveModalStatus = false" style="margin-left: 20px">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import MyTree from '@/components/MyTree.vue'
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { RootNode } from "@/types";
|
||||
import { cloneDeep, isEmpty } from "lodash";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import { get, last } from "lodash";
|
||||
|
||||
const props = defineProps<{
|
||||
rootType: RootNode;
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'on-change': [stage: number, subject: string, treeNode: any]
|
||||
}>()
|
||||
|
||||
|
||||
const curTreeNode = defineModel<any>('curTreeNode', { default: {} })
|
||||
//学科学段
|
||||
const curSubjectInfo = defineModel<any>('curSubjectInfo', {
|
||||
default: {
|
||||
stage: null,
|
||||
subject: null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const store = useDefaultStore();
|
||||
const { $utils } = useCurrentInstance();
|
||||
|
||||
const refMyTree = useTemplateRef('refMyTree');
|
||||
const refFormData = useTemplateRef('refFormData')
|
||||
const refNodeNameInput = useTemplateRef('refNodeNameInput')
|
||||
|
||||
|
||||
const treeSetting = ref({
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showIcon: false,
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: false,
|
||||
},
|
||||
check: {
|
||||
enable: false,
|
||||
},
|
||||
callback: {},
|
||||
})
|
||||
|
||||
const moveTreeSetting = ref({
|
||||
view: { dblClickExpand: false, showIcon: true },
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: {
|
||||
isMove: true,
|
||||
isCopy: false,
|
||||
showArrow: true,
|
||||
prev: true,
|
||||
next: true,
|
||||
inner: true,
|
||||
autoExpandTrigger: true
|
||||
}
|
||||
},
|
||||
check: {
|
||||
enable: false
|
||||
},
|
||||
callback: {}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
const showMoveModalStatus = ref(false)
|
||||
const editNodeModal = reactive({
|
||||
modal: false,
|
||||
title: '',
|
||||
loading: false,
|
||||
name: null,
|
||||
id: null,
|
||||
})
|
||||
|
||||
|
||||
const curSubJectId = ref<number | null>(null);
|
||||
|
||||
const subjectComp = computed(() => {
|
||||
return store.getStageSubjects;
|
||||
})
|
||||
|
||||
const paramsComp = computed(() => {
|
||||
const find = subjectComp.value.find((item: any) => item.k == curSubJectId.value)
|
||||
if (!find) return null;
|
||||
else
|
||||
return {
|
||||
rootType: props.rootType,
|
||||
stage: find.stage,
|
||||
subject: find.code
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const curNodePath = computed(() => {
|
||||
if (!curTreeNode.value) return [];
|
||||
let arr: any[] = curTreeNode.value.getPath().map((x: any) => x.name)
|
||||
return arr;
|
||||
})
|
||||
|
||||
const moveTreeData = computed(() => {
|
||||
function formatTree(list: any[]): any[] {
|
||||
return list
|
||||
.map(node => {
|
||||
const [, , canMove] = $utils.unpackPerms(node.permissions)
|
||||
if (!canMove) return null
|
||||
|
||||
const copy = cloneDeep(node)
|
||||
copy.drag = true
|
||||
copy.children = copy.children?.length
|
||||
? formatTree(copy.children)
|
||||
: null
|
||||
return copy
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
return formatTree(treeData.value)
|
||||
})
|
||||
|
||||
const { loading: treeLoading, data: treeData, send: sendReloadTree, onSuccess: onNodeTreeSuccess } = useRequest(
|
||||
() => Apis.NodeTree.getNodeTree({ params: paramsComp.value }),
|
||||
{
|
||||
initialData: [],
|
||||
immediate: false,
|
||||
})
|
||||
|
||||
watch(() => curSubJectId.value, async (val) => {
|
||||
await nextTick();
|
||||
curTreeNode.value = null;
|
||||
curSubjectInfo.value.stage = paramsComp.value?.stage;
|
||||
curSubjectInfo.value.subject = paramsComp.value?.subject;
|
||||
if (!!val)
|
||||
sessionStorage.setItem('bsch.learnmanage.cur_subject_id', val.toString())
|
||||
sendReloadTree(-1);
|
||||
})
|
||||
|
||||
onNodeTreeSuccess(() => reloadTree(-1))
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
|
||||
if (subjectComp.value.length > 0) {
|
||||
|
||||
let cacheStr = sessionStorage.getItem('bsch.learnmanage.cur_subject_id');
|
||||
if (!!cacheStr) {
|
||||
curSubJectId.value = Number(cacheStr);
|
||||
}
|
||||
else
|
||||
curSubJectId.value = subjectComp.value[0].k;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
async function reloadTree(id?: number) {
|
||||
await nextTick();
|
||||
console.log(id);
|
||||
|
||||
treeData.value.forEach((n: any) => (n.open = true));
|
||||
if (!!curTreeNode.value && !!curTreeNode.value.id)
|
||||
refMyTree.value?.reloadTree(curTreeNode.value.id)
|
||||
else
|
||||
refMyTree.value?.reloadTree(id)
|
||||
}
|
||||
|
||||
|
||||
function showAddNode() {
|
||||
if (!nodePermissions.value.canCreate) return;
|
||||
editNodeModal.title = '新增节点';
|
||||
editNodeModal.modal = true;
|
||||
editNodeModal.id = null;
|
||||
editNodeModal.name = null;
|
||||
nextTick().then(() => {
|
||||
refNodeNameInput.value?.focus()
|
||||
})
|
||||
}
|
||||
function showEditNode() {
|
||||
if (!nodePermissions.value.canUpdate) return;
|
||||
editNodeModal.title = '编辑节点';
|
||||
editNodeModal.modal = true;
|
||||
|
||||
editNodeModal.id = curTreeNode.value.id;
|
||||
editNodeModal.name = curTreeNode.value.name;
|
||||
nextTick().then(() => {
|
||||
refNodeNameInput.value.focus()
|
||||
})
|
||||
}
|
||||
function showMoveNode() {
|
||||
if (!nodePermissions.value.canMove) return;
|
||||
showMoveModalStatus.value = true;
|
||||
|
||||
}
|
||||
async function showDeleteNode() {
|
||||
if (!nodePermissions.value.canDelete) return;
|
||||
const temp = cloneDeep(curTreeNode.value);
|
||||
const parentNode = temp.getParentNode();
|
||||
Modal.confirm({
|
||||
title: '删除知识树节点',
|
||||
content: `确定删除名为【${curTreeNode.value.name}】的知识树节点记录?`,
|
||||
onOk: async () => {
|
||||
const id = curTreeNode.value.id;
|
||||
await Apis.NodeTree.deleteNode({ params: { nodeId: id } })
|
||||
Message.success('删除成功!');
|
||||
curTreeNode.value = parentNode;
|
||||
sendReloadTree()
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async function saveEditNode() {
|
||||
let isAdd = !!editNodeModal.id ? false : true;
|
||||
const validate = await refFormData.value.validate();
|
||||
if (!validate) {
|
||||
Message.error('请填写红色星号内容!');
|
||||
return;
|
||||
}
|
||||
const { id, name } = editNodeModal;
|
||||
editNodeModal.loading = true;
|
||||
if (isAdd) {
|
||||
const parentId = get(curTreeNode.value, 'id', -1)
|
||||
const brotherId = get(last(get(curTreeNode.value, 'children')), 'id', 0);
|
||||
const params: any = {
|
||||
rootType: props.rootType,
|
||||
name, parentId, brotherId,
|
||||
stage: paramsComp.value?.stage,
|
||||
subject: paramsComp.value?.subject
|
||||
};
|
||||
await Apis.NodeTree.addNode({ params })
|
||||
sendReloadTree()
|
||||
}
|
||||
else {
|
||||
await Apis.NodeTree.updateNodeName({ params: { name, nodeId: id } })
|
||||
|
||||
sendReloadTree();
|
||||
}
|
||||
|
||||
editNodeModal.loading = false;
|
||||
editNodeModal.modal = false;
|
||||
}
|
||||
|
||||
async function refreshMoveTree() {
|
||||
// const id = selectedNode.value.id;
|
||||
// await Apis.NodeTree.refreshSortTree({ params: { nodeId: id } })
|
||||
// Message.success('刷新成功!');
|
||||
// refTreeNode.value?.reloadTree()
|
||||
}
|
||||
//@ts-ignore
|
||||
async function onTreeDragMove({ treeNodes, targetNode, moveType }) {
|
||||
let mType = moveType == "inner" ? 3 : moveType == "prev" ? 1 : moveType == "next" ? 2 : 0;
|
||||
let currentNode = treeNodes[0];
|
||||
let nativeNode = currentNode;
|
||||
const params = formatMoveTree(treeNodes[0], targetNode, mType);
|
||||
await Apis.NodeTree.move({
|
||||
params: params,
|
||||
})
|
||||
sendReloadTree();
|
||||
}
|
||||
|
||||
function formatMoveTree(currentNode: any, targetNode: any, moveType: any) {
|
||||
|
||||
let nativeNode = currentNode;
|
||||
let treeNode = targetNode;
|
||||
let pathArr = treeNode.path.split(",");
|
||||
let index2 = pathArr.findIndex((ele: any) => {
|
||||
return ele == nativeNode.id;
|
||||
});
|
||||
if (index2 != -1) {
|
||||
Message.warning("父节点无法移动到子节点下方");
|
||||
return;
|
||||
}
|
||||
let newBrother = ""; //新兄长节点 【接口参数】
|
||||
let newParent = ""; //新父节点 【接口参数】
|
||||
|
||||
console.log('treeNode', treeNode);
|
||||
|
||||
var parentNode = treeNode.getParentNode();
|
||||
|
||||
let chilList = !!parentNode ? parentNode.children : treeNode.children;
|
||||
let index = chilList.findIndex((ele: any) => {
|
||||
return ele.id == treeNode.id;
|
||||
});
|
||||
if (treeNode.id == nativeNode.id || (treeNode.id == nativeNode.parentId && moveType == 3)) {
|
||||
//选中同一节点,无效移动
|
||||
Message.warning("目标节点与选中节点冲突,请选择其他目标节点");
|
||||
return;
|
||||
} else if ((moveType == 1 || moveType == 2) && nativeNode.parentId == treeNode.parentId) {
|
||||
if (moveType == 1) {
|
||||
console.log('idnex:' + index);
|
||||
index--;
|
||||
//前面
|
||||
newBrother = index == 0 ? 0 : chilList[index - 1].id;
|
||||
} else {
|
||||
//后面
|
||||
newBrother = chilList[index].id;
|
||||
}
|
||||
newParent = "";
|
||||
} else {
|
||||
//跨父节点移动
|
||||
if (moveType == 1) {
|
||||
//选择的是前面
|
||||
//前面
|
||||
newBrother = index == 0 ? index : chilList[index - 1].id;
|
||||
newParent = treeNode.parentId;
|
||||
} else if (moveType == 2) {
|
||||
//选择的是后面
|
||||
//后面
|
||||
newBrother = chilList[index].id;
|
||||
newParent = treeNode.parentId;
|
||||
} else {
|
||||
//选择的是里面
|
||||
//里面 选中的children里面的最后一个
|
||||
newBrother = isEmpty(treeNode.children)
|
||||
? 0
|
||||
: treeNode.children[treeNode.children.length - 1].id;
|
||||
newParent = treeNode.id;
|
||||
}
|
||||
}
|
||||
return {
|
||||
targetId: nativeNode.id,
|
||||
newParent: newParent,
|
||||
newBrother: newBrother,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function treeLink(treeNode: any) {
|
||||
const copyNode = cloneDeep(treeNode)
|
||||
copyNode.children = [];
|
||||
curTreeNode.value = copyNode;
|
||||
// console.log('click', treeNode);
|
||||
// console.log(copyNode.getPath().map((x: any) => x.name));
|
||||
|
||||
curSubjectInfo.value.stage = paramsComp.value?.stage;
|
||||
curSubjectInfo.value.subject = paramsComp.value?.subject;
|
||||
const { stage, subject } = curSubjectInfo.value;
|
||||
emit('on-change', stage, subject, copyNode)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const nodePermissions = computed(() => {
|
||||
if (!curTreeNode.value) return { canCreate: false, canUpdate: false, canMove: false, canDelete: false };
|
||||
const [canCreate, canUpdate, canMove, canDelete] = $utils.unpackPerms(curTreeNode.value.permissions)
|
||||
return {
|
||||
canCreate,
|
||||
canUpdate,
|
||||
canMove,
|
||||
canDelete,
|
||||
}
|
||||
})
|
||||
|
||||
async function onChangeSubject() { }
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.opera-ul {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 10px;
|
||||
|
||||
li {
|
||||
padding: 10px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
li.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#modal-sortTreeNode-id {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
height: 380px;
|
||||
|
||||
.tree-div {
|
||||
z-index: 1001;
|
||||
border: 1px solid #cacaca;
|
||||
border-radius: 5px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.ztree {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,469 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="opera-ul">
|
||||
<Tooltip content="新增节点" placement="top" transfer>
|
||||
<li @click="showAddNode" :class="{ disabled: !nodePermissions.canCreate }">
|
||||
<SvgIcon name="tree-icon-1" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip content="编辑节点" placement="top" transfer>
|
||||
<li @click="showEditNode" :class="{ disabled: !nodePermissions.canUpdate }">
|
||||
<SvgIcon name="tree-icon-2" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip content="移动节点" placement="top" transfer>
|
||||
<li @click="showMoveNode" :class="{ disabled: !nodePermissions.canMove }">
|
||||
<SvgIcon name="tree-icon-3" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="删除节点" placement="top" transfer>
|
||||
<li @click="showDeleteNode" :class="{ disabled: !nodePermissions.canDelete }">
|
||||
<SvgIcon name="tree-icon-4" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
<BschSubjectSelect class="mt-8" @on-change="handleSubjectChange" />
|
||||
|
||||
<MyTree v-if="!treeLoading" ref="refMyTree" :autoClick="false" :treeData="treeData" :setting="treeSetting"
|
||||
@on-tree-click="treeLink" />
|
||||
|
||||
<div class="modal-wrap">
|
||||
<Modal v-model="editNodeModal.modal" :title="editNodeModal.title" width="450px" :mask-closable="false">
|
||||
<Form ref="refFormData" :label-width="100" :model="editNodeModal" class="myform mb-20 mr-20">
|
||||
<Form-Item label="当前位置:">
|
||||
{{ curNodePath.join(' > ') }}
|
||||
</Form-Item>
|
||||
<Form-Item label="节点名称:" prop="name"
|
||||
:rules="{ required: true, message: '请输入节点名称', trigger: 'blur' }">
|
||||
<Input ref="refNodeNameInput" maxlength="30" v-model="editNodeModal.name" />
|
||||
</Form-Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="editNodeModal.modal = false">取消</Button>
|
||||
<Button type="primary" :loading="editNodeModal.loading" @click="saveEditNode">确认</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<Modal id="modal-sortTreeNode-id" class-name="up-dowm-components" v-model="showMoveModalStatus" title="移动节点"
|
||||
:closable="false" :mask-closable="false" width="600">
|
||||
<div class="fix">
|
||||
<Alert class="fl">拖拽到目标节点时,确保节点是展开状态,以保证节点数据一致</Alert>
|
||||
<Button class="fr" @click="sendReloadTree()">刷新节点树</Button>
|
||||
</div>
|
||||
<MyTree class="tree-div" ref="refSortMyTree" :autoClick="false" :treeData="moveTreeData"
|
||||
:setting="moveTreeSetting" @on-tree-drag="onTreeDragMove" />
|
||||
|
||||
<template #footer>
|
||||
<Button @click="showMoveModalStatus = false" style="margin-left: 20px">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import MyTree from '@/components/MyTree.vue'
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { RootNode } from "@/types";
|
||||
import { cloneDeep, isEmpty } from "lodash";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import BschSubjectSelect from '@/components/BschSubjectSelect.vue'
|
||||
import { get, last } from "lodash";
|
||||
|
||||
const props = defineProps<{
|
||||
rootType: RootNode;
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'on-change': [stage: number, subject: string, treeNode: any]
|
||||
}>()
|
||||
|
||||
|
||||
const curTreeNode = defineModel<any>('curTreeNode', { default: {} })
|
||||
//学科学段
|
||||
const curSubjectInfo = defineModel<any>('curSubjectInfo', {
|
||||
default: {
|
||||
stage: null,
|
||||
subject: null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const store = useDefaultStore();
|
||||
const { $utils } = useCurrentInstance();
|
||||
|
||||
const refMyTree = useTemplateRef('refMyTree');
|
||||
const refFormData = useTemplateRef('refFormData')
|
||||
const refNodeNameInput = useTemplateRef('refNodeNameInput')
|
||||
|
||||
|
||||
const treeSetting = ref({
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showIcon: false,
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: false,
|
||||
},
|
||||
check: {
|
||||
enable: false,
|
||||
},
|
||||
callback: {},
|
||||
})
|
||||
|
||||
const moveTreeSetting = ref({
|
||||
view: { dblClickExpand: false, showIcon: true },
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: {
|
||||
isMove: true,
|
||||
isCopy: false,
|
||||
showArrow: true,
|
||||
prev: true,
|
||||
next: true,
|
||||
inner: true,
|
||||
autoExpandTrigger: true
|
||||
}
|
||||
},
|
||||
check: {
|
||||
enable: false
|
||||
},
|
||||
callback: {}
|
||||
})
|
||||
|
||||
|
||||
const showMoveModalStatus = ref(false)
|
||||
const editNodeModal = reactive({
|
||||
modal: false,
|
||||
title: '',
|
||||
loading: false,
|
||||
name: null,
|
||||
id: null,
|
||||
})
|
||||
|
||||
const curSubject = reactive({
|
||||
subject: '',
|
||||
stage: 0,
|
||||
})
|
||||
|
||||
|
||||
const paramsComp = computed(() => {
|
||||
return {
|
||||
rootType: props.rootType,
|
||||
stage: curSubject.stage,
|
||||
subject: curSubject.subject
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const curNodePath = computed(() => {
|
||||
if (!curTreeNode.value) return [];
|
||||
let arr: any[] = curTreeNode.value.getPath().map((x: any) => x.name)
|
||||
return arr;
|
||||
})
|
||||
|
||||
const moveTreeData = computed(() => {
|
||||
function formatTree(list: any[]): any[] {
|
||||
return list
|
||||
.map(node => {
|
||||
const [, , canMove] = $utils.unpackPerms(node.permissions)
|
||||
if (!canMove) return null
|
||||
|
||||
const copy = cloneDeep(node)
|
||||
copy.drag = true
|
||||
copy.children = copy.children?.length
|
||||
? formatTree(copy.children)
|
||||
: null
|
||||
return copy
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
return formatTree(treeData.value)
|
||||
})
|
||||
|
||||
const { loading: treeLoading, data: treeData, send: sendReloadTree, onSuccess: onNodeTreeSuccess } = useRequest(
|
||||
() => Apis.NodeTree.getNodeTree({ params: paramsComp.value }),
|
||||
{
|
||||
initialData: [],
|
||||
immediate: false,
|
||||
})
|
||||
|
||||
|
||||
onNodeTreeSuccess(() => reloadTree(-props.rootType))
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
async function reloadTree(id?: number) {
|
||||
await nextTick();
|
||||
console.log(id);
|
||||
refMyTree.value?.reloadTree(id)
|
||||
// treeData.value.forEach((n: any) => (n.open = true));
|
||||
// if (!!curTreeNode.value && !!curTreeNode.value.id)
|
||||
// refMyTree.value?.reloadTree(curTreeNode.value.id)
|
||||
// else
|
||||
// refMyTree.value?.reloadTree(id)
|
||||
}
|
||||
|
||||
|
||||
function showAddNode() {
|
||||
if (!nodePermissions.value.canCreate) return;
|
||||
editNodeModal.title = '新增节点';
|
||||
editNodeModal.modal = true;
|
||||
editNodeModal.id = null;
|
||||
editNodeModal.name = null;
|
||||
nextTick().then(() => {
|
||||
refNodeNameInput.value?.focus()
|
||||
})
|
||||
}
|
||||
function showEditNode() {
|
||||
if (!nodePermissions.value.canUpdate) return;
|
||||
editNodeModal.title = '编辑节点';
|
||||
editNodeModal.modal = true;
|
||||
|
||||
editNodeModal.id = curTreeNode.value.id;
|
||||
editNodeModal.name = curTreeNode.value.name;
|
||||
nextTick().then(() => {
|
||||
refNodeNameInput.value.focus()
|
||||
})
|
||||
}
|
||||
function showMoveNode() {
|
||||
if (!nodePermissions.value.canMove) return;
|
||||
showMoveModalStatus.value = true;
|
||||
|
||||
}
|
||||
async function showDeleteNode() {
|
||||
if (!nodePermissions.value.canDelete) return;
|
||||
const temp = cloneDeep(curTreeNode.value);
|
||||
const parentNode = temp.getParentNode();
|
||||
Modal.confirm({
|
||||
title: '删除知识树节点',
|
||||
content: `确定删除名为【${curTreeNode.value.name}】的知识树节点记录?`,
|
||||
onOk: async () => {
|
||||
const id = curTreeNode.value.id;
|
||||
await Apis.NodeTree.deleteNode({ params: { nodeId: id } })
|
||||
Message.success('删除成功!');
|
||||
curTreeNode.value = parentNode;
|
||||
sendReloadTree()
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async function saveEditNode() {
|
||||
let isAdd = !!editNodeModal.id ? false : true;
|
||||
const validate = await refFormData.value.validate();
|
||||
if (!validate) {
|
||||
Message.error('请填写红色星号内容!');
|
||||
return;
|
||||
}
|
||||
const { id, name } = editNodeModal;
|
||||
editNodeModal.loading = true;
|
||||
if (isAdd) {
|
||||
const parentId = get(curTreeNode.value, 'id', -1)
|
||||
const brotherId = get(last(get(curTreeNode.value, 'children')), 'id', 0);
|
||||
const params: any = {
|
||||
rootType: props.rootType,
|
||||
name, parentId, brotherId,
|
||||
stage: paramsComp.value?.stage,
|
||||
subject: paramsComp.value?.subject
|
||||
};
|
||||
await Apis.NodeTree.addNode({ params })
|
||||
sendReloadTree()
|
||||
}
|
||||
else {
|
||||
await Apis.NodeTree.updateNodeName({ params: { name, nodeId: id } })
|
||||
|
||||
sendReloadTree();
|
||||
}
|
||||
|
||||
editNodeModal.loading = false;
|
||||
editNodeModal.modal = false;
|
||||
}
|
||||
|
||||
async function refreshMoveTree() {
|
||||
// const id = selectedNode.value.id;
|
||||
// await Apis.NodeTree.refreshSortTree({ params: { nodeId: id } })
|
||||
// Message.success('刷新成功!');
|
||||
// refTreeNode.value?.reloadTree()
|
||||
}
|
||||
//@ts-ignore
|
||||
async function onTreeDragMove({ treeNodes, targetNode, moveType }) {
|
||||
let mType = moveType == "inner" ? 3 : moveType == "prev" ? 1 : moveType == "next" ? 2 : 0;
|
||||
let currentNode = treeNodes[0];
|
||||
let nativeNode = currentNode;
|
||||
const params = formatMoveTree(treeNodes[0], targetNode, mType);
|
||||
await Apis.NodeTree.move({
|
||||
params: params,
|
||||
})
|
||||
sendReloadTree();
|
||||
}
|
||||
|
||||
function formatMoveTree(currentNode: any, targetNode: any, moveType: any) {
|
||||
|
||||
let nativeNode = currentNode;
|
||||
let treeNode = targetNode;
|
||||
let pathArr = treeNode.path.split(",");
|
||||
let index2 = pathArr.findIndex((ele: any) => {
|
||||
return ele == nativeNode.id;
|
||||
});
|
||||
if (index2 != -1) {
|
||||
Message.warning("父节点无法移动到子节点下方");
|
||||
return;
|
||||
}
|
||||
let newBrother = ""; //新兄长节点 【接口参数】
|
||||
let newParent = ""; //新父节点 【接口参数】
|
||||
|
||||
console.log('treeNode', treeNode);
|
||||
|
||||
var parentNode = treeNode.getParentNode();
|
||||
|
||||
let chilList = !!parentNode ? parentNode.children : treeNode.children;
|
||||
let index = chilList.findIndex((ele: any) => {
|
||||
return ele.id == treeNode.id;
|
||||
});
|
||||
if (treeNode.id == nativeNode.id || (treeNode.id == nativeNode.parentId && moveType == 3)) {
|
||||
//选中同一节点,无效移动
|
||||
Message.warning("目标节点与选中节点冲突,请选择其他目标节点");
|
||||
return;
|
||||
} else if ((moveType == 1 || moveType == 2) && nativeNode.parentId == treeNode.parentId) {
|
||||
if (moveType == 1) {
|
||||
console.log('idnex:' + index);
|
||||
index--;
|
||||
//前面
|
||||
newBrother = index == 0 ? 0 : chilList[index - 1].id;
|
||||
} else {
|
||||
//后面
|
||||
newBrother = chilList[index].id;
|
||||
}
|
||||
newParent = "";
|
||||
} else {
|
||||
//跨父节点移动
|
||||
if (moveType == 1) {
|
||||
//选择的是前面
|
||||
//前面
|
||||
newBrother = index == 0 ? index : chilList[index - 1].id;
|
||||
newParent = treeNode.parentId;
|
||||
} else if (moveType == 2) {
|
||||
//选择的是后面
|
||||
//后面
|
||||
newBrother = chilList[index].id;
|
||||
newParent = treeNode.parentId;
|
||||
} else {
|
||||
//选择的是里面
|
||||
//里面 选中的children里面的最后一个
|
||||
newBrother = isEmpty(treeNode.children)
|
||||
? 0
|
||||
: treeNode.children[treeNode.children.length - 1].id;
|
||||
newParent = treeNode.id;
|
||||
}
|
||||
}
|
||||
return {
|
||||
targetId: nativeNode.id,
|
||||
newParent: newParent,
|
||||
newBrother: newBrother,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function treeLink(treeNode: any) {
|
||||
const copyNode = cloneDeep(treeNode)
|
||||
copyNode.children = [];
|
||||
curTreeNode.value = copyNode;
|
||||
// console.log('click', treeNode);
|
||||
// console.log(copyNode.getPath().map((x: any) => x.name));
|
||||
|
||||
curSubjectInfo.value.stage = paramsComp.value?.stage;
|
||||
curSubjectInfo.value.subject = paramsComp.value?.subject;
|
||||
const { stage, subject } = curSubjectInfo.value;
|
||||
emit('on-change', stage, subject, copyNode)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const nodePermissions = computed(() => {
|
||||
if (!curTreeNode.value) return { canCreate: false, canUpdate: false, canMove: false, canDelete: false };
|
||||
const [canCreate, canUpdate, canMove, canDelete] = $utils.unpackPerms(curTreeNode.value.permissions)
|
||||
return {
|
||||
canCreate,
|
||||
canUpdate,
|
||||
canMove,
|
||||
canDelete,
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSubjectChange(stage: number, subject: string) {
|
||||
curSubject.stage = stage;
|
||||
curSubject.subject = subject;
|
||||
await nextTick();
|
||||
sendReloadTree();
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.opera-ul {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 10px;
|
||||
|
||||
li {
|
||||
padding: 10px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
li.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#modal-sortTreeNode-id {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
height: 380px;
|
||||
|
||||
.tree-div {
|
||||
z-index: 1001;
|
||||
border: 1px solid #cacaca;
|
||||
border-radius: 5px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.ztree {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="type_set-box">
|
||||
<div class="type_name">
|
||||
<i class="iconfont icon-stop fl mr-10" v-if="!isEdit && !active" style="color: #ff5959"></i>
|
||||
<Input v-if="isEdit" v-model="type.name" ref="type_input" @on-blur="update" @on-enter="update" />
|
||||
<span v-else class="fl">{{ type.name }}</span>
|
||||
</div>
|
||||
<div class="type_action flex">
|
||||
<div class="action_item" @click="open">
|
||||
<span v-if="active">停用</span>
|
||||
<span v-else>启用</span>
|
||||
</div>
|
||||
<div class="action_edit" @click="edit">
|
||||
<span>编辑</span>
|
||||
</div>
|
||||
<div class="action_delete" @click="remove">
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="divider2"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TypeSetBox",
|
||||
props: {
|
||||
type: {
|
||||
name: { type: String, default: "" },
|
||||
id: { type: String, default: "" },
|
||||
},
|
||||
active: { type: Boolean, default: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEdit: false,
|
||||
eventType: "",
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
type: {
|
||||
deep: true,
|
||||
handler(newVal) {
|
||||
if (this.isEdit) {
|
||||
this.isEdit = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
"type.name"(newVal) {
|
||||
if (this.isEdit) {
|
||||
this.isEdit = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.$emit("active", this.type.id);
|
||||
},
|
||||
edit() {
|
||||
this.isEdit = !this.isEdit;
|
||||
if (this.isEdit) this.$nextTick(() => this.$refs["type_input"].focus());
|
||||
},
|
||||
update(e) {
|
||||
if (!this.type.name) {
|
||||
this.$Message.error("值不得为空!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.eventType == "") {
|
||||
this.eventType = e.type;
|
||||
this.isEdit = false;
|
||||
this.$emit("edit", this.type.id, this.type.name);
|
||||
setTimeout(() => {
|
||||
this.eventType = "";
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
remove() {
|
||||
this.$emit("remove", this.type.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.type_set-box {
|
||||
width: 222px;
|
||||
background: #ffffff;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 15px;
|
||||
.type_name {
|
||||
height: 66px;
|
||||
border: 1px solid #e9e9e9;
|
||||
border-bottom: none;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
line-height: 66px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
span {
|
||||
height: 24px;
|
||||
font-size: 15px;
|
||||
font-family: PingFangSC-Semibold, PingFang SC;
|
||||
font-weight: 500;
|
||||
color: #272829;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 222px;
|
||||
display: inline-block;
|
||||
}
|
||||
.iconfont {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
.type_action {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 222px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
background: #f7f7f7;
|
||||
border-radius: 0px 0px 2px 2px;
|
||||
border-top: none;
|
||||
border: 1px solid #e9e9e9;
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
div {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
div:hover {
|
||||
color: #6c91f0;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
// position: relative;
|
||||
height: 14px;
|
||||
width: 1px;
|
||||
background: #e8e8e8;
|
||||
// left: 73px;
|
||||
// top: -23px;
|
||||
float: left;
|
||||
margin-left: 71px;
|
||||
margin-top: -22px;
|
||||
}
|
||||
.divider2 {
|
||||
// position: relative;
|
||||
// left: 146px;
|
||||
// top: -37px;
|
||||
height: 14px;
|
||||
width: 1px;
|
||||
background: #e8e8e8;
|
||||
float: left;
|
||||
margin-left: 147px;
|
||||
margin-top: -22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="improt-wrap">
|
||||
<slot></slot>
|
||||
<div class="upload-panel">
|
||||
<Upload type="drag" :on-success="onSuccess" :on-remove="onRemove" :on-error="onError"
|
||||
:before-upload="handleUploadBefore" :headers="headers" :data="uploadData" :action="uploadUrl"
|
||||
:with-credentials="true" :max-size="maxSize" :multiple="multiple">
|
||||
<div class="upload-box">
|
||||
<Icon type="ios-paper" size="52" style="color: #6281eb"></Icon>
|
||||
<h4>点击或将文件拖拽到这里上传</h4>
|
||||
<div class="info">文件大小不超过{{ $utils.getFileSize(maxSize) }}</div>
|
||||
</div>
|
||||
<Alert v-if="!!errorMsg" type="error" show-icon><span v-html="errorMsg"></span></Alert>
|
||||
</Upload>
|
||||
</div>
|
||||
<!-- <slot name="footer"></slot> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
model: {
|
||||
event: "update:fileList",
|
||||
prop: "fileList",
|
||||
},
|
||||
props: {
|
||||
fileList: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
uploadUrl: String,
|
||||
uploadData: Object,
|
||||
headers: Object,
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
maxSize: 200 * 1024 * 1024,
|
||||
errorMsg: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleUploadBefore(file) {
|
||||
var _this = this;
|
||||
if (file.size > _this.maxSize) {
|
||||
_this.$Message.error({
|
||||
content: "请上传小于" + _this.$utils.getFileSize(_this.maxSize) + "的文件!",
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
},
|
||||
onSuccess(res, file) {
|
||||
if (res.code == 0) {
|
||||
this.errorMsg = null;
|
||||
const { uid } = file;
|
||||
this.fileList.push({ ...res.data, uid });
|
||||
this.$emit("update:fileList", this.fileList);
|
||||
} else {
|
||||
let msg = res.data.error;
|
||||
this.errorMsg = msg;
|
||||
}
|
||||
},
|
||||
onRemove(file, fileList) {
|
||||
const { uid } = file;
|
||||
const findIndex = this.fileList.findIndex(item => item.uid === uid);
|
||||
this.fileList.splice(findIndex, 1);
|
||||
this.$emit("update:fileList", this.fileList);
|
||||
console.log(file, fileList);
|
||||
},
|
||||
onError(err, file) {
|
||||
this.errorMsg = JSON.stringify(err);
|
||||
console.log(err, file);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.improt-wrap {
|
||||
padding: 0 12px;
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-panel {
|
||||
.upload-box {
|
||||
background: #f7f8fc;
|
||||
padding: 30px 0;
|
||||
|
||||
h4 {
|
||||
color: #505c7e;
|
||||
margin: 3px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="public-upload-img fix">
|
||||
<li v-for="(item, index) in uploadList">
|
||||
<template v-if="item.status === 'finished'">
|
||||
<img :src="item.url" alt="" />
|
||||
<div class="upload-list-cover">
|
||||
<Icon
|
||||
type="ios-eye-outline"
|
||||
size="24"
|
||||
title="预览图片"
|
||||
@click.native="handleView(item.url)"
|
||||
></Icon>
|
||||
<Icon
|
||||
size="24"
|
||||
class="ml-10"
|
||||
title="删除图片"
|
||||
type="ios-trash-outline"
|
||||
@click.native="handleRemove(index)"
|
||||
></Icon>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Progress v-if="item.showProgress" :percent="item.percentage" class="mt-24" hide-info></Progress>
|
||||
</template>
|
||||
</li>
|
||||
<li class="add-li" v-show="uploadList.length < uploadCount">
|
||||
<Upload
|
||||
ref="upload"
|
||||
:show-upload-list="false"
|
||||
:on-success="handleSuccess"
|
||||
:format="['jpg', 'jpeg', 'png', 'gif']"
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png"
|
||||
:max-size="20480"
|
||||
:on-format-error="handleFormatError"
|
||||
:on-exceeded-size="handleMaxSize"
|
||||
:default-file-list="defaultList"
|
||||
:headers="uploadHeader"
|
||||
type="drag"
|
||||
:action="uploadUrl"
|
||||
:data="data"
|
||||
>
|
||||
<div class="add-box">
|
||||
<Icon type="ios-add" size="50"></Icon>
|
||||
</div>
|
||||
</Upload>
|
||||
</li>
|
||||
</ul>
|
||||
<Modal title="图片预览" v-model="visible" footer-hide>
|
||||
<img :src="imgName" v-if="visible" style="width: 100%" />
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
imgList: { type: Array, default: [] },
|
||||
uploadUrl: { type: String, default: "" },
|
||||
uploadCount: { type: Number, default: 1 },
|
||||
data: { type: Object, default: null },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imgName: "",
|
||||
visible: false,
|
||||
uploadList: [],
|
||||
defaultList: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.setDefaultList();
|
||||
this.$watch("imgList", val => {
|
||||
this.setDefaultList();
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
console.log(this.$refs.upload.fileList);
|
||||
this.uploadList = this.$refs.upload.fileList;
|
||||
},
|
||||
computed: {
|
||||
uploadHeader() {
|
||||
const bschUtil = BschLoadUtil.getInstance();
|
||||
if (!bschUtil.getIsSelf()) {
|
||||
return {
|
||||
ticket: sessionStorage.getItem("bschTicket"),
|
||||
platform: "grzx",
|
||||
};
|
||||
} else return null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleView(name) {
|
||||
this.imgName = name;
|
||||
this.visible = true;
|
||||
},
|
||||
handleRemove(index) {
|
||||
this.$refs.upload.fileList.splice(index, 1);
|
||||
this.imgList.splice(index, 1);
|
||||
},
|
||||
handleSuccess(res, file) {
|
||||
console.log(res, file);
|
||||
file.url = res.data.fileurl;
|
||||
this.imgList.push(res.data.fileurl);
|
||||
},
|
||||
handleFormatError(file) {
|
||||
this.$Notice.info({
|
||||
title: "不支持的文件类型!",
|
||||
desc: "请上传指定的文件类型",
|
||||
});
|
||||
},
|
||||
handleMaxSize(file) {
|
||||
this.$Notice.warning({
|
||||
title: "图片过大",
|
||||
desc: "图片最大上传大小为20M.",
|
||||
});
|
||||
},
|
||||
setDefaultList() {
|
||||
let arr = [];
|
||||
this.imgList.forEach(element => {
|
||||
arr.push({
|
||||
name: "",
|
||||
url: element,
|
||||
});
|
||||
});
|
||||
this.defaultList = arr;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<!-- 通知对象选择:全部用户 / 指定用户(自旧前端 user_check_input.vue 适配) -->
|
||||
<div class="check-user-wrap">
|
||||
<div class="check-user-box">
|
||||
<RadioGroup v-model="isCheckUserAll" class="radio" @on-change="userAllCheck">
|
||||
<Radio :disabled="editDisabled" :label="0">{{ isEdit ? "现有" : "全部" }}用户</Radio>
|
||||
<Radio :label="1">指定用户</Radio>
|
||||
</RadioGroup>
|
||||
<Input
|
||||
type="text"
|
||||
v-model="userNameStr"
|
||||
readonly
|
||||
:disabled="!isCheckUserAll"
|
||||
placeholder="选择指定的用户"
|
||||
class="input"
|
||||
></Input>
|
||||
<Button icon="md-add" @click="checkUserShow" :disabled="!isCheckUserAll"></Button>
|
||||
<Button icon="md-trash" @click="resetUser" :disabled="!isCheckUserAll"></Button>
|
||||
</div>
|
||||
<UserCheckModal
|
||||
ref="userCheckModal"
|
||||
:sources="sources"
|
||||
:userids="tempUserIds"
|
||||
@on-submit="submitUser"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* 通知对象选择输入(自旧前端 oldfront/src/components/user_check_input.vue 适配 Vue3)。
|
||||
* v-model 值为 "all" 或用户 id 逗号串;切换到「指定用户」直接弹出选择用户弹窗。
|
||||
*/
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { baseApi, settingApi } from "@/api/module/notice";
|
||||
|
||||
export default {
|
||||
name: "UserCheckInput",
|
||||
components: { UserCheckModal },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "all",
|
||||
},
|
||||
/** 编辑态:全部用户选项禁用为「现有用户」 */
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
isCheckUserAll: 0 as number,
|
||||
tempUserIds: [] as any[],
|
||||
userNameStr: "",
|
||||
editDisabled: false,
|
||||
sources: [
|
||||
{
|
||||
name: "基础平台",
|
||||
loader: () => baseApi.getSchoolAll().send() as Promise<any[]>,
|
||||
},
|
||||
{
|
||||
name: "自定义组",
|
||||
loader: () => settingApi.getGroupTree().send() as Promise<any[]>,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.modelValue === "all") {
|
||||
this.isCheckUserAll = 0;
|
||||
this.userNameStr = this.isEdit ? "现有用户" : "全部用户";
|
||||
} else {
|
||||
this.isCheckUserAll = 1;
|
||||
this.tempUserIds = this.modelValue.split(",").map((x) => parseInt(x)).filter((x) => !isNaN(x));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkUserShow() {
|
||||
(this.$refs.userCheckModal as any).show();
|
||||
},
|
||||
resetUser() {
|
||||
//清除用户
|
||||
this.tempUserIds = [];
|
||||
this.userNameStr = "";
|
||||
this.$emit("update:modelValue", "");
|
||||
},
|
||||
userAllCheck(type: number) {
|
||||
let tempIds: any = String(this.modelValue || "")
|
||||
.split(",")
|
||||
.map((x) => parseInt(x))
|
||||
.filter((x) => !isNaN(x));
|
||||
if (type == 0) {
|
||||
if (this.isEdit) {
|
||||
this.tempUserIds = tempIds;
|
||||
this.userNameStr = "现有用户";
|
||||
this.$emit("update:modelValue", String(tempIds));
|
||||
} else {
|
||||
this.tempUserIds = [];
|
||||
this.userNameStr = "全部用户";
|
||||
this.$emit("update:modelValue", "all");
|
||||
}
|
||||
} else {
|
||||
if (this.isEdit) {
|
||||
this.editDisabled = true;
|
||||
}
|
||||
this.tempUserIds = tempIds;
|
||||
this.userNameStr = "";
|
||||
this.checkUserShow();
|
||||
}
|
||||
},
|
||||
submitUser(arr: any[]) {
|
||||
//确认用户
|
||||
this.userNameStr = arr.map((x) => x.name).join("、");
|
||||
const ids = arr.map((x) => x.id).join(",");
|
||||
this.$emit("update:modelValue", ids || "all");
|
||||
if (!arr.length) {
|
||||
// 清空后回到全部用户
|
||||
this.isCheckUserAll = 0;
|
||||
this.userNameStr = "全部用户";
|
||||
this.$emit("update:modelValue", "all");
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(val: string) {
|
||||
if (val === "all") {
|
||||
this.isCheckUserAll = 0;
|
||||
this.userNameStr = this.isEdit ? "现有用户" : "全部用户";
|
||||
} else if (val === "") {
|
||||
this.userNameStr = "";
|
||||
} else {
|
||||
// 编辑回显:指定用户 id 串
|
||||
this.isCheckUserAll = 1;
|
||||
this.tempUserIds = val
|
||||
.split(",")
|
||||
.map((x) => parseInt(x))
|
||||
.filter((x) => !isNaN(x));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.check-user-wrap {
|
||||
width: 100%;
|
||||
.check-user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.radio {
|
||||
width: 180px;
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
line-height: 32px;
|
||||
}
|
||||
.input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,496 @@
|
||||
<template>
|
||||
<Modal
|
||||
v-model="modal"
|
||||
title="选择用户"
|
||||
:width="showSelected ? '720px' : '420px'"
|
||||
:mask-closable="false"
|
||||
:closable="false"
|
||||
>
|
||||
<Tabs
|
||||
v-if="sources.length > 1"
|
||||
:key="tabKey"
|
||||
type="card"
|
||||
:value="String(tabIndex)"
|
||||
@on-click="tabChange"
|
||||
>
|
||||
<TabPane v-for="(item, index) in sources" :key="index" :label="item.name" :name="index + ''"></TabPane>
|
||||
</Tabs>
|
||||
<AutoComplete
|
||||
@on-search="handleSearch"
|
||||
@on-select="handleSelect"
|
||||
placeholder="输入名字搜索"
|
||||
v-model="searchTreeName"
|
||||
:clearable="true"
|
||||
class="auto-box"
|
||||
>
|
||||
<Option v-for="item in searchResult" :key="item.id" :value="item.name">
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="fr info">{{ item.group }}</span>
|
||||
</Option>
|
||||
</AutoComplete>
|
||||
<div class="tree-wrap fix">
|
||||
<div class="tree-panel fl" :class="{ 'tree-panel--full': !showSelected }">
|
||||
<div class="tree-box">
|
||||
<div :id="ztreeDomId" class="ztree"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow-box fl" v-if="showSelected">
|
||||
<svg
|
||||
t="1626916477711"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="36"
|
||||
height="36"
|
||||
>
|
||||
<path
|
||||
d="M138.935 256.078v511.844h63.42V256.078h-63.42z m126.842 0v511.844h127.215V256.078H265.777zM885.064 512L583.254 64.323v191.755H456.414v511.844h126.842v191.755L885.064 512z"
|
||||
fill="#2d8cf0"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-panel fl" v-if="showSelected">
|
||||
<div class="title-box fix">
|
||||
<Checkbox @on-change="checkAllUser" v-model="isCheckUserAll">全选</Checkbox>
|
||||
<div class="fr">{{ tempUserList.length }}</div>
|
||||
<a
|
||||
href="javascript:;"
|
||||
@click="delCheckUser"
|
||||
v-if="tempUserList.findIndex((x: any) => x.isCheck) > -1"
|
||||
>删除选中</a
|
||||
>
|
||||
</div>
|
||||
<ul class="user-box">
|
||||
<li
|
||||
v-for="item in tempUserList"
|
||||
:key="item.id"
|
||||
:class="{ active: item.isCheck }"
|
||||
@click="checkUserOne(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<span class="info">{{ item.group }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button type="primary" @click="submit">提交</Button>
|
||||
<Button @click="modal = false" style="margin-left: 8px">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* 选择用户弹窗(自旧前端 oldfront/src/components/userCheck.vue 适配 Vue3)。
|
||||
* 左侧为带复选框的组织树(部门→人员,叶子=人员),勾选/取消按父子级联;
|
||||
* 右侧为已选人员列表,支持全选/删除选中;提交返回去重后的用户 id 数组与用户对象数组。
|
||||
*/
|
||||
import { Message } from "view-ui-plus";
|
||||
import { requireImg } from "@/utils/hook/useImg";
|
||||
|
||||
interface SelectSource {
|
||||
name: string;
|
||||
loader: () => Promise<any[]>;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "UserCheckModal",
|
||||
props: {
|
||||
/** 数据源 tab:{ name, loader },loader 返回 ztree 树数据(children 嵌套,叶子=人员) */
|
||||
sources: {
|
||||
type: Array as () => SelectSource[],
|
||||
default: () => [],
|
||||
},
|
||||
/** 已选用户 id(打开时自动勾选) */
|
||||
userids: {
|
||||
type: Array as () => number[],
|
||||
default: () => [],
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 是否显示右侧「已选列表」栏(含箭头/全选/计数)。false=仅保留左侧组织树(勾选即所选) */
|
||||
showSelected: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/** 多选模式(默认 true)。false=单选:勾中新叶子自动取消其他已选,组节点不可勾(预留能力,现调用方均为多选) */
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:userids", "on-submit"],
|
||||
data() {
|
||||
return {
|
||||
ztreeDomId: "user_check_tree_" + Math.floor(Math.random() * 1e8),
|
||||
modal: false,
|
||||
tabIndex: 0,
|
||||
/** 每次打开弹窗自增,强制重建 Tabs,避免上次 tab 高亮残留与数据错位 */
|
||||
tabKey: 0,
|
||||
/** 树加载序号:tab 快速切换时只渲染最后一次请求的结果 */
|
||||
treeSeq: 0,
|
||||
isCheckUserAll: false,
|
||||
cacheMap: new Map(),
|
||||
currentTree: [] as any[],
|
||||
searchTreeName: "",
|
||||
searchResult: [] as any[],
|
||||
tempUserList: [] as any[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** 当前树扁平化(含父级名称,供搜索展示;children 可能为 null,统一兜底) */
|
||||
currentTreeFlatten(): any[] {
|
||||
const flatten = (data: any[], pid: number, pname: string): any[] =>
|
||||
(data || []).reduce(
|
||||
(arr: any[], { id, name, children, type }: any) =>
|
||||
arr.concat([{ id, name, pid, type, group: pname }], flatten(children || [], id, name)),
|
||||
[]
|
||||
);
|
||||
return flatten(this.currentTree, 0, "");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.modal = true;
|
||||
// 重置到第一个 tab:显式赋值 + key 重建 Tabs,保证高亮与数据一致
|
||||
this.tabIndex = 0;
|
||||
this.tabKey++;
|
||||
this.changeEmpty();
|
||||
this.initTree();
|
||||
},
|
||||
tabChange(index: string) {
|
||||
const idx = Number(index);
|
||||
// 严格比较:v-model 场景下 iview 会把值写成字符串 name,
|
||||
// 宽松相等("1" == 1)会误判“未切换”导致 tab 永远不生效
|
||||
if (idx !== 0 && this.tabIndex === idx) return;
|
||||
this.tabIndex = idx;
|
||||
this.changeEmpty();
|
||||
this.initTree();
|
||||
},
|
||||
handleSearch(query: string) {
|
||||
let arr: any[] = [];
|
||||
const searchArr = this.currentTreeFlatten.filter(
|
||||
(x: any) => x.type == 2 && x.name.indexOf(query) != -1
|
||||
);
|
||||
for (const item of searchArr) {
|
||||
!arr
|
||||
.map((x) => this.getSplitId(x.id))
|
||||
.includes(this.getSplitId(item.id)) && arr.push(item);
|
||||
}
|
||||
this.searchResult = arr;
|
||||
},
|
||||
handleSelect(query: string) {
|
||||
if (query.indexOf(",") != -1) query = query.split(",")[0];
|
||||
var treeObj = ($.fn as any).zTree.getZTreeObj(this.ztreeDomId);
|
||||
var node = treeObj.getNodeByParam("name", query, null);
|
||||
if (!!node) treeObj.checkNode(node, true, true, true);
|
||||
},
|
||||
/**
|
||||
* ztree 数据预处理:拼接 pid 保证同人多组时 id 可区分;叶子标记 type=2。
|
||||
* 组/用户判定优先用接口 isParent(组=文件夹),缺失时按有无子节点兜底;
|
||||
* 并设置组/用户区分图标。
|
||||
* 注意:组织树接口(base/getStuTree、getTeaTree)的人员叶子 children 显式为 null
|
||||
* (SchTreeNodeDto「叶子为 null」+ Jackson 默认输出 null 字段),必须先归一为数组,
|
||||
* 否则 `typeof null !== "undefined"` 会放行并在 `.length` 处抛错导致整棵树建树失败。
|
||||
*/
|
||||
formatTree(tree: any[], level = 0, pid: number = 0) {
|
||||
tree.forEach((element: any) => {
|
||||
element.open = level == 0;
|
||||
element.pid = pid;
|
||||
element.id = element.id + "," + pid;
|
||||
element.checked = false;
|
||||
const kids = Array.isArray(element.children) ? element.children : [];
|
||||
if (typeof element.isParent !== "undefined") {
|
||||
element.type = element.isParent ? 1 : 2;
|
||||
} else {
|
||||
element.type = kids.length > 0 ? 1 : 2;
|
||||
}
|
||||
element.icon = requireImg(
|
||||
element.type == 1 ? "tree-icon-group.png" : "tree-icon-item.png",
|
||||
"image"
|
||||
);
|
||||
if (kids.length > 0) {
|
||||
element.children = this.formatTree(kids, level + 1, this.getSplitId(element.id));
|
||||
} else {
|
||||
// 叶子删掉 children 键:zTree 内部同样按真值判定,留 null 会二次踩坑
|
||||
delete element.children;
|
||||
}
|
||||
});
|
||||
return tree;
|
||||
},
|
||||
initTree() {
|
||||
const _this = this;
|
||||
// 竞态守卫:慢请求返回时若已切换 tab,丢弃结果
|
||||
const seq = ++this.treeSeq;
|
||||
const cacheKey = this.tabIndex;
|
||||
const source = this.sources[this.tabIndex];
|
||||
if (!source) return;
|
||||
const cached = this.cacheMap.get(cacheKey);
|
||||
if (cached) {
|
||||
this.createTree(cached);
|
||||
return;
|
||||
}
|
||||
source
|
||||
.loader()
|
||||
.then((data: any[]) => {
|
||||
if (seq !== _this.treeSeq) return;
|
||||
_this.cacheMap.set(cacheKey, data || []);
|
||||
_this.createTree(data || []);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (seq === _this.treeSeq) Message.error(e?.message || "加载组织数据失败");
|
||||
});
|
||||
},
|
||||
createTree(rawData: any[]) {
|
||||
const _this = this;
|
||||
var jtree = JSON.parse(JSON.stringify(rawData));
|
||||
var ftree = _this.formatTree(jtree);
|
||||
_this.currentTree = ftree;
|
||||
|
||||
let treeSetting: any = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showIcon: true,
|
||||
},
|
||||
check: {
|
||||
enable: true,
|
||||
chkboxType: { Y: "ps", N: "ps" },
|
||||
},
|
||||
callback: {},
|
||||
};
|
||||
/** 勾选变化后同步右侧已选列表:收集勾选节点子树内的所有叶子 */
|
||||
function formatUser(treeNode: any, checked: boolean) {
|
||||
const recursionCheckIds = (tree: any): any[] => {
|
||||
let arrIds: any[] = [];
|
||||
for (const key in tree) {
|
||||
const element = tree[key];
|
||||
if (element.type == 2 && !element.chkDisabled) {
|
||||
arrIds.push({ id: element.id, tid: element.tId });
|
||||
}
|
||||
if (typeof element.children !== "undefined" && element.children) {
|
||||
arrIds = [...arrIds, ...recursionCheckIds(element.children)];
|
||||
}
|
||||
}
|
||||
return arrIds;
|
||||
};
|
||||
const checkIds = recursionCheckIds([treeNode]);
|
||||
const checkUserList = _this.currentTreeFlatten.filter((x: any) =>
|
||||
checkIds.map((x) => x.id).includes(x.id)
|
||||
);
|
||||
const checkUserArr: any[] = [];
|
||||
for (const element of checkUserList) {
|
||||
var findObj = checkIds.find((x) => x.id == element.id);
|
||||
if (
|
||||
!!findObj &&
|
||||
checkUserArr.findIndex((x) => x.id == element.id) === -1 &&
|
||||
_this.tempUserList.findIndex((x) => x.id == element.id) === -1
|
||||
) {
|
||||
checkUserArr.push({ ...element, isCheck: false, tid: findObj.tid });
|
||||
}
|
||||
}
|
||||
if (checked) {
|
||||
_this.tempUserList = _this.tempUserList.concat(checkUserArr);
|
||||
} else {
|
||||
var ids = checkUserList.map((x) => x.id);
|
||||
_this.tempUserList = _this.tempUserList.filter((x) => !ids.includes(x.id));
|
||||
}
|
||||
}
|
||||
treeSetting.callback.onClick = (event: any, treeId: string, treeNode: any) => {
|
||||
let check = treeNode.checked;
|
||||
let treeObj = ($.fn as any).zTree.getZTreeObj(treeId);
|
||||
treeObj.checkNode(treeNode, !check, true);
|
||||
_this.enforceSingle(treeId, treeNode);
|
||||
formatUser(treeNode, treeNode.checked);
|
||||
};
|
||||
treeSetting.callback.onCheck = (event: any, treeId: string, treeNode: any) => {
|
||||
_this.enforceSingle(treeId, treeNode);
|
||||
formatUser(treeNode, treeNode.checked);
|
||||
};
|
||||
var treeObj = ($.fn as any).zTree.init($("#" + _this.ztreeDomId), treeSetting, ftree);
|
||||
// 回显:已选用户打勾(callbackFlag=true 会触发 onCheck,同步右侧列表)
|
||||
// 限定人员叶子:teaId/stuId 与组节点 id 撞号时不误勾整组(勾组会级联全选其下人员)
|
||||
var nodes = treeObj.getNodesByFilter((x: any) =>
|
||||
x.type == 2 && _this.userids.includes(_this.getSplitId(x.id))
|
||||
);
|
||||
var arr: any[] = [];
|
||||
nodes.forEach((element: any) => {
|
||||
var splitId = _this.getSplitId(element.id); //重复id只选择第一个
|
||||
if (!arr.includes(splitId)) treeObj.checkNode(element, true, true, true);
|
||||
if (_this.isDisabled) treeObj.setChkDisabled(element, true);
|
||||
arr.push(splitId);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 单选约束(v7,multiple=false 时生效):
|
||||
* ① 组节点被勾选 → 回退取消并提示(单选只允许选叶子人员);
|
||||
* ② 叶子被勾选 → 取消其他已选子树(callbackFlag=false 避免递归 onCheck),
|
||||
* 并同步 tempUserList 仅保留当前子树叶子。多选模式无操作。
|
||||
*/
|
||||
enforceSingle(treeId: string, treeNode: any) {
|
||||
if (this.multiple) return;
|
||||
const treeObj = ($.fn as any).zTree.getZTreeObj(treeId);
|
||||
if (!treeObj || !treeNode) return;
|
||||
if (treeNode.checked && treeNode.type == 1) {
|
||||
treeObj.checkNode(treeNode, false, true, false);
|
||||
Message.info("单选模式:请勾选具体人员");
|
||||
return;
|
||||
}
|
||||
if (!treeNode.checked) return;
|
||||
const keep = new Set<string>();
|
||||
const collect = (n: any) => {
|
||||
keep.add(n.tId);
|
||||
(n.children || []).forEach(collect);
|
||||
};
|
||||
collect(treeNode);
|
||||
treeObj.getCheckedNodes(true).forEach((n: any) => {
|
||||
if (!keep.has(n.tId)) treeObj.checkNode(n, false, true, false);
|
||||
});
|
||||
this.tempUserList = this.tempUserList.filter((x: any) => keep.has(x.tid));
|
||||
},
|
||||
checkAllUser(check: boolean) {
|
||||
this.tempUserList.forEach((element: any) => {
|
||||
element.isCheck = check;
|
||||
});
|
||||
},
|
||||
checkUserOne(item: any) {
|
||||
item.isCheck = !item.isCheck;
|
||||
const tlist = this.tempUserList;
|
||||
this.isCheckUserAll =
|
||||
tlist.length > 0 && tlist.filter((x) => x.isCheck).length == tlist.length;
|
||||
},
|
||||
delCheckUser() {
|
||||
const _this = this;
|
||||
const tlist = _this.tempUserList;
|
||||
const delArr = tlist.filter((item: any) => item.isCheck);
|
||||
var treeObj = ($.fn as any).zTree.getZTreeObj(_this.ztreeDomId);
|
||||
delArr.forEach((element: any) => {
|
||||
var node = treeObj.getNodeByTId(element.tid);
|
||||
treeObj.checkNode(node, false, true, true);
|
||||
});
|
||||
_this.isCheckUserAll = false;
|
||||
},
|
||||
changeEmpty() {
|
||||
this.tempUserList = [];
|
||||
this.searchTreeName = "";
|
||||
this.searchResult = [];
|
||||
},
|
||||
submit() {
|
||||
const _this = this;
|
||||
let idsArr = this.tempUserList.map((x: any) => _this.getSplitId(x.id));
|
||||
idsArr = Array.from(new Set(idsArr));
|
||||
this.$emit("update:userids", idsArr);
|
||||
function delKey(obj: any, ...args: string[]) {
|
||||
args.forEach((v) => delete obj[v]);
|
||||
return obj;
|
||||
}
|
||||
const objArr: any[] = [];
|
||||
idsArr.forEach((element) => {
|
||||
let findObj = _this.tempUserList.find((x: any) => _this.getSplitId(x.id) == element);
|
||||
findObj = delKey({ ...findObj }, "id", "isCheck", "tid", "pid", "type");
|
||||
objArr.push({ ...findObj, id: element });
|
||||
});
|
||||
this.$emit("on-submit", objArr);
|
||||
this.modal = false;
|
||||
},
|
||||
/** 树节点 id 形如 "1,301"(原id,父id),取真实 id */
|
||||
getSplitId(strId: any) {
|
||||
if (typeof strId === "string" && strId.indexOf(",") != -1) return parseInt(strId.split(",")[0]);
|
||||
return strId;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tree-wrap {
|
||||
height: 430px;
|
||||
position: relative;
|
||||
}
|
||||
.tree-panel {
|
||||
border: 1px solid #eee;
|
||||
width: 280px;
|
||||
height: 430px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tree-panel--full {
|
||||
width: 100%;
|
||||
}
|
||||
.tree-box {
|
||||
padding: 4px 0;
|
||||
height: 428px;
|
||||
width: auto;
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
}
|
||||
.user-panel {
|
||||
border: 1px solid #eee;
|
||||
width: 280px;
|
||||
.title-box {
|
||||
background: #f7f7f7;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
padding: 0 12px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
float: right;
|
||||
margin-right: 20px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
.user-box {
|
||||
height: 396px;
|
||||
overflow-y: auto;
|
||||
li {
|
||||
line-height: 32px;
|
||||
padding-left: 12px;
|
||||
cursor: pointer;
|
||||
.info {
|
||||
float: right;
|
||||
margin-right: 12px;
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
li:hover {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
li.active {
|
||||
background: #e4edf6;
|
||||
}
|
||||
}
|
||||
}
|
||||
.arrow-box {
|
||||
width: 128px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
margin-top: -17px;
|
||||
}
|
||||
.auto-box {
|
||||
width: 200px;
|
||||
margin-bottom: 10px;
|
||||
:deep(.ivu-select-item .info) {
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
:deep(.ztree li) {
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ztree li a) {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.ztree li a:hover) {
|
||||
text-decoration: none;
|
||||
}
|
||||
:deep(.ivu-auto-complete .ivu-select-dropdown-list) {
|
||||
max-height: 310px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,317 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeMount, onMounted, onUnmounted, watch, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { resolveMenuPage, normalizeModList } from "@/utils/menuRoute";
|
||||
import { noticeApi } from "@/api/module/notice";
|
||||
|
||||
const props = defineProps<{ layoutData: Record<string, any> }>();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const isPanel = ref(false);
|
||||
const currPath = ref<string>("");
|
||||
const navPath = ref<any[]>([]);
|
||||
|
||||
const baseHost = computed(() => props.layoutData?.baseHost || "");
|
||||
const ssoHost = computed(() => props.layoutData?.ssoHost || "");
|
||||
const appName = computed(() => props.layoutData?.appName || "校园公告");
|
||||
const appList = computed(() => props.layoutData?.appList || []);
|
||||
const userInfo = computed(() => props.layoutData?.userInfo || { name: "管理员", avatar: "" });
|
||||
const isAlone = computed(() => props.layoutData?.isAlone ?? false);
|
||||
const modList = computed(() => props.layoutData?.modList || []);
|
||||
|
||||
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||||
|
||||
/* 菜单管线(orderNumber 定序 → path 归一 → 草稿箱注入)已下沉 utils/menuRoute.normalizeModList,
|
||||
栏目名/顺序均以平台 GetUserMod 配置为准,前端不改名不重排(见执行计划任务方案 v10/v11) */
|
||||
|
||||
onBeforeMount(() => {
|
||||
routeChange(route);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => routeChange(route)
|
||||
);
|
||||
|
||||
const flaten = (tree: any[], pid = 0): any[] => {
|
||||
const arr: any[] = [];
|
||||
tree.forEach((element: any) => {
|
||||
const { id, name, modId } = element;
|
||||
// 平台配置的 path 格式不可控,统一解析为内部路由 path(名称/modId 兜底)
|
||||
const resolved = resolveMenuPage(element.page, element.modId ?? element.id, element.name);
|
||||
arr.push({ id, name, modId, page: resolved.path, external: resolved.external, pid });
|
||||
if (element.children && element.children.length) {
|
||||
arr.push(...flaten(element.children, id));
|
||||
}
|
||||
});
|
||||
return arr;
|
||||
};
|
||||
|
||||
const menuArr = computed(() => flaten(normalizeModList(modList.value)));
|
||||
const displayModList = computed(() => normalizeModList(modList.value));
|
||||
|
||||
const routeChange = (to: any) => {
|
||||
const meta: any = to.meta || {};
|
||||
const { modId, title, parentId } = meta;
|
||||
const navArr: any[] = [];
|
||||
|
||||
// 优先按 path 匹配菜单(平台模块 id 与路由 meta.modId 不保证一致),
|
||||
// 匹配不到再退回 modId 对照
|
||||
const find: any =
|
||||
menuArr.value.find((x: any) => x.page && x.page === to.path) ||
|
||||
menuArr.value.find((x: any) => x.modId === modId);
|
||||
if (find) {
|
||||
currPath.value = find.page || "";
|
||||
if (find.pid) {
|
||||
const pfind = menuArr.value.find((x: any) => x.id === find.pid);
|
||||
if (pfind) navArr.push({ name: pfind.name });
|
||||
}
|
||||
navArr.push({ name: find.name || title, page: find.page });
|
||||
navPath.value = navArr;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentId) {
|
||||
const parent = menuArr.value.find((x: any) => x.modId === parentId);
|
||||
if (parent) {
|
||||
currPath.value = parent.page || "";
|
||||
if (parent.pid) {
|
||||
const gp = menuArr.value.find((x: any) => x.id === parent.pid);
|
||||
if (gp) navArr.push({ name: gp.name });
|
||||
}
|
||||
navArr.push({ name: parent.name, page: parent.page });
|
||||
navArr.push({ name: title });
|
||||
navPath.value = navArr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const r = router.getRoutes().find((rt: any) => (rt.meta as any)?.modId === modId);
|
||||
if (r) {
|
||||
currPath.value = r.path;
|
||||
navPath.value = [{ name: (r.meta as any).title, page: r.path }];
|
||||
return;
|
||||
}
|
||||
|
||||
currPath.value = "";
|
||||
navPath.value = [];
|
||||
};
|
||||
|
||||
const linkMenu = (item: any) => {
|
||||
const page: string = item?.page || "";
|
||||
if (!page) {
|
||||
// 解析不出有效页面:显式提示,而不是静默不动或被重定向吞到默认页
|
||||
Message.warning(`菜单「${item?.name || "未命名"}」未配置有效页面路径,请检查基础平台模块设置`);
|
||||
return;
|
||||
}
|
||||
// 平台配置的外部链接模块:整页跳转
|
||||
if (item?.external) {
|
||||
window.location.href = page;
|
||||
return;
|
||||
}
|
||||
if (page === route.path) {
|
||||
router.go(0);
|
||||
} else {
|
||||
router.push(page);
|
||||
}
|
||||
};
|
||||
|
||||
/* ============ 审批中心待审角标(ApprovalCount,60s 轮询 + 进页刷新) ============ */
|
||||
const approvalBadge = ref(0);
|
||||
let approvalTimer: number | undefined;
|
||||
|
||||
async function refreshApprovalBadge() {
|
||||
try {
|
||||
const n = await noticeApi.approvalCount().send();
|
||||
approvalBadge.value = Number(n) || 0;
|
||||
} catch {
|
||||
/* 未配置审批或接口异常时角标静默置 0 */
|
||||
approvalBadge.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshApprovalBadge();
|
||||
approvalTimer = window.setInterval(refreshApprovalBadge, 60000);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
if (approvalTimer) window.clearInterval(approvalTimer);
|
||||
});
|
||||
|
||||
const linkUserInfo = () => {
|
||||
window.location.href = baseHost.value + "/v/userinfo";
|
||||
};
|
||||
const logout = () => {
|
||||
let url = window.location.href;
|
||||
url = "/logout?service=" + url;
|
||||
window.location.href = ssoHost.value + url;
|
||||
};
|
||||
const childChange = (index: number) => {
|
||||
const arr = displayModList.value || [];
|
||||
if (arr[index]) {
|
||||
arr[index].childState = !arr[index].childState;
|
||||
}
|
||||
};
|
||||
|
||||
/** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖),
|
||||
* 兜底保留旧版 modId 对照。图标为本地 bsch-icons(iconfont 掩码类,见 styles/icons.css) */
|
||||
const getMenuIcon = (item?: any): string => {
|
||||
const page: string = item?.page || "";
|
||||
if (page === "/list/new") return "icon-xiugaijl";
|
||||
if (page === "/drafts") return "icon-caogaoxiang";
|
||||
if (page === "/list") return "icon-fasong";
|
||||
if (page === "/" || page === "/accept") return "icon-sjx";
|
||||
if (page === "/noticelist") return "icon-liebiao";
|
||||
if (page === "/approval") return "icon-shenpi";
|
||||
if (page === "/stats") return "icon-tongji";
|
||||
if (page === "/setting") return "icon-shezhi";
|
||||
switch (item?.modId ?? item?.id) {
|
||||
case 1: return "icon-xiugaijl";
|
||||
case 2: return "icon-fasong";
|
||||
case 3: return "icon-sjx";
|
||||
case 4: return "icon-liebiao";
|
||||
case 5: return "icon-shenpi";
|
||||
case 6: return "icon-quanxian";
|
||||
case 21: return "icon-shezhi";
|
||||
case 22: return "icon-xiugaijl";
|
||||
default: return "";
|
||||
}
|
||||
};
|
||||
|
||||
function linkHomePage() {
|
||||
if (baseHost.value) {
|
||||
window.location.href = baseHost.value + "/BschHome/front/index.html";
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-wrap">
|
||||
<header class="header-container fix">
|
||||
<div class="fl align-middle icon-box">
|
||||
<div title="常用应用" :class="['icon-btn', isPanel ? 'active' : '']" @click="isPanel = !isPanel">
|
||||
<i class="iconfont icon-apps"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="delimiter fl align-middle"></div>
|
||||
<h4 class="fl align-middle">{{ appName }}</h4>
|
||||
<div class="school-bg"></div>
|
||||
<div class="header-aside fr">
|
||||
<img :src="userInfo.avatar" class="fl align-middle" @click="linkUserInfo" />
|
||||
<a class="name fl align-middle text-ellipsis" href="javascript:void(0);" @click="linkUserInfo">
|
||||
{{ userInfo.name }}
|
||||
</a>
|
||||
<div class="delimiter fl align-middle"></div>
|
||||
<a href="javascript:;" @click="logout" class="logout fl align-middle" title="退出登陆">
|
||||
<i class="iconfont icon-logout"></i>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="left-menu" v-if="!isAlone">
|
||||
<div class="menu-top-box"></div>
|
||||
<ul>
|
||||
<template v-for="(item, index) in displayModList" :key="'menu_li_' + index">
|
||||
<template v-if="item.children && item.children.length > 0">
|
||||
<li>
|
||||
<a href="javascript:;" @click="childChange(index)">
|
||||
<i :class="item.childState ? 'arrow-down' : 'arrow-up'"></i>
|
||||
<i v-if="getMenuIcon(item)" :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</li>
|
||||
<transition name="fade_child_ul">
|
||||
<div class="child-item-wrap" v-show="item.childState">
|
||||
<li
|
||||
:class="citem.page == currPath ? 'active child-item' : 'child-item'"
|
||||
v-for="citem in item.children"
|
||||
:key="citem.id"
|
||||
>
|
||||
<a href="javascript:;" @click="linkMenu(citem)">
|
||||
{{ citem.name }}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<li v-else :class="item.page == currPath ? 'active' : ''">
|
||||
<a href="javascript:;" @click="linkMenu(item)">
|
||||
<i v-if="getMenuIcon(item)" :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
{{ item.name }}
|
||||
<span v-if="item.page === '/approval' && approvalBadge > 0" class="menu-badge">
|
||||
{{ approvalBadge > 99 ? "99+" : approvalBadge }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div class="clear-box"></div>
|
||||
</div>
|
||||
|
||||
<div class="path-nav" :style="pathNavStyle">
|
||||
<Breadcrumb separator=">">
|
||||
<a href="javascript:;" @click="linkHomePage">
|
||||
<BreadcrumbItem>
|
||||
<i class="iconfont icon-category"></i>
|
||||
首页
|
||||
</BreadcrumbItem>
|
||||
</a>
|
||||
<BreadcrumbItem v-if="!isAlone" to="/">{{ appName }}</BreadcrumbItem>
|
||||
<template v-for="(item, index) in navPath" :key="index">
|
||||
<BreadcrumbItem v-if="index != navPath.length - 1" :to="item.page">
|
||||
{{ item.name }}
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem v-else>{{ item.name }}</BreadcrumbItem>
|
||||
</template>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
|
||||
<transition name="fade_apps">
|
||||
<div class="app-panel" v-if="isPanel">
|
||||
<div class="tips-box"></div>
|
||||
<ul class="fix">
|
||||
<li class="fl" v-for="(item, index) in appList" :key="index">
|
||||
<a :href="item.url" target="_blank" rel="noopener noreferrer">
|
||||
<div class="icon-box">
|
||||
<img :src="item.icon" alt="" />
|
||||
</div>
|
||||
<p class="text-ellipsis">{{ item.name }}</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="link-panel">
|
||||
<a :href="baseHost + '/BschHome/front/index.html#/apps'" target="_blank" rel="noopener noreferrer">
|
||||
全部应用
|
||||
<i>→</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 仅覆盖/补充必要的动态样式,其余样式由全局 layout.css 提供 */
|
||||
/* 菜单图标(本地 bsch-icons 掩码类)与文字间距 */
|
||||
.left-menu a .iconfont {
|
||||
margin-right: 8px;
|
||||
}
|
||||
/* 审批中心待审角标 */
|
||||
.menu-badge {
|
||||
display: inline-block;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
padding: 0 5px;
|
||||
margin-left: 6px;
|
||||
border-radius: 9px;
|
||||
background: #ed4014;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div :id="domId" :ref="domId" class="ztree"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
treeData: {
|
||||
type: Object
|
||||
},
|
||||
setting: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showIcon: false
|
||||
},
|
||||
check: {
|
||||
enable: false
|
||||
},
|
||||
callback: {}
|
||||
};
|
||||
}
|
||||
},
|
||||
autoClick: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domId: ""
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$watch("treeData", val => {
|
||||
if (val) {
|
||||
this.$nextTick(() => {
|
||||
this.initTree(this.treeData, this.domId);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.domId = this.uuid();
|
||||
},
|
||||
methods: {
|
||||
uuid() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
var r = (Math.random() * 16) | 0,
|
||||
v = c == "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
},
|
||||
reloadTree(nodeId) {
|
||||
this.initTree(this.treeData, this.domId);
|
||||
if (nodeId != null) {
|
||||
this.$nextTick(() => {
|
||||
this.selectNode(nodeId, true); // 第二个参数 true 会 emit on-tree-click
|
||||
});
|
||||
}
|
||||
},
|
||||
initTree(_data, _domId) {
|
||||
const _this = this;
|
||||
_this.setting.callback.onClick = (event, treeId, treeNode, clickFlag) => {
|
||||
// console.log(event, treeId, treeNode, clickFlag);
|
||||
_this.$emit("on-tree-click", treeNode);
|
||||
};
|
||||
_this.setting.callback.onExpand = (event, treeId, treeNode) => {
|
||||
if (treeNode.isParent && !treeNode.children) {
|
||||
_this.$emit("on-tree-expand", treeNode);//触发展开事件
|
||||
}
|
||||
};
|
||||
|
||||
_this.setting.callback.beforeClick = (treeId, treeNode, clickFlag) => {
|
||||
return treeNode.click != false;
|
||||
};
|
||||
var zNodes = _this.openFistNode(_data);
|
||||
$.fn.zTree.init($("#" + _domId), _this.setting, zNodes);
|
||||
|
||||
if (_this.autoClick) {
|
||||
if (Array.isArray(this.treeData)) {
|
||||
let id = this.treeData[0].id;
|
||||
_this.selectNode(id);
|
||||
} else {
|
||||
let id = this.treeData.id;
|
||||
_this.selectNode(id);
|
||||
}
|
||||
}
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
setTimeout(() => {
|
||||
this.selectNode(0);
|
||||
}, 0);
|
||||
},
|
||||
selectNode(id, isClick = true) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
if (node == null) return false;
|
||||
treeObj.selectNode(node);
|
||||
treeObj.expandNode(node, true, false, false);
|
||||
treeObj.selectNode(node);
|
||||
if (isClick) this.$emit("on-tree-click", node);
|
||||
},
|
||||
|
||||
openFistNode(_zNodes) {
|
||||
//展开一级节点
|
||||
if (_zNodes && _zNodes.length > 0) {
|
||||
for (var key in _zNodes) {
|
||||
if (_zNodes.hasOwnProperty(key)) {
|
||||
var element = _zNodes[key];
|
||||
element.open = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_zNodes.open = true;
|
||||
}
|
||||
return _zNodes;
|
||||
},
|
||||
selectNode(id, isClick = true) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||
if (node == null) return false;
|
||||
treeObj.selectNode(node);
|
||||
treeObj.expandNode(node, true, false, false);
|
||||
treeObj.selectNode(node);
|
||||
if (isClick) this.$emit("on-tree-click", node);
|
||||
},
|
||||
addNode(pid, id, name, isParent) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
var pnode = treeObj.getNodesByFilter(node => node.id == pid, true);
|
||||
var newNode = { pid, id, name, isParent };
|
||||
treeObj.addNodes(pnode, -1, newNode);
|
||||
},
|
||||
addNodes(treeNode, children) {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
const parentZNode = treeObj.getNodeByParam("id", treeNode.id, null);
|
||||
treeObj.addNodes(parentZNode, children, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createApp } from "vue";
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "@/router";
|
||||
|
||||
import utils from "@/utils";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
// @ts-ignore
|
||||
import ViewUIPlus from "view-ui-plus";
|
||||
import mittBus from "@/utils/mitt";
|
||||
|
||||
import "view-ui-plus/dist/styles/viewuiplus.css";
|
||||
|
||||
import "@/assets/styles/main.scss";
|
||||
// 本地化的布局/主题样式(原远程 base.layout.beta / _iview_override / default-theme 合并)
|
||||
import "@/assets/styles/layout.css";
|
||||
// 规范 iconfont 图标本地实现(icon-browse / icon-edit / icon-ashbin 等,平台主题不含字体)
|
||||
import "@/assets/styles/icons.css";
|
||||
//导入自定义指令
|
||||
import directive from "@/utils/directive/index";
|
||||
|
||||
import "virtual:uno.css";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.use(ViewUIPlus);
|
||||
|
||||
import config from "@/assets/config/host.config";
|
||||
app.config.globalProperties.$conf = config;
|
||||
app.config.globalProperties.$utils = utils;
|
||||
app.config.globalProperties.$bus = mittBus;
|
||||
|
||||
// direct(app);
|
||||
directive(app);
|
||||
|
||||
import "virtual:svg-icons-register";
|
||||
|
||||
app.mount("#app");
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import routes from "./routes";
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
export default router;
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
|
||||
/**
|
||||
* 校园通知模块路由(扁平短路由,与基础平台模块 path 一致)。
|
||||
* 页面间参数通过 query 传递:
|
||||
* - /list/new?serid= (编辑/草稿续编/驳回重提)
|
||||
* - /list/new?fid= (转发)
|
||||
* - /list/new?tempid=(模板引用)
|
||||
* - /list/detail?serid=&type=、/noticelist/detail?serid=&type=
|
||||
* type: 0=编辑视角(不标读) 1=普通查看(标读)
|
||||
*
|
||||
* modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 现行数据):
|
||||
* 1=我的通知 2=已发管理 3=全部通知 4=通知统计 5=审批中心 21=应用设置
|
||||
* (6=审核设置已于 v18 下线:路由删除,菜单管线 utils/menuRoute.normalizeModList 剔除该模块)
|
||||
* 写通知为「已发管理」的深层子页(modId 22 不在菜单树,meta.parentId=2 关联高亮),
|
||||
* 草稿箱暂无平台 modId(菜单项由 utils/menuRoute.normalizeModList 注入,meta.parentId=2 关联高亮兜底),
|
||||
* 与 UserBase「应用管理>应用类型管理」范式一致。
|
||||
*/
|
||||
const routes: RouteRecordRaw[] = [
|
||||
// 我的通知(收件箱):后端菜单 page 配置为 "/",即应用入口页
|
||||
{
|
||||
path: "/",
|
||||
name: "notice_accept",
|
||||
component: () => import("@/views/notice/NoticeAccept.vue"),
|
||||
meta: { title: "我的公告", modId: 1 },
|
||||
},
|
||||
// 已发管理
|
||||
{
|
||||
path: "/list",
|
||||
name: "notice_list",
|
||||
component: () => import("@/views/notice/NoticeList.vue"),
|
||||
meta: { title: "已发管理", modId: 2 },
|
||||
},
|
||||
// 写通知(新建/编辑/草稿续编/转发/模板):已发管理的深层子页,不在菜单里,
|
||||
// layout.vue 按.meta.parentId 推导面包屑「已发管理>写通知」并高亮父菜单
|
||||
{
|
||||
path: "/list/new",
|
||||
name: "new_notice",
|
||||
component: () => import("@/views/notice/NewNotice.vue"),
|
||||
meta: { title: "写公告", modId: 22, parentId: 2 },
|
||||
},
|
||||
// 草稿箱(无独立平台 modId,菜单项由 layout 注入,挂「已发管理」之后)
|
||||
{
|
||||
path: "/drafts",
|
||||
name: "notice_drafts",
|
||||
component: () => import("@/views/notice/DraftList.vue"),
|
||||
meta: { title: "草稿箱", parentId: 2 },
|
||||
},
|
||||
// 全部通知(管理端)
|
||||
{
|
||||
path: "/noticelist",
|
||||
name: "notice_manage",
|
||||
component: () => import("@/views/notice/NoticeList.vue"),
|
||||
meta: { title: "全部公告", modId: 3 },
|
||||
},
|
||||
// 通知详情(同一组件挂两个路由,面包屑/菜单高亮由 meta.parentId 确定父节点:
|
||||
// 已发管理下 /list/detail,全部通知下 /noticelist/detail;type 区分是否标读)
|
||||
{
|
||||
path: "/list/detail",
|
||||
name: "notice_list_detail",
|
||||
component: () => import("@/views/notice/NoticeDetails.vue"),
|
||||
meta: { title: "公告详情", modId: 23, parentId: 2 },
|
||||
},
|
||||
{
|
||||
path: "/noticelist/detail",
|
||||
name: "notice_manage_detail",
|
||||
component: () => import("@/views/notice/NoticeDetails.vue"),
|
||||
meta: { title: "公告详情", modId: 24, parentId: 3 },
|
||||
},
|
||||
// 审批中心(多步流程:需审批后发布的通知在此审批)
|
||||
{
|
||||
path: "/approval",
|
||||
name: "notice_approval",
|
||||
component: () => import("@/views/notice/ApprovalList.vue"),
|
||||
meta: { title: "审批中心", modId: 5 },
|
||||
},
|
||||
// 通知统计(旧版行为:整页跳转外部统计系统)
|
||||
{
|
||||
path: "/stats",
|
||||
name: "notice_stats",
|
||||
component: () => import("@/views/notice/NoticeStats.vue"),
|
||||
meta: { title: "公告统计", modId: 4 },
|
||||
},
|
||||
// 应用设置(发文部门 + 自定义组)
|
||||
{
|
||||
path: "/setting",
|
||||
name: "notice_setting",
|
||||
component: () => import("@/views/notice/NoticeSetting.vue"),
|
||||
meta: { title: "应用设置", modId: 21 },
|
||||
},
|
||||
|
||||
/* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */
|
||||
{ path: "/accept", redirect: "/" },
|
||||
{ path: "/details", redirect: "/list/detail" },
|
||||
{ path: "/newnotice", redirect: "/list/new" },
|
||||
{ path: "/edit", redirect: "/list/new" },
|
||||
{ path: "/manage", redirect: "/noticelist" },
|
||||
{ path: "/notice", redirect: "/accept" },
|
||||
{ path: "/notice/my", redirect: "/" },
|
||||
{ path: "/notice/inbox", redirect: "/" },
|
||||
{ path: "/notice/drafts", redirect: "/drafts" },
|
||||
{ path: "/notice/newnotice", redirect: "/list/new" },
|
||||
{ path: "/notice/edit", redirect: "/list/new" },
|
||||
{ path: "/notice/notice_accept", redirect: "/accept" },
|
||||
{ path: "/notice/notice_list", redirect: "/list" },
|
||||
{ path: "/notice/manage", redirect: "/noticelist" },
|
||||
{ path: "/notice/notice_details", redirect: "/list/detail" },
|
||||
{ path: "/notice/statistics", redirect: "/stats" },
|
||||
{ path: "/notice/setting", redirect: "/setting" },
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
/** 左侧菜单模块项(对齐后端 GetUserMod 返回结构) */
|
||||
export interface IModule {
|
||||
id: number;
|
||||
modId?: number;
|
||||
name: string;
|
||||
page?: string;
|
||||
children?: IModule[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const useDefaultStore = defineStore("defaultStore", {
|
||||
state: () => ({
|
||||
userRole: "school" as string,
|
||||
}),
|
||||
actions: {
|
||||
setUserRole(role: string) {
|
||||
this.userRole = role;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 公共类型
|
||||
*/
|
||||
|
||||
export declare interface IObj {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export declare interface PageParams {
|
||||
page: !number;
|
||||
count: !number;
|
||||
[keys: string]: any; //任意属性
|
||||
}
|
||||
|
||||
export declare interface PageData {
|
||||
list: T;
|
||||
total: number;
|
||||
[keys: string]: any; //任意属性
|
||||
}
|
||||
interface RelayConf {
|
||||
ziyuankuHost: string; //资源库
|
||||
zhuanxiangHost: string; //专项
|
||||
|
||||
}
|
||||
|
||||
/* 系统配置 */
|
||||
export interface BaseConf {
|
||||
baseHost: string;
|
||||
ssoHost: string;
|
||||
serviceHost: string;
|
||||
relayConf: RelayConf;
|
||||
isMock: Boolean;
|
||||
isDebug?: Boolean;
|
||||
[keys: string]: any; //任意属性
|
||||
}
|
||||
|
||||
/* PC端角色 */
|
||||
export interface PcDebugUser {
|
||||
DebugUser: string;
|
||||
AppType: string;
|
||||
// yschapirequest: number;
|
||||
}
|
||||
|
||||
/* webapp端角色 */
|
||||
export declare interface WappDebugUser {
|
||||
ticket: string;
|
||||
schoolId: Number;
|
||||
userId: Number;
|
||||
roleId: Number;
|
||||
}
|
||||
|
||||
export declare interface CurrentRole {
|
||||
childId: number | null;
|
||||
roleId: number;
|
||||
schId: number;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export interface layoutData {
|
||||
appName: String;
|
||||
appList: string[];
|
||||
modList: Array<string>;
|
||||
userInfo: Object;
|
||||
ssoHost: String;
|
||||
baseHost: String;
|
||||
isAlone: Boolean; //是否只有一个选项卡
|
||||
}
|
||||
|
||||
export interface BaseDict {
|
||||
k: any;
|
||||
v: string | null;
|
||||
[keys: string]: any; //任意属性
|
||||
}
|
||||
|
||||
/* 计算表格高度的参数类型 */
|
||||
export interface UseTableSetting {
|
||||
/** 是否有查询面板 */
|
||||
isSearch: boolean = true;
|
||||
/** 是否有分页 */
|
||||
isPage: boolean = true;
|
||||
/** 是否是tabs布局 */
|
||||
isTabs: boolean = false;
|
||||
/** 是否有按钮操作 */
|
||||
isOperat: boolean = true;
|
||||
/** 额外的高度 */
|
||||
otherH: number = 0;
|
||||
}
|
||||
|
||||
/* 布局组件类型 */
|
||||
export interface LayoutData {
|
||||
appName: string | null;
|
||||
appList: Array<any>;
|
||||
modList: Array<any>;
|
||||
userInfo: any;
|
||||
ssoHost: string | null;
|
||||
baseHost: string | null;
|
||||
isAlone: boolean;
|
||||
}
|
||||
|
||||
/* 自定义弹出对话框 */
|
||||
export interface MyConfirm {
|
||||
title?: string;
|
||||
msg: string;
|
||||
info?: string;
|
||||
isLoading?: boolean;
|
||||
confirmText?: string;
|
||||
closeText?: string;
|
||||
onOk?: Function;
|
||||
onCancel?: Function;
|
||||
}
|
||||
|
||||
export declare enum NODE_ENV {
|
||||
/**
|
||||
*开发环境
|
||||
*/
|
||||
development = "development",
|
||||
/**
|
||||
*生产环境
|
||||
*/
|
||||
production = "production",
|
||||
}
|
||||
|
||||
export const _: any;
|
||||
export const $: any;
|
||||
|
||||
declare global {
|
||||
const $: any;
|
||||
const _: any;
|
||||
}
|
||||
|
||||
import Utils from "@/utils/index";
|
||||
import mittBus from '@/utils/mitt'
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
|
||||
export interface VueGlobal {
|
||||
$conf: BaseConf;
|
||||
$bus: typeof mittBus;
|
||||
$utils: typeof Utils;
|
||||
$echarts: echarts;
|
||||
[key:string]:any;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** 节点类型 */
|
||||
export enum NodeEnum {
|
||||
Root = 1,
|
||||
Group,
|
||||
Item,
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { BaseDict } from "./Basics";
|
||||
|
||||
export enum OperatorEnum {
|
||||
add,
|
||||
del,
|
||||
edit,
|
||||
sel,
|
||||
}
|
||||
|
||||
export enum UserRoleEnum {
|
||||
SuperAdmin = 1,
|
||||
School,
|
||||
}
|
||||
|
||||
//学段
|
||||
export enum SchoolStage {
|
||||
PRIMARY = 2,
|
||||
MIDDLE,
|
||||
HIGH,
|
||||
}
|
||||
|
||||
export const SchoolStageDict: BaseDict[] = [
|
||||
{
|
||||
k: SchoolStage.PRIMARY,
|
||||
v: "小学",
|
||||
},
|
||||
{
|
||||
k: SchoolStage.MIDDLE,
|
||||
v: "初中",
|
||||
},
|
||||
{
|
||||
k: SchoolStage.HIGH,
|
||||
v: "高中",
|
||||
},
|
||||
];
|
||||
|
||||
export enum RootNode {
|
||||
/** 资源 */
|
||||
ZIYUANKU = 1,
|
||||
/** 辅导 */
|
||||
TUTORING = 2,
|
||||
/** 题库 */
|
||||
QUESTION = 3,
|
||||
/** 试卷 */
|
||||
SHIJUAN = 4,
|
||||
/** 专项 */
|
||||
ZHUANXIANG = 5,
|
||||
}
|
||||
|
||||
export const RootNodeDict: BaseDict[] = [
|
||||
{
|
||||
k: RootNode.ZIYUANKU,
|
||||
v: "资源",
|
||||
path: "ziyuan",
|
||||
},
|
||||
{
|
||||
k: RootNode.TUTORING,
|
||||
v: "辅导",
|
||||
path: "fudao",
|
||||
},
|
||||
{
|
||||
k: RootNode.QUESTION,
|
||||
v: "题库",
|
||||
path: "tiku",
|
||||
},
|
||||
{
|
||||
k: RootNode.SHIJUAN,
|
||||
v: "试卷",
|
||||
path: "shijuan",
|
||||
},
|
||||
{
|
||||
k: RootNode.ZHUANXIANG,
|
||||
v: "专项",
|
||||
path: "zhuanxiang",
|
||||
},
|
||||
];
|
||||
|
||||
export enum BooxType {
|
||||
/** 教材 */
|
||||
TextBook = 1,
|
||||
/** 教辅 */
|
||||
TeachingTutoring,
|
||||
}
|
||||
|
||||
export const BooxTypeDict: BaseDict[] = [
|
||||
{
|
||||
k: BooxType.TextBook,
|
||||
v: "教材",
|
||||
},
|
||||
{
|
||||
k: BooxType.TeachingTutoring,
|
||||
v: "教辅",
|
||||
},
|
||||
];
|
||||
|
||||
//学校学段学制
|
||||
export interface SchLenLevel {
|
||||
stage: SchoolStage;
|
||||
name: string;
|
||||
grades: LenGrade[];
|
||||
}
|
||||
|
||||
export interface LenGrade {
|
||||
gradeId: number;
|
||||
gradeName: string;
|
||||
stage: number;
|
||||
}
|
||||
|
||||
export interface AcademicYear {
|
||||
year: number;
|
||||
semester: number | null;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Subject {
|
||||
id: number;
|
||||
tenantId: number;
|
||||
baseId: null;
|
||||
creatorId: number;
|
||||
createDate: string;
|
||||
name: string;
|
||||
code: string;
|
||||
orderNum: number;
|
||||
alias: string;
|
||||
stage: number;
|
||||
enable: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1 积分详情
|
||||
* 2 分数详情
|
||||
*/
|
||||
export type ScoreModalType = 1 | 2;
|
||||
@@ -0,0 +1,534 @@
|
||||
/**
|
||||
* 校园通知前端类型契约(P1-7 + 校园通知改造扩展)
|
||||
*
|
||||
* 与后端 VO/DTO 一一对齐:
|
||||
* - NoticeVo → 后端 model/notice/NoticeVo.java(Jackson 默认驼峰序列化)
|
||||
* - NoticeDetailVo → 后端 model/notice/NoticeDetailVo.java(@JsonProperty 保留旧字段名)
|
||||
* - BasePageResult → 后端 model/BasePageResult.java
|
||||
* - NoticePageResult extends BasePageResult → 后端 model/notice/NoticePageResult.java
|
||||
* - UpFileResultVo → 后端 model/notice/UpFileResultVo.java(@JsonProperty)
|
||||
* - UpFileVo → 后端 model/notice/UpFileVo.java
|
||||
* - InitializeRedVo → 后端 model/notice/InitializeRedVo.java(@JsonProperty)
|
||||
* - NoticeStatisticsVo → 后端 model/notice/NoticeStatisticsVo.java
|
||||
* - NoticeRequest → 后端 model/notice/NoticeRequest.java
|
||||
* - NoticeQuery → 后端 model/notice/NoticeQuery.java + BasePageQuery.java
|
||||
* - PushScopeItem / ScopeTreeNode / ScopePreviewVo → 改造方案 3.1 推送范围
|
||||
* - ReadReceiptVo → 后端 model/notice/ReadReceiptVo.java(阅读回执分组)
|
||||
* - ReadTabCountVo → 后端 model/notice/ReadTabCountVo.java(v17「已收到」三 tab 统计)
|
||||
* - SubmitRequest / ApproverSuggestVo → 审核改造(步骤表 + 审核记录表 + 记忆表)
|
||||
* - ApprovalRecordVo → 详情 approvalRecords 审批轨迹
|
||||
*/
|
||||
|
||||
// ============================ 分页外壳 ============================
|
||||
|
||||
/** 统一分页响应外壳(对齐 BasePageResult.java) */
|
||||
export interface BasePageResult<T> {
|
||||
total: number;
|
||||
content: T[];
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
}
|
||||
|
||||
/** 通知分页结果(对齐 NoticePageResult.java,扩展 unreadCount) */
|
||||
export interface NoticePageResult<T> extends BasePageResult<T> {
|
||||
/** 当前用户未读通知总数 */
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「已收到」三 tab 统计(对齐 ReadTabCountVo.java,v17)。
|
||||
* 口径 = GET /api/notice/List 在同一 keyword/type 等过滤下 readType=0/1/2 的 total,
|
||||
* readType 不是入参,故恒有 all = unread + read。
|
||||
*/
|
||||
export interface ReadTabCountVo {
|
||||
/** 全部(可见已收总数) */
|
||||
all: number;
|
||||
/** 未读 */
|
||||
unread: number;
|
||||
/** 已读 */
|
||||
read: number;
|
||||
}
|
||||
|
||||
// ============================ 通知列表项 ============================
|
||||
|
||||
/** 通知视图对象(对齐 NoticeVo.java,Jackson 默认驼峰序列化) */
|
||||
export interface NoticeVo {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
type: number;
|
||||
typeDesc: string;
|
||||
publisherAccount: string;
|
||||
publisherName: string;
|
||||
deptId: string;
|
||||
deptName: string;
|
||||
/** 状态:0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回 */
|
||||
status: number;
|
||||
statusDesc: string;
|
||||
topmost: number;
|
||||
/** 已读人数 */
|
||||
readCount: number;
|
||||
/** 应读人数 */
|
||||
allowCount: number;
|
||||
/** 当前用户是否已读 */
|
||||
read: boolean;
|
||||
/** 是否撤回:0 否 / 1 是 */
|
||||
isPulled: number;
|
||||
/** 定时发送时间 */
|
||||
timingSendTime: string | null;
|
||||
/** 父通知ID(转发时关联原通知) */
|
||||
parentId: number | null;
|
||||
/** 短信发送标记 */
|
||||
smsFlag: number;
|
||||
/** 接收对象类型:class=班级(学生场景,旧链路) */
|
||||
receiveType?: string;
|
||||
/** 接收列表:JSON 串("ALL"/账号数组)或 班级 id 逗号串(receiveType=class) */
|
||||
receiveList: string | null;
|
||||
/** 是否需要学员阅读回执:0 否 / 1 是 */
|
||||
receiptFlag?: number;
|
||||
/** 定时发送已真正发布标记:0 未触发(定时中)/ 1 已发布 */
|
||||
timingPublished?: number;
|
||||
/** 审批教师ID快照(JSON 数组串 [101,102],展示审核人用) */
|
||||
approverTeaIds?: string | null;
|
||||
/** 是否启用审批:0 免审直接发布 / 1 需审批后发布 */
|
||||
approvalEnabled: number;
|
||||
/** 审批状态:none 免审 / pending 待审批 / approved 已通过 / rejected 已驳回 */
|
||||
approvalStatus: ApprovalStatus;
|
||||
/** 审批状态文本(待审批带步骤进度,如「第1/2步 校级审核·会签 0/2」) */
|
||||
approvalStatusDesc: string;
|
||||
/** 当前待审步骤下标(从 0 起) */
|
||||
approvalLevel?: number;
|
||||
/** 当前用户是否为该单当前步骤的待签审批人 */
|
||||
canApprove?: boolean;
|
||||
/** 当前步待签审批人姓名(审批中心「当前审批人」列;非待审批态为 null) */
|
||||
pendingApproverNames?: string[] | null;
|
||||
/** 最新轮末条处理意见(驳回=驳回意见/通过=末步通过意见;待审批/免审为 null,「审批意见」列) */
|
||||
lastOpinion?: string | null;
|
||||
/** 附件ID串("1,2,3",列表页下载入口用) */
|
||||
UpFile?: string;
|
||||
createBy: string | null;
|
||||
createTime: string | null;
|
||||
updateBy: string | null;
|
||||
updateTime: string | null;
|
||||
}
|
||||
|
||||
/** 审批状态枚举 */
|
||||
export type ApprovalStatus = "none" | "pending" | "approved" | "rejected";
|
||||
|
||||
// ============================ 推送范围(改造方案 3.1) ============================
|
||||
|
||||
/** 推送范围项(发布/编辑入参与详情回显,对齐 PushScopeItem.java) */
|
||||
export interface PushScopeItem {
|
||||
/** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员 */
|
||||
scopeType: number;
|
||||
/** 组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId */
|
||||
scopeId: number;
|
||||
/** 节点名称快照 */
|
||||
scopeName: string;
|
||||
}
|
||||
|
||||
/** bschclass 组织树节点(base/getStuTree / base/getTeaTree,对齐 SchTreeNodeDto.java) */
|
||||
export interface SchTreeNode {
|
||||
/** 组=业务ID;人员叶子=stuId/teaId */
|
||||
id: number;
|
||||
name: string;
|
||||
pid: number;
|
||||
no: number;
|
||||
/** 1=组节点,2=人员叶子 */
|
||||
type: number;
|
||||
isParent?: boolean;
|
||||
photo?: string;
|
||||
/** 叶子为 null,组节点非 null */
|
||||
children?: SchTreeNode[] | null;
|
||||
}
|
||||
|
||||
/** 推送范围树节点(面板视图节点:由 base/getStuTree 自底向上归类 scopeType;全校根 + 学段→年级→班级) */
|
||||
export interface ScopeTreeNode {
|
||||
scopeType: number;
|
||||
scopeId: number;
|
||||
name: string;
|
||||
children?: ScopeTreeNode[];
|
||||
}
|
||||
|
||||
/** 推送范围预估 VO(notice/ScopePreview) */
|
||||
export interface ScopePreviewVo {
|
||||
/** 范围展开学员数(按账号去重) */
|
||||
studentCount: number;
|
||||
/** 教师定向名单数(receiveList 解析) */
|
||||
teacherCount: number;
|
||||
/** 合计预估接收人数 */
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
// ============================ 阅读回执 ============================
|
||||
|
||||
/** 已发送通知阅读回执(notice/ReadReceipt,按 年级→班级→学生 分组 + 教师组) */
|
||||
export interface ReadReceiptVo {
|
||||
serid: number;
|
||||
/** 应读人数 */
|
||||
allowCount: number;
|
||||
/** 已读人数 */
|
||||
readCount: number;
|
||||
grades: {
|
||||
gradeId: number;
|
||||
gradeName: string;
|
||||
classes: {
|
||||
classId: number;
|
||||
className: string;
|
||||
students: ReadReceiptReader[];
|
||||
}[];
|
||||
}[];
|
||||
/** 教师接收行(无年级/班级维度) */
|
||||
teachers: ReadReceiptReader[];
|
||||
}
|
||||
|
||||
/** 学生/教师阅读项 */
|
||||
export interface ReadReceiptReader {
|
||||
account: string;
|
||||
name: string;
|
||||
/** 是否已读 */
|
||||
read: boolean;
|
||||
/** 阅读时间(未读为 null) */
|
||||
readTime: string | null;
|
||||
}
|
||||
|
||||
// ============================ 审批提交/记忆 ============================
|
||||
|
||||
/** 提交审批入参(notice/Submit,对齐 SubmitRequest.java) */
|
||||
export interface SubmitRequest {
|
||||
/** 通知ID */
|
||||
serid: number;
|
||||
/** 多步审批流([{name, vote, approverTeaIds}],教师树路径);空=复用草稿已存流程定义 */
|
||||
steps?: { name?: string; vote: "all" | "any"; approverTeaIds: number[] }[];
|
||||
/** 提交说明(可选) */
|
||||
opinion?: string;
|
||||
}
|
||||
|
||||
/** 审批人记忆回显 VO(notice/ApproverSuggest,无记忆返回空列表) */
|
||||
export interface ApproverSuggestVo {
|
||||
teaIds: number[];
|
||||
}
|
||||
|
||||
// ============================ 通知详情 ============================
|
||||
|
||||
/** 通知详情视图对象(对齐 NoticeDetailVo.java,@JsonProperty 保留旧字段名) */
|
||||
export interface NoticeDetailVo {
|
||||
serid: number;
|
||||
title: string;
|
||||
/** 通知内容(旧字段名 Contents) */
|
||||
Contents: string;
|
||||
/** 发布部门ID(旧字段名 DeptID) */
|
||||
DeptID: string;
|
||||
/** 发布部门名称(旧字段名 DeptName) */
|
||||
DeptName: string;
|
||||
/** 发布人姓名(旧字段名 TeaName) */
|
||||
TeaName: string;
|
||||
/** 发布时间(旧字段名 EditDate,已格式化为字符串) */
|
||||
EditDate: string;
|
||||
/** 应读人数 */
|
||||
numcount: number;
|
||||
/** 已读人数 */
|
||||
readnumcount: number;
|
||||
/** 接收人列表 */
|
||||
ReceiveList: string | null;
|
||||
/** 定时发送时间(旧字段名 DsfsTime,已格式化) */
|
||||
DsfsTime: string | null;
|
||||
/** 是否定时发送 */
|
||||
isDsfs: boolean;
|
||||
/** 附件列表(旧字段名 ChaoSend) */
|
||||
ChaoSend: NoticeDetailFileItem[];
|
||||
/** 接收对象列表:"all" 或 账号/班级 id 数组 */
|
||||
tlist: string | string[];
|
||||
/** 接收对象类型:class=班级(旧链路;后端 Detail 暂未返回,旧数据续编不回填范围) */
|
||||
receiveType?: string;
|
||||
/** 重要程度:1 普通 / 2 重要(编辑回显) */
|
||||
type?: number;
|
||||
/** 是否置顶:0/1(编辑回显) */
|
||||
topmost?: number;
|
||||
/** 短信提醒标记:0/1(编辑回显) */
|
||||
smsFlag?: number;
|
||||
/** 状态:0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回 */
|
||||
status?: number;
|
||||
/** 状态描述 */
|
||||
statusDesc?: string;
|
||||
/** 是否需要学员阅读回执:0/1 */
|
||||
receiptFlag?: number;
|
||||
/** 推送范围快照(编辑回显) */
|
||||
pushScopes?: PushScopeItem[];
|
||||
/** 审批轨迹(详情页时间线:提交/通过/驳回按轮次留痕) */
|
||||
approvalRecords?: ApprovalRecordVo[];
|
||||
/** 当前绑定的审批教师ID快照(旧链路回显) */
|
||||
approverTeaIds?: number[];
|
||||
/** 是否需审批 */
|
||||
approvalEnabled?: number;
|
||||
/** 审批状态 */
|
||||
approvalStatus?: ApprovalStatus;
|
||||
/** 审批状态文本(待审批带步骤进度) */
|
||||
approvalStatusDesc?: string;
|
||||
/** 当前待审步骤下标(从 0 起) */
|
||||
approvalLevel?: number;
|
||||
/** 多步审批流进度(也用于发布/编辑回显审批人配置) */
|
||||
approvalFlow?: ApprovalFlowStep[];
|
||||
/** 当前用户是否为该单当前步骤的待签审批人 */
|
||||
canApprove?: boolean;
|
||||
}
|
||||
|
||||
/** 多步审批流步骤(详情 approvalFlow / 编辑回显,对齐 ApprovalFlowVo.java) */
|
||||
export interface ApprovalFlowStep {
|
||||
name: string;
|
||||
vote: "all" | "any";
|
||||
/** 审批人账号(编辑回显用) */
|
||||
approvers?: string[];
|
||||
/** 审批人教师ID(教师树选人回显用) */
|
||||
approverTeaIds?: number[];
|
||||
/** 审批人姓名(展示用,与 approvers 顺序一致) */
|
||||
approverNames?: string[];
|
||||
status: "approved" | "pending" | "rejected" | "waiting";
|
||||
records: { account: string; name: string; result: "approved" | "rejected"; time: string; remark: string }[];
|
||||
/** 当前步待签人姓名(仅审批中步骤返回) */
|
||||
pending: string[];
|
||||
}
|
||||
|
||||
/** 审批轨迹记录(详情 approvalRecords,对齐 ApprovalRecordVo.java) */
|
||||
export interface ApprovalRecordVo {
|
||||
/** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */
|
||||
action: number;
|
||||
actionDesc: string;
|
||||
operatorAccount: string;
|
||||
operatorName: string;
|
||||
/** 审批意见(提交行=提交说明) */
|
||||
opinion: string;
|
||||
/** 提交行(action=1)选定的审批教师ID快照(用户中心 teaId;v15 前端补声明,用于「选定审批人 N 人」) */
|
||||
assigneeTeaIds?: number[];
|
||||
/** 操作时间 */
|
||||
actionTime: string;
|
||||
}
|
||||
|
||||
/** 通知详情附件项(对齐 NoticeDetailVo.FileItem) */
|
||||
export interface NoticeDetailFileItem {
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
fileid: number;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
// ============================ 文件上传 ============================
|
||||
|
||||
/** 文件上传结果(对齐 UpFileResultVo.java,@JsonProperty) */
|
||||
export interface UpFileResultVo {
|
||||
fileid: number;
|
||||
name: string;
|
||||
fileurl: string;
|
||||
host: string;
|
||||
type: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/** 附件视图对象(对齐 UpFileVo.java) */
|
||||
export interface UpFileVo {
|
||||
id: number;
|
||||
fileTrueName: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
fileType: string;
|
||||
topicId: number;
|
||||
topicType: string;
|
||||
}
|
||||
|
||||
// ============================ 一键已读 ============================
|
||||
|
||||
/** 一键已读结果(对齐 InitializeRedVo.java,@JsonProperty) */
|
||||
export interface InitializeRedVo {
|
||||
count: number;
|
||||
}
|
||||
|
||||
// ============================ 上一篇/下一篇 ============================
|
||||
|
||||
/** 上一篇/下一篇通知ID */
|
||||
export interface NextPrevVo {
|
||||
prevId: number;
|
||||
nextId: number;
|
||||
}
|
||||
|
||||
// ============================ 阅读情况 ============================
|
||||
|
||||
/** 阅读情况分组项(后端 Map 返回,字段名固定) */
|
||||
export interface ReadGroup {
|
||||
GroupName: string;
|
||||
teaslist: ReadGroupUser[];
|
||||
}
|
||||
|
||||
/** 阅读情况用户项(后端 Map 返回) */
|
||||
export interface ReadGroupUser {
|
||||
account: string;
|
||||
teaname: string;
|
||||
states: 'read' | 'unread';
|
||||
}
|
||||
|
||||
// ============================ 统计 ============================
|
||||
|
||||
/** 通知统计视图(对齐 NoticeStatisticsVo.java) */
|
||||
export interface NoticeStatisticsVo {
|
||||
total: number;
|
||||
publishedCount: number;
|
||||
totalReadRecords: number;
|
||||
unreadForCurrentUser: number;
|
||||
byPublisher: { publisherName: string; count: number }[];
|
||||
byMonth: { year: number; month: number; count: number }[];
|
||||
}
|
||||
|
||||
// ============================ 请求入参 ============================
|
||||
|
||||
/** 通知发布/编辑入参(对齐 NoticeRequest.java) */
|
||||
export interface NoticeRequest {
|
||||
title: string;
|
||||
content?: string;
|
||||
/** 重要程度:1 普通 / 2 重要 */
|
||||
type?: number;
|
||||
deptId: string;
|
||||
deptName?: string;
|
||||
topmost?: number;
|
||||
/** 教师定向名单(v3 起写通知表单已下线;仅旧链路/详情口径保留字段) */
|
||||
receiveList?: string;
|
||||
/** 接收对象类型:class=班级(旧链路) */
|
||||
receiveType?: string;
|
||||
/** 推送范围列表(学员):pushScopes 解析学员,receiveList 作为教师定向名单 */
|
||||
pushScopes?: PushScopeItem[];
|
||||
/** 附件文件ID列表(逗号分隔串) */
|
||||
fileIds?: string;
|
||||
timingSendTime?: string;
|
||||
/** 短信发送标记 */
|
||||
smsFlag?: number;
|
||||
/** 是否需要学员阅读回执:0 否 / 1 是 */
|
||||
receiptFlag?: number;
|
||||
/** 发布方式:direct 直接发布(默认)/ draft 存为草稿 / approve 提交审批 */
|
||||
publishType?: "direct" | "draft" | "approve";
|
||||
/** 审批人账号列表(单步:选人即送审;空=不送审直接发布) */
|
||||
approvers?: string[];
|
||||
/** 单步表决:any=或签(默认)/ all=会签 */
|
||||
vote?: "all" | "any";
|
||||
/** 多步审批流(教师树路径):[{name,vote,approverTeaIds}],后端按学校教师名单绑定姓名 */
|
||||
flow?: { name?: string; vote: "all" | "any"; approverTeaIds: number[] }[];
|
||||
/** 旧版审批教师ID快照(编辑态传空数组=清空旧快照,对齐 api-test buildNoticeBody) */
|
||||
approverTeaIds?: number[];
|
||||
/** 转发源通知ID */
|
||||
sourceId?: number;
|
||||
}
|
||||
|
||||
/** 通知列表查询参数(对齐 NoticeQuery.java + BasePageQuery.java) */
|
||||
export interface NoticeQuery {
|
||||
pageIndex?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
/** 状态过滤:0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回;不传=默认排除草稿 */
|
||||
status?: number;
|
||||
/** 类型过滤:1 普通 / 2 重要 */
|
||||
type?: number;
|
||||
/** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */
|
||||
scope?: string;
|
||||
deptId?: string;
|
||||
publisherAccount?: string;
|
||||
/** 开始时间(yyyy-MM-dd 短日期) */
|
||||
startTime?: string;
|
||||
/** 结束时间(yyyy-MM-dd 短日期) */
|
||||
endTime?: string;
|
||||
/** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */
|
||||
readType?: number;
|
||||
/** 排序:1 时间倒序 / 2 时间正序 */
|
||||
orderByTime?: string;
|
||||
}
|
||||
|
||||
// ============================ 字典/联系组 ============================
|
||||
|
||||
/** 系统字典下拉项 */
|
||||
export interface DictOption {
|
||||
k: number | string;
|
||||
v: string | null;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 发文部门管理列表项 */
|
||||
export interface DeptManageItem {
|
||||
SerID: number;
|
||||
CodeID: string;
|
||||
Name: string;
|
||||
Enable: number;
|
||||
}
|
||||
|
||||
/** 联系组列表项 */
|
||||
export interface ContactGroupItem {
|
||||
serid: number;
|
||||
GroupName: string;
|
||||
}
|
||||
|
||||
/** 联系组分页结果 */
|
||||
export interface ContactGroupPage {
|
||||
list: ContactGroupItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 联系组用户项 */
|
||||
export interface ContactUser {
|
||||
account: string;
|
||||
name: string;
|
||||
userId?: number;
|
||||
id?: number;
|
||||
}
|
||||
|
||||
/** 联系组用户分页结果 */
|
||||
export interface ContactUserPage {
|
||||
list: ContactUser[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 组织树节点(基础平台返回) */
|
||||
export interface SchoolTreeNode {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// ============================ 班级 / 审批 ============================
|
||||
|
||||
/** 年级→班级树节点(叶子=班级,id 为数字班级 id,附学生数) */
|
||||
export interface ClassTreeNode {
|
||||
id: number;
|
||||
name: string;
|
||||
isParent: boolean;
|
||||
studentCount?: number;
|
||||
children?: ClassTreeNode[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 审批列表查询参数 */
|
||||
export interface ApprovalQuery {
|
||||
pageIndex?: number;
|
||||
pageSize?: number;
|
||||
/** 审批状态 tab:pending / approved / rejected / all(缺省 pending;v7 对齐后端新参数名) */
|
||||
auditState?: string;
|
||||
/** @deprecated 旧参数名,后端别名兼容保留;新代码用 auditState */
|
||||
status?: string;
|
||||
keyword?: string;
|
||||
/** "user" 只看我提交的审批 */
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
/** 审批中心四 tab 统计(v13:GET /api/notice/ApprovalTabCount,口径与各 tab 默认过滤一致、不含 keyword) */
|
||||
export interface ApprovalTabCountVo {
|
||||
/** 待审批(当前步审批人快照含我) */
|
||||
pending: number;
|
||||
/** 已通过(走流程发布) */
|
||||
approved: number;
|
||||
/** 已驳回 */
|
||||
rejected: number;
|
||||
/** 全部(已审合一) */
|
||||
all: number;
|
||||
}
|
||||
|
||||
/** 审批操作入参 */
|
||||
export interface ApprovalAction {
|
||||
serid: number;
|
||||
/** 驳回意见 */
|
||||
remark?: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @description 扩展ruoter-meta的类型 此处必须要export {} 不然找不到类型
|
||||
*/
|
||||
declare module "vue-router" {
|
||||
interface RouteMeta {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
modId?: number;
|
||||
parentId?: number;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,13 @@
|
||||
import Vue, { VNode } from "vue";
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
// tslint:disable no-empty-interface
|
||||
interface Element extends VNode {}
|
||||
// tslint:disable no-empty-interface
|
||||
interface ElementClass extends Vue {}
|
||||
interface IntrinsicElements {
|
||||
[elem: string]: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { createAlova, Method, StatesHook } from "alova";
|
||||
import fetchAdapter from "alova/fetch";
|
||||
import VueHook from "alova/vue";
|
||||
import Utils from "..";
|
||||
/* ============= 类型定义 ============= */
|
||||
export interface RequestHooks {
|
||||
/** 前置票据/通用头注入 */
|
||||
injectHeaders?: (method: Method) => void;
|
||||
/** 加载开始 */
|
||||
onLoadingStart?: () => void;
|
||||
/** 加载结束 */
|
||||
onLoadingEnd?: () => void;
|
||||
/** 业务错误提示(code !== 0)*/
|
||||
onBizError?: (msg: string) => void;
|
||||
/** http 错误提示(status !== 200)*/
|
||||
onHttpError?: (msg: string) => void;
|
||||
}
|
||||
|
||||
/** 和后端约定的通用响应格式 */
|
||||
export interface BschResponse<T = any> {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data: T;
|
||||
urlToRedirectTo?: string;
|
||||
}
|
||||
|
||||
/* ============= 默认空实现 ============= */
|
||||
const defHooks: Required<RequestHooks> = {
|
||||
injectHeaders: () => {},
|
||||
onLoadingStart: () => {},
|
||||
onLoadingEnd: () => {},
|
||||
onBizError: () => {},
|
||||
onHttpError: () => {},
|
||||
};
|
||||
|
||||
/* ============= 错误处理(原 axios 逻辑平移) ============= */
|
||||
function errorHandle<T>(res: BschResponse<T>, status: number): T {
|
||||
// http 层错误
|
||||
if (status !== 200) {
|
||||
let msg = "网络错误";
|
||||
switch (status) {
|
||||
case 401:
|
||||
case 403:
|
||||
break;
|
||||
case 404:
|
||||
msg = "请求的资源不存在!";
|
||||
break;
|
||||
default:
|
||||
msg = res?.msg || "内部服务错误!";
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// 业务层错误
|
||||
switch (res.code) {
|
||||
case 0:
|
||||
return res.data ?? (null as T);
|
||||
case 302:
|
||||
console.log(res);
|
||||
// 后端 CasSsoApiRedirectFilter 已在 service 中拼好 ;redirectToReferer=<hex>,
|
||||
// 直接跳转即可;登录后由 CasSsoRefererRedirectFilter 解码 302 回前端页。
|
||||
// 跳转后返回永不 resolve 的挂起 Promise:调用方 await 悬停,
|
||||
// 全局 onBizError 与各页面的 catch 均不再触发(不弹 redirect 报错)
|
||||
window.location.href = res.urlToRedirectTo!;
|
||||
return new Promise(() => {}) as T;
|
||||
case 4:
|
||||
throw new Error(res.msg || "参数错误!");
|
||||
default:
|
||||
throw new Error(res.msg || "业务错误");
|
||||
}
|
||||
}
|
||||
|
||||
/* ============= 工厂函数 ============= */
|
||||
export function createBschAlova(
|
||||
baseURL: string,
|
||||
hooks: RequestHooks = {},
|
||||
statesHook: StatesHook<any> = VueHook,
|
||||
) {
|
||||
const { injectHeaders, onLoadingStart, onLoadingEnd, onBizError, onHttpError } = {
|
||||
...defHooks,
|
||||
...hooks,
|
||||
};
|
||||
|
||||
let reqCount = 0;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
|
||||
/* ---- 请求拦截 ---- */
|
||||
function beforeRequest(method: Method) {
|
||||
reqCount++;
|
||||
if (reqCount === 1) timer = setTimeout(() => onLoadingStart(), 800);
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
if (reqCount === 0) {
|
||||
clearTimeout(timer);
|
||||
onLoadingEnd();
|
||||
}
|
||||
});
|
||||
// 1. 用户注入票据/角色等头
|
||||
injectHeaders(method);
|
||||
|
||||
method.config.credentials = "include";
|
||||
// 2. 通用头
|
||||
method.config.headers = Object.assign(method.config.headers || {}, {
|
||||
yschapirequest: "1",
|
||||
timeout: 20000,
|
||||
yschreferer: Utils.strToHexCharCode(window.location.href),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- 响应拦截 ---- */
|
||||
async function onSuccess(response: Response) {
|
||||
reqCount--;
|
||||
if (reqCount === 0) {
|
||||
clearTimeout(timer);
|
||||
onLoadingEnd();
|
||||
}
|
||||
const res: BschResponse = await response.json();
|
||||
try {
|
||||
return errorHandle(res, response.status);
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
|
||||
onBizError(e.message);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onError(err: any) {
|
||||
reqCount = 0;
|
||||
clearTimeout(timer);
|
||||
onLoadingEnd();
|
||||
|
||||
const msg = err?.message || "网络请求出错";
|
||||
onHttpError(msg);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
async function onComplete(method: Method) {
|
||||
if (method.fromCache) {
|
||||
reqCount--;
|
||||
if (reqCount === 0) {
|
||||
clearTimeout(timer);
|
||||
onLoadingEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createAlova({
|
||||
statesHook,
|
||||
requestAdapter: fetchAdapter(),
|
||||
baseURL,
|
||||
cacheFor: null,
|
||||
beforeRequest,
|
||||
responded: { onSuccess, onError, onComplete },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// @/utils/alova-iview.ts
|
||||
import { createBschAlova } from "./alova-core";
|
||||
import { Message } from "view-ui-plus";
|
||||
import conf from "@/assets/config/host.config";
|
||||
|
||||
/* 全局 loading 遮罩实例 */
|
||||
let spinInstance: any = null;
|
||||
|
||||
/* 主动关闭 loading */
|
||||
function closeLoading() {
|
||||
if (spinInstance) {
|
||||
Message.destroy();
|
||||
spinInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
const baseUrl = conf.serviceHost;
|
||||
|
||||
/* 创建带 View Design UI 的 Alova 实例 */
|
||||
export const alovaInstance = createBschAlova(baseUrl, {
|
||||
injectHeaders(method) {
|
||||
// currentRole: encodeURI(JSON.stringify({ userId, roleId, schId: schoolId })),
|
||||
if (localStorage.getItem("currentRole") != null) {
|
||||
method.config.headers["currentRole"] = localStorage.getItem("currentRole") as string;
|
||||
}
|
||||
},
|
||||
|
||||
/* 2. 加载动画 – View Design Spin */
|
||||
onLoadingStart() {
|
||||
spinInstance = Message.loading("加载中...");
|
||||
},
|
||||
onLoadingEnd: closeLoading,
|
||||
onBizError(msg: string) {
|
||||
if (!!msg) Message.error(msg);
|
||||
},
|
||||
|
||||
onHttpError(msg: string) {
|
||||
Message.error({ title: "网络错误", content: msg });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
// @/utils/alova-vant.ts
|
||||
import { createBschAlova } from "./alova-core";
|
||||
import { Toast, Dialog, showFailToast, showDialog, showLoadingToast, closeToast } from "vant";
|
||||
import conf from "@/assets/config/host.config";
|
||||
|
||||
import { useBschTicketStore } from "@/store/TicketStore";
|
||||
import { WappUserHeader } from "@/types/Basics";
|
||||
|
||||
let toastLoading: ReturnType<typeof Toast.loading> | null = null;
|
||||
|
||||
/* 主动关闭 loading */
|
||||
function closeLoading() {
|
||||
if (toastLoading) {
|
||||
toastLoading.close();
|
||||
toastLoading = null;
|
||||
} else {
|
||||
closeToast();
|
||||
}
|
||||
}
|
||||
|
||||
// const baseUrl = conf.serviceHost + "/api";
|
||||
const baseUrl = conf.serviceHost + "/mobiapi";
|
||||
/* 创建带 Vant UI 的 Alova 实例 */
|
||||
export const alovaInstance = createBschAlova(baseUrl, {
|
||||
injectHeaders(method) {
|
||||
const store = useBschTicketStore();
|
||||
const authoHeaders = store.getAuthoHeaders;
|
||||
const userHeaders = store.getTicket;
|
||||
|
||||
const bschTicket: WappUserHeader = store.getTicket;
|
||||
|
||||
for (const key in authoHeaders) method.config.headers[key] = authoHeaders[key];
|
||||
for (const key in bschTicket) method.config.headers[key] = (bschTicket as any)[key];
|
||||
|
||||
// method.config.headers.currentRole = encodeURIComponent(
|
||||
// JSON.stringify({ roleId: userHeaders.roleId, childId: userHeaders.userId, schId: userHeaders.schoolId }),
|
||||
// );
|
||||
},
|
||||
/* 2. 加载动画 – Vant Toast */
|
||||
onLoadingStart() {
|
||||
toastLoading = showLoadingToast({
|
||||
message: "加载中...",
|
||||
forbidClick: true,
|
||||
duration: 0,
|
||||
});
|
||||
},
|
||||
onLoadingEnd: closeLoading,
|
||||
onBizError(msg) {
|
||||
showFailToast(msg);
|
||||
},
|
||||
onHttpError(msg) {
|
||||
showDialog({ title: "网络错误", message: msg });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { BschResponse, AxiosResponse } from "axios";
|
||||
import axios from "axios";
|
||||
import { initAxiosInstance } from "./interceptor";
|
||||
|
||||
import { baseConfig } from "@/assets/config/api.config";
|
||||
|
||||
export interface BschResult {
|
||||
url: string;
|
||||
params?: object;
|
||||
data?: object;
|
||||
isMsg?: boolean; //是否自动显示错误提示
|
||||
}
|
||||
|
||||
class Abstract {
|
||||
private apiAxios<T>({ method, url, params, data, isMsg = true }: any): Promise<T> {
|
||||
const instance = initAxiosInstance(baseConfig);
|
||||
return new Promise((resolve, reject) => {
|
||||
instance({
|
||||
method,
|
||||
url,
|
||||
params,
|
||||
data,
|
||||
})
|
||||
.then((res: AxiosResponse<BschResponse, number>) => {
|
||||
// console.log(res);
|
||||
let { data } = res;
|
||||
resolve(data as T);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
// throw err;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*GET类型的网络请求
|
||||
*
|
||||
* @protected
|
||||
* @template T
|
||||
* @param {BschResult} { url, params }
|
||||
* @returns {Promise<T>}
|
||||
* @memberof Abstract
|
||||
*/
|
||||
protected getRequest<T>({ url, params, isMsg = true }: BschResult): Promise<T> {
|
||||
return this.apiAxios<T>({ method: "GET", url, params, isMsg });
|
||||
}
|
||||
|
||||
/**
|
||||
*POST类型的网络请求
|
||||
*
|
||||
* @protected
|
||||
* @template T
|
||||
* @param {BschResult} { url, data }
|
||||
* @returns {Promise<T>}
|
||||
* @memberof Abstract
|
||||
*/
|
||||
protected postRequest<T>({ url, params, data, isMsg = true }: BschResult): Promise<T> {
|
||||
return this.apiAxios<T>({ method: "POST", url, params, data, isMsg });
|
||||
}
|
||||
|
||||
/**
|
||||
*PUT类型的网络请求
|
||||
*
|
||||
* @protected
|
||||
* @template T
|
||||
* @param {BschResult} { url, data }
|
||||
* @returns {Promise<T>}
|
||||
* @memberof Abstract
|
||||
*/
|
||||
protected putRequest<T>({ url, params, data, isMsg = true }: BschResult): Promise<T> {
|
||||
return this.apiAxios<T>({ method: "PUT", url, params, data, isMsg });
|
||||
}
|
||||
|
||||
/**
|
||||
*DELETE类型的网络请求
|
||||
*
|
||||
* @protected
|
||||
* @template T
|
||||
* @param {BschResult} { url, params }
|
||||
* @returns {Promise<T>}
|
||||
* @memberof Abstract
|
||||
*/
|
||||
protected deleteRequest<T>({ url, params, data, isMsg = true }: BschResult): Promise<T> {
|
||||
return this.apiAxios<T>({ method: "DELETE", url, params, data, isMsg });
|
||||
}
|
||||
}
|
||||
|
||||
export default Abstract;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { AxiosResponse, BschResponse } from "axios";
|
||||
|
||||
function formrHandle(response: AxiosResponse) {
|
||||
let errorMsg: string = "";
|
||||
let { data, status } = response;
|
||||
if (status == 200) {
|
||||
switch (data.code) {
|
||||
case 0:
|
||||
return data;
|
||||
case 4:
|
||||
errorMsg = data.msg;
|
||||
break;
|
||||
case 302:
|
||||
errorMsg = "redirect";
|
||||
window.location.href = data.urlToRedirectTo;
|
||||
break;
|
||||
// errorMsg = "redirect";
|
||||
default:
|
||||
errorMsg = data.msg;
|
||||
}
|
||||
} else {
|
||||
switch (status) {
|
||||
case 401:
|
||||
break;
|
||||
case 403:
|
||||
break;
|
||||
case 404:
|
||||
errorMsg = "请求的资源不存在!";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!errorMsg) errorMsg = typeof data.code == "undefined" ? "内部服务错误!" : data.msg;
|
||||
}
|
||||
throw errorMsg;
|
||||
}
|
||||
|
||||
function formrHandle2(response: AxiosResponse<BschResponse, number>) {
|
||||
let errorMsg: string = "";
|
||||
let { data, status } = response;
|
||||
if (status == 200) {
|
||||
if (!!data.State) return response;
|
||||
else errorMsg = data.Message || JSON.stringify(data.Data);
|
||||
} else {
|
||||
switch (status) {
|
||||
case 401:
|
||||
errorMsg = "您的登录已过期!";
|
||||
break;
|
||||
case 403:
|
||||
break;
|
||||
case 404:
|
||||
errorMsg = "请求的资源不存在!";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!errorMsg) errorMsg = typeof data.State == "undefined" ? "内部服务错误!" : data.Message;
|
||||
}
|
||||
throw errorMsg;
|
||||
}
|
||||
|
||||
export default formrHandle;
|
||||