Merge pull request 'dev' (#8) from dev into master

Reviewed-on: #8
This commit is contained in:
hy 2025-08-26 19:03:28 +08:00
commit 6ff7c1f06e
61 changed files with 8522 additions and 772 deletions

View File

@ -7,4 +7,8 @@ VITE_PUBLIC_PATH = /
# 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数" # 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"
VITE_API_BASEURL = "http://localhost:5199/api" VITE_API_BASEURL = "http://192.168.2.33:5199/api"
# 接口地址
# VITE_API_BASEURL = "http://localhost:5199/api"
#数据中心后台地址
VITE_API_USERCENTER_URL = "https://dca.w.23544.com:8843/api"

View File

@ -10,4 +10,10 @@ VITE_CDN = false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件 # 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认 # 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认 # 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none" VITE_COMPRESSION = "none"
# 接口地址
VITE_API_BASEURL = "https://learn-archives-admin.23544.com/api"
#数据中心后台地址
VITE_API_USERCENTER_URL = "https://dcb.23544.com/api"

View File

@ -8,9 +8,15 @@ VITE_PUBLIC_PATH = /
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false # 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = true VITE_CDN = false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件 # 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认 # 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认 # 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none" VITE_COMPRESSION = "none"
# 接口地址
VITE_API_BASEURL = "https://learn-archives-admin-dev.23544.com/api"
#数据中心后台地址
VITE_API_USERCENTER_URL = "https://dca.w.23544.com:8843/api"

4
.gitignore vendored
View File

@ -19,4 +19,6 @@ tests/**/coverage/
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
.vscode
.vscode/settings.json

View File

@ -1,8 +0,0 @@
#!/bin/sh
# shellcheck source=./_/husky.sh
. "$(dirname "$0")/_/husky.sh"
PATH="/usr/local/bin:$PATH"
npx --no-install commitlint --edit "$1"

View File

@ -1,9 +0,0 @@
#!/bin/sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# Workaround for Windows 10, Git Bash and Pnpm
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi

View File

@ -1,10 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
[ -n "$CI" ] && exit 0
PATH="/usr/local/bin:$PATH"
# Perform lint check on files in the staging area through .lintstagedrc configuration
pnpm exec lint-staged

View File

@ -1,13 +1,14 @@
{ {
"editor.formatOnType": true, "editor.formatOnType": true,
"editor.formatOnSave": true, "editor.formatOnSave": false,
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.formatOnPaste": true, "editor.formatOnPaste": true,
"editor.guides.bracketPairs": "active", "editor.guides.bracketPairs": "active",
"files.autoSave": "afterDelay", // "files.autoSave": "afterDelay",
"files.autoSave": "off",
"git.confirmSync": false, "git.confirmSync": false,
"workbench.startupEditor": "newUntitledFile", "workbench.startupEditor": "newUntitledFile",
"editor.suggestSelection": "first", "editor.suggestSelection": "first",
@ -40,4 +41,5 @@
"v-ripple" "v-ripple"
], ],
"vscodeCustomCodeColor.highlightValueColor": "#b392f0", "vscodeCustomCodeColor.highlightValueColor": "#b392f0",
"vue3snippets.enable-compile-vue-file-on-did-save-code": true,
} }

View File

@ -0,0 +1,41 @@
# 构建阶段
FROM m.daocloud.io/docker.io/library/node:22.14.0 AS builder
# 设置工作目录
WORKDIR /app
# 设置 npm 镜像源
RUN npm config set registry https://registry.npmmirror.com/
RUN npm config set fetch-retries 3
RUN npm config set fetch-retry-mintimeout 5000
RUN npm config set fetch-retry-maxtimeout 60000
# 安装pnpm
RUN npm install -g pnpm
# 复制源代码
COPY . .
# 设置 pnpm 下载源
RUN pnpm config set registry https://registry.npmmirror.com/
# 安装依赖
RUN pnpm i --fetch-timeout 300000
# 构建项目
RUN pnpm build:staging
# 部署阶段
FROM m.daocloud.io/docker.io/library/nginx:alpine
# 复制构建产物到 Nginx 目录
COPY --from=builder --chown=nginx:nginx /app/dist /usr/share/nginx/html
# 复制 Nginx 配置
COPY Dockerfiles/Dockerfile-staging/default.conf /etc/nginx/conf.d/default.conf
# 暴露端口
EXPOSE 80
# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,47 @@
server {
listen 80;
server_name localhost;
# 基础设置
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
client_max_body_size 100m;
# 错误页面配置
error_page 500 502 503 504 /50x.html;
location = /50x.html {
internal; # 仅用于内部错误请求
}
# Gzip 压缩设置
gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css text/javascript application/json
application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
# 静态资源缓存优化
location ~* \.(jpg|jpeg|gif|ico|css|js)$ {
expires 7d;
add_header Cache-Control "public, no-transform";
}
# API 代理配置
location /api/ {
proxy_pass http://learn-archives-api-svc:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # 添加协议头
# 优化代理性能
proxy_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering on;
}
}

View File

@ -2,6 +2,10 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" /> <meta name="renderer" content="webkit" />
<meta <meta

View File

@ -1,3 +1,4 @@
import { ComboModel } from "@/components/hTable/hTable";
import { http } from "@/utils/http"; import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types"; import type { Res } from "@/utils/http/types";
@ -7,7 +8,7 @@ import type { Res } from "@/utils/http/types";
* @return {object} * @return {object}
*/ */
export function getenum(type) { export function getenum(type) {
return http.request<Res<any>>("get", `public/enum/${type}`); return http.request<Res<ComboModel[]>>("get", `Public/enum/${type}`);
} }
/** /**
* @description * @description
@ -15,5 +16,5 @@ export function getenum(type) {
* @return {object} * @return {object}
*/ */
export function getenumDic(type) { export function getenumDic(type) {
return http.request<Res<any>>("get", `public/enum/${type}/Dic`); return http.request<Res<any>>("get", `Public/enum/${type}/Dic`);
} }

35
src/api/exam.ts Normal file
View File

@ -0,0 +1,35 @@
import { http } from "@/utils/http";
// import type { Res } from "@/utils/http/types";
/**
* @description
* @return {object}
*/
export function ImportExamInfo(id: number, file: File) {
let formData = new FormData();
formData.append("eId", id.toString());
formData.append("file", file);
return http.request<any>(
"post",
`ExamClassInfo/Import`,
{
data: formData
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
responseType: "blob"
}
);
}
/**
* @description
* @return {object}
*/
export function DeleteExamInfo(data: { classId: number; examId: number }) {
return http.request<any>("post", `ExamClassInfo/DeleteExamInfo`, {
data
});
}

View File

@ -1,5 +1,6 @@
import { http } from "@/utils/http"; import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types"; import type { Res } from "@/utils/http/types";
import type { ComboModel } from "@/components/hTable/hTable";
export class hTableAPI { export class hTableAPI {
url = ""; url = "";
@ -10,13 +11,9 @@ export class hTableAPI {
PageList(data = {}) { PageList(data = {}) {
return http.request<Res<any>>("post", `${this.url}/PageList`, { data }); return http.request<Res<any>>("post", `${this.url}/PageList`, { data });
} }
Info(id, tag = {}) { Info(tag) {
const pUrl = `${this.url}/${id}`; const pUrl = `${this.url}/${tag}`;
let getUrl = pUrl; let getUrl = pUrl;
for (const key in tag) {
const el = tag[key];
getUrl += (getUrl === pUrl ? "?" : "&") + key + "=" + el;
}
return http.request<Res<any>>("get", getUrl); return http.request<Res<any>>("get", getUrl);
} }
edit(data) { edit(data) {
@ -26,6 +23,9 @@ export class hTableAPI {
return http.request<Res<any>>("post", `${this.url}/Del`, { data }); return http.request<Res<any>>("post", `${this.url}/Del`, { data });
} }
querycombo(data) { querycombo(data) {
return http.request<Res<any>>("post", `${this.url}/QueryCombo`, { data }); return http.request<Res<ComboModel[]>>("post", `${this.url}/QueryCombo`, {
data
});
} }
} }

View File

@ -3,7 +3,7 @@ import type { Res } from "@/utils/http/types";
// 定义菜单项接口 // 定义菜单项接口
export interface MenuItem { export interface MenuItem {
id: number; id?: number;
name: string; name: string;
path?: string; path?: string;
isButton: boolean; isButton: boolean;
@ -22,3 +22,28 @@ export interface MenuItem {
export function MenuAll() { export function MenuAll() {
return http.request<Res<MenuItem[]>>("get", `Menu/All`); return http.request<Res<MenuItem[]>>("get", `Menu/All`);
} }
/**
* @description
* @return {object}
*/
export function Edit(info: MenuItem) {
return http.request<Res<MenuItem[]>>("post", `Menu/Edit`, { data: info });
}
/**
* @description
* @return {object}
*/
export function Del(ids: number[]) {
return http.request<Res<MenuItem[]>>("post", `Menu/Del`, { data: ids });
}
/**获取角色的菜单 */
export function RoleMenu(roleId: number) {
return http.request<Res<number[]>>("get", `MenuRelation/RoleMenu?roleId=${roleId}`);
}
/**修改角色菜单 */
export function SetMenu(data: { roleId: number; menuId: number[] }) {
return http.request<Res<MenuItem[]>>("post", `MenuRelation/SetMenu`, {
data: data
});
}

30
src/api/school.ts Normal file
View File

@ -0,0 +1,30 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenum(type) {
return http.request<Res<any>>("get", `public/enum/${type}`);
}
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenumDic(type) {
return http.request<Res<any>>("get", `public/enum/${type}/Dic`);
}
export function getProvince() {
return http.request<Res<any>>("get", `address/province`);
}
export function getcity(c) {
return http.request<Res<any>>("get", `address/${c}/city`);
}
export function getregion(r) {
return http.request<Res<any>>("get", `address/${r}/region`);
}

74
src/api/student.ts Normal file
View File

@ -0,0 +1,74 @@
import { http } from "@/utils/http";
import { Res } from "@/utils/http/types";
// import type { Res } from "@/utils/http/types";
/**
* @description
* @return {object}
*/
export function ImportTeacher(file: File) {
let formData = new FormData();
formData.append("file", file);
return http.request<any>(
"post",
`Student/ImportTeacher`,
{
data: formData
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
responseType: "blob"
}
);
}
/**
* @description
* @return {object}
*/
export function ImportStudent(file: File) {
let formData = new FormData();
formData.append("file", file);
return http.request<any>(
"post",
`Student/Import`,
{
data: formData
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
responseType: "blob"
}
);
}
/**
* @description
* @return {object}
*/
export function EditStudent(data) {
return http.request<Res<any>>("post", `Student/EditInfo`, {
data
});
}
/**
* @description PageList
* @return {object}
*/
export function PageList(data) {
return http.request<Res<any>>("post", `Student/PageList`, {
data
});
}
/**
* @description StudentInfo
* @return {object}
*/
export function StudentInfo(uid) {
return http.request<Res<any>>("get", `Student/Info?uid=${uid}`);
}

View File

@ -0,0 +1,79 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenumApi(data) {
return http.request<Res<any>>("post", `/SchoolBusiness/QueryCombo`, {
data
});
}
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getPageListApi(data) {
return http.request<Res<any>>("post", `/SchoolBusiness/QueryPageList`, {
data
});
}
/**
* @description id:0()
* @return {object}
*/
export function addOrEditApi(data: any) {
return http.request<Res<any>>("post", `/SchoolBusiness/Edit`, {
data
});
}
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getSchoolBusinessDetailApi(id: string | number) {
return http.request<Res<any>>("get", `/SchoolBusiness/${id}`);
}
/**
* @description
* @return {object}
*/
export function deleteSchoolBusinessApi(data: Array<string | number>) {
return http.request<Res<any>>("post", `/SchoolBusiness/Del`, {
data
});
}
/**
* @description
* @return {object}
*/
export function getSchoolBusinessPeopleListApi(data: object) {
return http.request<Res<any>>("post", `/Admin/QueryCombo`, {
data
});
}
/**
* @description excel
* @return {object}
*/
export function importExcel(file: File) {
let formData = new FormData();
formData.append("file", file);
return http.request<any>(
"post",
`SchoolBusiness/Import`,
{
data: formData
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
responseType: "blob"
}
);
}

201
src/api/userCenter.ts Normal file
View File

@ -0,0 +1,201 @@
import { ComboModel } from "@/components/hTable/hTable";
import { http } from "@/utils/http";
import type { Res, ResPage } from "@/utils/http/types";
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getPageUserList(data: any) {
return http.request<ResPage<UserDetail[]>>(
"post",
`userCenter/back/users/getpageuserlist`,
{
data
}
);
}
/**
* @description
* @return {void}
*/
export function EditSchool(data: any) {
return http.request<Res<any>>("post", `userCenter/back/schools/add`, {
data
});
}
/**
* @description
* @return {void}
*/
export function getSchoolData() {
return http.request<Res<ComboModel[]>>(
"get",
`userCenter/public/getschooldata`
);
}
/**
* @description
* @return {void}
*/
export function getPositionList(data: any) {
return http.request<Res<any>>(
"post",
`userCenter/back/positions/getpositionlist`,
{ data }
);
}
/**
* @description
* @return {object}
*/
export function cloudSchoolCombo() {
let data = {
ValueName: "Id",
TextName: "Name"
};
return http.request<Res<any>>(
"post",
`userCenter/back/cloudschool/querycombo`,
{
data
}
);
}
/**
* @description
* @return {object}
*/
export function getClassInfo(id) {
return http.request<Res<any>>("get", `userCenter/back/classes/${id}`);
}
/**
* @description
* @return {object}
*/
export function getSchoolInfo(id) {
return http.request<Res<any>>("get", `userCenter/back/schools/${id}`);
}
/**
* @description
* @return {object}
*/
export function getUserInfo(id) {
return http.request<Res<any>>(
"get",
`userCenter/back/users/getuserinfo?id=${id}`
);
}
/**
* @description
* @return {object}
*/
export function addClasses(info: any) {
return http.request<Res<any>>("post", `userCenter/back/classes/addclass`, {
data: info
});
}
/**
* @description
* @return {object}
*/
export function editUser(data: any) {
return http.request<Res<any>>("post", `userCenter/back/users/edituser`, {
data
});
}
/**
* @description
* @return {object}
*/
export function getClassCombo(info: any) {
return http.request<Res<any>>("post", `userCenter/public/getclasscombo`, {
data: info
});
}
/**
* @description
* @return {object}
*/
export function getSubjectData() {
return http.request<Res<any>>("get", `userCenter/public/getsubjectdata`);
}
/**
* @description
* @return {object}
*/
export function getPositions(data) {
return http.request<Res<any>>("post", `userCenter/back/positions/positions`, {
data
});
}
//--------------------------interface----------------------------
/**
*
*/
export interface Position {
userId: number;
id: number;
name: string;
schoolId: number;
enable: boolean;
endTime: string | null;
schoolName: string;
graduationYear: number;
grade: string;
classId: number;
className: string;
subjectId: number;
subjectName: string;
positionType: number;
positionLevel: number;
status: boolean;
}
/**
*
*/
export interface UserDetail {
id: number;
templateId: number;
phone: string;
edited: boolean;
userType: number;
cloudSchoolId: number;
account: string;
studentId: string;
realName: string;
sex: number;
subjectLevel: any;
birthDate: string;
residence: string;
national: string;
headImage: string;
idCard: string | null;
pid: number;
pname: string;
cid: number;
cname: string;
rid: number;
rname: string;
wx: string;
isPerfectInfo: number;
level: number;
state: number;
meetingAccount: string | null;
gkSubject: string | null;
glSubject: string | null;
gSubject1: string | null;
gSubject2: string | null;
thirdPartyId: string | null;
pointPenSN: string | null;
subjectLevels: null;
positions: Position[];
}

View File

@ -1,68 +1,98 @@
export interface Dialog { export interface Dialog {
/* 对话框是否可见 */ /** 对话框是否可见 */
visible: boolean; visible: boolean;
/* 是否显示关闭按钮 */ /** 是否显示关闭按钮 */
close: boolean; close: boolean;
/* 对话框标题 */ /** 对话框标题 */
title: string; title: string;
/* 对话框宽度 */ /** 对话框宽度 */
width: string; width: string;
/**自定义弹窗数据 */ /**自定义弹窗数据 */
custom: { custom: {
/* 自定义对话框高度 */ /** 自定义对话框高度 */
height: string; height: string;
/* 自定义对话框数据 */ /** 自定义对话框数据 */
data: any[]; data: any[];
/* 自定义组件路径 */ /** 自定义组件路径 */
src?: string; src?: string;
/* 自定义配置项 */ /** 自定义配置项 */
custom: Record<string, any>; custom: Record<string, any>;
/* 自定义对话框是否可见 */ /** 自定义对话框是否可见 */
visible: boolean; visible: boolean;
/* 异步加载组件 */ /** 异步加载组件 */
component: any; component: any;
}; };
edit: { edit: {
/* 编辑项ID */ /** 编辑项ID */
id: number; id: number;
/* 编辑对话框标题 */ /** 编辑对话框标题 */
title: string; title: string;
/* 编辑对话框是否可见 */ /** 编辑对话框是否可见 */
visible: boolean; visible: boolean;
row?: any; row?: any;
tagData?: any; tagData?: any;
}; };
} }
/* 按钮自定义配置 */ /** 按钮自定义配置 */
export interface ButtonCustomConfig { export interface ButtonCustomConfig {
/* 弹出框标题 */ /** 弹出框标题 */
title: string; title: string;
/* 组件路径 */ /** 组件路径 */
src: string; src: string;
/* 弹框宽度 */ /** 弹框宽度 */
width: string; width: string;
/* 弹框高度 */ /** 弹框高度 */
height: string; height: string;
} }
/* 操作按钮配置 */ /** 操作按钮配置 */
export interface OperationButton { export interface OperationButton {
/* 是否为头部按钮 */ /** 是否为头部按钮 */
topBtn: boolean; topBtn: boolean;
/* 是否显示 */ /** 按钮权限码 */
perms?: string;
/** 是否显示 */
show?: boolean; show?: boolean;
/* 按钮文本 */ /** 按钮文本 */
label: string; label: string;
/* 按钮类型 */ /**
btnType: "add" | "edit" | "del" | "custom"; * @tips btnType
/* 按钮样式 */ * @param obj
btnStyle?: "success" | "danger"; * @param row
/* 自定义按钮配置 */ * @param handleReloadPaged
*/
click?: (obj, row, handleReloadPaged: (reload?: boolean) => void) => void;
/** 按钮类型 */
btnType?: "add" | "edit" | "del" | "custom";
/** 按钮样式 */
btnStyle?: "success" | "info" | "primary" | "danger" | "warning";
/** 自定义按钮配置 */
custom?: ButtonCustomConfig; custom?: ButtonCustomConfig;
} }
/* 字段设置项 */ /** 类型判断枚举 */
export enum ConditionalType {
Equal,
Like,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
In,
NotIn,
LikeLeft,
LikeRight,
NoEqual,
IsNullOrEmpty,
IsNot,
NoLike,
EqualNull,
InLike,
Range
}
/** 字段设置项 */
export interface FieldSetting { export interface FieldSetting {
/**map 时Value的取值的属性 */ /**map 时Value的取值的属性 */
mapValue?: string; mapValue?: string;
@ -77,50 +107,45 @@ export interface FieldSetting {
* @returns url * @returns url
*/ */
imgUrl?: (value: any, row: any) => string; imgUrl?: (value: any, row: any) => string;
/* 数据源 */ /** 数据源 */
datasource?: Array<{ datasource?: ComboModel[];
Value: any;
Text: string;
}>;
} }
///* 表格列配置 */ ///** 表格列配置 */
//export interface TableEditColumn {} //export interface TableEditColumn {}
export interface ComboModel {
/* 表格列配置 */ value: any;
text: string;
}
/** 表格列配置 */
export interface TableColumn { export interface TableColumn {
/* 显示标签 */ /** 显示标签 */
label: string; label: string;
/* 是否可搜索 */ /** 是否可搜索 */
search: boolean; search: boolean;
/* 搜索类型 */ /** 搜索类型 */
searchType?: searchType?: ConditionalType;
| "Equal" /** 是否允许添加 [false]*/
| "NoEqual" add?: boolean;
| "Like" /** 是否允许修改 [false]*/
| "GreaterThan" edit?: boolean;
| "LessThan" /** 列宽度 [auto]*/
| "NoLike";
/* 是否允许添加 */
add: boolean;
/* 是否允许修改 */
edit: boolean;
/* 列宽度 */
width?: string; width?: string;
/* 字段类型 */ /** 字段类型 */
type?: string; type?: "string" | "dropdown" | "switch" | "img" | "datetime" | "textarea";
/** 是否多选 */ /** 是否多选 */
multiple?: boolean; multiple?: boolean;
/** 编辑时显示列 */ /** 编辑时显示列 */
editShow?: boolean; editShow?: boolean;
/**校验规则 */
rules?: any | Array<any>; rules?: any | Array<any>;
/** 显示列 */ /** 显示列 */
show?: boolean; show?: boolean;
/* 字段设置 */ /** 字段设置 */
setting?: FieldSetting; setting?: FieldSetting;
/* 修改时的编辑值 */ /** 修改时的编辑值 */
valueE?: Array<string> | string; valueE?: Array<string> | string | number | boolean | Date;
/* 查询值 */ /** 查询值 */
value?: Array<string> | string; value?: Array<string> | string | number | boolean | Date;
/** textarea编辑时的行数 */ /** textarea编辑时的行数 */
editRows?: number; editRows?: number;
/**编辑时值发生变化 */ /**编辑时值发生变化 */
@ -129,54 +154,73 @@ export interface TableColumn {
custom?: (row: any) => string; custom?: (row: any) => string;
} }
/* 分页数据 */ /** 分页数据 */
export interface PageData { export interface PageData {
/* 总条数 */ /** 总条数 */
total: number; total: number;
} }
/* 搜索条件 */ /** 分页数据 */
export interface ConditionalModel {
/** 字段名称 */
FieldName: string;
/** 字段查询值 */
FieldValue: string;
/** 查询方式 */
ConditionalType?: ConditionalType;
/** C#类型名称 */
CSharpTypeName?: string;
}
/** 搜索条件 */
export interface SearchConditions { export interface SearchConditions {
/* 是否显示搜索 */ /** 是否显示搜索 */
show: boolean; show: boolean;
/* 当前页码 */ /** 当前页码 */
PageIndex: number; PageIndex: number;
/* 每页大小 */ /** 每页大小 */
PageSize: number; PageSize: number;
/* 排序字段 */ /** 排序字段 */
OrderBy: string; OrderBy: string;
/* 默认查询条件 */ /**
defaultConditions: any[]; * @tips 0:升序 1:降序
/* 查询条件 */ * @默认 = 1
*/
OrderByType?: 0 | 1;
/** 默认查询条件 */
defaultConditions: ConditionalModel[];
/** 查询条件 */
Conditions: any[]; Conditions: any[];
} }
/* 表格配置 */ /** 表格配置 */
export interface TableConfig { export interface TableConfig {
/* 搜索回调函数 */ /** 搜索回调函数 */
searchCallback?: (s: SearchConditions) => void; searchCallback?: (s: SearchConditions) => void;
/* 新增/修改回调函数 */ /** 新增/修改回调函数 */
editCallback?: (from: any) => void; editCallback?: (from: any) => void;
/* API地址 */ /** API地址 */
apiUrl: string; apiUrl: string;
/* 是否显示选择列 */ /** 是否显示选择列 */
selectColumn: boolean; selectColumn: boolean;
/* 搜索配置 */ /** 搜索配置 */
search: SearchConditions; search: SearchConditions;
/* 是否显示操作列 */ /** 是否显示操作列 */
operationColumn: boolean; operationColumn: boolean;
/* 操作按钮配置 */ /** 操作按钮配置 */
operationColumnData: OperationButton[]; operationColumnData: OperationButton[];
/* 列配置 */ /** 列配置 */
column: Record<string, TableColumn>; column: Record<string, TableColumn>;
/* 表格数据 */ /** 表格数据 */
data: any[]; data: any[];
/**显示头部操作按钮 */ /**显示头部操作按钮 */
operationTop?: boolean; operationTop?: boolean;
/* 分页数据 */ /** 分页数据 */
pageData: PageData; pageData: PageData;
/* 选中行 */ /** 选中行 */
selectRows: any[]; selectRows: any[];
/* 是否显示边框 */ /** 是否显示边框 */
border: boolean; border: boolean;
/**是否显示 */
show?: boolean;
} }

View File

@ -8,24 +8,24 @@ const props = defineProps({
//** */ //** */
id: { id: {
type: Number, type: Number,
default: -1 default: -1,
}, },
tableData: { tableData: {
type: Object as PropType<TableConfig>, type: Object as PropType<TableConfig>,
default: null default: null,
}, },
row: { row: {
type: Object, type: Object,
default: null default: null,
}, },
tagData: { tagData: {
type: Object, type: Object,
default: () => {} default: () => {},
} },
}); });
const emit = defineEmits(["handlePagedCallback"]); const emit = defineEmits(["handlePagedCallback"]);
const editFormRef = ref<FormInstance>(); const editFormRef = ref<FormInstance>();
const column: Record<string, TableColumn> = {}; const column = ref<Record<string, TableColumn>>({});
const editData = ref({ const editData = ref({
frorm: {}, frorm: {},
isedit: props.id !== -1, isedit: props.id !== -1,
@ -34,12 +34,12 @@ const editData = ref({
{ {
required: true, required: true,
message: "不能为空", message: "不能为空",
trigger: "blur" trigger: "blur",
} },
], ],
formLabelWidth: "120px", formLabelWidth: "120px",
size: "small", size: "small",
loading: false loading: false,
}); });
const Api = new hTableAPI(editData.value.table.apiUrl); const Api = new hTableAPI(editData.value.table.apiUrl);
onMounted(() => { onMounted(() => {
@ -69,16 +69,16 @@ function handlePagedCallback() {
emit("handlePagedCallback"); // emit("handlePagedCallback"); //
} }
function handleSubmitForm() { function handleSubmitForm() {
editFormRef.value.validate(valid => { editFormRef.value.validate((valid) => {
if (!valid) { if (!valid) {
return; return;
} }
editData.value.loading = true; editData.value.loading = true;
let form = {}; let form: any = {};
if (editData.value.isedit) { if (editData.value.isedit) {
form = props.row; form = props.row;
} } else form.id = 0;
for (const key in column.value) { for (const key in column.value) {
const element = column.value[key]; const element = column.value[key];
if (element.valueE !== null && element.valueE !== "") { if (element.valueE !== null && element.valueE !== "") {
@ -88,7 +88,7 @@ function handleSubmitForm() {
if (editData.value.table.editCallback) { if (editData.value.table.editCallback) {
editData.value.table.editCallback(form); editData.value.table.editCallback(form);
} }
Api.edit(form).then(res => { Api.edit(form).then((res) => {
editData.value.loading = false; editData.value.loading = false;
if (res.code === 200) { if (res.code === 200) {
ElMessage.success("操作成功"); ElMessage.success("操作成功");
@ -105,7 +105,7 @@ function handleResetForm() {
if (Array.isArray(item.valueE)) { if (Array.isArray(item.valueE)) {
item.valueE = []; item.valueE = [];
} else if (typeof item.valueE === "number") { } else if (typeof item.valueE === "number") {
item.valueE = 0; item.valueE = "";
} else if (typeof item.valueE === "boolean") { } else if (typeof item.valueE === "boolean") {
item.valueE = false; item.valueE = false;
} else { } else {
@ -116,9 +116,9 @@ function handleResetForm() {
function fetchInitData() {} function fetchInitData() {}
function fetchFormData() { function fetchFormData() {
editData.value.loading = false; editData.value.loading = false;
handleResetForm();
if (editData.value.isedit) { if (editData.value.isedit) {
Api.Info(props.id).then(res => { handleResetForm();
Api.Info(props.id).then((res) => {
if (res.code === 200) { if (res.code === 200) {
editData.value.frorm = res.data; editData.value.frorm = res.data;
for (const key in column.value) { for (const key in column.value) {
@ -141,7 +141,6 @@ function fetchFormData() {
ref="editFormRef" ref="editFormRef"
:model="editData.table.column" :model="editData.table.column"
:label-width="editData.formLabelWidth" :label-width="editData.formLabelWidth"
size="small"
clearable clearable
> >
<el-form-item <el-form-item
@ -154,12 +153,12 @@ function fetchFormData() {
> >
<div v-if="o.type.trim() == 'datetime'"> <div v-if="o.type.trim() == 'datetime'">
<el-date-picker <el-date-picker
v-model="o.valueE" v-model="o.valueE as Date"
format="yyyy-MM-dd HH:mm:ss" type="date"
value-format="yyyy-MM-dd HH:mm:ss" format="YYYY-MM-DD"
type="datetime" value-format="YYYY-MM-DD"
:placeholder="o.label" :placeholder="o.label"
style="width: 100%" class="elWidth"
@change="o.change" @change="o.change"
/> />
</div> </div>
@ -170,12 +169,12 @@ function fetchFormData() {
clearable clearable
filterable filterable
:placeholder="o.label" :placeholder="o.label"
style="width: 100%" class="elWidth"
@change="o.change" @change="o.change"
> >
<el-option <el-option
v-for="item in o.setting.datasource" v-for="item in o.setting.datasource"
:key="item.Value" :key="item.value"
autocomplete="off" autocomplete="off"
:label="item[o.setting.maplabel]" :label="item[o.setting.maplabel]"
:value="item[o.setting.mapValue]" :value="item[o.setting.mapValue]"
@ -188,27 +187,26 @@ function fetchFormData() {
:rows="o.editRows || 4" :rows="o.editRows || 4"
type="textarea" type="textarea"
:placeholder="o.label" :placeholder="o.label"
class="elWidth"
@change="o.change" @change="o.change"
/> />
</div> </div>
<div v-else-if="o.type.trim() == 'switch'"> <div v-else-if="o.type.trim() == 'switch'">
<el-switch <el-switch
v-model="o.valueE as string" v-model="o.valueE as boolean"
active-text="启用" active-text="启用"
inactive-text="禁用" inactive-text="禁用"
class="elWidth"
@change="o.change" @change="o.change"
/> />
</div> </div>
<div v-else> <div v-else>
<el-input v-model="o.valueE as string" :placeholder="o.label" /> <el-input v-model="o.valueE as string" class="elWidth" :placeholder="o.label" />
</div> </div>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button <el-button type="primary" :loading="!editData.loading" @click="handleSubmitForm()"
type="primary"
:loading="!editData.loading"
@click="handleSubmitForm()"
>立即提交</el-button >立即提交</el-button
> >
<el-button @click="handleResetForm()">重置</el-button> <el-button @click="handleResetForm()">重置</el-button>
@ -216,3 +214,9 @@ function fetchFormData() {
</el-form> </el-form>
</div> </div>
</template> </template>
<style>
.elWidth {
width: 100%;
min-width: 220px;
}
</style>

View File

@ -10,33 +10,49 @@ import {
onUnmounted, onUnmounted,
getCurrentInstance, getCurrentInstance,
onBeforeMount, onBeforeMount,
PropType PropType,
shallowRef,
} from "vue"; } from "vue";
import { Search } from "@element-plus/icons-vue"; import { Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { defineAsyncComponent, AsyncComponentLoader } from "vue"; import { defineAsyncComponent, AsyncComponentLoader } from "vue";
import { Dialog, TableConfig } from "./hTable"; import {
ButtonCustomConfig,
ConditionalType,
Dialog,
TableColumn,
TableConfig,
} from "./hTable";
import hTableEdit from "./hTableEdit.vue"; import hTableEdit from "./hTableEdit.vue";
import { hTableAPI } from "@/api/hTable"; import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum"; import { getenum } from "@/api/enum";
//
import { hasPerms } from "@/utils/auth";
const modules = import.meta.glob("/src/views/**/*.{vue,tsx}");
const props = defineProps({ const props = defineProps({
//** */ //** */
Row: { Row: {
type: Object as PropType<any>, type: Object as PropType<any>,
default: () => ({}) default: () => ({}),
}, },
tableConfig: { tableConfig: {
type: Object as PropType<TableConfig>, type: Object as PropType<TableConfig>,
default: () => ({}) default: () => ({}),
} },
}); });
const table = ref<TableConfig>(props.tableConfig); const table = ref<TableConfig>(props.tableConfig);
const tableShowColumn = ref<Record<string, TableColumn>>();
onBeforeMount(() => { onBeforeMount(() => {
/* 初始化系统配置 */ /* 初始化系统配置 */
nextTick(async () => { nextTick(async () => {
intdata(); intdata();
//
tableShowColumn.value = Object.fromEntries(
Object.entries(table.value.column).filter(([_, s]) => s.show)
);
Api = new hTableAPI(table.value.apiUrl); Api = new hTableAPI(table.value.apiUrl);
init.value = true; init.value = true;
tableShow.value = true; tableShow.value = true;
@ -55,7 +71,6 @@ onUnmounted(() => {});
// }); // });
const tableShow = ref(false); const tableShow = ref(false);
let Api: hTableAPI = null; let Api: hTableAPI = null;
const instance = getCurrentInstance();
const init = ref(false); const init = ref(false);
const tableHeight = ref(0); const tableHeight = ref(0);
@ -70,13 +85,13 @@ const dialog = ref<Dialog>({
data: [], data: [],
custom: {}, custom: {},
component: null, // component: null, //
visible: false // visible: false, //
}, },
edit: { edit: {
id: -1, id: -1,
title: "", // title title: "", // title
visible: false // visible: false, //
} },
}); });
const appB = ref<HTMLElement>(null); const appB = ref<HTMLElement>(null);
@ -86,7 +101,8 @@ function appStyle() {
tableHeight.value = tableHeight.value =
appB.value.parentElement.parentElement.offsetHeight - appB.value.parentElement.parentElement.offsetHeight -
145 - 145 -
appB_S.value.offsetHeight; appB_S.value.offsetHeight +
0;
return tableHeight; return tableHeight;
} }
@ -100,18 +116,18 @@ function intdata() {
// column // column
for (const key in table.value.column) { for (const key in table.value.column) {
const element = table.value.column[key]; const element = table.value.column[key];
if (element.add === undefined) element.add = false;
if (element.edit === undefined) element.edit = false;
// Vue 3 $set // Vue 3 $set
if (element.valueE === undefined) if (element.valueE === undefined) element.valueE = element.multiple ? [] : "";
element.valueE = element.multiple ? [] : "";
if (element.value === undefined) element.value = element.multiple ? [] : ""; if (element.value === undefined) element.value = element.multiple ? [] : "";
if (!element.type) element.type = "string"; if (!element.type) element.type = "string";
if (element.show === undefined) element.show = true; if (element.show === undefined) element.show = true;
if (element.editShow === undefined) element.editShow = true; if (element.editShow === undefined) element.editShow = true;
if (!element.setting) if (!element.setting)
element.setting = { datasource: [], mapValue: "Value", maplabel: "Text" }; element.setting = { datasource: [], mapValue: "value", maplabel: "text" };
if (!element.setting.mapValue) element.setting.mapValue = "Value"; if (!element.setting.mapValue) element.setting.mapValue = "value";
if (!element.setting.maplabel) element.setting.maplabel = "Text"; if (!element.setting.maplabel) element.setting.maplabel = "text";
if (!element.change) element.change = () => {}; if (!element.change) element.change = () => {};
} }
@ -127,21 +143,21 @@ function rowKeyFun(row) {
return row.customId; return row.customId;
} }
function execute(obj, scope, btn) { function execute(obj, scope, btn) {
if (Object.prototype.toString.call(obj) === "[object Function]") if (Object.prototype.toString.call(obj) === "[object Function]") return obj(scope, btn);
return obj(scope, btn);
return eval(obj); return eval(obj);
} }
function getOperationColumnWidth() { function getOperationColumnWidth() {
let hFontSize = getComputedStyle(window.document.documentElement)[ let hFontSize = getComputedStyle(window.document.documentElement)["font-size"].replace(
"font-size" "px",
].replace("px", ""); ""
);
let defWidth = 10 + 2 + 30; let defWidth = 10 + 2 + 30;
let width = eval( let width = eval(
table.value.operationColumnData table.value.operationColumnData
.filter(s => !s.topBtn && s.show) .filter((s) => !s.topBtn && s.show)
.map(s => defWidth + s.label.length * (hFontSize || 16)) .map((s) => defWidth + s.label.length * (hFontSize || 16))
.join("+") .join("+")
); );
width = width < 100 ? 100 : width; width = width < 100 ? 100 : width;
@ -169,9 +185,12 @@ function handleAdd() {
dialog.value.edit.visible = true; dialog.value.edit.visible = true;
dialog.value.visible = true; dialog.value.visible = true;
dialog.value.width = "500px"; dialog.value.width = "500px";
dialog.value.edit.row = null;
dialog.value.edit.tagData = null;
} }
function handleEdit(obj, row) { function handleEdit(obj, row) {
dialog.value.edit.id = row[0].Id; dialog.value.edit.id = row[0].id;
dialog.value.edit.row = row[0]; dialog.value.edit.row = row[0];
dialog.value.edit.tagData = obj.tagData; dialog.value.edit.tagData = obj.tagData;
dialog.value.title = obj.label || "修改"; dialog.value.title = obj.label || "修改";
@ -180,13 +199,18 @@ function handleEdit(obj, row) {
dialog.value.visible = true; dialog.value.visible = true;
dialog.value.width = "500px"; dialog.value.width = "500px";
} }
function handleCustom(obj, row, custom) { async function handleCustom(obj, row, custom: ButtonCustomConfig) {
dialog.value.custom.data = row || []; dialog.value.custom.data = row || [];
dialog.value.custom.custom = custom || []; dialog.value.custom.custom = custom || [];
// //
dialog.value.custom.component = defineAsyncComponent({ // dialog.value.custom.component = defineAsyncComponent({
loader: () => import(/* @vite-ignore */ `../${custom.src}.vue`) // loader: () => import(/* @vite-ignore */ `../../views/${custom.src}.vue`),
}); // });
let r = shallowRef(null);
const module: any = await modules[`/src/views/${custom.src}.vue`](); //import(`@/views/${custom.src}.vue`);
r.value = module.default;
dialog.value.custom.component = r;
dialog.value.width = custom.width; dialog.value.width = custom.width;
dialog.value.title = custom.title; dialog.value.title = custom.title;
dialog.value.edit.visible = false; dialog.value.edit.visible = false;
@ -205,11 +229,11 @@ function handleDelete(obj, row) {
return; return;
} }
const ids: any[] = []; const ids: any[] = [];
row.forEach(it => { row.forEach((it) => {
ids.push(it.Id); ids.push(it.id);
}); });
ElMessageBox.confirm("此操作将永久删除勾选记录, 是否继续?").then(() => { ElMessageBox.confirm("此操作将永久删除勾选记录, 是否继续?").then(() => {
Api.delete(ids).then(res => { Api.delete(ids).then((res) => {
if (res.code === 200) { if (res.code === 200) {
handleReloadPaged(); handleReloadPaged();
ElMessage.success("删除成功"); ElMessage.success("删除成功");
@ -225,8 +249,23 @@ function handleAddCallback() {
dialog.value.visible = false; dialog.value.visible = false;
dialog.value.edit.visible = false; dialog.value.edit.visible = false;
dialog.value.custom.visible = false; dialog.value.custom.visible = false;
handleResetForm();
handleReloadPaged(); handleReloadPaged();
} }
function handleResetForm() {
for (const key in table.value.column) {
let item = table.value.column[key];
if (Array.isArray(item.valueE)) {
item.valueE = [];
} else if (typeof item.valueE === "number") {
item.valueE = "";
} else if (typeof item.valueE === "boolean") {
} else {
item.valueE = "";
}
}
}
function tableClose() { function tableClose() {
handleAddCallback(); handleAddCallback();
} }
@ -244,12 +283,22 @@ function handleSelectionChange(selection) {
table.value.selectRows = selection; table.value.selectRows = selection;
} }
function searchReload() {
for (let name in table.value.column) {
if (
!table.value.column[name].search ||
table.value.column[name].value === undefined ||
table.value.column[name].value === null ||
table.value.column[name].value === ""
) {
continue;
}
table.value.column[name].value = "";
}
}
// //
function handleReloadPaged(reload = true) { function handleReloadPaged(reload = true) {
if ( if (table.value.search === undefined || table.value.search.PageIndex === undefined) {
table.value.search === undefined ||
table.value.search.PageIndex === undefined
) {
table.value.search.PageIndex = 0; table.value.search.PageIndex = 0;
table.value.search.PageSize = 20; table.value.search.PageSize = 20;
} }
@ -266,17 +315,19 @@ function handleReloadPaged(reload = true) {
let data: any = { ConditionalType: 0 }; let data: any = { ConditionalType: 0 };
if (table.value.column[name].type === "datetime") { if (table.value.column[name].type === "datetime") {
// data.CSharpTypeName = 'DateTime' // data.CSharpTypeName = 'DateTime'
data.ConditionalType = 8; // '2023-10-07%' data.ConditionalType = ConditionalType.LikeLeft; // '2023-10-07%'
} else if (table.value.column[name].type === "switch") { } else if (table.value.column[name].type === "switch") {
data.CSharpTypeName = "Boolean"; data.CSharpTypeName = "Boolean";
} else if (table.value.column[name].type === "string") { } else if (table.value.column[name].type === "string") {
data.ConditionalType = "Like"; data.ConditionalType = ConditionalType.Like;
} } else data.ConditionalType = ConditionalType.Equal;
data.FieldName = name.charAt(0).toUpperCase() + name.slice(1); data.FieldName = name.charAt(0).toUpperCase() + name.slice(1);
data.FieldValue = table.value.column[name].value; data.FieldValue = table.value.column[name].value.toString();
if (table.value.column[name].searchType != undefined) { if (table.value.column[name].searchType != undefined) {
data.ConditionalType = table.value.column[name].searchType || 0; let v: number = table.value.column[name].searchType || 0;
data.ConditionalType = v;
} }
table.value.search.Conditions.push(data); table.value.search.Conditions.push(data);
} }
@ -295,11 +346,10 @@ async function fetchInitData() {
const element = table.value.column[key]; const element = table.value.column[key];
if (element.type === "dropdown") { if (element.type === "dropdown") {
if (!element.setting.datasource) { if (!element.setting.datasource) {
// //
// let rdata = await eval(element.setting.datasourceStr);
let rdata = await eval(element.setting.datasourceStr); // element.setting.datasource = rdata.data;
element.setting.datasource = rdata.data; // console.log(key + " " + element.setting.datasourceStr, rdata);
console.log(key + " " + element.setting.datasourceStr, rdata);
} }
} }
if ( if (
@ -309,14 +359,18 @@ async function fetchInitData() {
element.type === "string" || element.type === "string" ||
element.type === undefined) element.type === undefined)
) { ) {
element.custom = row => { if (element.type === "string" || element.type === undefined)
let sc = element.setting.datasource.find( element.custom = (row) => row[key];
s => s[element.setting.mapValue] + "" == row[key] + "" else {
); element.custom = (row) => {
return !sc ? row[key] : sc[element.setting.maplabel]; let sc = element.setting.datasource.find(
}; (s) => s[element.setting.mapValue] + "" == row[key] + ""
);
return !sc ? row[key] : sc[element.setting.maplabel];
};
}
} else if (element.custom == undefined) { } else if (element.custom == undefined) {
element.custom = row => row[key]; element.custom = (row) => row[key];
} }
} }
setTimeout(() => { setTimeout(() => {
@ -328,9 +382,7 @@ function showTips(item, value) {
if (item.width == undefined) { if (item.width == undefined) {
return false; return false;
} }
return ( return getByteLen(item.custom(value) + "") * 16 < item.width.replace("px", "");
getByteLen(item.custom(value) + "") * 16 < item.width.replace("px", "")
);
} }
function getByteLen(str) { function getByteLen(str) {
let len = 0; let len = 0;
@ -343,11 +395,11 @@ function getByteLen(str) {
function fetchPagedData() { function fetchPagedData() {
for (const iterator of table.value.search.defaultConditions) { for (const iterator of table.value.search.defaultConditions) {
if (!iterator) continue; if (!iterator) continue;
if (!table.value.search.Conditions.find(s => s == iterator)) { if (!table.value.search.Conditions.find((s) => s == iterator)) {
table.value.search.Conditions.push(iterator); table.value.search.Conditions.push(iterator);
} }
} }
Api.PageList(table.value.search).then(res => { Api.PageList(table.value.search).then((res) => {
if (res.code === 200) { if (res.code === 200) {
table.value.data = res.data.data.map((s, i) => { table.value.data = res.data.data.map((s, i) => {
return { ...s, customId: i }; return { ...s, customId: i };
@ -363,17 +415,13 @@ function fetchPagedData() {
<div ref="appB_S" class="search-container1"> <div ref="appB_S" class="search-container1">
<!-- 搜索项目 --> <!-- 搜索项目 -->
<el-form v-if="table.search.show" :inline="true" :model="table.search"> <el-form v-if="table.search.show" :inline="true" :model="table.search">
<el-form-item <el-form-item v-for="(o, n, i) in table.column" v-show="o.search" :key="i">
v-for="(o, n, i) in table.column"
v-show="o.search"
:key="i"
>
<el-date-picker <el-date-picker
v-if="o.type.trim() == 'datetime'" v-if="o.type.trim() == 'datetime'"
v-model="o.value" v-model="o.value as Date"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
type="date" type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:placeholder="o.label" :placeholder="o.label"
style="width: 100%" style="width: 100%"
/> />
@ -388,7 +436,7 @@ function fetchPagedData() {
> >
<el-option <el-option
v-for="item in o.setting.datasource" v-for="item in o.setting.datasource"
:key="item.Value" :key="item.value"
autocomplete="off" autocomplete="off"
:label="item[o.setting.maplabel]" :label="item[o.setting.maplabel]"
:value="item[o.setting.mapValue]" :value="item[o.setting.mapValue]"
@ -409,25 +457,60 @@ function fetchPagedData() {
<el-input v-else v-model="o.value as string" :placeholder="o.label" /> <el-input v-else v-model="o.value as string" :placeholder="o.label" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button <el-button type="primary" :icon="Search" @click="handleReloadPaged(false)"
type="primary"
:icon="Search"
@click="handleReloadPaged(false)"
>查询</el-button >查询</el-button
> >
<el-button type="default" @click="searchReload()">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="dialog-container">
<el-dialog
v-if="dialog.visible"
v-model="dialog.visible"
:class="dialog.title ? '' : 'noHeader'"
:title="dialog.title"
:width="dialog.width"
:close-on-click-modal="false"
:close-on-press-escape="dialog.close"
:before-close="tableClose"
append-to-body
>
<hTableEdit
v-if="dialog.edit.visible"
:id="dialog.edit.id"
:tableData="table"
:row="dialog.edit.row"
:tagData="dialog.edit.tagData"
@handlePagedCallback="handleAddCallback"
/>
<component
:is="dialog.custom.component"
v-if="dialog.custom.visible"
:style="{ height: 'calc( ' + dialog.custom.height + ' - 84px )' }"
:iscomponent="true"
:custom="dialog.custom.custom"
:CancelCallback="handleAddCallback"
:data="dialog.custom.data"
@handlePagedCallback="handleAddCallback"
/>
</el-dialog>
</div>
</div> </div>
<div v-if="table.operationTop" class="toolbar-container"> <div v-if="table.operationTop" class="toolbar-container">
<!-- 头部按钮组 --> <!-- 头部按钮组 -->
<el-button <el-button
v-for="(e, i) in table.operationColumnData.filter(s => s.topBtn)" v-for="(e, i) in table.operationColumnData.filter(
(s) => s.topBtn && hasPerms(s.perms)
)"
v-show="execute(e['show'], {}, e)" v-show="execute(e['show'], {}, e)"
:key="i" :key="i"
:type="e.btnStyle || 'info'" :type="e.btnStyle || 'info'"
@click="getbtnClick(e, null)" @click="getbtnClick(e, null)"
>{{ e.label }}</el-button> >{{ e.label }}</el-button
>
</div> </div>
<el-table <el-table
v-if="init" v-if="init"
@ -444,7 +527,7 @@ function fetchPagedData() {
<el-table-column <el-table-column
v-if=" v-if="
table.operationColumn && table.operationColumn &&
table.operationColumnData.filter(s => !s.topBtn).length > 0 table.operationColumnData.filter((s) => !s.topBtn).length > 0
" "
label="操作" label="操作"
:width="getOperationColumnWidth()" :width="getOperationColumnWidth()"
@ -452,7 +535,9 @@ function fetchPagedData() {
<template v-slot="scope"> <template v-slot="scope">
<div class="columnTemplate"> <div class="columnTemplate">
<el-button <el-button
v-for="(e, i) in table.operationColumnData.filter(s => !s.topBtn)" v-for="(e, i) in table.operationColumnData.filter(
(s) => !s.topBtn && hasPerms(s.perms)
)"
v-show="execute(e['show'], scope, e)" v-show="execute(e['show'], scope, e)"
:key="i" :key="i"
class="btn" class="btn"
@ -467,8 +552,7 @@ function fetchPagedData() {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
v-for="(item, name, i) of table.column" v-for="(item, name, i) of tableShowColumn"
v-show="item.show"
:key="i" :key="i"
:prop="name" :prop="name"
:label="item.label" :label="item.label"
@ -480,12 +564,6 @@ function fetchPagedData() {
<div v-if="item.type.trim() == 'math'"> <div v-if="item.type.trim() == 'math'">
{{ item.custom(scope.row) }} {{ item.custom(scope.row) }}
</div> </div>
<!-- <ShowMathJax
v-if="item.type.trim() == 'math'"
style="zoom: 0.8"
:str="item.custom(scope.row)"
:divId="'MathJax_' + scope.row.Id"
/> -->
<el-image <el-image
v-else-if="item.type.trim() == 'img'" v-else-if="item.type.trim() == 'img'"
style="width: 300px; height: 100px" style="width: 300px; height: 100px"
@ -513,12 +591,18 @@ function fetchPagedData() {
{{ item.custom(scope.row) }} {{ item.custom(scope.row) }}
</div> </div>
</el-tooltip> </el-tooltip>
<!-- <p ></p> -->
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div style="display: flex; align-items: center; justify-content: center"> <div
style="
padding-top: 15px;
display: flex;
align-items: center;
justify-content: center;
"
>
<el-pagination <el-pagination
:current-page="table.search.PageIndex + 1" :current-page="table.search.PageIndex + 1"
:page-sizes="[20, 40, 80, 100]" :page-sizes="[20, 40, 80, 100]"
@ -529,41 +613,6 @@ function fetchPagedData() {
@current-change="pageIndexChange" @current-change="pageIndexChange"
/> />
</div> </div>
<div class="dialog-container">
<el-dialog
v-if="dialog.visible"
ref="elDialog"
v-model:visible="dialog.visible"
:class="dialog.title ? '' : 'noHeader'"
:title="dialog.title"
:width="dialog.width"
:close-on-click-modal="false"
:close-on-press-escape="dialog.close"
:before-close="tableClose"
append-to-body
>
<hTableEdit
v-if="dialog.edit.visible"
:id="dialog.edit.id"
:data="table"
:tagData="dialog.edit.tagData"
:row="dialog.edit.row"
@handlePagedCallback="handleAddCallback"
/>
/>
<component
:is="dialog.custom.component"
v-if="dialog.custom.visible"
ref="custom"
:style="{ height: 'calc( ' + dialog.custom.height + ' - 84px )' }"
:iscomponent="true"
:custom="dialog.custom.custom"
:CancelCallback="handleAddCallback"
:data="dialog.custom.data"
@handlePagedCallback="handleAddCallback"
/>
</el-dialog>
</div>
</div> </div>
</template> </template>
@ -571,26 +620,28 @@ function fetchPagedData() {
.columnTemplate .btn { .columnTemplate .btn {
margin-left: 0 !important; margin-left: 0 !important;
} }
.columnTemplate { .columnTemplate {
display: flex; display: flex;
flex-direction: row;
flex-flow: row wrap;
gap: 5px; gap: 5px;
place-content: flex-start flex-start;
align-items: center;
-webkit-box-orient: horizontal; -webkit-box-orient: horizontal;
-webkit-box-direction: normal; -webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
align-content: flex-start;
} }
.sty .tab_tip { .sty .tab_tip {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.toolbar-container { .toolbar-container {
padding-bottom: 5px; padding-bottom: 5px;
} }
.maxWidth600px { .maxWidth600px {
max-width: 600px; max-width: 600px;
} }

View File

@ -19,7 +19,7 @@ const {
userName, userName,
userAvatar, userAvatar,
avatarsStyle, avatarsStyle,
toggleSideBar toggleSideBar,
} = useNav(); } = useNav();
</script> </script>
@ -45,7 +45,7 @@ const {
<!-- 全屏 --> <!-- 全屏 -->
<LaySidebarFullScreen id="full-screen" /> <LaySidebarFullScreen id="full-screen" />
<!-- 消息通知 --> <!-- 消息通知 -->
<LayNotice id="header-notice" /> <LayNotice id="header-notice" v-show="false" />
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none"> <span class="el-dropdown-link navbar-bg-hover select-none">
@ -55,20 +55,13 @@ const {
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item @click="logout"> <el-dropdown-item @click="logout">
<IconifyIconOffline <IconifyIconOffline :icon="LogoutCircleRLine" style="margin: 5px" />
:icon="LogoutCircleRLine"
style="margin: 5px"
/>
退出系统 退出系统
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<span <span class="set-icon navbar-bg-hover" title="打开系统配置" @click="onPanel">
class="set-icon navbar-bg-hover"
title="打开系统配置"
@click="onPanel"
>
<IconifyIconOffline :icon="Setting" /> <IconifyIconOffline :icon="Setting" />
</span> </span>
</div> </div>

View File

@ -8,11 +8,10 @@ const noticesNum = ref(0);
const notices = ref(noticesData); const notices = ref(noticesData);
const activeKey = ref(noticesData[0]?.key); const activeKey = ref(noticesData[0]?.key);
notices.value.map(v => (noticesNum.value += v.list.length)); notices.value.map((v) => (noticesNum.value += v.list.length));
const getLabel = computed( const getLabel = computed(() => (item) =>
() => item => item.name + (item.list.length > 0 ? `(${item.list.length})` : "")
item.name + (item.list.length > 0 ? `(${item.list.length})` : "")
); );
</script> </script>
@ -23,7 +22,7 @@ const getLabel = computed(
'dropdown-badge', 'dropdown-badge',
'navbar-bg-hover', 'navbar-bg-hover',
'select-none', 'select-none',
Number(noticesNum) !== 0 && 'mr-[10px]' Number(noticesNum) !== 0 && 'mr-[10px]',
]" ]"
> >
<el-badge :value="Number(noticesNum) === 0 ? '' : noticesNum" :max="99"> <el-badge :value="Number(noticesNum) === 0 ? '' : noticesNum" :max="99">
@ -40,11 +39,7 @@ const getLabel = computed(
class="dropdown-tabs" class="dropdown-tabs"
:style="{ width: notices.length === 0 ? '200px' : '330px' }" :style="{ width: notices.length === 0 ? '200px' : '330px' }"
> >
<el-empty <el-empty v-if="notices.length === 0" description="暂无消息" :image-size="60" />
v-if="notices.length === 0"
description="暂无消息"
:image-size="60"
/>
<span v-else> <span v-else>
<template v-for="item in notices" :key="item.key"> <template v-for="item in notices" :key="item.key">
<el-tab-pane :label="getLabel(item)" :name="`${item.key}`"> <el-tab-pane :label="getLabel(item)" :name="`${item.key}`">

View File

@ -166,7 +166,11 @@ router.beforeEach((to: ToRouteType, _from, next) => {
getTopMenu(true); getTopMenu(true);
// query、params模式路由传参数的标签页不在此处处理 // query、params模式路由传参数的标签页不在此处处理
if (route && route.meta?.title) { if (route && route.meta?.title) {
if (isAllEmpty(route.parentId) && route.meta?.backstage) { if (
isAllEmpty(route.parentId) &&
route.meta?.backstage &&
route.children
) {
// 此处为动态顶级路由(目录) // 此处为动态顶级路由(目录)
const { path, name, meta } = route.children[0]; const { path, name, meta } = route.children[0];
useMultiTagsStoreHook().handleTags("push", { useMultiTagsStoreHook().handleTags("push", {

View File

@ -15,29 +15,11 @@ export default {
{ {
path: "/welcome", path: "/welcome",
name: "Welcome", name: "Welcome",
component: () => import("@/views/welcome/index.vue"), component: () => import("@/views/toschoolinfomanage/index.vue"),
meta: { meta: {
title: "首页", title: "首页",
showLink: VITE_HIDE_HOME === "true" ? false : true showLink: VITE_HIDE_HOME === "true" ? false : true
} }
},
{
path: "/school",
name: "school",
component: () => import("@/views/school/index.vue"),
meta: {
title: "学校",
showLink: true
}
},
{
path: "/menu",
name: "Menu",
component: () => import("@/views/menu/index.vue"),
meta: {
title: "菜单",
showLink: true
}
} }
] ]
} satisfies RouteConfigsTable; } satisfies RouteConfigsTable;

View File

@ -150,7 +150,7 @@ function addPathMatch() {
/** 处理动态路由(后端返回的路由) */ /** 处理动态路由(后端返回的路由) */
function handleAsyncRoutes(routeList) { function handleAsyncRoutes(routeList) {
if (routeList.length === 0) { if (routeList == null ||routeList.length === 0) {
usePermissionStoreHook().handleWholeMenus(routeList); usePermissionStoreHook().handleWholeMenus(routeList);
} else { } else {
formatFlatteningRoutes(addAsyncRoutes(routeList)).map( formatFlatteningRoutes(addAsyncRoutes(routeList)).map(

View File

@ -84,8 +84,7 @@ export function setToken(data: DataInfo<Date>) {
permissions permissions
}); });
} }
if (data.userName && data.permissions) {
if (data.userName && data.roles) {
const { userName, roles } = data; const { userName, roles } = data;
setUserKey({ setUserKey({
avatar: data?.avatar ?? "", avatar: data?.avatar ?? "",
@ -129,7 +128,7 @@ export const formatToken = (token: string): string => {
/** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/ /** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/
export const hasPerms = (value: string | Array<string>): boolean => { export const hasPerms = (value: string | Array<string>): boolean => {
if (!value) return false; if (!value) return true;
const allPerms = "*:*:*"; const allPerms = "*:*:*";
const { permissions } = useUserStoreHook(); const { permissions } = useUserStoreHook();
if (!permissions) return false; if (!permissions) return false;

View File

@ -13,12 +13,70 @@ import { stringify } from "qs";
import NProgress from "../progress"; import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth"; import { getToken, formatToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
// import { string } from "vue-types";
import router from "@/router";
import { ElMessage } from "element-plus";
import { message } from "../message";
/**请求后端的地址 未配置则访问BaseURL */
const apiServiceConfig = {
userCenter: import.meta.env.VITE_API_USERCENTER_URL,
usercenter: import.meta.env.VITE_API_USERCENTER_URL
};
function setAPIUrl(c: PureHttpRequestConfig): void {
let url = c.url;
let token = url.startsWith("/") ? url.split("/")[1] : url.split("/")[0];
if (apiServiceConfig[token] != null) {
c.url = url.replaceAll(token, "");
c.baseURL = apiServiceConfig[token];
} else c.baseURL = import.meta.env.VITE_API_BASEURL;
}
const snakeToCamel = (str: string): string => {
// 处理蛇形命名user_id → userId
let result = str.replace(/_([a-zA-Z0-9])/g, (_, letter) =>
letter.toUpperCase()
);
// 处理大驼峰命名UserName → userName
if (result.length > 0) {
result = result.charAt(0).toLowerCase() + result.slice(1);
}
// 特殊场景处理连续下划线__type → Type
result = result.replace(/^_+/, "");
return result;
};
/**
* /
*/
const convertKeysToCamelCase = <T>(data: any): T => {
if (Array.isArray(data)) {
return data.map(item => convertKeysToCamelCase(item)) as unknown as T;
}
if (data !== null && typeof data === "object") {
return Object.keys(data).reduce((result, key) => {
const camelKey = snakeToCamel(key);
const value = data[key];
return {
...result,
[camelKey]: convertKeysToCamelCase(value)
};
}, {}) as T;
}
return data as T;
};
// 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1 // 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = { const defaultConfig: AxiosRequestConfig = {
baseURL: import.meta.env.VITE_API_BASEURL, baseURL: import.meta.env.VITE_API_BASEURL,
// 请求超时时间 // 请求超时时间
timeout: 10000, timeout: 20 * 1000,
headers: { headers: {
Accept: "application/json, text/plain, */*", Accept: "application/json, text/plain, */*",
"Content-Type": "application/json", "Content-Type": "application/json",
@ -64,6 +122,9 @@ class PureHttp {
async (config: PureHttpRequestConfig): Promise<any> => { async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画 // 开启进度条动画
NProgress.start(); NProgress.start();
if (config.url.indexOf("http") === -1) {
setAPIUrl(config);
}
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调 // 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof config.beforeRequestCallback === "function") { if (typeof config.beforeRequestCallback === "function") {
config.beforeRequestCallback(config); config.beforeRequestCallback(config);
@ -124,6 +185,8 @@ class PureHttp {
const $config = response.config; const $config = response.config;
// 关闭进度条动画 // 关闭进度条动画
NProgress.done(); NProgress.done();
if (!(response.data instanceof Blob))
response.data = convertKeysToCamelCase(response.data);
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调 // 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof $config.beforeResponseCallback === "function") { if (typeof $config.beforeResponseCallback === "function") {
$config.beforeResponseCallback(response); $config.beforeResponseCallback(response);
@ -140,6 +203,12 @@ class PureHttp {
$error.isCancelRequest = Axios.isCancel($error); $error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画 // 关闭进度条动画
NProgress.done(); NProgress.done();
if (error.response?.status === 403) {
// 跳转到403页面
router.push({
path: "/error/403"
});
}
// 所有的响应异常 区分来源为取消请求/非取消请求 // 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error); return Promise.reject($error);
} }
@ -164,10 +233,16 @@ class PureHttp {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
PureHttp.axiosInstance PureHttp.axiosInstance
.request(config) .request(config)
.then((response: undefined) => { .then((response: any) => {
resolve(response); if (response.code != null && response.code !== 200) {
message(response.message, { type: "error" });
} resolve(response);
}) })
.catch(error => { .catch(error => {
if (error.status != 200) {
ElMessage.warning("请求失败" + error.message);
}
reject(error); reject(error);
}); });
}); });

View File

@ -18,6 +18,14 @@ export interface PureHttpError extends AxiosError {
isCancelRequest?: boolean; isCancelRequest?: boolean;
} }
export type ResPage<T> = {
code: number;
data: {
data: T;
total: number;
};
message: string;
};
export type Res<T> = { export type Res<T> = {
code: number; code: number;
data: T; data: T;

56
src/utils/rules.ts Normal file
View File

@ -0,0 +1,56 @@
export const ruleRequired = [
{ required: true, message: "不能为空", trigger: "blur" }
];
/** 验证长度不得超过多少 */
type Rule = {
required?: boolean;
message: string;
trigger: string;
max?: number;
min?: number;
pattern?: RegExp;
};
export const ruleRequiredI = (max: number = 20, min: number = 0): Rule[] => {
let res: Rule[] = [
{ required: true, message: "不能为空", trigger: "blur" },
{ max: max, message: `长度不能超过${max}`, trigger: "blur" }
];
if (min > 0)
res.push({ min: min, message: `长度不能小于${min}`, trigger: "blur" });
return res;
};
export const ruleNumber = [
{
pattern: /^\d*\.?\d+$/,
message: "请输入正确数字",
trigger: "blur"
}
];
export const ruleRequiredNumber = [
{ required: true, message: "不能为空", trigger: "blur" },
{
pattern: /^\d*\.?\d+$/,
message: "请输入正确数字",
trigger: "blur"
}
];
export const rulePassword = [
{ required: true, message: "不能为空", trigger: "blur" },
{ min: 6, message: "长度必须大于5", trigger: "blur" }
];
export const ruleAccount = [
{ required: true, message: "不能为空", trigger: "blur" },
{ min: 9, message: "长度必须大于8", trigger: "blur" }
];
export const rulePhone = [
{ required: true, message: "手机号不能为空", trigger: "blur" },
{
pattern: /^1[3-9]\d{9}$/,
message: "请输入正确的手机号",
trigger: "blur"
}
];

152
src/views/admin/index.vue Normal file
View File

@ -0,0 +1,152 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref, defineOptions } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import {
ruleAccount,
rulePassword,
rulePhone,
ruleRequired,
ruleRequiredI,
} from "@/utils/rules";
const ControllerName = "Admin";
defineOptions({
name: ControllerName,
});
function searchCallback(data) {
// let c = data.Conditions.find(s => s.FieldName === "Enable");
// if (c) {
// if (c.FieldValue == "true") {
// c.FieldValue = 1;
// } else {
// c.FieldValue = 0;
// }
// }
}
const RoleApi = new hTableAPI("AdminRole");
const table = ref<{ initTable: (config: TableConfig) => void }>(null);
const tableData: TableConfig = {
apiUrl: ControllerName,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 20,
OrderBy: "CreateTime", //
defaultConditions: [], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [
{
//
topBtn: false, //
label: "修改",
btnType: "edit", // add edit del custom
},
{
//
topBtn: true, //
label: "添加",
btnStyle: "success",
btnType: "add", // add edit del custom
},
{
topBtn: false, //
show: true,
label: "删除",
btnType: "del", // add edit del
btnStyle: "danger", // topBtn: true success danger
},
],
column: {
//
id: {
label: "编号",
search: true,
add: false, //
edit: false, //
width: "150px",
},
name: {
label: "名称",
width: "180px",
rules: ruleRequiredI(12, 2),
search: true,
searchType: ConditionalType.Like,
add: true, //
edit: true, //
},
phone: {
label: "手机号",
rules: rulePhone,
width: "200px",
search: true,
add: true, //
edit: true, //
},
account: {
label: "账号",
rules: ruleRequiredI(20, 8),
search: true,
add: true, //
edit: false, //
},
password: {
label: "密码",
show: false,
/**长度必须大于6 */
rules: ruleRequiredI(32, 6),
search: false,
add: true, //
edit: false, //
},
enable: {
label: "启用",
type: "switch",
search: false,
add: true, //
edit: true, //
valueE: true, //
},
roleId: {
label: "角色",
type: "dropdown",
rules: ruleRequired,
search: true,
add: true, //
edit: false, //
setting: {
datasource: [],
},
},
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
};
const showTable = ref(false);
onMounted(async () => {
//
tableData.column.roleId.setting.datasource = (
await RoleApi.querycombo({ TextName: "Name", ValueName: "Id" })
).data;
showTable.value = true;
});
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
</template>

125
src/views/admin/role.vue Normal file
View File

@ -0,0 +1,125 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref, defineOptions } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
const ControllerName = "AdminRole";
defineOptions({
name: ControllerName,
});
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>(null);
const tableData: TableConfig = {
apiUrl: ControllerName,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 20,
OrderBy: "CreateTime", //
defaultConditions: [], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [
{
topBtn: false, //
show: true,
label: "角色授权",
perms: "角色授权", //
btnType: "custom", // add edit del
btnStyle: "success", // topBtn: true success danger
custom: {
// custom
title: "分配权限", // title
src: "menu/index", //
width: "1200px", //
height: "800px", //
},
},
{
//
topBtn: false, //
label: "修改",
btnType: "edit", // add edit del custom
},
{
//
topBtn: true, //
label: "添加",
btnStyle: "success",
btnType: "add", // add edit del custom
},
{
topBtn: false, //
show: true,
label: "删除",
btnType: "del", // add edit del
btnStyle: "danger", // topBtn: true success danger
},
],
column: {
//
id: {
label: "编号",
search: true,
add: false, //
edit: false, //
width: "150px",
},
name: {
label: "角色名称",
width: "180px",
search: true,
searchType: ConditionalType.Like,
add: true, //
edit: true, //
},
enable: {
label: "启用",
type: "switch",
search: false,
add: true, //
edit: true, //
valueE: true, //
},
createTime: {
label: "创建时间",
type: "datetime",
search: true,
add: false, //
edit: false, //
},
remark: {
label: "备注",
type: "textarea",
editRows: 3,
search: false,
add: true, //
edit: true, //
},
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
};
const showTable = ref(false);
onMounted(async () => {
//
showTable.value = true;
});
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
</template>

347
src/views/class/edit.vue Normal file
View File

@ -0,0 +1,347 @@
<template>
<div>
<el-form
ref="classesAddForm"
:model="form"
:label-width="formLabelWidth"
clearable
>
<el-form-item label="学校" prop="SchoolId" :rules="ruleRequired">
<el-select
v-model="form.SchoolId"
filterable
placeholder="学校"
style="width: 100%"
>
<el-option
v-for="(item, id) in schoolList"
:key="id"
autocomplete="off"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="年级" prop="Grade" :rules="ruleRequired">
<el-select
v-model="form.Grade"
filterable
placeholder="年级"
style="width: 100%"
>
<el-option
v-for="(item, i) in gradeList"
:key="i"
autocomplete="off"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="班级类型" prop="Type" :rules="ruleRequired">
<el-select
v-model="form.Type"
filterable
placeholder="班级类型"
style="width: 100%"
>
<el-option
v-for="(item, i) in TypeList"
:key="i"
autocomplete="off"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="选修方向">
<el-col :span="24">
<div style="display: flex; gap: 10px">
<el-select
v-model="form.Elective1"
clearable
filterable
placeholder="历史/地理"
style="width: 180px"
>
<el-option
v-for="(item, id) in subject1"
:key="id"
:label="item.text"
:value="item.value"
/>
</el-select>
<el-select
v-model="form.Elective2"
clearable
filterable
placeholder="小学科"
style="width: 180px"
>
<el-option
v-for="(item, id) in subject2"
:key="id"
:label="item.text"
:value="item.value"
/>
</el-select>
<el-select
v-model="form.Elective3"
clearable
filterable
placeholder="小学科"
style="width: 180px"
>
<el-option
v-for="(item, id) in subject2"
:key="id"
:label="item.text"
:value="item.value"
/>
</el-select>
</div>
</el-col>
</el-form-item>
<el-form-item
label="班级名称"
v-if="isEdit"
prop="Name"
:rules="ruleRequired"
>
<el-select
v-model="form.Name"
filterable
placeholder="班级名称"
style="width: 100%"
>
<el-option
v-for="(item, i) in ClassNameList"
:key="i"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="添加班级" v-if="!isEdit">
<el-col :span="21">
<el-select
v-model="form.Name"
filterable
placeholder="班级名称"
style="width: 100%"
>
<el-option
v-for="(item, i) in filteredClassNameList"
:key="i"
:label="item"
:value="item"
/>
</el-select>
</el-col>
<el-col :span="1">
<el-button type="success" @click="ClassNameAdd" round>添加</el-button>
</el-col>
</el-form-item>
<el-form-item label="班级列表" :rules="ruleRequired" v-if="!isEdit">
<span v-show="ClassNames.length === 0">暂未添加!</span>
<el-tag
v-for="tag in ClassNames"
:key="tag"
closable
:disable-transitions="false"
@close="TagClose(tag)"
>
{{ tag }}
</el-tag>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSubmitForm">
立即提交
</el-button>
<el-button @click="handleResetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import type { FormInstance } from "element-plus";
import { getenum } from "@/api/enum";
import { hTableAPI } from "@/api/hTable";
import { ruleRequired } from "@/utils/rules";
import { addClasses } from "@/api/userCenter";
import { ComboModel } from "@/components/hTable/hTable";
interface Formdata {
Id: number;
Name: string;
SchoolId: string;
Grade: string;
Type: number;
Elective1: string;
Elective2: string;
Elective3: string;
}
const props = defineProps<{
id: number;
}>();
const emit = defineEmits(["handlePagedCallback"]);
//
const classesAddForm = ref<FormInstance>();
//
const form = ref<Formdata>({
Id: props.id,
Name: "",
SchoolId: "",
Grade: "",
Type: 0,
Elective1: "",
Elective2: "",
Elective3: ""
});
const subject1 = ref<ComboModel[]>([
{ value: 4, text: "物理" },
{ value: 8, text: "历史" }
]);
const subject2 = ref<ComboModel[]>([
{ value: 5, text: "化学" },
{ value: 6, text: "生物" },
{ value: 9, text: "地理" },
{ value: 7, text: "政治" }
]);
const formLabelWidth = "120px";
const size = "small";
const loading = ref(false);
const ClassNames = ref<string[]>([]);
const ClassNameList = ref<string[]>([]);
const schoolList = ref<ComboModel[]>([]);
const TypeList = ref<ComboModel[]>([]);
const gradeList = ref<ComboModel[]>([]);
//
const isEdit = computed(() => props.id > 0);
const filteredClassNameList = computed(() =>
ClassNameList.value.filter(s => !ClassNames.value.includes(s))
);
//
const TagClose = (tag: string) => {
ClassNames.value = ClassNames.value.filter(t => t !== tag);
};
const ClassNameAdd = () => {
if (form.value.Name === "") {
ElMessage.error("添加的班级名称不能为空");
return;
}
ClassNames.value.push(form.value.Name);
form.value.Name = "";
};
const handlePagedCallback = () => {
emit("handlePagedCallback");
};
const handleSubmitForm = async () => {
debugger;
if (props.id <= 0 || ClassNames.value.length < 1) {
ElMessage.error("班级列表为空!");
return;
}
if (!classesAddForm.value) return;
await classesAddForm.value.validate(valid => {
if (valid) {
loading.value = true;
let dataf = {
...form.value,
Id: props.id,
Name: ClassNames.value.join()
};
addClasses(dataf).then(res => {
loading.value = false;
if (res.code === 200) {
ElMessage.success("操作成功");
handlePagedCallback();
} else {
ElMessage.error(res.message);
}
});
}
});
};
const handleResetForm = () => {
ClassNames.value = [];
classesAddForm.value?.resetFields();
};
const SchoolApi = new hTableAPI("usercenter/back/schools");
const fetchInitdata = async () => {
//
ClassNameList.value = Array.from({ length: 500 }, (_, i) => `${i + 1}`);
//
const gradeRes = await getenum("GradeEnum");
gradeList.value = gradeRes.data.map((s: ComboModel) => ({
text: s.text,
value: s.text
}));
//
SchoolApi.querycombo({ TextName: "Name", ValueName: "Id" }).then(res => {
if (res.code === 200) {
schoolList.value = res.data;
}
});
//
const typeRes = await getenum("ClassTypeEnum");
TypeList.value = typeRes.data;
};
const fetchFormdata = () => {
handleResetForm();
};
//
onMounted(() => {
fetchInitdata();
fetchFormdata();
});
// props
watch(
() => props.id,
newVal => {
form.value.Id = newVal;
fetchFormdata();
}
);
</script>
<style scoped>
.el-tag {
margin-top: 4px;
margin-right: 10px;
}
</style>

188
src/views/class/index.vue Normal file
View File

@ -0,0 +1,188 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref, defineOptions } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import { ruleRequired } from "@/utils/rules";
const ControllerName = "classes";
defineOptions({
name: ControllerName,
});
const SchoolApi = new hTableAPI("usercenter/back/schools");
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = {
apiUrl: "usercenter/back/classes",
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 20,
OrderBy: "CreateTime", //
defaultConditions: [], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [
{
//
topBtn: false, //
label: "修改",
btnType: "edit", // add edit del custom
},
{
topBtn: true, //
label: "新增",
btnType: "custom", // add edit del custom
btnStyle: "success", // topBtn: true success danger
custom: {
// custom
title: "新增班级", // title
src: "class/edit", //
width: "550px", //
height: "520px", //
},
},
{
topBtn: false, //
show: true,
label: "删除",
btnType: "del", // add edit del
btnStyle: "danger", // topBtn: true success danger
},
],
column: {
//
id: {
label: "编号",
search: true,
add: false, //
edit: false, //
width: "150px",
},
schoolId: {
label: "学校",
rules: ruleRequired,
width: "180px",
search: true,
type: "dropdown",
add: true, //
edit: true, //
setting: {},
},
name: {
label: "名称",
rules: ruleRequired,
width: "180px",
search: true,
searchType: ConditionalType.Like, //
add: true, //
edit: true, //
},
Grade: {
label: "年级",
rules: ruleRequired,
width: "180px",
type: "dropdown",
custom: (row) => `${row.grade ?? ""}`,
// `${row.grade ?? ""} ${row.gradeLevel + row.graduationYear}`,
search: true,
setting: {},
add: true, //
edit: false, //
},
// GradeLevel: {
// label: "",
// search: false,
// type: "dropdown",
// searchType: ConditionalType.Like,
// add: true, //
// edit: true, //
// width: "70px",
// setting: {
// datasource: [
// { text: "", value: "" },
// { text: "", value: "" },
// { text: "", value: "" }
// ]
// }
// },
// GraduationYear: {
// label: "",
// search: false,
// add: true, //
// edit: true, //
// width: "80px"
// },
type: {
label: "类型",
rules: ruleRequired,
// width: "150px",
type: "dropdown",
search: true,
add: true, //
edit: true, //
setting: {},
},
// createTime: {
// label: "",
// width: "180px",
// type: "datetime",
// search: true,
// add: false, //
// edit: false //
// },
// remark: {
// label: "",
// type: "textarea",
// editRows: 3,
// search: false,
// add: true, //
// edit: true //
// }
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
};
const showTable = ref(false);
onMounted(async () => {
//
tableData.column.Grade.setting.datasource = [
{ text: "初一", value: "初一" },
{ text: "初二", value: "初二" },
{ text: "初三", value: "初三" },
{ text: "高一", value: "高一" },
{ text: "高二", value: "高二" },
{ text: "高三", value: "高三" },
{ text: "一年级", value: "一年级" },
{ text: "二年级", value: "二年级" },
{ text: "三年级", value: "三年级" },
{ text: "四年级", value: "四年级" },
{ text: "五年级", value: "五年级" },
{ text: "六年级", value: "六年级" },
];
tableData.column.type.setting.datasource = (await getenum("ClassTypeEnum")).data;
tableData.column.schoolId.setting.datasource = (
await SchoolApi.querycombo({ TextName: "Name", ValueName: "Id" })
).data;
showTable.value = true;
});
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
</template>

View File

@ -0,0 +1,163 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref, defineOptions } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import { ruleRequired, ruleRequiredNumber } from "@/utils/rules";
import { DeleteExamInfo, ImportExamInfo } from "@/api/exam";
import { entryExamInfo } from "./examFun";
import { ElMessage, ElMessageBox } from "element-plus";
const ControllerName = "ExamClassInfo";
defineOptions({
name: ControllerName,
});
const props = defineProps<{
data: any;
}>();
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = {
apiUrl: ControllerName,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 20,
OrderBy: "Id", //
defaultConditions: [
{
FieldName: "ExamId",
FieldValue: props.data[0].id + "",
ConditionalType: ConditionalType.Equal,
},
], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [
{
topBtn: false, //
show: true,
label: "删除",
click: deleteInfo,
btnStyle: "danger", // topBtn: true success danger
},
{
topBtn: false, //
show: true,
label: "重新录入",
click: reloadImportInfo,
btnStyle: "primary", // topBtn: true success danger
},
{
topBtn: false, //
show: true,
label: "学生成绩详情",
btnType: "custom",
btnStyle: "primary",
custom: {
title: "考试学生班级详情", // title
src: "exam/userDetails", //
width: "1600px", //
height: "880px", //
},
},
],
column: {
//
schoolName: {
label: "学校",
search: true,
searchType: ConditionalType.Like, //
add: false, //
edit: false, //
width: "180px",
},
grade: {
label: "年级",
width: "100px",
search: true,
add: false, //
edit: false, //
},
className: {
label: "班级",
width: "150px",
search: true,
searchType: ConditionalType.Like, //
add: false, //
edit: false, //
},
peopleCount: {
label: "参考人数",
width: "100px",
search: false,
add: false, //
edit: false, //
},
entryPerson: {
label: "录入人",
width: "200px",
search: true,
add: false, //
edit: false, //
},
createTime: {
label: "录入时间",
width: "200px",
search: false,
add: false, //
edit: false, //
},
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
};
async function deleteInfo(o, row, c) {
try {
await ElMessageBox.confirm("是否删除考试信息?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await DeleteExamInfo({ examId: row[0].examId, classId: row[0].classId });
} catch (error) {
ElMessage.info("取消删除");
}
}
async function reloadImportInfo(o, row, c) {
try {
await ElMessageBox.confirm("是否重新录入考试信息?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await DeleteExamInfo({ examId: row[0].examId, classId: row[0].classId });
entryExamInfo(row[0].examId);
} catch (error) {
ElMessage.info("取消重新录入");
}
}
const showTable = ref(false);
onMounted(async () => {
//
showTable.value = true;
});
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
</template>

View File

@ -0,0 +1,121 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import { ruleRequired, ruleRequiredNumber } from "@/utils/rules";
const ControllerName = "ExamClassInfo";
defineOptions({
name: "ClassExam",
});
const props = defineProps<{
data: any;
}>();
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = {
apiUrl: ControllerName,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 20,
OrderBy: "Id", //
OrderByType: 1, //
defaultConditions: [], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [
{
topBtn: false, //
show: true,
label: "详情",
btnType: "custom",
btnStyle: "primary",
custom: {
title: "考试学生班级详情", // title
src: "exam/classExamRecord", //
width: "1600px", //
height: "800px", //
},
},
],
column: {
//
schoolName: {
label: "学校",
search: true,
searchType: ConditionalType.Like, //
add: false, //
edit: false, //
width: "180px",
},
grade: {
label: "年级",
width: "120px",
// custom: (s) => `${s.gradeLevel}${s.gradeYear}`,
search: true,
add: false, //
edit: false, //
},
className: {
label: "班级",
width: "80px",
search: true,
searchType: ConditionalType.Like, //
add: false, //
edit: false, //
},
examName: {
label: "最近考试",
width: "200px",
search: false,
},
peopleCount: {
label: "参考人数",
width: "100px",
search: false,
},
onLineCount: {
label: "重本人数",
width: "100px",
search: false,
},
onLineRate: {
label: "重本率",
width: "100px",
custom: (row) => `${row.onLineRate * 100}%`,
search: false,
},
onLineRanking: {
label: "重本率排名",
search: false,
},
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
};
const showTable = ref(false);
onMounted(async () => {
//
showTable.value = true;
});
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
</template>

View File

@ -0,0 +1,171 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref, defineOptions } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import { ruleRequired, ruleRequiredNumber } from "@/utils/rules";
import { DeleteExamInfo, ImportExamInfo } from "@/api/exam";
import { entryExamInfo } from "./examFun";
import { ElMessage, ElMessageBox } from "element-plus";
import { average } from "@pureadmin/utils";
const ControllerName = "ClassExamRecord";
defineOptions({
name: ControllerName,
});
const props = defineProps<{
data: any;
}>();
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = {
apiUrl: `ExamClassInfo`,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 20,
OrderByType: 1, //
OrderBy: "Id", //
defaultConditions: [
{
FieldName: "ClassId",
FieldValue: props.data[0].classId + "",
ConditionalType: ConditionalType.Equal,
},
], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [
{
topBtn: false, //
show: true,
label: "学生成绩详情",
btnType: "custom",
btnStyle: "primary",
custom: {
title: "考试学生班级详情", // title
src: "exam/userDetails", //
width: "1600px", //
height: "880px", //
},
},
],
column: {
//
examName: {
label: "考试名称",
search: true,
width: "150px",
},
type: {
label: "考试类型",
search: true,
type: "dropdown",
setting: {},
width: "80px",
},
testPaperType: {
label: "试卷类型",
search: true,
type: "dropdown",
setting: {},
width: "80px",
},
grade: {
label: "年级",
search: true,
// type: "dropdown",
// setting: {},
width: "60px",
},
onLineCount: {
label: "重本人数",
search: false,
width: "80px",
},
onLineRate: {
label: "重本率",
search: false,
custom: (row) => `${Math.round(row.onLineRate * 100)}%`,
width: "80px",
},
onLineRanking: {
label: "重本率排名",
search: false,
width: "100px",
},
maxScore: {
label: "最高分[赋分]",
search: false,
width: "140px",
},
minScore: {
label: "最低分[赋分]",
search: false,
width: "140px",
},
average: {
label: "总平均分[赋分]",
search: false,
custom: (row) => `${Math.round(row.average)}`,
width: "140px",
},
average1: {
label: "资源校平均分[赋分]",
search: false,
width: "160px",
},
averageRank: {
label: "总平均分排名",
search: false,
width: "110px",
},
rank: {
label: "远端平均/资源校平均",
search: false,
width: "95px",
custom: (row) =>
`${
row.baseSchoolScore == 0
? "--"
: Math.round((row.average / row.baseSchoolScore) * 100)
}%`,
},
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
};
const showTable = ref(false);
onMounted(async () => {
//
// tableData.column.grade.setting.datasource = (await getenum("GradeEnum")).data.map(
// (x) => {
// return { text: x.text, value: x.text };
// }
// );
tableData.column.testPaperType.setting.datasource = (
await getenum("TestPaperTypeEnum")
).data;
tableData.column.type.setting.datasource = (await getenum("ExamTypeEnum")).data;
showTable.value = true;
});
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
</template>

36
src/views/exam/examFun.ts Normal file
View File

@ -0,0 +1,36 @@
import { ImportExamInfo } from "@/api/exam";
import { ElMessage } from "element-plus";
export function entryExamInfo(eid: number) {
let fileE = document.createElement("input");
fileE.type = "file";
var formData = new window.FormData();
fileE.onchange = async function () {
formData.append("File", fileE.files[0]);
let res = await ImportExamInfo(eid, fileE.files[0]);
if (res.code != undefined) {
if (res.code !== 200) return ElMessage.error(res.message);
else return ElMessage.success("所有数据录入成功");
} else if (res.type === "application/json") {
let json = JSON.parse(await res.text());
if (json !== undefined && json.code !== 200) {
return ElMessage.error(json.message);
} else {
return ElMessage.success("操所有数据录入成功作成功");
}
} else if (res === undefined || res.size === 0)
return ElMessage.success("所有数据录入成功");
const url = res && window.URL.createObjectURL(res);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "未成功导入的考试信息数据" + ".xlsx");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
ElMessage.success("操作成功,已导出重复数据");
};
try {
fileE.click();
} catch (error) { }
}

203
src/views/exam/index.vue Normal file
View File

@ -0,0 +1,203 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import {
ruleNumber,
ruleRequired,
ruleRequiredI,
ruleRequiredNumber,
} from "@/utils/rules";
import { ImportExamInfo } from "@/api/exam";
import { ElMessage } from "element-plus";
import { entryExamInfo } from "./examFun";
const ControllerName = "Exam";
defineOptions({
name: ControllerName,
});
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = {
apiUrl: ControllerName,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 20,
OrderBy: "Id", //
defaultConditions: [], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [
{
//
topBtn: false, //
label: "修改",
btnType: "edit", // add edit del custom
},
{
//
topBtn: true, //
label: "添加",
btnStyle: "success",
btnType: "add", // add edit del custom
},
{
topBtn: false, //
show: true,
label: "删除",
btnType: "del", // add edit del
btnStyle: "danger", // topBtn: true success danger
},
{
topBtn: false, //
show: true,
label: "详情",
btnType: "custom", // add edit del
btnStyle: "primary",
custom: {
title: "考试班级详情", // title
src: "exam/classDetails", //
width: "1300px", //
height: "800px", //
},
},
{
topBtn: false, //
show: true,
label: "录入成绩",
click: entryExam,
btnStyle: "primary", // topBtn: true success danger
},
{
topBtn: true, //
show: true,
label: "录入成绩模板",
click: DwImportTemplate,
btnStyle: "info", // topBtn: true success danger
},
],
column: {
//
id: {
label: "编号",
search: true,
add: false, //
edit: false, //
width: "150px",
},
name: {
label: "考试名称",
rules: ruleRequired,
width: "200px",
search: true,
searchType: ConditionalType.Like, //
add: true, //
edit: true, //
setting: {},
},
grade: {
label: "年级",
rules: ruleRequiredI(6, 2),
width: "100px",
search: true,
add: true, //
edit: false, //
},
testPaperType: {
label: "试卷类型",
rules: ruleRequired,
width: "100px",
type: "dropdown",
setting: {},
search: true,
add: true, //
edit: true, //
},
type: {
label: "考试类型",
rules: ruleRequired,
width: "100px",
type: "dropdown",
setting: {},
search: true,
add: true, //
edit: true, //
},
scoreLine: {
label: "划线分数",
rules: ruleNumber,
search: false,
width: "100px",
add: true, //
edit: true, //
},
baseSchoolScore: {
label: "资源校平均分",
rules: ruleNumber,
search: false,
width: "150px",
add: true, //
edit: true, //
},
startTime: {
label: "考试时间",
width: "210px",
rules: ruleRequired,
search: true,
type: "datetime",
setting: {},
add: true, //
edit: true, //
},
createTime: {
label: "创建时间",
type: "datetime",
search: false,
add: false, //
edit: false, //
},
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
};
function DwImportTemplate(obj, row, callBack) {
const baseUrl = import.meta.env.VITE_API_BASEURL;
const excelImportUsersUrl = `${baseUrl}/ExamClassInfo/DwImportTemplate`;
window.open(excelImportUsersUrl, "_blank");
}
function entryExam(obj, row, callBack) {
entryExamInfo(row[0].id);
}
const showTable = ref(false);
onMounted(async () => {
//
// tableData.column.level.setting.datasource = (await getenum("GradeEnum")).data;
tableData.column.testPaperType.setting.datasource = (
await getenum("TestPaperTypeEnum")
).data;
tableData.column.type.setting.datasource = (await getenum("ExamTypeEnum")).data;
showTable.value = true;
});
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
</template>

View File

@ -0,0 +1,181 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import { ruleRequired, ruleRequiredNumber } from "@/utils/rules";
const ControllerName = "ExamUserInfo";
defineOptions({
name: ControllerName,
});
const props = defineProps<{
data: any;
}>();
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = {
apiUrl: ControllerName,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 60,
OrderBy: "AssignRanking", //
OrderByType: 0,
defaultConditions: [
{
FieldName: "ExamId",
FieldValue: props.data[0].examId + "",
},
{
FieldName: "ClassId",
FieldValue: props.data[0].classId + "",
},
], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [
{
//
topBtn: true, //
label: "成绩升序",
btnStyle: "primary",
click: (o, r, c) => {
tableData.search.OrderByType = 1;
c();
},
},
{
//
topBtn: true, //
label: "成绩降序",
btnStyle: "primary",
click: (o, r, c) => {
tableData.search.OrderByType = 0;
c();
},
},
{
topBtn: false, //
label: "个人详情",
btnType: "custom",
btnStyle: "primary",
custom: {
title: "考试学生班级详情", // title
src: "exam/userExam", //
width: "1600px", //
height: "800px", //
},
},
],
column: {
//
userName: {
label: "姓名",
search: true,
searchType: ConditionalType.Like, //
width: "180px",
},
语文: {
label: "语文",
search: false,
width: "100px",
custom: (row) => row.subjectDic.语文,
},
数学: {
label: "数学",
search: false,
width: "100px",
custom: (row) => row.subjectDic.数学,
},
英语: {
label: "英语",
search: false,
width: "100px",
custom: (row) => row.subjectDic.英语,
},
物理: {
label: "物理",
search: false,
width: "100px",
custom: (row) => row.subjectDic.物理,
},
化学: {
label: "化学",
search: false,
width: "100px",
custom: (row) => row.subjectDic.化学,
},
生物: {
label: "生物",
search: false,
width: "100px",
custom: (row) => row.subjectDic.生物,
},
政治: {
label: "政治",
search: false,
width: "100px",
custom: (row) => row.subjectDic.政治,
},
历史: {
label: "历史",
search: false,
width: "100px",
custom: (row) => row.subjectDic.历史,
},
地理: {
label: "地理",
search: false,
width: "100px",
custom: (row) => row.subjectDic.地理 ?? "--",
},
assignScore: {
label: "赋分总分",
search: false,
width: "180px",
},
assignRanking: {
label: "赋分后的排名",
search: false,
width: "200px",
},
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
};
const showTable = ref(false);
onMounted(async () => {
//
showTable.value = true;
});
const exam = props.data[0];
</script>
<template>
<div>
<div class="p-[10px] text-[1.5rem]">
<strong>学校</strong>{{ exam.schoolName }}&nbsp;&nbsp; <strong>年级</strong
>{{ exam.gradeLevel + exam.gradeYear }}&nbsp;&nbsp; <strong>班级</strong
>{{ exam.className }}&nbsp;&nbsp; <strong>考试名称</strong
>{{ exam.examName }}&nbsp;&nbsp;
<!-- <strong>考试类型</strong>{{exam.className}}&nbsp;&nbsp;
<strong>试卷类型</strong>{{exam.className}}&nbsp;&nbsp; -->
</div>
<ahTable v-if="showTable" ref="table" :tableConfig="tableData" />
</div>
</template>

173
src/views/exam/userExam.vue Normal file
View File

@ -0,0 +1,173 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import { ruleRequired, ruleRequiredNumber } from "@/utils/rules";
import { getClassInfo, getSchoolInfo } from "@/api/userCenter";
const ControllerName = "ExamUserInfo";
defineOptions({
name: ControllerName,
});
const props = defineProps<{
data: any;
}>();
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = {
apiUrl: ControllerName,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 60,
OrderBy: "Id", //
OrderByType: 1,
defaultConditions: [
{
FieldName: "UserId",
FieldValue: props.data[0].userId + "",
},
], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [],
column: {
//
examName: {
label: "考试名称",
search: true,
searchType: ConditionalType.Like, //
width: "180px",
},
//
type: {
label: "考试类型",
search: true,
type: "dropdown",
setting: {},
width: "150px",
},
//
grade: {
label: "考试阶段",
search: true,
searchType: ConditionalType.Like, //
width: "120px",
}, //
testPaperType: {
label: "试卷类型",
search: true,
type: "dropdown",
setting: {},
width: "150px",
},
语文: {
label: "语文",
search: false,
width: "80px",
custom: (row) => row.subjectDic.语文 ?? "--",
},
数学: {
label: "数学",
search: false,
width: "80px",
custom: (row) => row.subjectDic.数学 ?? "--",
},
英语: {
label: "英语",
search: false,
width: "80px",
custom: (row) => row.subjectDic.英语 ?? "--",
},
物理: {
label: "物理",
search: false,
width: "80px",
custom: (row) => row.subjectDic.物理 ?? "--",
},
化学: {
label: "化学",
search: false,
width: "80px",
custom: (row) => row.subjectDic.化学 ?? "--",
},
生物: {
label: "生物",
search: false,
width: "80px",
custom: (row) => row.subjectDic.生物 ?? "--",
},
政治: {
label: "政治",
search: false,
width: "80px",
custom: (row) => row.subjectDic.政治 ?? "--",
},
历史: {
label: "历史",
search: false,
width: "80px",
custom: (row) => row.subjectDic.历史 ?? "--",
},
地理: {
label: "地理",
search: false,
width: "80px",
custom: (row) => row.subjectDic.地理 ?? "--",
},
assignScore: {
label: "赋分总分",
search: false,
width: "80px",
},
assignRanking: {
label: "赋分后的排名",
search: false,
width: "120px",
},
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
};
const showTable = ref(false);
const exam = props.data[0];
onMounted(async () => {
//
getClassInfo(exam.classId).then((res) => {
exam.className = res.data.name;
});
getSchoolInfo(exam.schoolId).then((res) => {
exam.schoolName = res.data.name;
});
tableData.column.testPaperType.setting.datasource = (
await getenum("TestPaperTypeEnum")
).data;
tableData.column.type.setting.datasource = (await getenum("ExamTypeEnum")).data;
showTable.value = true;
});
</script>
<template>
<div>
<div class="p-[10px] text-[1.3rem]">
<strong>学生{{ exam.userName }}</strong
>&nbsp; &nbsp; {{ exam.schoolName }} {{ exam.grade }}
{{ exam.className }}
</div>
<ahTable v-if="showTable" ref="table" :tableConfig="tableData" />
</div>
</template>

136
src/views/grade/index.vue Normal file
View File

@ -0,0 +1,136 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import { ruleRequired } from "@/utils/rules";
const ControllerName = "Grade";
defineOptions({
name: ControllerName
});
const SchoolApi = new hTableAPI("usercenter/back/schools");
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = {
apiUrl: ControllerName,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 20,
OrderBy: "CreateTime", //
defaultConditions: [], //
Conditions: []
},
operationColumn: true, //
operationColumnData: [
{
//
topBtn: false, //
label: "修改",
btnType: "edit" // add edit del custom
},
{
//
topBtn: true, //
label: "添加",
btnStyle: "success",
btnType: "add" // add edit del custom
},
{
topBtn: false, //
show: true,
label: "删除",
btnType: "del", // add edit del
btnStyle: "danger" // topBtn: true success danger
}
],
column: {
//
id: {
label: "编号",
search: true,
add: false, //
edit: false, //
width: "150px"
},
schoolId: {
label: "学校",
rules: ruleRequired,
width: "200px",
search: true,
type: "dropdown",
add: true, //
edit: true, //
setting: {}
},
name: {
label: "名称[动态]",
rules: ruleRequired,
width: "100px",
search: false,
searchType: ConditionalType.Like,
add: false, //
edit: false //
},
level: {
label: "年级",
rules: ruleRequired,
width: "80px",
type: "dropdown",
search: true,
setting: {},
add: true, //
edit: true //
},
year: {
label: "毕业届",
width: "80px",
rules: ruleRequired,
search: true,
setting: {},
add: true, //
edit: true //
},
createTime: {
label: "创建时间",
type: "datetime",
search: true,
add: false, //
edit: false //
}
},
data: [],
pageData: {
total: 0
},
selectRows: []
};
const showTable = ref(false);
onMounted(async () => {
//
tableData.column.level.setting.datasource = (
await getenum("GradeLevelEnum")
).data;
tableData.column.schoolId.setting.datasource = (
await SchoolApi.querycombo({ TextName: "Name", ValueName: "Id" })
).data;
showTable.value = true;
});
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
</template>

View File

@ -21,7 +21,7 @@ import Lock from "~icons/ri/lock-fill";
import User from "~icons/ri/user-3-fill"; import User from "~icons/ri/user-3-fill";
defineOptions({ defineOptions({
name: "Login" name: "Login",
}); });
const router = useRouter(); const router = useRouter();
@ -37,22 +37,22 @@ dataThemeChange(overallStyle.value);
const { title } = useNav(); const { title } = useNav();
const ruleForm = reactive({ const ruleForm = reactive({
account: "admin", account: "",
password: "123456" password: "",
}); });
const onLogin = async (formEl: FormInstance | undefined) => { const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
await formEl.validate(valid => { await formEl.validate((valid) => {
if (valid) { if (valid) {
loading.value = true; loading.value = true;
useUserStoreHook() useUserStoreHook()
.loginByUsername({ .loginByUsername({
account: ruleForm.account, account: ruleForm.account,
password: ruleForm.password password: ruleForm.password,
}) })
.then(res => { .then((res) => {
if ((res.code = 200)) { if (res.code == 200) {
// //
return initRouter().then(() => { return initRouter().then(() => {
disabled.value = true; disabled.value = true;
@ -64,7 +64,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
.finally(() => (disabled.value = false)); .finally(() => (disabled.value = false));
}); });
} else { } else {
message("登录失败", { type: "error" }); // message("", { type: "error" });
} }
}) })
.finally(() => (loading.value = false)); .finally(() => (loading.value = false));
@ -72,18 +72,10 @@ const onLogin = async (formEl: FormInstance | undefined) => {
}); });
}; };
const immediateDebounce: any = debounce( const immediateDebounce: any = debounce((formRef) => onLogin(formRef), 1000, true);
formRef => onLogin(formRef),
1000,
true
);
useEventListener(document, "keydown", ({ code }) => { useEventListener(document, "keydown", ({ code }) => {
if ( if (["Enter", "NumpadEnter"].includes(code) && !disabled.value && !loading.value)
["Enter", "NumpadEnter"].includes(code) &&
!disabled.value &&
!loading.value
)
immediateDebounce(ruleFormRef.value); immediateDebounce(ruleFormRef.value);
}); });
</script> </script>
@ -126,8 +118,8 @@ useEventListener(document, "keydown", ({ code }) => {
{ {
required: true, required: true,
message: '请输入账号', message: '请输入账号',
trigger: 'blur' trigger: 'blur',
} },
]" ]"
prop="account" prop="account"
> >

371
src/views/menu/edit.vue Normal file
View File

@ -0,0 +1,371 @@
<template>
<div class="menu-edit-container">
<el-form
ref="menuFormRef"
:model="menuForm"
:rules="formRules"
label-position="top"
class="menu-form"
>
<el-row :gutter="30">
<!-- 左侧表单区域 -->
<el-col :span="12">
<el-form-item label="路由名称" prop="name">
<el-input
v-model="menuForm.name"
placeholder="请输入路由唯一名称"
clearable
/>
</el-form-item>
<el-form-item label="菜单标题" prop="title">
<el-input
v-model="menuForm.title"
placeholder="请输入菜单标题"
clearable
/>
</el-form-item>
<el-form-item label="路由路径" prop="path">
<el-input
v-model="menuForm.path"
placeholder="请输入路由路径,如 /dashboard"
clearable
/>
<div class="form-tip">如果是按钮权限可留空</div>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input
v-model="menuForm.icon"
placeholder="请输入图标类名,如 el-icon-menu"
clearable
>
</el-input>
<div v-if="menuForm.icon" class="icon-preview">
<i
:class="menuForm.icon"
style="font-size: 24px; margin-top: 8px"
></i>
</div>
</el-form-item>
<el-form-item label="授权码" prop="auths">
<el-input
v-model="menuForm.auths"
placeholder="请输入授权码,多个用逗号分隔"
clearable
/>
<div class="form-tip">按钮权限需要的授权码</div>
</el-form-item>
</el-col>
<!-- 右侧表单区域 -->
<el-col :span="12">
<el-form-item label="父级菜单" prop="parentId">
<el-tree-select
v-model="menuForm.parentId"
:data="menuTreeData"
:props="treeProps"
check-strictly
placeholder="请选择父级菜单"
clearable
:expand-on-click-node="false"
:render-after-expand="false"
:default-expand-all="true"
:highlight-current="true"
/>
</el-form-item>
<el-form-item label="排序排名" prop="rank">
<el-input-number
v-model="menuForm.rank"
:min="0"
:max="100"
controls-position="right"
/>
<div class="form-tip">数值越小排名越靠前</div>
</el-form-item>
<el-form-item label="菜单类型">
<el-switch
v-model="menuForm.isButton"
active-text="按钮权限"
inactive-text="常规菜单"
style="
--el-switch-on-color: #13ce66;
--el-switch-off-color: #409eff;
"
/>
</el-form-item>
<el-form-item label="显示状态">
<el-switch
v-model="menuForm.showLink"
active-text="显示"
inactive-text="隐藏"
/>
</el-form-item>
<div v-if="menuForm.isButton" class="button-permission-info">
<el-alert title="按钮权限说明" type="info" :closable="false">
<p>1. 按钮权限不会显示在导航菜单中</p>
<p>2. 需要填写授权码用于权限控制</p>
<p>3. 路由路径可留空</p>
</el-alert>
</div>
</el-col>
</el-row>
</el-form>
<el-divider />
<div class="header">
<div class="header-actions">
<el-button @click="resetForm">重置</el-button>
<el-button type="primary" @click="submitForm" :loading="submitting">
{{ isEditMode ? "更新菜单" : "创建菜单" }}
</el-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineComponent, ref, reactive, onMounted, onBeforeMount } from "vue";
import { MenuItem, Edit } from "@/api/menu";
import {
ElMessage,
ElMessageBox,
FormInstance,
FormValidateCallback
} from "element-plus";
import propTypes from "@/utils/propTypes";
defineOptions({
name: "MenuEdit"
});
const props = defineProps({
treeData: {
type: Object as PropType<MenuItem[]>
},
info: {
type: Object as PropType<MenuItem>,
default: null
}
});
const emit = defineEmits(["callbackFun"]);
const menuFormRef = ref<FormInstance>();
const menuForm = reactive<MenuItem>({
name: "",
path: "",
isButton: false,
title: "",
icon: "",
auths: "",
rank: 50,
showLink: true,
parentId: undefined
});
const menuTreeData = ref<MenuItem[]>(props.treeData);
const treeProps = {
value: "id",
label: "title",
children: "children"
};
const isEditMode = ref(false);
const submitting = ref(false);
const iconDialogVisible = ref(false);
//
const icons = ref([
"el-icon-menu",
"el-icon-setting",
"el-icon-user",
"el-icon-s-home",
"el-icon-s-data",
"el-icon-s-check",
"el-icon-s-opportunity",
"el-icon-s-order",
"el-icon-s-platform",
"el-icon-s-promotion",
"el-icon-s-shop",
"el-icon-s-marketing",
"el-icon-s-flag",
"el-icon-s-comment",
"el-icon-s-finance"
]);
const formRules = {
name: [
{ required: true, message: "请输入菜单名称", trigger: "blur" },
{ min: 2, max: 20, message: "长度在 2 到 20 个字符", trigger: "blur" }
],
title: [
{ required: true, message: "请输入菜单标题", trigger: "blur" },
{ min: 2, max: 20, message: "长度在 2 到 20 个字符", trigger: "blur" }
],
path: [
{
validator: (rule: any, value: string, callback: any) => {
if (!menuForm.isButton && !value) {
callback(new Error("常规菜单必须填写路由路径"));
} else {
callback();
}
},
trigger: "blur"
}
],
auths: [
{
validator: (rule: any, value: string, callback: any) => {
if (menuForm.isButton && !value) {
callback(new Error("按钮权限必须填写授权码"));
} else {
callback();
}
},
trigger: "blur"
}
],
rank: [{ required: true, message: "请输入排序值", trigger: "blur" }]
};
//
onMounted(() => {
Object.assign(menuForm, props.info);
if (props.info != null && props.info.id > 0) {
isEditMode.value = true;
} else menuForm.id = 0;
});
const submitForm = () => {
menuFormRef.value?.validate(valid => {
if (valid) {
submitting.value = true;
Edit(menuForm).then(res => {
submitting.value = false;
ElMessage.success(
isEditMode.value ? "菜单更新成功!" : "菜单创建成功!"
);
goBack();
});
} else {
ElMessage.warning("请正确填写表单内容");
}
});
};
//
const resetForm = () => {
ElMessageBox.confirm("确定要重置表单内容吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
menuFormRef.value?.resetFields();
if (!isEditMode.value) {
Object.assign(menuForm, {
isButton: false,
showLink: true,
rank: 50,
parentId: menuForm.parentId
});
}
});
};
//
const goBack = () => emit("callbackFun"); // ;
</script>
<style scoped>
.menu-edit-container {
max-width: 1200px;
margin: 0 auto;
}
.header {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 20px;
}
.header h1 {
margin: 0;
font-size: 24px;
color: #303133;
}
.header-actions {
display: flex;
gap: 10px;
}
.menu-form {
margin-top: 20px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.button-permission-info {
margin-top: 20px;
padding: 15px;
background-color: #f4f4f5;
border-radius: 4px;
}
.icon-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 15px;
max-height: 400px;
overflow-y: auto;
}
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px 10px;
border: 1px solid #ebeef5;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.icon-item:hover {
background-color: #ecf5ff;
border-color: #409eff;
}
.icon-item.selected {
background-color: #ecf5ff;
border-color: #409eff;
color: #409eff;
}
.icon-item i {
font-size: 24px;
margin-bottom: 8px;
}
.icon-name {
font-size: 12px;
text-align: center;
word-break: break-all;
}
.el-alert p {
margin: 5px 0;
font-size: 13px;
line-height: 1.5;
}
</style>

View File

@ -1,97 +1,165 @@
<template> <template>
<div> <div>
<div style="padding-bottom: 5px;"> <div style="padding-bottom: 5px">
<el-button type="primary"> 分配权限 </el-button> <el-button type="success" v-show="!isAuthorized" @click="() => showDialog(null)">
新增根菜单
</el-button>
<el-button type="primary" v-show="isAuthorized" @click="callBack">
分配权限
</el-button>
</div> </div>
<el-tree <el-tree
:data="treeData" :data="treeData"
:props="treeProps" :props="treeProps"
:default-checked-keys="defaultCheckedKeys"
node-key="id" node-key="id"
:default-expand-all="true" :default-expand-all="true"
:highlight-current="true" :highlight-current="true"
:expand-on-click-node="false"
show-checkbox show-checkbox
class="menu-tree" ref="treeRef"
:class="isAuthorized ? `menu-tree menu-tree1` : `menu-tree`"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<div class="menu-node"> <div v-if="!data.isButton" class="menu-node">
<i v-if="data.icon" :class="data.icon" class="menu-icon"></i> <div class="menu-node">
<span class="menu-title">{{ data.title }}</span> <i v-if="data.icon" :class="data.icon" class="menu-icon"></i>
<span class="menu-path" v-if="data.path">{{ data.path }}</span> <span class="menu-title">{{ data.title }}</span>
<span class="menu-path-Rank" v-if="data.rank">排序[{{ data.rank }}]</span>
<span class="menu-path" v-if="data.path">{{ data.path }}</span>
</div>
<div style="display: flex; gap: 6px" v-show="!isAuthorized">
<el-button
type="success"
link
@click="() => showDialog({ parentId: data.id } as MenuItem)"
>
添加
</el-button>
<el-button type="primary" link @click="() => showDialog(data)">
编辑
</el-button>
<el-button type="danger" link @click.stop="() => delMenu(data.id)">
删除
</el-button>
</div>
</div> </div>
<div style="display: flex; gap: 6px"> <div v-else class="menu-node" style="background-color: #f7f7f7">
<el-button type="success" link @click="() => {}"> 添加 </el-button> <div>
<el-button type="primary" link @click="() => {}"> 编辑 </el-button> <i v-if="data.icon" :class="data.icon" class="menu-icon"></i>
<el-button type="danger" link @click="() => {}"> 删除 </el-button> <span>菜单按钮权限: </span>
<el-button type="primary">
{{ data.title }}
</el-button>
</div>
<div style="display: flex; gap: 6px" v-show="!isAuthorized">
<el-button type="primary" link @click="() => showDialog(data)">
编辑
</el-button>
<el-button type="danger" link @click.stop="() => delMenu(data.id)">
删除
</el-button>
</div>
</div> </div>
</template> </template>
</el-tree> </el-tree>
<el-dialog
v-if="dialogData.visible"
v-model="dialogData.visible"
title="菜单编辑"
width="800"
:before-close="handleClose"
>
<MenuEdit
@callbackFun="EditCallback"
:info="dialogData.info"
:treeData="treeData"
></MenuEdit>
</el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { MenuAll, MenuItem } from "@/api/menu"; import { MenuAll, MenuItem, Del, RoleMenu, SetMenu } from "@/api/menu";
import { ElMessage } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import MenuEdit from "./edit.vue";
import { ref, computed, onMounted } from "vue"; import { ref, computed, onMounted } from "vue";
defineOptions({ defineOptions({
name: "Menu" name: "Menu",
}); });
// API const props = defineProps<{
const mockMenuData: MenuItem[] = [ data: any;
{ }>();
id: 1, const isAuthorized = props.data != null && props.data.length > 0;
name: "dashboard", const treeRef = ref();
path: "/dashboard",
isButton: false, // ID
title: "控制台", const defaultCheckedKeys = ref([0]);
icon: "el-icon-monitor", /** 显示弹窗 */
rank: 1, const dialogData = ref({
showLink: true, visible: false,
parentId: 0 info: null,
}, });
{ function showDialog(info: MenuItem) {
id: 2, dialogData.value.visible = true;
name: "system", dialogData.value.info = info;
path: "/system", }
isButton: false, async function EditCallback(info: MenuItem) {
title: "系统管理", await fetchInitData();
icon: "el-icon-setting", dialogData.value.visible = false;
rank: 2, }
showLink: true, const handleClose = (done: () => void) => {
parentId: 0 done();
}, // ElMessageBox.confirm("?")
{ // .then(() => {
id: 3, // done();
name: "user", // })
path: "/system/user", // .catch(() => {
isButton: false, // // catch error
title: "用户管理", // });
icon: "el-icon-user", };
rank: 1, async function callBack() {
showLink: true, //
parentId: 2 const checkedNodes = treeRef.value.getCheckedNodes();
}, //
{ const halfCheckedNodes = treeRef.value.getHalfCheckedNodes();
id: 4, //
name: "role", SetMenu({
path: "/system/role", roleId: props.data[0].id,
isButton: false, menuId: checkedNodes
title: "角色管理", .map((node: MenuItem) => node.id)
icon: "el-icon-s-custom", .concat(halfCheckedNodes.map((node: MenuItem) => node.id)),
rank: 2, })
showLink: true, .then((res) => {
parentId: 2 if (res.code === 200) {
}, ElMessage.success("分配权限成功");
{ } else {
id: 5, ElMessage.error(res.message);
name: "createUser", }
isButton: true, })
title: "创建用户", .catch((error) => {
rank: 1, ElMessage.error("分配权限失败");
showLink: true, });
parentId: 3 }
}
]; async function delMenu(menuId: number) {
try {
await ElMessageBox.confirm("确定要删除此菜单?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
const res = await Del([menuId]);
if (res.code === 200) {
ElMessage.success(res.message);
await fetchInitData();
} else {
ElMessage.error(res.message);
}
} catch (error) {}
}
// //
const convertToTree = (menus: MenuItem[]): MenuItem[] => { const convertToTree = (menus: MenuItem[]): MenuItem[] => {
@ -99,12 +167,12 @@ const convertToTree = (menus: MenuItem[]): MenuItem[] => {
const tree: MenuItem[] = []; const tree: MenuItem[] = [];
// children // children
menus.forEach(menu => { menus.forEach((menu) => {
menuMap.set(menu.id, { ...menu, children: [] }); menuMap.set(menu.id, { ...menu, children: [] });
}); });
// //
menuMap.forEach(menu => { menuMap.forEach((menu) => {
if (menu.parentId === 0) { if (menu.parentId === 0) {
tree.push(menu); tree.push(menu);
} else { } else {
@ -118,7 +186,7 @@ const convertToTree = (menus: MenuItem[]): MenuItem[] => {
// //
const sortChildren = (nodes: MenuItem[]) => { const sortChildren = (nodes: MenuItem[]) => {
nodes.sort((a, b) => a.rank - b.rank); nodes.sort((a, b) => a.rank - b.rank);
nodes.forEach(node => { nodes.forEach((node) => {
if (node.children && node.children.length > 0) { if (node.children && node.children.length > 0) {
sortChildren(node.children); sortChildren(node.children);
} }
@ -135,28 +203,19 @@ const treeData = ref<MenuItem[]>([]);
// //
const treeProps = { const treeProps = {
label: "title", label: "title",
children: "children" children: "children",
}; };
async function fetchInitData() {
// API
const fetchMenuData = async (): Promise<MenuItem[]> => {
// API:
// const response = await fetch('/api/menus')
// return await response.json()
// 使
return new Promise(resolve => {
setTimeout(() => resolve(mockMenuData), 300);
});
};
onMounted(async () => {
const flatData = await MenuAll(); const flatData = await MenuAll();
if (flatData.code === 200) { if (flatData.code === 200) {
treeData.value = convertToTree(flatData.data); treeData.value = convertToTree(flatData.data);
} else { } else {
ElMessage.error(flatData.message); ElMessage.error(flatData.message);
} }
if (isAuthorized) defaultCheckedKeys.value = (await RoleMenu(props.data[0].id)).data;
}
onMounted(async () => {
await fetchInitData();
}); });
</script> </script>
@ -165,6 +224,11 @@ onMounted(async () => {
padding: 15px; padding: 15px;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;
max-height: calc(88vh - 48px - 42px);
overflow-y: auto;
}
.menu-tree1 {
max-height: calc(88vh - 120px);
} }
.menu-node { .menu-node {
@ -185,11 +249,16 @@ onMounted(async () => {
margin-right: 10px; margin-right: 10px;
} }
.menu-path-Rank {
font-size: 13px;
margin-right: 10px;
color: #909399;
}
.menu-path { .menu-path {
font-size: 12px; font-size: 12px;
color: #909399; color: #909399;
margin-right: 10px; margin-right: 10px;
flex-grow: 1; flex-grow: 0;
} }
.menu-tree { .menu-tree {
padding: 20px; /* 增加内边距 */ padding: 20px; /* 增加内边距 */

View File

@ -1,99 +0,0 @@
<script setup lang="ts">
import { hasAuth, getAuths } from "@/router/utils";
defineOptions({
name: "PermissionButtonRouter"
});
</script>
<template>
<div>
<p class="mb-2!">当前拥有的code列表{{ getAuths() }}</p>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">组件方式判断权限</div>
</template>
<el-space wrap>
<Auth value="permission:btn:add">
<el-button plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
</Auth>
<Auth :value="['permission:btn:edit']">
<el-button plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
</Auth>
<Auth
:value="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
>
<el-button plain type="danger">
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</Auth>
</el-space>
</el-card>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">函数方式判断权限</div>
</template>
<el-space wrap>
<el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-if="
hasAuth([
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
])
"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
<el-card shadow="never">
<template #header>
<div class="card-header">
指令方式判断权限该方式不能动态修改权限
</div>
</template>
<el-space wrap>
<el-button v-auth="'permission:btn:add'" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button v-auth="['permission:btn:edit']" plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-auth="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
</div>
</template>

View File

@ -1,109 +0,0 @@
<script setup lang="ts">
import { hasPerms } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
const { permissions } = useUserStoreHook();
defineOptions({
name: "PermissionButtonLogin"
});
</script>
<template>
<div>
<p class="mb-2!">当前拥有的code列表{{ permissions }}</p>
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2!">
*:*:* 代表拥有全部按钮级别权限
</p>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">组件方式判断权限</div>
</template>
<el-space wrap>
<Perms value="permission:btn:add">
<el-button plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
</Perms>
<Perms :value="['permission:btn:edit']">
<el-button plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
</Perms>
<Perms
:value="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
>
<el-button plain type="danger">
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</Perms>
</el-space>
</el-card>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">函数方式判断权限</div>
</template>
<el-space wrap>
<el-button v-if="hasPerms('permission:btn:add')" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button
v-if="hasPerms(['permission:btn:edit'])"
plain
type="primary"
>
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-if="
hasPerms([
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
])
"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
<el-card shadow="never">
<template #header>
<div class="card-header">
指令方式判断权限该方式不能动态修改权限
</div>
</template>
<el-space wrap>
<el-button v-perms="'permission:btn:add'" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button v-perms="['permission:btn:edit']" plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-perms="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
</div>
</template>

View File

@ -1,66 +0,0 @@
<script setup lang="ts">
import { initRouter } from "@/router/utils";
import { storageLocal } from "@pureadmin/utils";
import { type CSSProperties, ref, computed } from "vue";
import { useUserStoreHook } from "@/store/modules/user";
import { usePermissionStoreHook } from "@/store/modules/permission";
defineOptions({
name: "PermissionPage"
});
const elStyle = computed((): CSSProperties => {
return {
width: "85vw",
justifyContent: "start"
};
});
const userName = ref(useUserStoreHook()?.userName);
const options = [
{
value: "admin",
label: "管理员角色"
},
{
value: "common",
label: "普通角色"
}
];
function onChange() {
useUserStoreHook()
.loginByUsername({ userName: userName.value, password: "admin123" })
.then(res => {
if (res.code === 200) {
storageLocal().removeItem("async-routes");
usePermissionStoreHook().clearAllCachePage();
initRouter();
}
});
}
</script>
<template>
<div>
<p class="mb-2!">
模拟后台根据不同角色返回对应路由观察左侧菜单变化管理员角色可查看系统管理菜单普通角色不可查看系统管理菜单
</p>
<el-card shadow="never" :style="elStyle">
<template #header>
<div class="card-header">
<span>当前角色{{ userName }}</span>
</div>
</template>
<el-select v-model="userName" class="w-[160px]!" @change="onChange">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-card>
</div>
</template>

View File

@ -0,0 +1,244 @@
<template>
<div>
<el-form
ref="schoolAddForm"
:model="eData.form"
:label-width="eData.formLabelWidth"
clearable
>
<el-form-item label="学校名称:" prop="Name" :rules="eData.rules.Required">
<el-input
@change="inputChange"
type="text"
v-model="eData.form.Name"
autocomplete="off"
maxlength="20"
:show-word-limit="true"
/>
</el-form-item>
<el-form-item label="地区" :rules="eData.rules.Required" prop="pname">
<el-col :span="24">
<div style="display: flex; gap: 10px">
<el-select
v-model="eData.form.pid"
clearable
@change="(v) => selectChange(v, 1)"
filterable
placeholder="省份"
style="width: 180px"
>
<el-option
v-for="item in eData.LocaArr1"
:key="item.id"
autocomplete="off"
:label="item.Text"
:value="item.Value"
>
</el-option>
</el-select>
<el-select
v-model="eData.form.cid"
clearable
@change="(v) => selectChange(v, 2)"
filterable
placeholder="市"
style="width: 180px"
>
<el-option
v-for="item in eData.LocaArr2"
:key="item.id"
autocomplete="off"
:label="item.Text"
:value="item.Value"
>
</el-option>
</el-select>
<el-select
v-model="eData.form.rid"
clearable
@change="(v) => selectChange(v, 3)"
filterable
placeholder="区"
style="width: 180px"
>
<el-option
v-for="item in eData.LocaArr3"
:key="item.id"
autocomplete="off"
:label="item.Text"
:value="item.Value"
>
</el-option>
</el-select>
</div>
</el-col>
</el-form-item>
<el-form-item label="启用:" prop="Enable">
<el-switch v-model="eData.form.Enable" active-text="启用" inactive-text="禁用">
</el-switch>
</el-form-item>
<div style="padding-left: 4rem">
<br />
</div>
<el-form-item>
<el-button type="primary" :loading="eData.loading" @click="handleSubmitForm()"
>立即提交</el-button
>
<el-button @click="handleResetForm()">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { getregion, getcity, getProvince } from "@/api/school";
import { EditSchool } from "@/api/userCenter";
import { ElMessage, FormInstance } from "element-plus";
import { onMounted, ref } from "vue";
const props = defineProps({
id: {
type: Number,
default: -1,
},
row: {
type: Object as PropType<any[]>,
default: null,
},
});
const eData = ref({
LocaArr1: [],
LocaArr2: [],
LocaArr3: [],
rules: {
Required: {
required: true,
message: "必填项",
trigger: "blur",
},
},
formLabelWidth: "120px",
size: "small",
loading: false,
form: {
Name: "",
id: props.id,
pid: "",
cid: "",
rid: "",
pname: "",
cname: "",
rname: "",
Enable: true,
},
});
onMounted(() => {
fetchInitData();
fetchFormData();
});
async function selectChange(value, index) {
let nid = value;
let GetInfo = getcity;
// eslint-disable-next-line no-unused-vars
let loc = eData.value.LocaArr2;
if (index == 1) {
eData.value.form.pname =
value == "" ? "" : eData.value.LocaArr1.find((s) => s.Value == value).Text;
eData.value.form.cname = "";
eData.value.form.rname = "";
eData.value.form.cid = "";
eData.value.form.rid = "";
} else if (index == 2) {
loc = eData.value.LocaArr3;
GetInfo = getregion;
eData.value.form.cname =
value == "" ? "" : eData.value.LocaArr2.find((s) => s.Value == value).Text;
eData.value.form.rname = "";
eData.value.form.rid = "";
} else {
eData.value.form.rname =
value == "" ? "" : eData.value.LocaArr3.find((s) => s.Value == value).Text;
}
if (value == "" && index == 3) return;
let nodes = (await GetInfo(nid)).data.map((item) => ({
Value: item.rid || item.cid || item.pid,
Text: item.rname || item.cname || item.pname,
}));
loc.splice(0, loc.length);
loc.push(...nodes);
}
async function inputChange() {
if (eData.value.form.pname == "") {
let p = eData.value.LocaArr1.find((s) => eData.value.form.Name.includes(s.Text));
if (!p) return;
eData.value.form.pid = p.Value;
await selectChange(eData.value.form.pid, 1);
let p1 = eData.value.LocaArr2.find((s) => eData.value.form.Name.includes(s.Text));
if (!p1) return;
eData.value.form.cid = p1.Value;
await selectChange(eData.value.form.cid, 2);
let p2 = eData.value.LocaArr3.find((s) => eData.value.form.Name.includes(s.Text));
if (!p2) return;
eData.value.form.rid = p2.Value;
await selectChange(eData.value.form.rid, 3);
}
}
const emit = defineEmits(["handlePagedCallback"]);
function handlePagedCallback() {
emit("handlePagedCallback");
}
const schoolAddForm = ref<FormInstance>();
function handleSubmitForm() {
schoolAddForm.value.validate((valid) => {
if (valid) {
eData.value.loading = true;
let ids = ["pid", "cid", "rid"];
for (const key of ids) {
eData.value.form[key] = eData.value.form[key] == "" ? 0 : eData.value.form[key];
}
if (!(props.id !== -1)) {
eData.value.form.id = 0;
}
EditSchool(eData.value.form).then((res) => {
eData.value.loading = false;
if (res.code === 200) {
ElMessage.success("操作成功");
handlePagedCallback();
} else {
ElMessage.error(res.message);
}
});
}
});
}
function handleResetForm() {
schoolAddForm.value.resetFields();
eData.value.form.pid = "";
eData.value.form.cid = "";
eData.value.form.rid = "";
eData.value.form.pname = "";
eData.value.form.cname = "";
eData.value.form.rname = "";
}
async function fetchInitData() {
eData.value.LocaArr1 = (await getProvince()).data.map((item) => ({
Value: item.rid || item.cid || item.pid,
Text: item.rname || item.cname || item.pname,
}));
}
function fetchFormData() {
handleResetForm();
if (props.id !== -1) {
eData.value.form = props.row[0];
}
}
</script>

View File

@ -1,25 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import ahTable from "@/components/hTable/index.vue"; import ahTable from "@/components/hTable/index.vue";
import { TableConfig } from "@/components/hTable/hTable"; import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { fa } from "element-plus/es/locales.mjs";
defineOptions({ defineOptions({
name: "School" name: "School",
}); });
onMounted(() => {}); onMounted(() => {});
function searchCallback(data) { function searchCallback(data) {
let c = data.Conditions.find(s => s.FieldName === "Enable"); //let c = data.Conditions.find((s) => s.FieldName == "Pname");
if (c) {
if (c.FieldValue == "true") {
c.FieldValue = 1;
} else {
c.FieldValue = 0;
}
}
} }
const table = ref<{ initTable: (config: TableConfig) => void }>(null); const table = ref<{ initTable: (config: TableConfig) => void }>(null);
const tableData: TableConfig = { const tableData: TableConfig = {
apiUrl: "School", apiUrl: "usercenter/back/schools",
selectColumn: false, // selectColumn: false, //
border: false, // border: false, //
searchCallback: searchCallback, searchCallback: searchCallback,
@ -30,7 +24,7 @@ const tableData: TableConfig = {
PageSize: 20, PageSize: 20,
OrderBy: "CreateTime", // OrderBy: "CreateTime", //
defaultConditions: [], // defaultConditions: [], //
Conditions: [] Conditions: [],
}, },
operationColumn: true, // operationColumn: true, //
operationColumnData: [ operationColumnData: [
@ -38,83 +32,66 @@ const tableData: TableConfig = {
// //
topBtn: false, // topBtn: false, //
label: "修改", label: "修改",
btnType: "edit" // add edit del custom btnType: "edit", // add edit del custom
}, },
{ {
topBtn: true, // topBtn: true, //
show: true, label: "新增学校",
label: "新增", btnType: "custom", // add edit del custom
btnType: "add", // add edit del custom btnStyle: "success", // topBtn: true success danger
btnStyle: "success" // topBtn: true success danger custom: {
// custom
title: "新增学校", // title
src: "school/SchoolEdit", //
width: "550px", //
height: "300px", //
},
}, },
// {
// topBtn: true, //
// label: "",
// btnType: "custom", // add edit del custom
// btnStyle: "success", // topBtn: true success danger
// custom: {
// // custom
// title: "", // title
// src: "school/SchoolEdit", //
// width: "550px", //
// height: "300px" //
// }
// },
{
topBtn: false, //
show: true,
label: "删除",
btnType: "del", // add edit del
btnStyle: "danger" // topBtn: true success danger
}
], ],
column: { column: {
// //
id: { id: {
label: "编号", label: "编号",
search: true, search: true,
add: true, // add: false, //
edit: true, // edit: false, //
width: "150px" width: "150px",
}, },
name: { name: {
label: "学校名称", label: "学校名称",
width: "300px", width: "300px",
search: true, search: true,
searchType: "Like", searchType: ConditionalType.Like,
add: true, // add: true, //
edit: true // edit: true, //
}, },
loc: { pname: {
label: "地区", label: "地区",
width: "300px", width: "300px",
search: true, search: false,
custom: row => `${row.pname}-${row.cname}-${row.rname}`, custom: (row) => `${row.pname}-${row.cname}-${row.rname}`,
add: true, // add: false, //
edit: true // edit: false, //
}, },
enable: { enable: {
label: "启用", label: "启用",
type: "dropdown", type: "switch",
search: true, search: true,
custom: (row) => (row.enable ? "启用" : "禁用"),
add: true, // add: true, //
edit: true, // edit: true, //
setting: { },
datasource: [
{ Value: "true", Text: "√" },
{ Value: "false", Text: "X" }
]
}
}
}, },
data: [], data: [],
pageData: { pageData: {
total: 0 total: 0,
}, },
selectRows: [] selectRows: [],
}; };
</script> </script>
<template> <template>
<div><ahTable ref="table" :tableConfig="tableData" /></div> <div>
<ahTable ref="table" :tableConfig="tableData" />
</div>
</template> </template>

579
src/views/student/edit.vue Normal file
View File

@ -0,0 +1,579 @@
<template>
<div>
<el-form ref="userEditForm" :model="form" :label-width="formLabelWidth" clearable>
<el-row>
<el-col :span="12">
<el-form-item label="姓名:" prop="realName" :rules="ruleRequired">
<el-input
type="text"
v-model="form.realName"
autocomplete="off"
maxlength="20"
:show-word-limit="true"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="家长电话" prop="phone">
<el-input type="text" v-model="form.phone" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="入班时间:" prop="joinTime">
<el-date-picker v-model="form.joinTime" type="date" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="退出时间:" prop="exitTime">
<el-date-picker v-model="form.exitTime" type="date" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="当前状态:" prop="status">
<el-select
v-model="form.status"
filterable
placeholder="就读/退出"
style="width: 180px"
>
<el-option key="1" label="未录入" :value="0" />
<el-option key="2" label="就读" :value="1" />
<el-option key="3" label="退出" :value="10" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请减免" prop="amountRelief">
<el-switch v-model="form.reliefApplication" />
</el-form-item>
</el-col>
</el-row>
<el-row v-show="form.reliefApplication" class="pb-4.5">
<el-col :span="24">
<div style="display: flex; gap: 10px">
<label for="Level" class="el-form-item__label" style="width: 120px"
>减免详情:
</label>
<el-input-number
v-model="form.amountRelief"
:precision="2"
:step="0.1"
style="width: 180px"
placeholder="减免金额(元)"
:max="99999999"
/>
<el-select
v-model="form.reliefType"
filterable
placeholder="减免类型"
style="width: 180px"
>
<el-option
v-for="(item, i) in reliefTypeEnum"
:key="i"
autocomplete="off"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
<el-date-picker
placeholder="减免时间"
style="width: 180px"
v-model="form.reliefSubTime"
type="date"
/>
</div>
</el-col>
</el-row>
<el-row class="pb-4.5">
<el-col :span="24">
<div style="display: flex; gap: 10px">
<label for="Level" class="el-form-item__label" style="width: 120px"
>选修方向</label
>
<el-select
v-model="form.gLSubject"
filterable
clearable
placeholder="历史/地理"
style="width: 180px"
>
<el-option
v-for="(item, i) in subject1"
:key="i"
clearable
autocomplete="off"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
<el-select
v-model="form.gSubject1"
filterable
clearable
placeholder="小学科"
style="width: 180px"
>
<el-option
v-for="(item, i) in subject2"
:key="i"
autocomplete="off"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
<el-select
v-model="form.gSubject2"
filterable
placeholder="小学科"
style="width: 180px"
>
<el-option
v-for="(item, i) in subject2"
:key="i"
autocomplete="off"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</div>
</el-col>
</el-row>
<el-form-item label="备注:" prop="remark">
<el-input v-model="form.remark" maxlength="500" type="textarea" />
</el-form-item>
<el-row>
<el-col :span="12"> </el-col>
</el-row>
<el-row class="pt-4">
<el-col :span="24">
<el-form-item label="就读班级:" prop="positionIds" :rules="ruleRequired">
<el-button type="success" @click="CheckPosition()">选择就读班级</el-button>
</el-form-item>
</el-col>
</el-row>
<div
v-for="(position, index) in positionList"
:key="index"
style="padding-left: 120px; padding-bottom: 20px"
>
<div class="subjectTagEnableDiv" v-if="position.enable === false">
<el-tag type="info">{{ "禁用" }}</el-tag>
<el-tag type="info">{{ position.schoolName || "-" }}</el-tag>
<el-tag type="info">{{
position.graduationYear ? position.graduationYear + "届" : "-"
}}</el-tag>
<el-tag type="info">{{ position.grade || "-" }}</el-tag>
<el-tag type="info">{{ position.className || "-" }}</el-tag>
<el-tag type="info">{{ position.subjectName || "-" }}</el-tag>
<el-tag type="info">{{ position.name || "-" }}</el-tag>
<el-tag type="info">{{ position.endTime }}</el-tag>
</div>
<div class="subjectTagEnableDiv" v-else>
<el-tag>{{ position.schoolName || "-" }}</el-tag>
<el-tag type="warning">{{
position.graduationYear ? position.graduationYear + "届" : "-"
}}</el-tag>
<el-tag type="success">{{ position.grade || "-" }}</el-tag>
<el-tag type="primary" class="classTag">{{ position.className || "-" }}</el-tag>
<el-tag type="info" class="subjectTag">{{
position.subjectName || "-"
}}</el-tag>
<el-tag type="danger">{{ position.name || "-" }}</el-tag>
</div>
</div>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSubmitForm()"
>立即提交</el-button
>
<el-button @click="handleResetForm()">重置</el-button>
</el-form-item>
</el-form>
<div class="dialog-container">
<el-dialog
v-if="dialog.visible"
ref="PositionCheckFromDialog"
:title="dialog.title"
v-model="dialog.visible"
:width="dialog.width"
:close-on-click-modal="dialog.close"
:close-on-press-escape="dialog.close"
append-to-body
>
<PositionForm
:userType="form.userType"
:positions="PositionFormIds"
@handleCheckCallback="handleCheckCallback"
/>
</el-dialog>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { EditStudent, StudentInfo } from "@/api/student";
import { cloudSchoolCombo, getUserInfo, editUser, Position } from "@/api/userCenter";
import PositionForm from "../teacher/positionForm.vue";
import { getenum, getenumDic } from "@/api/enum";
import { ruleRequired, rulePhone } from "@/utils/rules";
import { ElMessage, FormInstance } from "element-plus";
import { ComboModel } from "@/components/hTable/hTable";
interface FormData {
id: number;
uId?: number;
account: string;
userType: number;
level: number;
passWord: string;
realName: string;
studentId: string;
templateId: number;
phone: string | number;
cloudSchoolId: number;
subjectLevels: any[];
subjectLevel: Record<string, any>;
positionIds: number[];
positionFormIds?: number[];
gLSubject?: number;
gSubject1?: number;
gSubject2?: number;
idCard?: string;
exitTime?: string;
joinTime?: string;
remark?: string;
status?: string;
amountRelief?: number;
reliefSubTime?: number;
reliefType?: number;
reliefApplication?: number;
}
interface DialogConfig {
close: boolean;
title: string;
visible: boolean;
width: string;
}
defineOptions({
name: "UserEditForm",
});
const userEditForm = ref<FormInstance>();
const props = defineProps<{
id: number;
}>();
const formLabelWidth = "120px";
const size = "small";
const loading = ref(false);
const reliefTypeEnum = ref<ComboModel[]>();
const subject1 = ref<ComboModel[]>([
{ value: 4, text: "物理" },
{ value: 8, text: "历史" },
]);
const subject2 = ref<ComboModel[]>([
{ value: 5, text: "化学" },
{ value: 6, text: "生物" },
{ value: 9, text: "地理" },
{ value: 7, text: "政治" },
]);
const userTypeList = ref<ComboModel[]>([]);
const userLevelList = ref<ComboModel[]>([]);
const subjectLEnum = ref<Record<string, string>>({});
const positionList = ref<Position[]>([]);
const CloudSchoolArr = ref<ComboModel[]>([]);
const Template = ref<any[]>([]);
const PositionFormIds = ref<number[]>([]);
const defaultSubjectLevel = reactive({
UserId: 0,
Level: 0,
Subject1: 0,
Subject2: 0,
Subject3: 0,
Subject4: 0,
Subject5: 0,
Subject6: 0,
Subject7: 0,
Subject8: 0,
Subject9: 0,
CreatePositionId: 1,
});
const form = ref<FormData>({
id: props.id,
account: "",
userType: 1,
level: 0,
passWord: "",
realName: "",
studentId: "",
templateId: 0,
phone: "",
cloudSchoolId: 0,
subjectLevels: [],
subjectLevel: { ...defaultSubjectLevel },
positionIds: [],
positionFormIds: [],
});
const dialog = reactive<DialogConfig>({
close: false,
title: "",
visible: false,
width: "1200px",
});
const customeRules = reactive({
mobile: [
{ required: false, message: "手机号必填", trigger: "blur" },
{
pattern: /^1[3456789]\d{9}$/,
message: "手机号码格式不正确",
trigger: "blur",
},
],
});
const getUserSubjectLevel = (obj: Record<string, any>) => {
if (!obj.id) {
form.value.subjectLevel = { ...defaultSubjectLevel };
obj = form.value.subjectLevel;
}
return Object.entries(obj).filter((s) => s[0].includes("Subject"));
};
const userLevel2subject = (str: string) => {
const name = str.match(/[0-9]+/)?.[0] || "";
return subjectLEnum.value[name];
};
const emit = defineEmits(["handlePagedCallback"]);
const handlePagedCallback = () => {
// Emit event to parent if needed
emit("handlePagedCallback");
};
const handleSubmitForm = () => {
userEditForm.value.validate(async (valid) => {
if (valid) {
loading.value = true;
const formData = {
id: form.value.uId || 0,
userType: form.value.userType || 1,
level: form.value.level || 0,
account: form.value.account || "",
// PassWord: form.value.id === 0 ? md5(form.value.PassWord).toUpperCase() : "",
realName: form.value.realName || "",
studentId: form.value.studentId || "",
templateId: form.value.templateId || 0,
subjectLevels: form.value.subjectLevels || [],
subjectLevel: form.value.subjectLevel || { ...defaultSubjectLevel },
positionIds: form.value.positionIds || [],
gLSubject: form.value.gLSubject,
gSubject1: form.value.gSubject1,
gSubject2: form.value.gSubject2,
idCard: form.value.idCard,
cloudSchoolId: form.value.cloudSchoolId,
phone: form.value.phone,
};
let res = await editUser(formData);
if (res.code !== 200) {
loading.value = false;
ElMessage.error(res.message);
return;
}
res = await EditStudent({
...form.value,
userCenterId: res.data,
});
if (res.code !== 200) {
loading.value = false;
ElMessage.error(res.message);
return;
}
loading.value = false;
ElMessage.success("操作成功");
handlePagedCallback();
//edit info
}
});
// Form validation and submission logic
};
const handleResetForm = () => {
Object.assign(form, {
id: props.id,
account: "",
userType: 1,
level: 0,
passWord: "",
realName: "",
studentId: "",
subjectLevels: [],
subjectLevel: { ...defaultSubjectLevel },
positionIds: [],
idCard: "",
phone: "",
cloudSchoolId: "",
pointPenSN: "",
});
positionList.value = [];
};
const fetchInitData = async () => {
reliefTypeEnum.value = `
1.复读生
2.艺术生
3.春招生
4.领导承诺批准全免
5.资源班
6.国际班
7.合同制收费学校
8.渠道商家属
9.新开班但领导承诺第一学期不收费`
.split("\n")
.filter((s) => s.trim())
.map((s) => {
const [value, text] = s.trim().split(".");
return { value: text.trim(), text: text.trim() };
});
};
const fetchFormData = async () => {
handleResetForm();
if (props.id !== 0) {
let res = await getUserInfo(props.id);
if (res.data.SubjectLevel && res.data.SubjectLevel.CreatePositionId) {
delete res.data.SubjectLevel.CreatePositionId;
}
let sInfo = await StudentInfo(props.id);
Object.assign(form.value, {
id: res.data.id,
uId: res.data.id,
userType: res.data.userType,
level: res.data.level,
account: res.data.account,
passWord: res.data.passWord,
realName: res.data.realName,
studentId: res.data.studentId,
templateId: res.data.templateId,
subjectLevels: res.data.subjectLevels,
subjectLevel: res.data.subjectLevel,
positionIds: res.data.positions
.filter((s: any) => s.enable !== false)
.map((w: any) => w.id),
gLSubject: res.data.gLSubject,
gSubject1: res.data.gSubject1,
gSubject2: res.data.gSubject2,
idCard: res.data.idCard,
phone: res.data.phone,
cloudSchoolId: res.data.cloudSchoolId,
pointPenSN: res.data.pointPenSN,
...sInfo.data,
});
positionList.value = res.data.positions;
PositionFormIds.value = res.data.positions
.filter((s: any) => s.Enable !== false)
.map((s: any) => s.id);
}
};
const userTypeChange = () => {
if (form.value.userType === 2) {
form.value.studentId = "";
}
customeRules.mobile[0].required = form.value.userType !== 1;
};
const CheckPosition = () => {
dialog.title = "选择就读班级";
dialog.visible = true;
PositionFormIds.value = positionList.value
.filter((s) => s.enable !== false)
.map((s) => s.id);
};
const handleCheckCallback = (checkPosition: Position[]) => {
dialog.visible = false;
positionList.value = checkPosition;
form.value.positionIds = positionList.value.map((w) => w.id);
};
onMounted(async () => {
await fetchInitData();
fetchFormData();
});
</script>
<style scoped>
.userform_ul {
list-style: none;
padding: 0;
margin: 0;
}
.userform_ul li {
margin-bottom: 10px;
}
.subjectTagEnableDiv {
margin-top: 5px;
}
.subjectTagEnableDiv .el-tag {
margin-right: 5px;
}
.classTag {
background-color: #409eff;
color: white;
}
.subjectTag {
background-color: #909399;
color: white;
}
.classTag {
color: #a3bf08 !important;
background-color: #f4fbd1 !important;
border-color: #f4fbd1 !important;
}
.subjectTagEnableDiv {
padding: 1px;
}
.subjectTag {
color: #eb0de4 !important;
background-color: #fbd9ff !important;
border-color: #fbd9ff !important;
}
.userform_ul {
list-style: none;
}
</style>

785
src/views/student/index.vue Normal file
View File

@ -0,0 +1,785 @@
<template>
<div class="app-container" style="padding: 5px">
<div class="search-container" style="padding-top: 5px">
<!-- 搜索项目 -->
<el-form :inline="true" :model="search">
<el-form-item>
<el-input v-model="search.searchStr" placeholder="姓名/账号/学号" />
</el-form-item>
<el-form-item>
<el-select
v-model="search.schoolId"
placeholder="学校"
clearable
filterable
@change="schoolChange"
>
<el-option
v-for="item in schoolList"
:key="item.value"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item v-show="search.schoolId != 0" style="width: 100px">
<el-select
v-model="search.grade"
placeholder="年级"
clearable
filterable
@change="gradeChange"
>
<el-option
v-for="item in gradeList"
:key="item.value"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item style="width: 100px" v-show="search.schoolId != 0">
<el-select v-model="search.classId" placeholder="班级" clearable filterable>
<el-option
v-for="item in classList"
:key="item.value"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleReloadPaged" :icon="Search"
>查询</el-button
>
<el-button type="default" @click="searchReload">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="toolbar-container" v-show="!selectUser">
<!-- 按钮组 -->
<el-button type="success" @click="importData">导入用户</el-button>
<el-button type="default" @click="downLoadImportUsersTemplate"
>下载学生模板</el-button
>
<!-- <el-button title="根据当前筛选条件导出" type="primary" @click="exportUser"
>导出用户</el-button
> -->
</div>
<div class="toolbar-container" v-show="!selectUser">
<!-- 按钮组 -->
<el-button type="success" @click="AddDialog" plain>新增</el-button>
</div>
<el-table
@row-dblclick="setCurrent"
@row-click="selectUserClick"
ref="selectUserTable"
:data="table.data"
@selection-change="handleSelectionChange"
style="width: 100%"
:max-height="maxTableHeight"
>
<!-- <el-table-column type="selection" width="40" /> -->
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button text type="primary" @click="EditDialog(scope.row)" plain
>修改</el-button
>
</template>
</el-table-column>
<el-table-column label="学生信息" width="450">
<template #default="scope">
<el-tooltip :content="`id ` + scope.row.id" placement="top" effect="light">
<div>
<span :tips="scope.row.id">{{ scope.row.realName }}</span>
<div
class="inline-block"
v-for="(position, index) in scope.row.positions"
:key="'Position' + index"
v-show="index < 3 || (index >= 3 && showAllPosition.includes(scope.row))"
>
<div class="subjectTagEnableDiv">
<el-tag v-if="position.enable === false" type="info">已禁用</el-tag>
<el-tag>{{ position.schoolName || "-" }}</el-tag>
<el-tag type="warning">{{
position.graduationYear ? position.graduationYear + "届" : "-"
}}</el-tag>
<el-tag type="success">{{ position.grade || "-" }}</el-tag>
<el-tag type="primary" class="classTag">{{
position.className || "-"
}}</el-tag>
</div>
</div>
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="phone" label="家长电话" width="150" />
<el-table-column prop="gkSubject" label="选修" width="140" />
<el-table-column prop="joinTime" label="入班时间" width="100" />
<el-table-column prop="exitTime" label="退出时间" width="100" />
<el-table-column label="减免情况(元)" width="230">
<template #default="scope">
<span v-if="!scope.row.reliefApplication">未申请</span>
<span v-else
>{{ scope.row.reliefSubTime }} {{ scope.row.amountRelief }}
{{ scope.row.reliefType }}
</span>
</template>
</el-table-column>
<el-table-column label="状态">
<template #default="scope">
<span v-if="scope.row.status == 0">未录入</span>
<span v-else-if="scope.row.status == 1">就读</span>
<span v-else>退出</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" />
</el-table>
<el-pagination
style="display: flex; justify-content: center; padding-top: 10px"
@size-change="pageSizeChange"
@current-change="pageIndexChange"
:current-page="pagination.now"
:page-sizes="[10, 20, 40, 80, 100]"
:page-size="pagination.size"
layout="prev, pager, next,sizes, total"
:total="pagination.total"
/>
<div class="dialog-container">
<el-dialog
v-if="dialog.update.visible"
ref="UserEditFromDialog"
:title="dialog.update.title"
v-model="dialog.update.visible"
:width="dialog.update.width"
:close-on-click-modal="dialog.close"
:close-on-press-escape="dialog.close"
append-to-body
>
<UserForm :id="editId" @handlePagedCallback="handleAddCallback" />
</el-dialog>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import type { UploadProps } from "element-plus";
import UserForm from "./edit.vue";
import {
getSchoolData,
getClassCombo,
getSubjectData,
getPageUserList,
UserDetail,
Position,
} from "@/api/userCenter";
import { getenum } from "@/api/enum";
import { hTableAPI } from "@/api/hTable";
import { text } from "stream/consumers";
import {
Check,
Delete,
Edit,
Message,
ArrowDownBold,
Search,
Star,
} from "@element-plus/icons-vue";
import { ComboModel } from "@/components/hTable/hTable";
import { ImportStudent, PageList } from "@/api/student";
const classAPI = new hTableAPI("usercenter/back/classes");
const schoolsAPI = new hTableAPI("usercenter/back/schools");
interface SearchParams {
searchStr: string;
userType: string | number;
level: string | number;
schoolId: string | number;
graduationYear: string | number;
grade: string;
classId: string | number;
subjectId: string | number;
positionId: string | number;
}
interface TableData {
data: UserDetail[];
selectRows: UserDetail[];
sort: string;
border: boolean;
}
interface PaginationData {
index: number;
size: number;
now?: number;
total: number;
}
interface DialogData {
close: boolean;
update: {
title: string;
visible: boolean;
width: string;
};
editLevel: {
userIds: number[];
title: string;
visible: boolean;
width: string;
};
editSubjectLevel: {
userIds: number[];
title: string;
visible: boolean;
width: string;
};
bindUser: {
title: string;
visible: boolean;
width: string;
height: string;
};
userBindInfo: {
title: string;
visible: boolean;
width: string;
height: string;
};
}
const props = defineProps({
selectUser: {
type: Boolean,
default: false,
},
selectCallBack: {
type: Function,
default: () => {},
},
maxTableHeight: {
type: Number,
default: 580,
},
searchData: {
type: Object as () => SearchParams,
default: undefined,
},
});
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const excelImportUsersUrl = `${baseUrl}api/back/users/downloadimportusersexceltemplate`;
const excelImportMeetingUrl = `${baseUrl}api/back/users/downloadimportmeetingexceltemplate`;
const excelImportOrdersUrl = `${baseUrl}api/back/users/downloadimportordersexceltemplate`;
const editId = ref(0);
const showAllPosition = ref<UserDetail[]>([]);
const selectUserTable = ref();
const search = reactive<SearchParams>({
searchStr: "",
userType: "",
level: "",
schoolId: "",
graduationYear: "",
grade: "",
classId: "",
subjectId: "",
positionId: "",
});
const userTypeList = ref<ComboModel[]>([
{ value: 1, text: "学生" },
{ value: 2, text: "教师" },
{ value: 3, text: "管理员" },
]);
const userLevelList = ref<ComboModel[]>([]);
const schoolList = ref<ComboModel[]>([]);
const gradeList = ref<ComboModel[]>([
{ value: "初一", text: "初一" },
{ value: "初二", text: "初二" },
{ value: "初三", text: "初三" },
{ value: "高一", text: "高一" },
{ value: "高二", text: "高二" },
{ value: "高三", text: "高三" },
]);
const classList = ref<ComboModel[]>([]);
const subjectList = ref<ComboModel[]>([]);
const positionList = ref<any[]>([]);
const table = reactive<TableData>({
data: [],
selectRows: [],
sort: "",
border: true,
});
const pagination = reactive<PaginationData>({
index: 1,
size: 10,
total: 0,
});
const dialog = reactive<DialogData>({
close: false,
update: {
title: "",
visible: false,
width: "800px",
},
editLevel: {
userIds: [],
title: "",
visible: false,
width: "400px",
},
editSubjectLevel: {
userIds: [],
title: "",
visible: false,
width: "450px",
},
bindUser: {
title: "分配权限码",
visible: false,
width: "1150px",
height: "",
},
userBindInfo: {
title: "用户权限码",
visible: false,
width: "1150px",
height: "",
},
});
const checkUserBindInfo = () => {
if (table.selectRows.length != 1) {
ElMessage.warning("请选择一个用户");
return;
}
dialog.userBindInfo.visible = true;
};
const showPosition = (row: UserDetail) => {
if (showAllPosition.value.includes(row)) {
showAllPosition.value.splice(showAllPosition.value.indexOf(row), 1);
} else {
showAllPosition.value.push(row);
}
};
const codeBindUser = () => {
if (table.selectRows.length == 0) {
ElMessage.warning("请选择需要分配权限的用户");
return;
}
dialog.bindUser.visible = true;
};
const initSearchData = () => {
if (props.searchData !== undefined) {
for (const key in props.searchData) {
search[key] = props.searchData[key];
}
}
};
const selectUserClick = (row: UserDetail) => {
if (props.selectUser) {
selectUserTable.value.toggleRowSelection(row);
}
};
const setCurrent = (row: UserDetail) => {
selectUserTable.value.toggleRowSelection(row);
};
const selectUserCallBack = () => {
const u = table.selectRows;
props.selectCallBack(u);
selectUserTable.value.clearSelection();
};
const exportUser = async () => {
const data = {
SearchStr: search.searchStr,
UserType: search.userType || 0,
Level: search.level || 0,
SchoolId: search.schoolId || 0,
GraduationYear: search.graduationYear || 0,
Grade: search.grade,
ClassId: search.classId || 0,
SubjectId: search.subjectId || 0,
PositionId: search.positionId || 0,
PageIndex: pagination.index,
PageSize: pagination.size,
};
// const res = await exportUserApi(data);
// if (res.type === "application/json") {
// const json = await readerBlob(res);
// if (json !== undefined && json.code !== 200) {
// ElMessage.error(json.Message);
// }
// } else if (res !== undefined && res.size !== 0) {
// ElMessage.success("🕖");
// const url = window.URL.createObjectURL(res);
// const link = document.createElement("a");
// link.href = url;
// link.setAttribute("download", ".xlsx");
// document.body.appendChild(link);
// link.click();
// document.body.removeChild(link);
// } else {
// ElMessage.warning(",");
// }
};
const fetchInitData = async () => {
schoolList.value = (await getSchoolData()).data;
subjectList.value = (await getenum("SubjectEnum")).data;
userLevelList.value = (await getenum("StudentLevelEnum")).data;
userTypeList.value = (await getenum("UserTypeEnum")).data;
};
const userTypeChange = () => {
search.level = "";
};
const schoolChange = () => {
search.graduationYear = "";
search.grade = "";
search.classId = "";
search.subjectId = "";
getClass();
};
const gradeChange = () => {
search.classId = "";
search.subjectId = "";
getClass();
};
const getClass = () => {
const data = {
schoolId: search.schoolId || 0,
graduationYear: search.graduationYear || 0,
grade: search.grade,
};
getClassCombo(data).then((res) => {
if (res.code === 200) {
classList.value = res.data;
}
});
};
const fetchPagedData = (searchUnUse = false) => {
const data = {
SearchStr: search.searchStr,
UserType: 1, //
Level: search.level || 0,
SchoolId: search.schoolId || 0,
GraduationYear: search.graduationYear || 0,
Grade: search.grade,
ClassId: search.classId || 0,
SubjectId: search.subjectId || 0,
PositionId: search.positionId || 0,
PageIndex: pagination.index,
PageSize: pagination.size,
UnUsed: searchUnUse,
};
PageList(data).then((res) => {
if (res.code === 200) {
pagination.total = res.data.total;
res.data.data.forEach((item) => {
if (item.positions) {
item.positions = PositionsSort(item.positions);
}
});
table.data = res.data.data;
}
});
};
const handleSelectionChange = (selection: UserDetail[]) => {
table.selectRows = selection;
};
function searchReload() {
for (const key in search) {
search[key] = "";
}
}
const handleDelete = () => {
// if (table.selectRows.length === 0) {
// ElMessage.warning("");
// return;
// }
// const ids: number[] = [];
// table.selectRows.forEach(it => {
// ids.push(it.Id);
// });
// ElMessageBox.confirm(", ?", "", {
// confirmButtonText: "",
// cancelButtonText: "",
// type: "warning"
// }).then(() => {
// delUser(ids).then(res => {
// if (res.code === 200) {
// handleReloadPaged();
// ElMessage.success("");
// } else {
// ElMessage.error(res.Message);
// }
// });
// });
};
const handleRestPass = async () => {
// if (table.selectRows.length === 0) {
// ElMessage.warning("");
// } else {
// const ids: number[] = [];
// table.selectRows.forEach(it => {
// ids.push(it.Id);
// });
// try {
// await ElMessageBox.confirm("?", "", {
// confirmButtonText: "",
// cancelButtonText: "",
// type: "warning"
// });
// const res = await restUserPass(ids);
// if (res.code === 200) {
// ElMessage.success("");
// } else {
// ElMessage.error("");
// }
// } catch (error) {
// // User cancelled
// }
// }
};
const getUserTypeTag = (type: number) => {
return type === 1 ? "info" : "warning";
};
const getUserLevelTag = (level: number) => {
return level === 0
? "info"
: level === 1
? "success"
: level === 2
? "warning"
: "error";
};
const getUserLevelText = (level: number) => {
const r = userLevelList.value.filter((w) => w.value === level);
if (r.length > 0) {
return r[0].text;
}
return "";
};
const PositionsSort = (arr: Position[]) => {
arr.sort((a, b) => {
if (a.enable === b.enable) {
return 0;
} else if (a.enable) {
return -1;
} else {
return 1;
}
});
return arr;
};
const handleReloadPaged = (event?: any, searchUnUse?: boolean) => {
pagination.index = 1;
table.selectRows = [];
fetchPagedData(searchUnUse);
};
function pageSizeChange(o) {
pagination.size = o;
fetchPagedData();
}
function pageIndexChange(o) {
console.log(o, "当前索引");
pagination.index = o;
fetchPagedData();
}
const AddDialog = () => {
editId.value = 0;
dialog.update.title = "添加用户";
dialog.update.visible = true;
};
const EditDialog = (row?) => {
if (row == null && table.selectRows.length !== 1) {
ElMessage.warning("请选择要修改用户");
return;
}
dialog.update.title = "修改用户";
editId.value = row != null ? row.id : table.selectRows[0].id;
dialog.update.visible = true;
};
const handleAddCallback = () => {
dialog.update.visible = false;
handleReloadPaged();
};
const handleEditLevel = () => {
if (table.selectRows.length === 0) {
ElMessage.warning("请选择要修改用户");
return;
}
dialog.editLevel.title = "修改学生层次";
dialog.editLevel.userIds = table.selectRows.map((w) => w.id);
dialog.editLevel.visible = true;
};
const handleEditLevelCallback = () => {
dialog.editLevel.visible = false;
handleReloadPaged();
};
const handleEditSubjectLevel = () => {
if (table.selectRows.length === 0) {
ElMessage.warning("请选择要修改用户");
return;
}
dialog.editSubjectLevel.title = "修改学生科目层次";
dialog.editSubjectLevel.userIds = table.selectRows.map((w) => w.id);
dialog.editSubjectLevel.visible = true;
};
const handleEditSubjectLevelCallback = () => {
dialog.editSubjectLevel.visible = false;
handleReloadPaged();
};
const importData = () => {
console.log("批量导入");
let fileE = document.createElement("input");
fileE.type = "file";
var formData = new window.FormData();
fileE.onchange = async function () {
formData.append("file", fileE.files[0]);
let res = await ImportStudent(fileE.files[0]);
if (res.code != undefined) {
if (res.code !== 200) return ElMessage.error(res.message);
else return ElMessage.success("所有数据录入成功");
} else if (res.type === "application/json") {
let json = await res.text();
if (json !== undefined && json.Code !== 200) {
return ElMessage.error(json.Message);
} else {
return ElMessage.success("操所有数据录入成功作成功");
}
} else if (res === undefined || res.size === 0)
return ElMessage.success("所有数据录入成功");
const url = res && window.URL.createObjectURL(res);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "未成功导入的考试信息数据" + ".xlsx");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
ElMessage.success("导入失败,已导出错误数据");
};
try {
fileE.click();
} catch (error) {}
};
const readerBlob = (data: Blob): Promise<any> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsText(data, "utf-8");
reader.onload = function () {
const dd = JSON.parse(reader.result as string);
resolve(dd);
};
});
};
const downLoadImportUsersTemplate = () => {
let impUrl = import.meta.env.VITE_API_BASEURL + "/Student/DwImportTemplate";
window.open(impUrl, "_blank");
};
onMounted(async () => {
await initSearchData();
await fetchInitData();
fetchPagedData();
});
</script>
<style lang="scss" scoped>
.userTagRowItop {
transform: rotate(180deg) !important;
}
.userTagRow i {
padding-top: 3px;
transform: rotate(0deg);
font-size: 1.3rem;
cursor: pointer;
}
.userTagRow {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
.subjectTagEnableDiv {
display: flex;
padding: 3px;
gap: 5px;
}
.classTag {
color: #a3bf08 !important;
background-color: #f4fbd1 !important;
border-color: #f4fbd1 !important;
}
.subjectTag {
color: #eb0de4 !important;
background-color: #fbd9ff !important;
border-color: #fbd9ff !important;
}
.subjectlevel_ul {
margin: 0;
padding: 0;
list-style: none;
}
.toolbar-container {
margin-bottom: 5px;
}
</style>

411
src/views/teacher/edit.vue Normal file
View File

@ -0,0 +1,411 @@
<template>
<div>
<el-form ref="userEditForm" :model="form" :label-width="formLabelWidth" clearable>
<el-row>
<el-col :span="12">
<el-form-item label="电话号码" :rules="rulePhone" prop="phone">
<el-input type="text" v-model="form.phone" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="姓名:" prop="realName" :rules="ruleRequired">
<el-input
type="text"
v-model="form.realName"
autocomplete="off"
maxlength="20"
:show-word-limit="true"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="职务:" prop="studentId">
<el-input
type="text"
v-model="form.studentId"
autocomplete="off"
maxlength="16"
:show-word-limit="true"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="任教信息:" prop="positionIds" :rules="ruleRequired">
<el-button type="success" @click="CheckPosition()">分配职位</el-button>
</el-form-item>
</el-col>
</el-row>
<div class="max-h-[400px] overflow-auto pb-1">
<div
v-for="(position, index) in positionList"
:key="index"
style="padding-left: 120px; padding-bottom: 20px"
>
<div class="subjectTagEnableDiv">
<el-tag v-if="!position.enable" type="info">已禁用</el-tag>
<el-tag>{{ position.schoolName || "-" }}</el-tag>
<el-tag type="warning">{{
position.graduationYear ? position.graduationYear + "届" : "-"
}}</el-tag>
<el-tag type="success" v-show="position.grade">{{ position.grade }}</el-tag>
<el-tag type="primary" v-show="position.className" class="classTag">{{
position.className
}}</el-tag>
<el-tag type="info" v-show="position.subjectName" class="subjectTag">{{
position.subjectName
}}</el-tag>
<el-tag type="danger">{{ position.name || "-" }}</el-tag>
</div>
</div>
</div>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSubmitForm()"
>立即提交</el-button
>
<el-button @click="handleResetForm()">重置</el-button>
</el-form-item>
</el-form>
<div class="dialog-container">
<el-dialog
v-if="dialog.visible"
ref="PositionCheckFromDialog"
:title="dialog.title"
v-model="dialog.visible"
:width="dialog.width"
:close-on-click-modal="dialog.close"
:close-on-press-escape="dialog.close"
append-to-body
>
<PositionForm
:userType="form.userType"
:positions="PositionFormIds"
@handleCheckCallback="handleCheckCallback"
/>
</el-dialog>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import {} from "@/api/user";
import { cloudSchoolCombo, getUserInfo, editUser, Position } from "@/api/userCenter";
import PositionForm from "./positionForm.vue";
import { getenum, getenumDic } from "@/api/enum";
import { ruleRequired, rulePhone } from "@/utils/rules";
import { ElMessage, FormInstance } from "element-plus";
import { ComboModel } from "@/components/hTable/hTable";
import { InformationEvent } from "http";
interface FormData {
id: number;
account: string;
userType: number;
level: number;
passWord: string;
realName: string;
studentId: string;
templateId: number;
phone: string | number;
cloudSchoolId: number;
subjectLevels: any[];
subjectLevel: Record<string, any>;
positionIds: number[];
positionFormIds?: number[];
gLSubject?: number;
gSubject1?: number;
gSubject2?: number;
idCard?: string;
pointPenSN?: string;
}
interface DialogConfig {
close: boolean;
title: string;
visible: boolean;
width: string;
}
defineOptions({
name: "UserEditForm",
});
const props = defineProps<{
id: number;
}>();
const formLabelWidth = "120px";
const size = "small";
const loading = ref(false);
const subject1 = ref<ComboModel[]>([
{ value: 4, text: "物理" },
{ value: 8, text: "历史" },
]);
const subject2 = ref<ComboModel[]>([
{ value: 5, text: "化学" },
{ value: 6, text: "生物" },
{ value: 9, text: "地理" },
{ value: 7, text: "政治" },
]);
const emit = defineEmits(["handlePagedCallback"]);
const userTypeList = ref<ComboModel[]>([]);
const userLevelList = ref<ComboModel[]>([]);
const subjectLEnum = ref<Record<string, string>>({});
const positionList = ref<Position[]>([]);
const CloudSchoolArr = ref<ComboModel[]>([]);
const Template = ref<any[]>([]);
const PositionFormIds = ref<number[]>([]);
const defaultSubjectLevel = reactive({
UserId: 0,
Level: 0,
Subject1: 0,
Subject2: 0,
Subject3: 0,
Subject4: 0,
Subject5: 0,
Subject6: 0,
Subject7: 0,
Subject8: 0,
Subject9: 0,
CreatePositionId: 1,
});
const form = ref<FormData>({
id: props.id,
account: "",
userType: 2,
level: 0,
passWord: "",
realName: "",
studentId: "",
templateId: 0,
phone: "",
cloudSchoolId: 0,
subjectLevels: [],
subjectLevel: { ...defaultSubjectLevel },
positionIds: [],
positionFormIds: [],
});
const dialog = reactive<DialogConfig>({
close: false,
title: "",
visible: false,
width: "1200px",
});
const customeRules = reactive({
mobile: [
{ required: false, message: "手机号必填", trigger: "blur" },
{
pattern: /^1[3456789]\d{9}$/,
message: "手机号码格式不正确",
trigger: "blur",
},
],
});
const getUserSubjectLevel = (obj: Record<string, any>) => {
if (!obj.id) {
form.value.subjectLevel = { ...defaultSubjectLevel };
obj = form.value.subjectLevel;
}
return Object.entries(obj).filter((s) => s[0].includes("Subject"));
};
const userLevel2subject = (str: string) => {
const name = str.match(/[0-9]+/)?.[0] || "";
return subjectLEnum.value[name];
};
const handlePagedCallback = () => {
emit("handlePagedCallback");
};
const userEditForm = ref<FormInstance>();
const handleSubmitForm = () => {
userEditForm.value.validate((valid) => {
if (valid) {
loading.value = true;
const formData = {
...form.value,
account: form.value.phone || "",
positionIds: form.value.positionIds || [],
};
editUser(formData).then((res) => {
loading.value = false;
if (res.code === 200) {
ElMessage.success("操作成功");
handlePagedCallback();
} else {
ElMessage.error(res.message);
}
});
}
});
// Form validation and submission logic
};
const handleResetForm = () => {
Object.assign(form, {
id: props.id,
account: "",
userType: 1,
level: 0,
passWord: "",
realName: "",
studentId: "",
subjectLevels: [],
subjectLevel: { ...defaultSubjectLevel },
positionIds: [],
idCard: "",
phone: "",
cloudSchoolId: "",
pointPenSN: "",
});
positionList.value = [];
};
const fetchInitData = async () => {
//
const levelRes = await getenum("StudentLevelEnum");
userLevelList.value = levelRes.data;
const typeRes = await getenum("UserTypeEnum");
userTypeList.value = typeRes.data;
// //
// const schoolRes = await cloudSchoolCombo();
// if (schoolRes.code === 200) {
// CloudSchoolArr.value = schoolRes.data;
// }
const enumDicRes = await getenumDic("SubjectEnum");
subjectLEnum.value = enumDicRes.data;
};
const fetchFormData = () => {
handleResetForm();
if (props.id !== 0) {
getUserInfo(props.id).then((res) => {
if (res.code === 200) {
if (res.data.SubjectLevel && res.data.SubjectLevel.CreatePositionId) {
delete res.data.SubjectLevel.CreatePositionId;
}
Object.assign(form.value, {
id: res.data.id,
userType: res.data.userType,
level: res.data.level,
account: res.data.account,
passWord: res.data.passWord,
realName: res.data.realName,
studentId: res.data.studentId,
templateId: res.data.templateId,
subjectLevels: res.data.subjectLevels,
subjectLevel: res.data.subjectLevel,
positionIds: res.data.positions
.filter((s: any) => s.enable !== false)
.map((w: any) => w.id),
gLSubject: res.data.gLSubject,
gSubject1: res.data.gSubject1,
gSubject2: res.data.gSubject2,
idCard: res.data.idCard,
phone: res.data.phone,
cloudSchoolId: res.data.cloudSchoolId,
pointPenSN: res.data.pointPenSN,
});
positionList.value = res.data.positions;
PositionFormIds.value = res.data.positions
.filter((s: any) => s.Enable !== false)
.map((s: any) => s.id);
}
});
}
};
const userTypeChange = () => {
if (form.value.userType === 2) {
form.value.studentId = "";
}
customeRules.mobile[0].required = form.value.userType !== 1;
};
const CheckPosition = () => {
dialog.title = "选择职位";
dialog.visible = true;
PositionFormIds.value = positionList.value
.filter((s) => s.enable !== false)
.map((s) => s.id);
};
const handleCheckCallback = (checkPosition: Position[]) => {
dialog.visible = false;
positionList.value = checkPosition.map((s) => {
s.enable = true;
return s;
});
form.value.positionIds = positionList.value.map((w) => w.id);
};
onMounted(async () => {
await fetchInitData();
fetchFormData();
});
</script>
<style scoped>
.userform_ul {
list-style: none;
padding: 0;
margin: 0;
}
.userform_ul li {
margin-bottom: 10px;
}
.subjectTagEnableDiv {
margin-top: 5px;
}
.subjectTagEnableDiv .el-tag {
margin-right: 5px;
}
.classTag {
background-color: #409eff;
color: white;
}
.subjectTag {
background-color: #909399;
color: white;
}
.classTag {
color: #a3bf08 !important;
background-color: #f4fbd1 !important;
border-color: #f4fbd1 !important;
}
.subjectTagEnableDiv {
padding: 1px;
}
.subjectTag {
color: #eb0de4 !important;
background-color: #fbd9ff !important;
border-color: #fbd9ff !important;
}
.userform_ul {
list-style: none;
}
</style>

786
src/views/teacher/index.vue Normal file
View File

@ -0,0 +1,786 @@
<template>
<div class="app-container" style="padding: 5px">
<div class="search-container" style="padding-top: 5px">
<!-- 搜索项目 -->
<el-form ref="searchForm" :inline="true" :model="search">
<el-form-item>
<el-input v-model="search.searchStr" placeholder="姓名/账号/学号" />
</el-form-item>
<!-- <el-form-item style="width: 100px">
<el-select
v-model="search.userType"
placeholder="用户类型"
clearable
filterable
@change="userTypeChange"
>
<el-option
v-for="item in userTypeList"
:key="item.value"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item> -->
<el-form-item v-show="search.userType === 1" style="width: 100px">
<el-select v-model="search.level" placeholder="学生层次" clearable filterable>
<el-option
v-for="item in userLevelList"
:key="item.value"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select
v-model="search.schoolId"
placeholder="学校"
clearable
filterable
@change="schoolChange"
>
<el-option
v-for="item in schoolList"
:key="item.value"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item v-show="search.schoolId != 0" style="width: 100px">
<el-select
v-model="search.grade"
placeholder="年级"
clearable
filterable
@change="gradeChange"
>
<el-option
v-for="item in gradeList"
:key="item.value"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item v-show="search.schoolId != 0" style="width: 100px">
<el-select v-model="search.classId" placeholder="班级" clearable filterable>
<el-option
v-for="item in classList"
:key="item.value"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item v-show="search.schoolId != 0" style="width: 100px">
<el-select v-model="search.subjectId" placeholder="科目" clearable filterable>
<el-option
v-for="item in subjectList"
:key="item.value"
:label="item.text"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleReloadPaged" :icon="Search"
>查询</el-button
>
<el-button type="default" @click="searchReload">重置</el-button>
</el-form-item>
<el-form-item v-show="selectUser">
<el-button type="success" @click="selectUserCallBack()" icon="el-icon-check"
>选择用户</el-button
>
</el-form-item>
</el-form>
</div>
<div class="toolbar-container" v-show="!selectUser">
<!-- 按钮组 -->
<el-button type="success" @click="importData">导入用户</el-button>
<el-button type="default" @click="downLoadImportUsersTemplate"
>下载导入用户模板</el-button
>
<!-- <el-button title="根据当前筛选条件导出" type="primary" @click="exportUser"
>导出用户</el-button
> -->
</div>
<div class="toolbar-container" v-show="!selectUser">
<!-- 按钮组 -->
<el-button type="success" @click="AddDialog" plain>新增</el-button>
</div>
<el-table
@row-dblclick="setCurrent"
@row-click="selectUserClick"
ref="selectUserTable"
:data="table.data"
@selection-change="handleSelectionChange"
style="width: 100%"
:max-height="maxTableHeight"
>
<!-- <el-table-column type="selection" width="40" /> -->
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button text type="primary" @click="EditDialog(scope.row)" plain
>修改</el-button
>
</template>
</el-table-column>
<el-table-column prop="id" label="用户Id" width="100" />
<el-table-column label="用户信息" width="200">
<template #default="scope">
<el-tag :type="getUserTypeTag(scope.row.userType)" style="margin-right: 5px">{{
userTypeList.find((s) => s.value == scope.row.userType)?.text
}}</el-tag>
<span>{{ scope.row.realName }} </span>
</template>
</el-table-column>
<el-table-column prop="account" label="账号" width="120" />
<el-table-column prop="phone" label="手机号" width="120" />
<!-- <el-table-column prop="studentId" label="学号" width="120" />
<el-table-column prop="gKSubject" label="新高考学科" width="150" /> -->
<el-table-column prop="studentId" label="职务" width="120" />
<el-table-column label="任教信息">
<template #default="scope">
<div
v-for="(position, index) in scope.row.positions"
:key="'Position' + index"
v-show="index < 3 || (index >= 3 && showAllPosition.includes(scope.row))"
>
<div v-if="position.enable === false">
<el-tag type="info">{{ position.schoolName || "-" }}</el-tag>
<el-tag type="info">{{
position.graduationYear ? position.graduationYear + "届" : "-"
}}</el-tag>
<el-tag type="info">{{ position.grade || "-" }}</el-tag>
<el-tag type="info">{{ position.className || "-" }}</el-tag>
<el-tag type="info">{{ position.subjectName || "-" }}</el-tag>
<el-tag type="info">{{ position.name || "-" }}</el-tag>
</div>
<div class="subjectTagEnableDiv" v-else>
<el-tag>{{ position.schoolName || "-" }}</el-tag>
<el-tag type="warning">{{
position.graduationYear ? position.graduationYear + "届" : "-"
}}</el-tag>
<el-tag type="success">{{ position.grade || "-" }}</el-tag>
<el-tag type="primary" class="classTag">{{
position.className || "-"
}}</el-tag>
<el-tag type="info" class="subjectTag">{{
position.subjectName || "-"
}}</el-tag>
<el-tag type="danger">{{ position.name || "-" }}</el-tag>
</div>
</div>
<div
v-if="scope.row.positions != undefined && scope.row.positions.length > 3"
@click="showPosition(scope.row)"
class="userTagRow"
>
<el-icon
title="折叠职位"
class="userTagRowItop"
v-if="showAllPosition.includes(scope.row)"
><ArrowDownBold
/></el-icon>
<el-icon v-else title="展开更多职位"><ArrowDownBold /></el-icon>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
style="display: flex; justify-content: center"
@size-change="pageSizeChange"
@current-change="pageIndexChange"
:current-page="pagination.now"
:page-sizes="[10, 20, 40, 80, 100]"
:page-size="pagination.size"
layout="prev, pager, next,sizes, total"
:total="pagination.total"
/>
<div class="dialog-container">
<el-dialog
v-if="dialog.update.visible"
ref="UserEditFromDialog"
:title="dialog.update.title"
v-model="dialog.update.visible"
:width="dialog.update.width"
:close-on-click-modal="dialog.close"
:close-on-press-escape="dialog.close"
append-to-body
>
<UserForm :id="editId" @handlePagedCallback="handleAddCallback" />
</el-dialog>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import type { FormInstance, UploadProps } from "element-plus";
import UserForm from "./edit.vue";
import {
getSchoolData,
getClassCombo,
getSubjectData,
getPageUserList,
UserDetail,
Position,
} from "@/api/userCenter";
import { getenum } from "@/api/enum";
import { hTableAPI } from "@/api/hTable";
import { text } from "stream/consumers";
import {
Check,
Delete,
Edit,
Message,
ArrowDownBold,
Search,
Star,
} from "@element-plus/icons-vue";
import { ComboModel } from "@/components/hTable/hTable";
import { ImportTeacher } from "@/api/student";
const classAPI = new hTableAPI("usercenter/back/classes");
const schoolsAPI = new hTableAPI("usercenter/back/schools");
interface SearchParams {
searchStr: string;
userType: string | number;
level: string | number;
schoolId: string | number;
graduationYear: string | number;
grade: string;
classId: string | number;
subjectId: string | number;
positionId: string | number;
}
interface TableData {
data: UserDetail[];
selectRows: UserDetail[];
sort: string;
border: boolean;
}
interface PaginationData {
index: number;
size: number;
total: number;
now?: number;
}
interface DialogData {
close: boolean;
update: {
title: string;
visible: boolean;
width: string;
};
editLevel: {
userIds: number[];
title: string;
visible: boolean;
width: string;
};
editSubjectLevel: {
userIds: number[];
title: string;
visible: boolean;
width: string;
};
bindUser: {
title: string;
visible: boolean;
width: string;
height: string;
};
userBindInfo: {
title: string;
visible: boolean;
width: string;
height: string;
};
}
const props = defineProps({
selectUser: {
type: Boolean,
default: false,
},
selectCallBack: {
type: Function,
default: () => {},
},
maxTableHeight: {
type: Number,
default: 580,
},
searchData: {
type: Object as () => SearchParams,
default: undefined,
},
});
const baseUrl = import.meta.env.VITE_API_USERCENTER_URL;
const excelImportUsersUrl = `${baseUrl}/back/users/downloadimportusersexceltemplate`;
const excelImportMeetingUrl = `${baseUrl}/back/users/downloadimportmeetingexceltemplate`;
const excelImportOrdersUrl = `${baseUrl}/back/users/downloadimportordersexceltemplate`;
const searchForm = ref<FormInstance>();
const editId = ref(0);
const showAllPosition = ref<UserDetail[]>([]);
const selectUserTable = ref();
const search = reactive<SearchParams>({
searchStr: "",
userType: "",
level: "",
schoolId: "",
graduationYear: "",
grade: "",
classId: "",
subjectId: "",
positionId: "",
});
const userTypeList = ref<ComboModel[]>([
{ value: 1, text: "学生" },
{ value: 2, text: "教师" },
{ value: 3, text: "管理员" },
]);
const userLevelList = ref<ComboModel[]>([]);
const schoolList = ref<ComboModel[]>([]);
const gradeList = ref<ComboModel[]>([
{ value: "初一", text: "初一" },
{ value: "初二", text: "初二" },
{ value: "初三", text: "初三" },
{ value: "高一", text: "高一" },
{ value: "高二", text: "高二" },
{ value: "高三", text: "高三" },
]);
const classList = ref<ComboModel[]>([]);
const subjectList = ref<ComboModel[]>([]);
const positionList = ref<any[]>([]);
const table = reactive<TableData>({
data: [],
selectRows: [],
sort: "",
border: true,
});
const pagination = reactive<PaginationData>({
index: 1,
size: 10,
total: 0,
});
const dialog = reactive<DialogData>({
close: false,
update: {
title: "",
visible: false,
width: "1000px",
},
editLevel: {
userIds: [],
title: "",
visible: false,
width: "400px",
},
editSubjectLevel: {
userIds: [],
title: "",
visible: false,
width: "450px",
},
bindUser: {
title: "分配权限码",
visible: false,
width: "1150px",
height: "",
},
userBindInfo: {
title: "用户权限码",
visible: false,
width: "1150px",
height: "",
},
});
const checkUserBindInfo = () => {
if (table.selectRows.length != 1) {
ElMessage.warning("请选择一个用户");
return;
}
dialog.userBindInfo.visible = true;
};
const showPosition = (row: UserDetail) => {
if (showAllPosition.value.includes(row)) {
showAllPosition.value.splice(showAllPosition.value.indexOf(row), 1);
} else {
showAllPosition.value.push(row);
}
};
const codeBindUser = () => {
if (table.selectRows.length == 0) {
ElMessage.warning("请选择需要分配权限的用户");
return;
}
dialog.bindUser.visible = true;
};
const initSearchData = () => {
if (props.searchData !== undefined) {
for (const key in props.searchData) {
search[key] = props.searchData[key];
}
}
};
const selectUserClick = (row: UserDetail) => {
if (props.selectUser) {
selectUserTable.value.toggleRowSelection(row);
}
};
const setCurrent = (row: UserDetail) => {
selectUserTable.value.toggleRowSelection(row);
};
const selectUserCallBack = () => {
const u = table.selectRows;
props.selectCallBack(u);
selectUserTable.value.clearSelection();
};
const exportUser = async () => {
const data = {
SearchStr: search.searchStr,
UserType: search.userType || 0,
Level: search.level || 0,
SchoolId: search.schoolId || 0,
GraduationYear: search.graduationYear || 0,
Grade: search.grade,
ClassId: search.classId || 0,
SubjectId: search.subjectId || 0,
PositionId: search.positionId || 0,
PageIndex: pagination.index,
PageSize: pagination.size,
};
// const res = await exportUserApi(data);
// if (res.type === "application/json") {
// const json = await readerBlob(res);
// if (json !== undefined && json.code !== 200) {
// ElMessage.error(json.Message);
// }
// } else if (res !== undefined && res.size !== 0) {
// ElMessage.success("🕖");
// const url = window.URL.createObjectURL(res);
// const link = document.createElement("a");
// link.href = url;
// link.setAttribute("download", ".xlsx");
// document.body.appendChild(link);
// link.click();
// document.body.removeChild(link);
// } else {
// ElMessage.warning(",");
// }
};
const fetchInitData = async () => {
schoolList.value = (await getSchoolData()).data;
subjectList.value = (await getenum("SubjectEnum")).data;
userLevelList.value = (await getenum("StudentLevelEnum")).data;
userTypeList.value = (await getenum("UserTypeEnum")).data;
};
const userTypeChange = () => {
search.level = "";
};
const schoolChange = () => {
search.graduationYear = "";
search.grade = "";
search.classId = "";
search.subjectId = "";
getClass();
};
const gradeChange = () => {
search.classId = "";
search.subjectId = "";
getClass();
};
const getClass = () => {
const data = {
schoolId: search.schoolId || 0,
graduationYear: search.graduationYear || 0,
grade: search.grade,
};
getClassCombo(data).then((res) => {
if (res.code === 200) {
classList.value = res.data;
}
});
};
const fetchPagedData = (searchUnUse = false) => {
const data = {
SearchStr: search.searchStr,
UserType: 2,
Level: search.level || 0,
SchoolId: search.schoolId || 0,
GraduationYear: search.graduationYear || 0,
Grade: search.grade,
ClassId: search.classId || 0,
SubjectId: search.subjectId || 0,
PositionId: search.positionId || 0,
PageIndex: pagination.index,
PageSize: pagination.size,
UnUsed: searchUnUse,
};
getPageUserList(data).then((res) => {
if (res.code === 200) {
pagination.total = res.data.total;
res.data.data.forEach((item) => {
if (item.positions) {
item.positions = PositionsSort(item.positions);
}
});
table.data = res.data.data;
}
});
};
const handleSelectionChange = (selection: UserDetail[]) => {
table.selectRows = selection;
};
const getUserTypeTag = (type: number) => {
return type === 1 ? "info" : "warning";
};
const getUserLevelTag = (level: number) => {
return level === 0
? "info"
: level === 1
? "success"
: level === 2
? "warning"
: "error";
};
const getUserLevelText = (level: number) => {
const r = userLevelList.value.filter((w) => w.value === level);
if (r.length > 0) {
return r[0].text;
}
return "";
};
const PositionsSort = (arr: Position[]) => {
arr.sort((a, b) => {
if (a.enable === b.enable) {
return 0;
} else if (a.enable) {
return -1;
} else {
return 1;
}
});
return arr;
};
function searchReload() {
for (const key in search) {
search[key] = "";
}
}
const handleReloadPaged = (event?: any, searchUnUse?: boolean) => {
pagination.index = 1;
table.selectRows = [];
fetchPagedData(searchUnUse);
};
function pageSizeChange(o) {
pagination.size = o;
fetchPagedData();
}
function pageIndexChange(o) {
pagination.index = o;
fetchPagedData();
}
const AddDialog = () => {
editId.value = 0;
dialog.update.title = "添加教师";
dialog.update.visible = true;
};
const EditDialog = (row?) => {
if (row == null && table.selectRows.length !== 1) {
ElMessage.warning("请选择要修改用户");
return;
}
dialog.update.title = "修改用户";
editId.value = row != null ? row.id : table.selectRows[0].id;
dialog.update.visible = true;
};
const handleAddCallback = () => {
dialog.update.visible = false;
handleReloadPaged();
};
const handleEditLevel = () => {
if (table.selectRows.length === 0) {
ElMessage.warning("请选择要修改用户");
return;
}
dialog.editLevel.title = "修改学生层次";
dialog.editLevel.userIds = table.selectRows.map((w) => w.id);
dialog.editLevel.visible = true;
};
const handleEditLevelCallback = () => {
dialog.editLevel.visible = false;
handleReloadPaged();
};
const handleEditSubjectLevel = () => {
if (table.selectRows.length === 0) {
ElMessage.warning("请选择要修改用户");
return;
}
dialog.editSubjectLevel.title = "修改学生科目层次";
dialog.editSubjectLevel.userIds = table.selectRows.map((w) => w.id);
dialog.editSubjectLevel.visible = true;
};
const handleEditSubjectLevelCallback = () => {
dialog.editSubjectLevel.visible = false;
handleReloadPaged();
};
const importData = () => {
console.log("批量导入");
let fileE = document.createElement("input");
fileE.type = "file";
var formData = new window.FormData();
fileE.onchange = async function () {
formData.append("file", fileE.files[0]);
let res = await ImportTeacher(fileE.files[0]);
if (res.code != undefined) {
if (res.code !== 200) return ElMessage.error(res.message);
else return ElMessage.success("所有数据录入成功");
} else if (res.type === "application/json") {
let json = await res.text();
if (json !== undefined && json.Code !== 200) {
return ElMessage.error(json.Message);
} else {
return ElMessage.success("操所有数据录入成功作成功");
}
} else if (res === undefined || res.size === 0)
return ElMessage.success("所有数据录入成功");
const url = res && window.URL.createObjectURL(res);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "未成功导入的老师信息数据" + ".xlsx");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
ElMessage.success("导入失败,已导出错误数据");
};
try {
fileE.click();
} catch (error) {}
};
const downLoadImportUsersTemplate = () => {
const baseUrl = import.meta.env.VITE_API_BASEURL;
const excelImportUsersUrl = `${baseUrl}/Student/DwImportTeacherTemplate`;
window.open(excelImportUsersUrl, "_blank");
};
onMounted(async () => {
await initSearchData();
await fetchInitData();
fetchPagedData();
});
</script>
<style lang="scss" scoped>
.userTagRowItop {
transform: rotate(180deg) !important;
}
.userTagRow i {
padding-top: 3px;
transform: rotate(0deg);
font-size: 1.3rem;
cursor: pointer;
}
.userTagRow {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
.subjectTagEnableDiv {
display: flex;
padding: 3px;
gap: 5px;
}
.classTag {
color: #a3bf08 !important;
background-color: #f4fbd1 !important;
border-color: #f4fbd1 !important;
}
.subjectTag {
color: #eb0de4 !important;
background-color: #fbd9ff !important;
border-color: #fbd9ff !important;
}
.subjectlevel_ul {
margin: 0;
padding: 0;
list-style: none;
}
.toolbar-container {
margin-bottom: 5px;
}
</style>

View File

@ -0,0 +1,501 @@
<template>
<div class="app-container">
<div
style="color: #606266; font-size: 1.5em; font-weight: bold"
v-if="selectionCount == 1"
>
双击选中职位
</div>
<h2></h2>
<div class="search-container1">
<!-- 搜索项目 -->
<el-form :inline="true" :model="search">
<el-form-item>
<el-select
v-model="search.schoolId"
placeholder="学校"
clearable
filterable
@change="schoolChange"
>
<el-option
v-for="item in schoolList"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-select
v-model="search.positionType"
placeholder="职位类型"
clearable
filterable
:disabled="userType > 0"
>
<el-option
v-for="item in positionList"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-select
v-model="search.grade"
placeholder="年级"
clearable
filterable
@change="gradeChange"
>
<el-option
v-for="item in gradeList"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="search.classId" placeholder="班级" clearable filterable>
<el-option
v-for="item in classList"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="search.subjectId" placeholder="科目" clearable filterable>
<el-option
v-for="item in subjectList"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- <el-form-item>
<el-select
v-model="search.status"
placeholder="状态"
clearable
filterable
>
<el-option
v-for="item in statusList"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item> -->
<el-form-item>
<el-button type="primary" @click="handleReloadPaged" :icon="Search"
>查询</el-button
>
</el-form-item>
<el-form-item>
<el-button type="success" @click="asyncPosition">{{
userType == 1 ? "选择勾选班级" : "选择勾选职位"
}}</el-button>
</el-form-item>
</el-form>
</div>
<!-- <div class="toolbar-container">
<el-button type="success" plain @click="handleAdd"
>新增</el-button
>
<el-button type="primary" plain @click="handleEdit"
>修改</el-button
>
</div> -->
<div class="tableBox">
<el-table
:data="table.data"
ref="positionTb"
row-key="id"
:border="table.border"
@row-dblclick="rowDblclick"
height="600px"
style="width: 768px; max-width: 768px"
:expand-row-keys="tableExpandRowKeys"
:tree-props="{ children: 'children' }"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="40" />
<el-table-column prop="name" label="职位名称[双击行快速选择]" width="220">
<template #default="scope">
<span
>{{ scope.row.name }}
<el-tag v-show="selectPositions.find((s) => scope.row.Id == s.id)"
>已选</el-tag
>
</span>
</template>
</el-table-column>
<el-table-column prop="positionLevel" label="职级" width="80">
<template #default="scope">
<el-tag v-if="scope.row.positionLevel === 1" type="danger">教委</el-tag>
<el-tag v-else-if="scope.row.positionLevel === 2" type="warning">校级</el-tag>
<el-tag v-else-if="scope.row.positionLevel === 3">年级</el-tag>
<el-tag v-else-if="scope.row.positionLevel === 4" type="success">班级</el-tag>
<el-tag v-else-if="scope.row.positionLevel === 5" type="info">教师</el-tag>
<el-tag v-else-if="scope.row.positionType === 1" type="info">学生</el-tag>
</template>
</el-table-column>
<el-table-column label="学校 - 年级 - 班级 - 学科">
<template #default="scope">
<el-tag v-if="!scope.row.status" type="danger">锁定</el-tag>
{{ scope.row.schoolName }} {{ scope.row.grade }} {{ scope.row.className }}
{{ scope.row.subjectName }}
</template>
</el-table-column>
</el-table>
<el-card class="box-card">
<div class="clearfix clearfixCss">
<span style="line-height: 32px; font-weight: 600"
>{{ userType == 1 ? "就读班级" : "已选职位" }}[{{
selectPositions.length
}}]</span
>
<el-button
style="float: right"
type="success"
@click="handleConfirm"
:icon="Check"
>提交分配职位</el-button
>
</div>
<div class="positionGap">
<el-tag
v-for="(p, o) in selectPositions"
:key="o"
closable
@close="tagClose(p)"
>
{{ p.name }} {{ p.schoolName }} {{ p.grade }} {{ p.className }}
{{ p.subjectName }}
</el-tag>
</div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import type { FormInstance } from "element-plus";
import {
getSchoolData,
getClassCombo,
getSubjectData,
getPositionList,
getPositions,
Position,
} from "@/api/userCenter";
import { ComboModel } from "@/components/hTable/hTable";
import { Check, Search } from "@element-plus/icons-vue";
interface SearchParams {
schoolId: string | number;
positionType: string | number;
grade: string;
classId: string | number;
subjectId: string | number;
status: string | number;
}
interface TableData {
data: Position[];
selectRows: Position[];
border: boolean;
}
interface Dialog {
id: number;
parentPosition: Position | null;
close: boolean;
title: string;
visible: boolean;
width: string;
}
const props = defineProps({
userType: {
type: Number,
default: 0,
},
positions: {
type: Array as () => number[],
default: () => [],
},
selectionCount: {
type: Number,
default: 999,
},
});
const emit = defineEmits(["handleCheckCallback", "handleCheckCallback"]);
const search = reactive<SearchParams>({
schoolId: "",
positionType: "",
grade: "",
classId: "",
subjectId: "",
status: 1,
});
const selectPositions = ref<Position[]>([]);
const schoolList = ref<ComboModel[]>([]);
const gradeList = ref<ComboModel[]>([
{ value: "初一", text: "初一" },
{ value: "初二", text: "初二" },
{ value: "初三", text: "初三" },
{ value: "高一", text: "高一" },
{ value: "高二", text: "高二" },
{ value: "高三", text: "高三" },
]);
const classList = ref<ComboModel[]>([]);
const subjectList = ref<ComboModel[]>([]);
const tableExpandRowKeys = ref<string[]>([]);
const positionList = ref<ComboModel[]>([
{ text: "学生", value: 1 },
{ text: "教师", value: 2 },
{ text: "管理员", value: 3 },
]);
const statusList = ref<ComboModel[]>([
{ text: "正常", value: 1 },
{ text: "锁定", value: 2 },
]);
const table = reactive<TableData>({
data: [],
selectRows: [],
border: true,
});
const dialog = reactive<Dialog>({
id: 0,
parentPosition: null,
close: false,
title: "编辑职位",
visible: false,
width: "800px",
});
const authDialog = reactive<Dialog>({
id: 0,
parentPosition: null,
close: false,
title: "职位授权",
visible: false,
width: "400px",
});
const positionTb = ref<FormInstance>();
const userTypeToPosition = () => {
switch (props.userType) {
case 1:
search.positionType = 1;
break;
case 2:
search.positionType = 2;
break;
case 15:
search.positionType = 3;
break;
default:
search.positionType = -1;
break;
}
};
const rowDblclick = (row: Position) => {
if (props.selectionCount === 1) {
emit("handleCheckCallback", [row]);
} else {
asyncPosition(null, [row]);
}
};
const handleReloadPaged = () => {
fetchPagedData();
};
const handleAdd = () => {
if (table.selectRows.length > 0) {
dialog.parentPosition = table.selectRows[0];
dialog.title = `${table.selectRows[0].name}-添加附属职位`;
} else {
dialog.title = "添加职位";
dialog.parentPosition = null;
}
dialog.id = 0;
dialog.visible = true;
};
const handleEdit = () => {
if (table.selectRows.length === 0) {
ElMessage.warning("未勾选记录");
return;
}
if (table.selectRows.length > 1) {
ElMessage.warning("当前操作只支持勾选一条记录");
return;
}
dialog.id = table.selectRows[0].id;
dialog.parentPosition = null;
dialog.title = "编辑职位";
dialog.visible = true;
};
const handleRefreshCallback = () => {
dialog.visible = false;
authDialog.visible = false;
handleReloadPaged();
};
const handleSelectionChange = (selection: Position[]) => {
table.selectRows = selection;
};
const fetchInitData = async () => {
const schoolRes = await getSchoolData();
if (schoolRes.code === 200) {
schoolList.value = schoolRes.data;
if (schoolList.value.length > 0) {
search.schoolId = schoolList.value[0].value;
schoolChange();
fetchPagedData();
}
}
const subjectRes = await getSubjectData();
if (subjectRes.code === 200) {
subjectList.value = subjectRes.data;
}
};
const schoolChange = () => {
search.grade = "";
search.classId = "";
search.subjectId = "";
getClass();
};
const gradeChange = () => {
search.classId = "";
search.subjectId = "";
getClass();
};
const getClass = async () => {
const data = {
schoolId: search.schoolId || 0,
grade: search.grade,
};
const res = await getClassCombo(data);
if (res.code === 200) {
classList.value = res.data;
}
};
const fetchPagedData = async () => {
const data = {
SchoolId: search.schoolId || 0,
Grade: search.grade,
ClassId: search.classId || 0,
SubjectId: search.subjectId || 0,
PositionType: search.positionType || 0,
Status: search.status || 0,
};
const res = await getPositionList(data);
if (res.code === 200) {
table.data = res.data;
tableExpandRowKeys.value = [];
tableExpandRowKeys.value = table.data.map((s) => s.id.toString());
tableExpandRowKeys.value.push(
res.data.find((s) => s.id == 1).children.map((s) => s.id.toString())
);
}
};
const handleConfirm = () => {
if (selectPositions.value.length === 0) {
ElMessage.warning("请选择要分配的职位");
return;
}
emit("handleCheckCallback", selectPositions.value);
};
const tagClose = (p: Position) => {
selectPositions.value = selectPositions.value.filter((s) => s !== p);
};
const asyncPosition = (event: Event | null, rows?: Position[]) => {
const datas = rows || table.selectRows;
if (datas.length === 0) {
ElMessage.warning("请选择要分配的职位");
return;
}
const pIds = selectPositions.value.map((s) => s.id);
selectPositions.value = selectPositions.value.concat(
datas.filter((s) => !pIds.includes(s.id))
);
};
onMounted(async () => {
userTypeToPosition();
if (props.positions && props.positions.length > 0) {
const res = await getPositions(props.positions);
selectPositions.value = res.data.map((s) => ({ ...s }));
}
fetchInitData();
});
</script>
<style scoped>
.tableBox {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: flex-start;
align-items: flex-start;
gap: 10px;
}
:deep(.el-card__header) {
padding: 8px 8px;
}
.clearfixCss {
width: 350px;
padding-bottom: 10px;
}
.positionGap {
height: calc(600px - 90px);
overflow-x: auto;
width: 350px;
gap: 10px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
}
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts" name="Testxb">
import { ref } from "vue";
// defineOptions({
// name: "Testxb"
// });
let name = ref("nihao");
</script>
<template>
<div>测试菜单</div>
</template>

View File

@ -0,0 +1,494 @@
<!-- 新建赴校信息 -->
<template>
<el-dialog
v-model="dialogVisible"
title="新建"
width="800px"
@close="onCancel"
align-center
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-divider>基础信息</el-divider>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="学校" prop="baseInfo.school">
<el-select
v-model="form.baseInfo.school"
placeholder="请选择学校"
clearable
filterable
>
<el-option
v-for="s in schoolOptions"
:key="s.value"
:label="s.label"
:value="s.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年级" prop="baseInfo.grade">
<el-select v-model="form.baseInfo.grade" placeholder="请选择年级" clearable>
<el-option
v-for="g in gradeOptions"
:key="g.value"
:label="g.label"
:value="g.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="赴校时间" prop="baseInfo.date">
<el-date-picker
v-model="form.baseInfo.date"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="赴校人员" prop="baseInfo.people">
<el-select
v-model="form.baseInfo.people"
placeholder="请选择赴校人员"
clearable
multiple
filterable
style="width: 100%"
>
<el-option
v-for="p in peopleOptions"
:key="p.value"
:label="p.text"
:value="p.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-divider>基础工作</el-divider>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开展座谈" prop="work.talk">
<el-radio-group v-model="form.work.talk">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="座谈情况">
<el-input
:maxlength="500"
v-model="form.work.talkDetail"
:disabled="!form.work.talk"
placeholder="请输入座谈情况"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开展班会" prop="work.classMeeting">
<el-radio-group v-model="form.work.classMeeting">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="班会情况">
<el-input
:maxlength="500"
v-model="form.work.classMeetingDetail"
:disabled="!form.work.classMeeting"
placeholder="请输入班会情况"
/>
</el-form-item>
</el-col>
</el-row>
<el-divider>反馈问题</el-divider>
<div style="height: 370px; overflow-y: auto">
<div v-for="group in feedbackGroups" :key="group.key" class="feedback-group">
<div class="feedback-header">
<span class="group-title">{{ group.name }}</span>
<el-button type="primary" link @click="addProblem(group.key)"
>添加问题</el-button
>
</div>
<div v-if="form.feedback[group.key].length === 0" class="feedback-empty">
暂无问题
</div>
<div
v-for="(item, idx) in form.feedback[group.key]"
:key="item.id"
class="feedback-item"
>
<el-input v-model="item.text" :placeholder="`请输入${group.name}问题描述`" />
<el-button type="danger" text @click="removeProblem(group.key, idx)"
>删除</el-button
>
</div>
<el-divider />
</div>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel"> </el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit"
> </el-button
>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="AddModal">
import { ref, reactive, computed, defineProps, defineEmits, watch } from "vue";
import type { FormInstance, FormRules } from "element-plus";
import { ElMessage } from "element-plus";
import { getSchoolData } from "@/api/userCenter";
import { getSchoolBusinessPeopleListApi, addOrEditApi } from "@/api/toschoolinfomanage";
const props = defineProps<{ visible: boolean }>();
// const emit = defineEmits<{ (e: "update:visible", value: boolean): void }>();
const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
(e: "handleReset"): void;
}>();
const dialogVisible = computed({
get: () => props.visible,
set: (v) => emit("update:visible", v),
});
const formRef = ref<FormInstance>();
function uid() {
return Math.random().toString(36).slice(2) + Date.now().toString(36);
}
const schoolOptions = ref([]);
const peopleOptions = ref([]);
const getSchoolDataFn = () => {
getSchoolData().then((res) => {
if (res.code == 200) {
schoolOptions.value = res.data.map((i: any) => ({
label: i.text,
value: i.value,
}));
}
});
};
/**
* 获取赴校人员下拉数据
*/
const getSchoolBusinessPeopleList = () => {
getSchoolBusinessPeopleListApi({}).then((res: any) => {
if (res.code == 200) {
peopleOptions.value = (res.data || []).map((i) => ({
label: i.text,
value: i.text,
}));
}
});
};
getSchoolDataFn();
getSchoolBusinessPeopleList();
const gradeOptions = [
{ label: "初一", value: "初一" },
{ label: "初二", value: "初二" },
{ label: "初三", value: "初三" },
{ label: "高一", value: "高一" },
{ label: "高二", value: "高二" },
{ label: "高三", value: "高三" },
];
type FeedbackKey = "leaders" | "classroom" | "equipment" | "students" | "others";
interface FeedbackItem {
id: string;
text: string;
}
interface FormModel {
baseInfo: {
school?: string;
grade?: string;
date?: string;
people: string[];
};
work: {
talk: boolean;
talkDetail: string;
classMeeting: boolean;
classMeetingDetail: string;
};
feedback: Record<FeedbackKey, FeedbackItem[]>;
}
const form = reactive<FormModel>({
baseInfo: {
school: undefined,
grade: undefined,
date: undefined,
people: [],
},
work: {
talk: false,
talkDetail: "",
classMeeting: false,
classMeetingDetail: "",
},
feedback: {
leaders: [],
classroom: [],
equipment: [],
students: [],
others: [],
},
});
watch(
() => form.work.talk,
(val) => {
!val ? (form.work.talkDetail = "") : "";
}
);
watch(
() => form.work.classMeeting,
(val) => {
!val ? (form.work.classMeetingDetail = "") : "";
}
);
const rules: FormRules = {
"baseInfo.school": [{ required: true, message: "请选择学校", trigger: "change" }],
"baseInfo.grade": [{ required: true, message: "请选择年级", trigger: "change" }],
"baseInfo.date": [{ required: true, message: "请选择赴校时间", trigger: "change" }],
"baseInfo.people": [
{ required: true, message: "请选择赴校人员", trigger: "change" },
{
type: "array",
min: 1,
message: "请至少选择一名赴校人员",
trigger: "change",
},
],
};
const feedbackGroups = [
{ key: "leaders", name: "学校领导班子" },
{ key: "classroom", name: "双师课堂" },
{ key: "equipment", name: "设备" },
{ key: "students", name: "学生" },
{ key: "others", name: "其他" },
] as { key: FeedbackKey; name: string }[];
function addProblem(key: FeedbackKey) {
form.feedback[key].push({ id: uid(), text: "" });
}
function removeProblem(key: FeedbackKey, index: number) {
form.feedback[key].splice(index, 1);
}
//
const feedbackGroupNameMap: Record<FeedbackKey, string> = {
leaders: "学校领导班子",
classroom: "双师课堂",
equipment: "设备",
students: "学生",
others: "其他",
};
function validateFeedbackNotEmpty() {
for (const g of feedbackGroups) {
const items = form.feedback[g.key] || [];
if (items.length === 0) continue; // 0
for (const it of items) {
if (!it.text || !it.text.trim()) {
ElMessage.error(`${feedbackGroupNameMap[g.key]} 的问题内容不能为空`);
return false;
}
}
}
return true;
}
const submitting = ref(false);
function onCancel() {
dialogVisible.value = false;
formRef.value.resetFields();
}
/**
* 把表单数据处理成接口需要的结构
* @param data
*/
const handleFeedback = (data: any) => {
const processData = (items: any[], questionType: number) => {
return items.map((item, idx) => ({
question: item.text,
questionType,
sort: (idx + 1).toString(),
}));
};
let handledData = [];
if (data.leaders.length > 0) handledData.push(...processData(data.leaders, 1));
if (data.classroom.length > 0) handledData.push(...processData(data.classroom, 10));
if (data.equipment.length > 0) handledData.push(...processData(data.equipment, 15));
if (data.students.length > 0) handledData.push(...processData(data.students, 20));
if (data.others.length > 0) handledData.push(...processData(data.others, 999));
return handledData;
};
let editParams = {
id: 0,
schoolId: 0,
schoolName: "string",
grade: "string",
gradeYear: 0,
gradeLevel: "string",
//
schoolBusinessUser: ["string"],
//
startTime: "2025-08-19T07:20:29.292Z",
//
remark: "string",
//
feedbackQuestions: [
{
//
questionType: 1,
//
sort: "string",
//
question: "string",
//
solution: "string",
//
endTime: "2025-08-19T07:20:29.292Z",
},
],
//
solutionRecord: {
//
solution: "string",
//
endRecord: "string",
//
endRecordTime: "string",
record: [
{
//
executionRecords: "string",
//
executionTime: "2025-08-19T07:20:29.292Z",
},
],
},
//
discussion: "string",
//
classMeeting: "string",
};
async function onSubmit() {
if (!formRef.value) return;
await formRef.value.validate((valid) => {
if (!valid) return;
//
if (!validateFeedbackNotEmpty()) return;
submitting.value = true;
console.log("Submit payload:", form);
// enum FeedbackQuestionTypeEnum {
// = 1,
// = 10,
// = 15,
// = 20,
// = 999
// }
let reqParams = {
id: 0, //id0
schoolId: form.baseInfo.school,
schoolName: schoolOptions.value.find((i) => i.value == form.baseInfo.school).label,
grade: form.baseInfo.grade,
//
gradeLevel: "",
schoolBusinessUser: form.baseInfo.people,
startTime: form.baseInfo.date,
isDiscussion: form.work.talk,
discussion: form.work.talkDetail,
isClassMeeting: form.work.classMeeting,
classMeeting: form.work.classMeetingDetail,
feedbackQuestions: handleFeedback(form.feedback),
};
// return;
console.log("提交数据", reqParams);
addOrEditApi(reqParams)
.then((res) => {
if (res.code === 200) {
ElMessage.success("提交成功");
dialogVisible.value = false;
resetForm();
formRef.value.resetFields();
emit("handleReset");
}
})
.finally(() => {
submitting.value = false;
});
// }, 600);
});
}
function resetForm() {
form.baseInfo.school = undefined;
form.baseInfo.grade = undefined;
form.baseInfo.date = undefined;
form.baseInfo.people = [];
form.work.talk = false;
form.work.talkDetail = "";
form.work.classMeeting = false;
form.work.classMeetingDetail = "";
(Object.keys(form.feedback) as FeedbackKey[]).forEach((k) => {
form.feedback[k] = [];
});
}
</script>
<style scoped lang="scss">
.feedback-group {
margin-bottom: 8px;
}
.feedback-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 6px 0;
}
.group-title {
font-weight: 600;
}
.feedback-item {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 6px;
}
.feedback-empty {
margin: 4px 0 8px;
font-size: 13px;
color: #999;
}
:deep(.el-dialog__body) {
max-height: 70vh;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,561 @@
<!-- 跟进 -->
<template>
<el-dialog
v-model="dialogVisible"
:title="isDetail ? '详情' : '跟进'"
width="800px"
@close="closeModal"
align-center
>
<!-- {{ isDetail }} -->
<div class="modal-header">
<div class="status-box">
<span>当前状态</span>
<el-tag :type="statusType">{{ statusText }}</el-tag>
</div>
<div class="action-box">
<el-button @click="onClickCancel">取消</el-button>
<el-button v-show="!isDetail" type="primary" @click="onClickSave">保存</el-button>
</div>
</div>
<div
style="height: 80vh; overflow-y: auto; padding-right: 22px"
v-loading="editModalLoading"
>
<!-- <el-divider /> -->
<el-descriptions title="基础信息" :column="2" border>
<el-descriptions-item label="学校">
{{ safeDetail.schoolName || safeDetail.school || "-" }}
</el-descriptions-item>
<el-descriptions-item label="年级">
{{ safeDetail.grade || safeDetail.gradeLevel || "-" }}
</el-descriptions-item>
<el-descriptions-item label="赴校人员">
{{
Array.isArray(safeDetail.schoolBusinessUser)
? safeDetail.schoolBusinessUser.join("")
: safeDetail.schoolBusinessUser || "-"
}}
</el-descriptions-item>
<el-descriptions-item label="赴校时间">
{{ safeDetail.startTimeStr || safeDetail.startTime }}
</el-descriptions-item>
</el-descriptions>
<el-divider />
<el-descriptions title="基础工作" :column="1" border>
<el-descriptions-item label="座谈">
<el-tag
:type="safeDetail.isDiscussion ? 'success' : 'info'"
style="margin-right: 8px"
>
{{ safeDetail.isDiscussion ? "已开展" : "未开展" }}
</el-tag>
<span class="inline-block max-w-[500px]!">{{
safeDetail.discussion || "-"
}}</span>
</el-descriptions-item>
<el-descriptions-item label="班会">
<el-tag
:type="safeDetail.isClassMeeting ? 'success' : 'info'"
style="margin-right: 8px"
>
{{ safeDetail.isClassMeeting ? "已开展" : "未开展" }}
</el-tag>
<span class="inline-block max-w-[500px]!">{{
safeDetail.classMeeting || "-"
}}</span>
</el-descriptions-item>
</el-descriptions>
<el-divider />
<el-descriptions title="反馈问题" :column="1" border> </el-descriptions>
<div style="display: flex; gap: 40px; margin-bottom: 5px">
<span> 问题总数{{ safeDetail.feedbackQuestions?.length }} </span>
<span> 未解决问题{{ unresolvedCount }} </span>
</div>
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane
v-for="(i, idx) in sortData(safeDetail.feedbackQuestions || [])"
:key="idx"
:label="'问题' + (idx + 1) + (i.solution ? '(✅已解决)' : '(未解决)')"
:name="idx"
>
<div style="font-size: 12px; margin-bottom: 4px">
<span>问题类型</span> <span>{{ queType[i.questionType] }}</span>
</div>
<div style="padding: 10px; background-color: #f3f3f3">
{{ i.question }}
</div>
<div v-if="i.solution" style="font-size: 12px; margin-top: 10px">
<span> 解决时间{{ i.endTimeStr || i.endTime }} </span>
<div style="padding: 10px; background-color: #f3f3f3">
{{ i.solution }}
</div>
</div>
<el-button
v-show="!isDetail"
type="text"
v-else
style="margin-top: 5px; font-size: 12px"
class="markTitle"
@click="markTitle(i)"
>
标记已解决
</el-button>
</el-tab-pane>
</el-tabs>
<el-divider />
<!-- {{ safeDetail.feedbackQuestions }} -->
<!-- .............................................................. -->
<el-descriptions title="备注" :column="1" border> </el-descriptions>
<span v-show="!safeDetail.remark">未录入备注</span>
<el-input
v-model="safeDetail.remark"
:rows="4"
type="textarea"
:disabled="isDetail"
/>
<el-divider />
<el-descriptions title="解决方案执行跟踪记录" :column="1" border> </el-descriptions>
<span>需求+解决方案</span>
<el-input v-model="solutionText" :rows="4" type="textarea" :disabled="isDetail" />
<!-- 添加按钮区域 -->
<div style="margin-top: 5px; display: flex; gap: 20px">
<el-button
type="text"
style="margin-top: 5px; font-size: 12px"
class="markTitle"
v-show="!isDetail"
@click="addRecord"
>
添加执行记录
</el-button>
<el-button
style="margin-top: 5px; font-size: 12px"
class="markTitle"
type="text"
v-show="!isDetail"
@click="addFinish"
>
{{ finishRecord ? "修改完结情况" : "添加完结情况" }}
</el-button>
</div>
<!-- 执行记录列表 -->
<div v-if="executionRecords.length > 0" style="margin-top: 15px">
<!-- <div style="font-weight: bold; margin-bottom: 8px">执行记录</div> -->
<div
v-for="(record, index) in executionRecords"
:key="index"
style="
margin-bottom: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
"
>
<div style="font-weight: bold; color: #409eff">
执行记录{{ index + 1 }}{{ record.operator }} {{ record.time }}
</div>
<div style="margin-top: 5px; white-space: pre-wrap">
{{ record.content }}
</div>
</div>
</div>
<!-- 完结情况 -->
<div v-if="finishRecord" style="margin-top: 15px">
<!-- <div style="font-weight: bold; margin-bottom: 8px">完结情况</div> -->
<div
style="
padding: 10px;
background-color: #f0f9ff;
border-radius: 4px;
border: 1px solid #b3d8ff;
"
>
<div style="font-weight: bold; color: #a69400">
完结情况{{ finishRecord.time }}
</div>
<div style="margin-top: 5px; white-space: pre-wrap">
{{ finishRecord.content }}
</div>
</div>
</div>
</div>
</el-dialog>
<!-- 操作弹窗 -->
<el-dialog v-model="operationDialogVisible" title="操作" width="500px" align-center>
<el-form
ref="operationFormRef"
:model="operationForm"
:rules="operationRules"
label-width="80px"
>
<el-form-item label="操作时间" prop="operationTime">
<el-date-picker
v-model="operationForm.operationTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择操作时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item :label="operationContentLabel" prop="operationContent">
<el-input
v-model="operationForm.operationContent"
type="textarea"
:rows="4"
maxlength="300"
show-word-limit
:placeholder="`请输入${operationContentLabel}`"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeOperationDialog">取消</el-button>
<el-button type="primary" @click="confirmOperation">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="EditModal">
import { ref, reactive, computed, defineProps, defineEmits, watch } from "vue";
import type { FormInstance, FormRules, TabsPaneContext } from "element-plus";
import { ElMessage } from "element-plus";
import { getSchoolData } from "@/api/userCenter";
import { getSchoolBusinessPeopleListApi, addOrEditApi } from "@/api/toschoolinfomanage";
import { setFips } from "crypto";
import { useUserStoreHook } from "@/store/modules/user";
import { isAllEmpty } from "@pureadmin/utils";
const activeName = ref<any>(0);
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab.props.name, event);
activeName.value = tab.props.name;
};
const props = defineProps<{
visible: boolean;
detailData: any;
editModalLoading: boolean;
isDetail: boolean;
}>();
const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
(e: "handleReset"): void;
}>();
//
const operationDialogVisible = ref(false);
const operationType = ref(""); // markSolved, addRecord, addFinish
const operationForm = reactive({
operationTime: "",
operationContent: "",
});
const operationFormRef = ref<FormInstance>();
//
const executionRecords = ref<Array<{ time: string; content: string; operator?: string }>>(
[]
);
const finishRecord = ref<{ time: string; content: string } | null>(null);
// detailData.solutionRecord
watch(
() => props.detailData,
(val) => {
const sr = (val as any)?.solutionRecord || {};
//
const recs = Array.isArray(sr?.record) ? sr.record : [];
executionRecords.value = recs.map((r: any) => ({
time: r?.executionTimeStr || r?.executionTime || "",
content: r?.executionRecords || "",
operator: r?.operator || "",
}));
//
if (sr?.endRecordTime || sr?.endRecord) {
finishRecord.value = {
time: sr?.endRecordTimeStr || "",
content: sr?.endRecord || "",
};
} else {
finishRecord.value = null;
}
},
{ immediate: true, deep: true }
);
//
const currentMarkedQuestion = ref<any | null>(null);
//
const operationContentLabel = computed(() => {
switch (operationType.value) {
case "markSolved":
return "解决情况";
case "addRecord":
return "执行记录";
case "addFinish":
return "完结情况";
default:
return "操作内容";
}
});
//
const operationRules: FormRules = {
operationTime: [{ required: true, message: "请选择操作时间", trigger: "change" }],
operationContent: [
{
required: true,
message: `请输入${operationContentLabel.value}`,
trigger: "blur",
},
],
};
const dialogVisible = computed({
get: () => props.visible,
set: (v) => emit("update:visible", v),
});
const closeModal = () => {
emit("update:visible", false);
activeName.value = 0;
};
const queType = {
1: "学校领导班子",
10: "双师课堂",
15: "设备",
20: "学生",
999: "其他",
};
/**
* 获取未解决问题数量
* @param data
*/
const handleUnHandleQust = (data: Array<any>) => {
return (data || []).filter((i) => !i?.solution).length;
};
const sortData = (data: Array<any>) => {
const categorizedData = [
...data
.filter((item) => item.questionType === 1)
.sort((a, b) => a.sort.localeCompare(b.sort)),
...data
.filter((item) => item.questionType === 10)
.sort((a, b) => a.sort.localeCompare(b.sort)),
...data
.filter((item) => item.questionType === 15)
.sort((a, b) => a.sort.localeCompare(b.sort)),
...data
.filter((item) => item.questionType === 20)
.sort((a, b) => a.sort.localeCompare(b.sort)),
...data
.filter((item) => item.questionType === 999)
.sort((a, b) => a.sort.localeCompare(b.sort)),
];
return categorizedData;
};
const safeDetail = computed(() => props.detailData || {});
const statusText = computed(() => (safeDetail.value?.solutionEnd ? "已完结" : "跟进中"));
const statusType = computed(() =>
safeDetail.value?.solutionEnd ? "success" : "warning"
);
const solutionText = computed({
get: () => safeDetail.value?.solutionRecord?.solution || "",
set: (value: string) => {
if (!safeDetail.value.solutionRecord) {
safeDetail.value.solutionRecord = {};
}
safeDetail.value.solutionRecord.solution = value;
},
});
// solution
const unresolvedCount = computed(() => {
const list = (safeDetail.value?.feedbackQuestions as any[]) || [];
return list.filter((item) => !item?.solution).length;
});
const markTitle = (data: any) => {
console.log("标记已解决", data);
operationType.value = "markSolved";
currentMarkedQuestion.value = data;
//
operationForm.operationTime = data?.endTime || "";
operationForm.operationContent = data?.solution || "";
operationFormRef.value?.clearValidate();
operationDialogVisible.value = true;
};
const addRecord = () => {
console.log("添加执行记录");
operationType.value = "addRecord";
operationDialogVisible.value = true;
};
const addFinish = () => {
console.log("添加完结情况");
operationType.value = "addFinish";
const sr = (props.detailData as any)?.solutionRecord;
// 使 finishRecord
if (sr && (sr.endRecordTime || sr.endRecord)) {
operationForm.operationTime = sr.endRecordTimeStr || sr.endRecordTime || "";
operationForm.operationContent = sr.endRecord || "";
} else if (finishRecord.value) {
operationForm.operationTime = finishRecord.value.time;
operationForm.operationContent = finishRecord.value.content;
} else {
//
operationForm.operationTime = "";
operationForm.operationContent = "";
}
operationDialogVisible.value = true;
};
//
const closeOperationDialog = () => {
operationDialogVisible.value = false;
//
operationForm.operationTime = "";
operationForm.operationContent = "";
//
operationFormRef.value?.clearValidate();
//
currentMarkedQuestion.value = null;
};
const confirmOperation = async () => {
if (!operationFormRef.value) return;
try {
await operationFormRef.value.validate();
const { operationTime, operationContent } = operationForm;
//
switch (operationType.value) {
case "addRecord":
//
executionRecords.value.push({
time: operationTime,
content: operationContent,
});
// solutionRecord.record
if (!props.detailData.solutionRecord) props.detailData.solutionRecord = {} as any;
if (!Array.isArray(props.detailData.solutionRecord.record))
props.detailData.solutionRecord.record = [];
const userName = computed(() => {
return isAllEmpty(useUserStoreHook()?.nickName)
? useUserStoreHook()?.userName
: useUserStoreHook()?.nickName;
});
props.detailData.solutionRecord.record.push({
executionTime: operationTime,
executionRecords: operationContent,
operator: userName.value,
});
break;
case "addFinish":
//
finishRecord.value = {
time: operationTime,
content: operationContent,
};
// solutionRecord.endRecordTime / endRecord
if (!props.detailData.solutionRecord) props.detailData.solutionRecord = {} as any;
props.detailData.solutionRecord.endRecordTime = operationTime;
props.detailData.solutionRecord.endRecord = operationContent;
break;
case "markSolved":
//
if (currentMarkedQuestion.value) {
currentMarkedQuestion.value.endTime = operationTime;
currentMarkedQuestion.value.solution = operationContent;
}
break;
}
console.log("确认操作", {
type: operationType.value,
time: operationTime,
content: operationContent,
});
closeOperationDialog();
} catch (error) {
console.log("表单验证失败", error);
}
};
function onClickCancel() {
console.log("取消");
emit("update:visible", false);
//
executionRecords.value = [];
finishRecord.value = null;
currentMarkedQuestion.value = null;
operationForm.operationTime = "";
operationForm.operationContent = "";
activeName.value = 0;
operationFormRef.value?.clearValidate();
}
function onClickSave() {
console.log("保存", props.detailData);
let copyParams = JSON.parse(JSON.stringify(props.detailData));
delete copyParams.solutionEnd;
addOrEditApi(copyParams).then((res) => {
if (res.code === 200) {
ElMessage.success("提交成功");
//
emit("update:visible", false);
//
executionRecords.value = [];
finishRecord.value = null;
currentMarkedQuestion.value = null;
operationForm.operationTime = "";
operationForm.operationContent = "";
activeName.value = 0;
operationFormRef.value?.clearValidate();
// handleReset
emit("handleReset");
}
});
}
</script>
<style scoped lang="scss">
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
}
.status-box {
display: flex;
align-items: center;
gap: 8px;
}
.action-box {
display: flex;
align-items: center;
gap: 8px;
}
.markTitle {
color: #409eff;
}
.markTitle:hover {
text-decoration: underline;
cursor: pointer;
user-select: none;
}
</style>

View File

@ -0,0 +1,506 @@
<template>
<div>
<!-- 搜索区域 -->
<el-form :model="query" inline class="search-form">
<el-form-item label="学校">
<el-select
v-model="query.school"
placeholder="请选择学校"
clearable
filterable
style="width: 180px"
>
<el-option
v-for="s in schoolOptions"
:key="s.value"
:label="s.label"
:value="s.value"
/>
</el-select>
</el-form-item>
<el-form-item label="年级">
<el-select
v-model="query.grade"
placeholder="请选择年级"
clearable
style="width: 140px"
>
<el-option
v-for="g in gradeOptions"
:key="g.value"
:label="g.label"
:value="g.value"
/>
</el-select>
</el-form-item>
<el-form-item label="赴校人员">
<el-select
v-model="query.people"
placeholder="请选择赴校人员"
clearable
filterable
style="width: 300px"
>
<el-option
v-for="p in peopleOptions"
:key="p.value"
:label="p.text"
:value="p.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="query.solutionEnd"
placeholder="请选择状态"
clearable
style="width: 140px"
>
<el-option label="已完结" :value="true" />
<el-option label="跟进中" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="赴校时间">
<el-date-picker
v-model="query.times"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
unlink-panels
style="width: 300px"
/>
</el-form-item>
<el-form-item> </el-form-item>
</el-form>
<!-- 操作按钮区域 -->
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<!-- 操作按钮区域 -->
<div style="margin-bottom: 10px">
<el-button type="primary" @click="handleAdd">新建</el-button>
<el-button type="success" @click="handleImport">批量导入</el-button>
<el-button type="info" @click="handleExport">导出</el-button>
<el-button type="info" @click="downLoadTpl">下载模版</el-button>
</div>
<!-- 表格区域 -->
<el-table :data="listData" style="width: 100%" :max-height="500">
<el-table-column label="操作" width="200">
<template #default="{ row }">
<!-- <el-button size="small" type="danger" plain @click="onDelete(row)"
>删除</el-button
> -->
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon-color="#626AEF"
title="确定删除吗?"
@confirm="onDelete(row)"
>
<template #reference>
<el-button type="danger" text size="small">删除</el-button>
</template>
</el-popconfirm>
<el-button size="small" type="primary" text @click="onDetailOrFollow(row, true)"
>详情</el-button
>
<el-button
v-if="row.canOperate && !row.solutionEnd"
size="small"
type="success"
text
@click="onDetailOrFollow(row, false)"
>跟进</el-button
>
</template>
</el-table-column>
<el-table-column prop="school" label="学校" min-width="140" />
<el-table-column label="状态" min-width="80">
<template #default="{ row }">
<el-tag :type="row.solutionEnd ? 'success' : 'warning'">
{{ row.solutionEnd ? "已完结" : "跟进中" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="grade" label="年级" min-width="100" />
<el-table-column prop="people" label="赴校人员" min-width="120" />
<el-table-column prop="times" label="赴校时间" min-width="140" />
<el-table-column prop="feedbackTotals" label="反馈问题数量" min-width="140" />
<el-table-column prop="solveTotals" label="解决问题数量" min-width="140" />
<!-- <el-table-column prop="lastTime" label="最后跟进时间" min-width="160" /> -->
</el-table>
<!-- 分页 -->
<div class="pager">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
<!-- 新建 -->
<AddModal v-model:visible="isShowAddModal" @handleReset="handleReset" />
<!-- 跟进 -->
<EditModal
v-model:visible="isShowEditModal"
:editModalLoading="editModalLoading"
:detailData="detailData"
:isDetail="isDetail"
@handleReset="handleReset"
/>
</div>
</template>
<!-- 赴校信息管理菜单 -->
<script setup lang="ts" name="Toschoolinfomanage">
import {
addOrEditApi,
getPageListApi,
getSchoolBusinessDetailApi,
deleteSchoolBusinessApi,
getSchoolBusinessPeopleListApi,
importExcel,
} from "@/api/toschoolinfomanage";
import { getSchoolData } from "@/api/userCenter";
import { ref, reactive, computed, onMounted } from "vue";
import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import AddModal from "./addModal.vue";
import { Check, Search } from "@element-plus/icons-vue";
import EditModal from "./editModal.vue";
import { message } from "@/utils/message";
interface TableItem {
id: number;
school: string;
grade: string;
people: string;
canOperate: boolean; //
times: string; // YYYY-MM-DD
feedbackTotals: number;
solveTotals: number;
solutionEnd: boolean; // true: , false:
lastTime: string; // YYYY-MM-DD
}
const schoolOptions = ref([]);
const peopleOptions = ref([]);
const isDetail = ref(false);
/**
* 获取学校下拉数据
*/
const getSchoolDataFn = () => {
getSchoolData().then((res) => {
if (res.code == 200) {
schoolOptions.value = res.data.map((i: any) => ({
label: i.text,
value: i.value,
}));
}
});
};
/**
* 获取赴校人员下拉数据
*/
const getSchoolBusinessPeopleList = () => {
getSchoolBusinessPeopleListApi({}).then((res: any) => {
if (res.code == 200) {
peopleOptions.value = (res.data || []).map((i) => {
return {
value: i.text,
text: i.text,
};
});
}
});
};
const isShowAddModal = ref(false);
const isShowEditModal = ref(false);
const editModalLoading = ref(false);
const detailData = ref({});
onMounted(() => {
// addOrEdit();
loadList();
getSchoolDataFn();
getSchoolBusinessPeopleList();
});
const gradeOptions = [
{ label: "初一", value: "初一" },
{ label: "初二", value: "初二" },
{ label: "初三", value: "初三" },
{ label: "高一", value: "高一" },
{ label: "高二", value: "高二" },
{ label: "高三", value: "高三" },
];
/**
* 新建赴校信息提交
*/
const addOrEdit = () => {
addOrEditApi({
id: 0, //id0
schoolId: 10079,
schoolName: "系统测试学校",
grade: "初二",
gradeLevel: "",
schoolBusinessUser: ["刘德华123"],
startTime: "2025-07-24T07:28:38",
// remark: "string",
feedbackQuestions: [
{
question: "xb测试反馈问题1双师课堂",
questionType: 10,
sort: "1111111111",
},
// {
// question: "xb2",
// questionType: 15,
// sort: "2"
// },
// {
// question: "xb2",
// questionType: 20,
// sort: "3"
// }
],
isDiscussion: true,
discussion: "开展座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈",
isClassMeeting: true,
classMeeting: "班会情况班会情况班会情况班会情况班会情况班会情况班会情况",
});
};
const query = reactive({
school: "" as string | undefined,
grade: "" as string | undefined,
people: "" as string | undefined,
solutionEnd: undefined,
times: [] as string[],
});
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const listData = ref<TableItem[]>([]);
function mapApiItemToRow(item: any): TableItem {
const peopleArr = Array.isArray(item.schoolBusinessUser) ? item.schoolBusinessUser : [];
const start = item.startTime ? dayjs(item.startTime).format("YYYY-MM-DD") : "";
let last = start;
const rec = item.solutionRecord?.record || [];
if (Array.isArray(rec) && rec.length > 0) {
const maxTs = rec
.map((r: any) => r.executionTime)
.filter(Boolean)
.map((t: string) => dayjs(t).valueOf())
.reduce((a: number, b: number) => Math.max(a, b), 0);
if (maxTs) last = dayjs(maxTs).format("YYYY-MM-DD");
}
return {
id: item.id,
school: item.schoolName || "",
grade: item.grade || "",
people: peopleArr.join(""),
times: start,
canOperate: item.canOperate || false, //
feedbackTotals: Number(item.feedbackCount) || 0,
solveTotals: Number(item.solveFeedbackCount) || 0,
solutionEnd: item.solutionEnd,
lastTime: last,
};
}
async function loadList() {
const payload: any = {
pageIndex: page.value,
pageSize: pageSize.value,
orderBy: "startTime",
};
if (query.school) payload.schoolId = query.school;
if (query.grade) payload.grade = query.grade;
if (query.people) {
payload.UserName = query.people;
}
if (typeof query.solutionEnd !== "undefined") payload.solutionEnd = query.solutionEnd;
if (Array.isArray(query.times) && query.times.length === 2) {
payload.startTime = query.times[0];
payload.endTime = query.times[1];
}
try {
const res = await getPageListApi(payload);
if (res.code === 200) {
const rows = Array.isArray(res.data?.data) ? res.data.data : [];
listData.value = rows.map(mapApiItemToRow);
total.value = Number(res.data?.total) || rows.length;
} else {
ElMessage.error(res.message || "加载失败");
}
} catch (e) {
ElMessage.error("列表加载失败");
}
}
function rand(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomDate(start: string, end: string) {
const startTs = dayjs(start).valueOf();
const endTs = dayjs(end).valueOf();
const v = rand(startTs, endTs);
return dayjs(v).format("YYYY-MM-DD");
}
function handleSearch() {
page.value = 1;
loadList();
}
function handleReset() {
query.school = "";
query.grade = "";
query.people = "";
query.solutionEnd = undefined;
query.times = [];
page.value = 1;
loadList();
}
function handlePageChange(p: number) {
page.value = p;
loadList();
}
function handleSizeChange(s: number) {
pageSize.value = s;
page.value = 1;
loadList();
}
function onDelete(row: TableItem) {
console.log(`删除`, row);
deleteSchoolBusinessApi([row.id]).then((res) => {
if (res.code === 200) {
message("删除成功", { type: "success" });
loadList();
}
});
}
/**
* 详情或者跟进
* @param row
*/
function onDetailOrFollow(row: TableItem, disabled = false) {
isDetail.value = disabled;
isShowEditModal.value = true;
editModalLoading.value = true;
getSchoolBusinessDetailApi(row.id)
.then((res) => {
if (res.code === 200 && res.data) {
detailData.value = res.data;
}
})
.finally(() => {
editModalLoading.value = false;
});
}
/**
* 跟进
* @param row
*/
// function onFollow(row: TableItem) {
// isShowEditModal.value = true;
// editModalLoading.value = true;
// console.log(``);
// getSchoolBusinessDetailApi(row.id)
// .then(res => {
// if (res.code === 200 && res.data) {
// detailData.value = res.data;
// }
// })
// .finally(() => {
// editModalLoading.value = false;
// });
// }
function handleAdd() {
console.log("新建");
isShowAddModal.value = true;
}
function handleImport() {
console.log("批量导入");
let fileE = document.createElement("input");
fileE.type = "file";
var formData = new window.FormData();
fileE.onchange = async function () {
formData.append("file", fileE.files[0]);
let res = await importExcel(fileE.files[0]);
if (res.code != undefined) {
if (res.code !== 200) return ElMessage.error(res.message);
else {
loadList();
return ElMessage.success("所有数据录入成功");
}
} else if (res.type === "application/json") {
let json = await res.text();
if (json !== undefined && json.Code !== 200) {
return ElMessage.error(json.Message);
} else {
loadList();
return ElMessage.success("所有数据录入成功");
}
} else if (res === undefined || res.size === 0) {
loadList();
return ElMessage.success("所有数据录入成功");
}
const url = res && window.URL.createObjectURL(res);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "未成功导入的考试信息数据" + ".xlsx");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
ElMessage.success("导入失败,已导出错误数据");
};
try {
fileE.click();
} catch (error) {}
}
function handleExport() {
console.log("导出");
}
function downLoadTpl() {
console.log("下载模版");
const baseUrl = import.meta.env.VITE_API_BASEURL;
const excelImportUsersUrl = `${baseUrl}/SchoolBusiness/DwImportTemplate`;
const link = document.createElement("a");
link.href = excelImportUsersUrl;
link.setAttribute("download", "导入赴校信息模板" + ".xlsx");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
ElMessage.success("下载成功!");
}
</script>
<style lang="scss" scoped>
.search-form {
margin-bottom: 0px;
}
.pager {
display: flex;
justify-content: center;
margin-top: 12px;
}
</style>

View File

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ defineOptions({
name: "Welcome" name: "Welcome",
}); });
</script> </script>
<template> <template>
<h1>Pure-Admin-Thin非国际化版本</h1> <h1>学校档案系统</h1>
</template> </template>