This commit is contained in:
guochunsi
2026-02-25 10:39:44 +08:00
144 changed files with 76646 additions and 21701 deletions

View File

@@ -1,56 +1,79 @@
<template>
<el-dialog
v-model="visible"
title="角色切换"
:title="dialogTitle"
width="50%"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
:before-close="handleBeforeClose"
center
>
<el-form>
<!-- <el-form-item label="学校">-->
<!-- <el-tag>{{schoolName}}</el-tag>-->
<!-- </el-form-item>-->
<el-form-item label="角色" class="role-form-item">
<el-form-item class="role-form-item">
<el-radio-group v-model="radio" class="role-radio-group" @change="handleChangeRole">
<el-radio-button
v-for="item in allRole"
:key="item.roleCode"
:label="item.roleCode"
>
{{ item.roleName }}
</el-radio-button>
<template v-for="(roles, groupName) in allRoleGroups" :key="groupName">
<div class="role-group">
<el-divider>{{ groupName }}</el-divider>
<el-radio-button
v-for="item in roles"
:key="item.roleCode"
:label="item.roleCode"
>
{{ item.roleName }}
</el-radio-button>
</div>
</template>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<!-- <el-button type="primary" @click="handleChangeRole">切换</el-button>-->
<template v-if="!requireSelectToClose" #footer>
<el-button @click="handleFooterClose"> </el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, toRef } from 'vue'
import { listAllRole } from '/@/api/admin/role'
import { Local } from '/@/utils/storage'
import { useMessage } from '/@/hooks/message'
/** 弹框标题,如「角色切换」「登录角色选择」 */
const props = withDefaults(
defineProps<{ title?: string; requireSelectToClose?: boolean }>(),
{ title: '角色切换', requireSelectToClose: false }
)
const dialogTitle = computed(() => props.title)
const visible = ref(false)
const radio = ref('')
const allRole = reactive<any[]>([])
/** 按分组名分组的角色列表:{ "未分组": [{ roleId, roleName, roleCode, ... }], ... } */
const allRoleGroups = ref<Record<string, any[]>>({})
const requireSelectToClose = toRef(props, 'requireSelectToClose')
const open = () => {
if (visible.value) return
visible.value = true
listAllRole().then((res) => {
Object.assign(allRole, res.data)
allRoleGroups.value = res.data && typeof res.data === 'object' && !Array.isArray(res.data)
? res.data
: { '未分组': Array.isArray(res.data) ? res.data : [] }
radio.value = Local.get('roleCode')
visible.value = true
})
}
/** 根据 roleCode 从分组数据中查找角色 */
const findRoleByCode = (code: string) => {
for (const roles of Object.values(allRoleGroups.value)) {
if (!Array.isArray(roles)) continue
const found = roles.find((r: any) => r.roleCode === code)
if (found) return found
}
return null
}
const canClose = () => {
if (!radio.value) {
useMessage().warning('请选择一个角色')
@@ -60,6 +83,10 @@ const canClose = () => {
}
const handleBeforeClose = (done: () => void) => {
if (requireSelectToClose.value) {
useMessage().warning('请先选择登录角色')
return
}
if (!canClose()) return
done()
}
@@ -70,7 +97,7 @@ const handleFooterClose = () => {
}
const handleChangeRole = (label: string) => {
const obj = allRole.find((v: any) => v.roleCode === label)
const obj = findRoleByCode(label)
if (!obj) return
Local.set('roleCode', obj.roleCode)
Local.set('roleName', obj.roleName)
@@ -94,6 +121,18 @@ defineExpose({
flex-wrap: wrap;
}
}
.role-group {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.group-name {
font-size: 13px;
color: var(--el-text-color-secondary);
margin-bottom: 6px;
}
.role-radio-group {
display: flex;
flex-wrap: wrap;

View File

@@ -1,59 +0,0 @@
<template>
<el-dialog v-model="visible" width="50%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" center>
<template #title>
<div style="margin: 0 auto;width:100%;text-align:center;font-size:18px;font-weight:bold;">
登录角色选择
</div>
</template>
<div style="margin: 0 auto;width:100%;text-align:center;font-size:18px;font-weight:bold;">
<el-radio-group v-model="radio">
<el-radio-button v-for="(item,index) in allRole" :key="index" :label="item.roleCode" @click.native="handleChangeRole(item.roleCode)">{{item.roleName}}</el-radio-button>
</el-radio-group>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import {listAllRole} from '/@/api/admin/role'
import {Local, Session} from '/@/utils/storage';
import {useMessage} from "/@/hooks/message";
// import {querySchoolName} from "/@/api/admin/tenant"
const visible=ref(false)
const radio=ref('')
const allRole=reactive([])
const schoolName=ref('')
const open=()=>{
// handleQuerySchoolName()
listAllRole().then(res=>{
Object.assign(allRole,res.data)
radio.value=Local.get("roleCode")
visible.value=true
})
}
const handleChangeRole=(label:any)=>{
let obj:any=allRole.find((v:any) => v.roleCode == label)
Local.set("roleCode",obj.roleCode)
Local.set("roleName",obj.roleName)
Local.set("roleId",obj.roleId)
useMessage().success("操作成功")
setTimeout(()=>{
window.location.reload()
},500)
}
// const handleQuerySchoolName=()=>{
// querySchoolName({id:Session.get("tenantId")}).then((res:any)=>{
// schoolName.value=res.data
// })
// }
defineExpose({
open
})
</script>
<style scoped>
</style>

View File

@@ -7,6 +7,12 @@
<el-form-item :label="$t('sysrole.roleCode')" prop="roleCode">
<el-input :placeholder="$t('sysrole.please_enter_the_role_Code')" :disabled="form.roleId !== ''" clearable v-model="form.roleCode"></el-input>
</el-form-item>
<el-form-item label="分组" prop="roleGroup">
<el-input placeholder="用于列表树状分组展示,可留空" clearable v-model="form.roleGroup" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="排序" prop="roleSort">
<el-input-number v-model="form.roleSort" :min="0" :max="9999" placeholder="数值越小越靠前" controls-position="right" style="width: 140px" />
</el-form-item>
<el-form-item :label="$t('sysrole.roleDesc')" prop="roleDesc">
<el-input
maxlength="100"
@@ -67,6 +73,8 @@ const form = reactive({
roleId: '',
roleName: '',
roleCode: '',
roleGroup: '',
roleSort: 0,
roleDesc: '',
dsType: 0,
dsScope: '',
@@ -187,6 +195,7 @@ const getRoleData = (id: string) => {
// 获取部门数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
if (res.data.roleSort == null) form.roleSort = 0;
if (res.data.dsScope) {
dataForm.checkedDsScope = res.data.dsScope.split(',');
} else {

View File

@@ -25,6 +25,12 @@
<el-button plain :disabled="multiple" icon="Delete" type="primary" class="ml10" v-auth="'sys_user_del'" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<el-button plain :disabled="multiple" type="primary" class="ml10" v-auth="'sys_role_edit'" @click="showBatchGroupDialog = true">
批量指定分组
</el-button>
<el-button plain :disabled="selectObjs.length !== 1" type="primary" class="ml10" v-auth="'sys_role_edit'" @click="openAssignUserDialog">
批量指定关联用户
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_role_export'"
@@ -36,55 +42,165 @@
</div>
</el-row>
<el-table
:data="state.dataList"
:data="roleTreeData"
v-loading="state.loading"
style="width: 100%"
row-key="roleId"
@selection-change="handleSelectionChange"
row-key="roleId"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@selection-change="handleSelectionChange"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" :selectable="handleSelectable" width="50" align="center" />
<el-table-column type="index" :label="$t('sysrole.index')" width="80" />
<el-table-column prop="roleName" :label="$t('sysrole.roleName')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleCode" :label="$t('sysrole.roleCode')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleDesc" :label="$t('sysrole.roleDesc')" show-overflow-tooltip></el-table-column>
<el-table-column prop="data_authority" :label="$t('sysrole.data_authority')" show-overflow-tooltip>
<el-table-column type="selection" :selectable="handleSelectable" width="50" align="left" />
<el-table-column type="index" :label="$t('sysrole.index')" width="80">
<template #default="scope">
<dict-tag :options="dictType" :value="scope.row.dsType"></dict-tag>
<span v-if="scope.row._isGroup"></span>
<span v-else>{{ scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('sysrole.createTime')" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="250">
<el-table-column prop="roleName" :label="$t('sysrole.roleName')" show-overflow-tooltip min-width="140" align="left">
<template #default="scope">
<el-button text type="primary" icon="edit-pen" v-auth="'sys_role_edit'" @click="roleDialogRef.openDialog(scope.row.roleId)">{{
$t('common.editBtn')
}}</el-button>
<el-button text type="primary" icon="turn-off" v-auth="'sys_role_perm'" @click="permessionRef.openDialog(scope.row)">{{
$t('sysrole.permissionTip')
}}</el-button>
<el-tooltip :content="$t('sysrole.deleteDisabledTip')" :disabled="scope.row.roleId !== '1'" placement="top">
<span style="margin-left: 12px">
<el-button
text
type="primary"
icon="delete"
:disabled="scope.row.roleId === '1'"
v-auth="'sys_role_del'"
@click="handleDelete([scope.row.roleId])"
>{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
<span v-if="scope.row._isGroup" class="role-group-name">{{ scope.row.roleName }}</span>
<span v-else>{{ scope.row.roleName }}</span>
</template>
</el-table-column>
<el-table-column prop="roleSort" label="排序" width="80" align="center">
<template #default="scope">{{ scope.row._isGroup ? '—' : (scope.row.roleSort ?? 0) }}</template>
</el-table-column>
<el-table-column prop="roleCode" :label="$t('sysrole.roleCode')" show-overflow-tooltip min-width="120">
<template #default="scope">{{ scope.row._isGroup ? '—' : scope.row.roleCode }}</template>
</el-table-column>
<el-table-column prop="roleDesc" :label="$t('sysrole.roleDesc')" show-overflow-tooltip min-width="140">
<template #default="scope">{{ scope.row._isGroup ? '—' : scope.row.roleDesc }}</template>
</el-table-column>
<el-table-column prop="data_authority" :label="$t('sysrole.data_authority')" show-overflow-tooltip width="100">
<template #default="scope">
<template v-if="scope.row._isGroup"></template>
<dict-tag v-else :options="dictType" :value="scope.row.dsType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('sysrole.createTime')" show-overflow-tooltip width="165">
<template #default="scope">{{ scope.row._isGroup ? '—' : scope.row.createTime }}</template>
</el-table-column>
<el-table-column :label="$t('common.action')" width="250" fixed="right">
<template #default="scope">
<template v-if="scope.row._isGroup"></template>
<template v-else>
<el-button text type="primary" icon="edit-pen" v-auth="'sys_role_edit'" @click="roleDialogRef.openDialog(scope.row.roleId)">{{
$t('common.editBtn')
}}</el-button>
<el-button text type="primary" icon="turn-off" v-auth="'sys_role_perm'" @click="permessionRef.openDialog(scope.row)">{{
$t('sysrole.permissionTip')
}}</el-button>
<el-button text type="primary" icon="user" v-auth="'sys_role_view'" @click="openRoleUsersDialog(scope.row)">查看关联用户</el-button>
<el-tooltip :content="$t('sysrole.deleteDisabledTip')" :disabled="scope.row.roleId !== '1'" placement="top">
<span style="margin-left: 12px">
<el-button
text
type="primary"
icon="delete"
:disabled="scope.row.roleId === '1'"
v-auth="'sys_role_del'"
@click="handleDelete([scope.row.roleId])"
>{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
</template>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 批量指定分组弹窗 -->
<el-dialog v-model="showBatchGroupDialog" title="批量指定分组" width="400px" @close="batchGroupName = ''">
<el-form label-width="80px">
<el-form-item label="分组名称">
<el-input v-model="batchGroupName" placeholder="输入分组名,留空为未分组" clearable maxlength="50" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showBatchGroupDialog = false">取消</el-button>
<el-button type="primary" :loading="batchGroupLoading" @click="handleBatchGroup">确定</el-button>
</template>
</el-dialog>
<!-- 批量指定关联用户弹窗 -->
<el-dialog
v-model="showAssignUserDialog"
title="批量指定关联用户"
width="720px"
destroy-on-close
@close="assignUserKeyword = ''; assignUserType = ''; assignUserList = []; assignUserTree = []; assignSelectedIds = []">
<template v-if="assignCurrentRole">
<div class="mb12"><el-text type="info">当前角色{{ assignCurrentRole.roleName }}{{ assignCurrentRole.roleCode }}</el-text></div>
<el-form :inline="true" class="mb12">
<el-form-item label="用户类型" required>
<el-radio-group v-model="assignUserType">
<el-radio label="1">教职工</el-radio>
<el-radio label="2">学生</el-radio>
<el-radio label="3">驻校单位</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="姓名/工号">
<el-input v-model="assignUserKeyword" placeholder="姓名或工号检索" clearable style="width: 180px" @keyup.enter="loadAssignUserList" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="assignUserLoading" @click="loadAssignUserList">查询</el-button>
</el-form-item>
</el-form>
<div class="assign-user-tip mb8" v-if="!assignUserType">请先选择用户类型后再查询</div>
<el-table
v-else
:data="assignUserTree"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
default-expand-all
v-loading="assignUserLoading"
max-height="360"
border
@selection-change="handleAssignUserSelectionChange">
<el-table-column type="selection" width="50" align="left" :selectable="(row: any) => !row._isDept" />
<el-table-column prop="label" label="部门 / 姓名" min-width="200">
<template #default="{ row }">
<span v-if="row._isDept" class="dept-row">{{ row.label }}</span>
<span v-else>{{ row.realName || row.username || row.label }}</span>
</template>
</el-table-column>
<el-table-column prop="username" label="工号" width="120">
<template #default="{ row }">{{ row._isDept ? '—' : row.username }}</template>
</el-table-column>
<el-table-column prop="deptName" label="部门" width="140">
<template #default="{ row }">{{ row._isDept ? '—' : (row.deptName || '—') }}</template>
</el-table-column>
</el-table>
</template>
<template #footer>
<el-button @click="showAssignUserDialog = false">取消</el-button>
<el-button type="primary" :loading="assignSubmitLoading" :disabled="assignSelectedIds.length === 0" @click="handleAssignUsersSubmit">确定已选 {{ assignSelectedIds.length }} </el-button>
</template>
</el-dialog>
<!-- 查看角色关联用户弹窗 -->
<el-dialog v-model="showRoleUsersDialog" title="关联用户" width="560px" destroy-on-close>
<template v-if="currentRoleForUsers">
<div class="mb12"><el-text type="info">角色{{ currentRoleForUsers.roleName }}{{ currentRoleForUsers.roleCode }}</el-text></div>
<el-table :data="roleUsersList" v-loading="roleUsersLoading" max-height="400" border>
<el-table-column prop="deptName" label="部门" min-width="140" show-overflow-tooltip />
<el-table-column prop="realName" label="姓名" width="100" show-overflow-tooltip />
<el-table-column prop="username" label="工号" width="120" show-overflow-tooltip />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button text type="danger" icon="delete" v-auth="'sys_role_edit'" @click="handleUnassignUser(row)">解除</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt8" v-if="!roleUsersLoading && roleUsersList.length === 0"><el-text type="info">该角色下暂无关联用户</el-text></div>
</template>
</el-dialog>
<!-- 角色编辑新增 -->
<role-dialog ref="roleDialogRef" @refresh="getDataList()" />
<!-- 导入角色 -->
@@ -101,8 +217,10 @@
</template>
<script setup lang="ts" name="systemRole">
import { computed, reactive, ref } from 'vue';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { pageList, delObj } from '/@/api/admin/role';
import { list, delObj, batchUpdateRoleGroup, assignUsersToRole, getUsersByRoleId, unassignUserFromRole } from '/@/api/admin/role';
import { pageList as userPageList } from '/@/api/admin/user';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
@@ -121,15 +239,103 @@ const showSearch = ref(true);
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
// 批量指定分组
const showBatchGroupDialog = ref(false);
const batchGroupName = ref('');
const batchGroupLoading = ref(false);
// 查看角色关联用户
const showRoleUsersDialog = ref(false);
const roleUsersList = ref<any[]>([]);
const roleUsersLoading = ref(false);
const currentRoleForUsers = ref<{ roleName: string; roleCode: string; roleId: string } | null>(null);
// 批量指定关联用户
const showAssignUserDialog = ref(false);
const assignUserType = ref('');
const assignUserKeyword = ref('');
const assignUserList = ref<any[]>([]);
const assignUserLoading = ref(false);
const assignSubmitLoading = ref(false);
const assignSelectedIds = ref<string[]>([]);
const assignCurrentRole = computed(() => {
const id = selectObjs.value && selectObjs.value[0];
if (!id) return null;
return (state.dataList || []).find((r: any) => r.roleId === id) || null;
});
/** 按部门分组的用户树(用于表格树形展示) */
const assignUserTree = computed(() => {
const list = assignUserList.value || [];
const map = new Map<string, any[]>();
list.forEach((u: any) => {
const deptKey = u.deptName && String(u.deptName).trim() ? u.deptName : '未分配部门';
if (!map.has(deptKey)) map.set(deptKey, []);
map.get(deptKey)!.push({ ...u, id: u.userId, _isDept: false });
});
const result: any[] = [];
map.forEach((users, deptName) => {
result.push({
id: `dept_${deptName}`,
label: deptName,
_isDept: true,
children: users,
});
});
result.sort((a, b) => a.label.localeCompare(b.label));
return result;
});
// 列表不分页,显示全部数据
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
roleName: '',
},
pageList: pageList, // H
isPage: false,
pageList: async (params: any) => {
const res = await list(params);
let data = res?.data || [];
if (Array.isArray(data) && params?.roleName) {
const kw = String(params.roleName).trim().toLowerCase();
if (kw) {
data = data.filter(
(r: any) =>
(r.roleName && r.roleName.toLowerCase().includes(kw)) ||
(r.roleCode && r.roleCode.toLowerCase().includes(kw))
);
}
}
return { data };
},
descs: ['create_time'],
});
/** 按分组构建树形数据:分组为父节点,角色为子节点 */
const roleTreeData = computed(() => {
const list = state.dataList || [];
const groupMap = new Map<string, any[]>();
list.forEach((row: any) => {
const groupName = row.roleGroup && String(row.roleGroup).trim() ? String(row.roleGroup).trim() : '未分组';
if (!groupMap.has(groupName)) groupMap.set(groupName, []);
groupMap.get(groupName)!.push(row);
});
const result: any[] = [];
groupMap.forEach((roles, groupName) => {
result.push({
roleId: `group_${groupName}`,
roleName: groupName,
_isGroup: true,
children: roles,
});
});
// 未分组放最后,其余按分组名排序
result.sort((a, b) => {
if (a.roleName === '未分组') return 1;
if (b.roleName === '未分组') return -1;
return a.roleName.localeCompare(b.roleName);
});
return result;
});
const dictType = ref([
{
label: '全部',
@@ -153,8 +359,8 @@ const dictType = ref([
},
]);
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// table hook(无分页,不暴露 currentChangeHandle/sizeChangeHandle
const { getDataList, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
@@ -167,15 +373,15 @@ const exportExcel = () => {
downBlobFile('/admin/role/export',Object.assign(state.queryForm,{ids:selectObjs}), 'role.xlsx');
};
// 是否可以多选
// 是否可以多选(分组行不可选,管理员角色不可选)
const handleSelectable = (row: any) => {
return row.roleId !== '1';
return !row._isGroup && row.roleId !== '1';
};
// 多选事件
// 多选事件(仅角色行可选,排除分组行)
const handleSelectionChange = (objs: { roleId: string }[]) => {
selectObjs.value = objs.map(({ roleId }) => roleId);
multiple.value = !objs.length;
selectObjs.value = objs.map(({ roleId }) => roleId).filter((id: string) => !String(id).startsWith('group_'));
multiple.value = !selectObjs.value.length;
};
// 删除操作
@@ -194,4 +400,129 @@ const handleDelete = async (ids: string[]) => {
useMessage().error(err.msg);
}
};
async function loadRoleUsersList() {
const role = currentRoleForUsers.value;
if (!role) return;
roleUsersLoading.value = true;
try {
const res = await getUsersByRoleId(role.roleId);
const data = res?.data;
roleUsersList.value = Array.isArray(data) ? data : [];
} catch {
roleUsersList.value = [];
} finally {
roleUsersLoading.value = false;
}
}
async function openRoleUsersDialog(row: any) {
currentRoleForUsers.value = { roleName: row.roleName, roleCode: row.roleCode, roleId: row.roleId };
showRoleUsersDialog.value = true;
roleUsersList.value = [];
await loadRoleUsersList();
}
async function handleUnassignUser(row: any) {
const roleId = currentRoleForUsers.value?.roleId;
const userId = row.userId;
if (!roleId || !userId) return;
try {
await useMessageBox().confirm(`确定解除「${row.realName || row.username}」与该角色的关联?`);
} catch {
return;
}
try {
await unassignUserFromRole(roleId, userId);
useMessage().success('已解除关联');
await loadRoleUsersList();
} catch (err: any) {
useMessage().error(err?.msg || '操作失败');
}
}
function openAssignUserDialog() {
if (selectObjs.value.length !== 1) return;
showAssignUserDialog.value = true;
assignUserType.value = '';
assignUserKeyword.value = '';
assignUserList.value = [];
assignSelectedIds.value = [];
}
async function loadAssignUserList() {
if (!assignUserType.value) return;
assignUserLoading.value = true;
try {
const res = await userPageList({
current: 1,
size: 2000,
userType: assignUserType.value,
realName: assignUserKeyword.value ? assignUserKeyword.value.trim() : undefined,
});
const records = res?.data?.records ?? res?.records ?? (Array.isArray(res?.data) ? res.data : []);
assignUserList.value = Array.isArray(records) ? records : [];
} catch (e) {
assignUserList.value = [];
} finally {
assignUserLoading.value = false;
}
}
function handleAssignUserSelectionChange(rows: any[]) {
assignSelectedIds.value = (rows || [])
.filter((r: any) => !r._isDept && r.userId)
.map((r: any) => r.userId);
}
async function handleAssignUsersSubmit() {
const roleId = assignCurrentRole.value?.roleId;
if (!roleId || !assignSelectedIds.value.length) {
useMessage().warning('请选择要关联的用户');
return;
}
assignSubmitLoading.value = true;
try {
await assignUsersToRole(roleId, assignSelectedIds.value);
useMessage().success('已关联 ' + assignSelectedIds.value.length + ' 名用户');
showAssignUserDialog.value = false;
} catch (err: any) {
useMessage().error(err?.msg || '操作失败');
} finally {
assignSubmitLoading.value = false;
}
}
// 批量指定分组
const handleBatchGroup = async () => {
const ids = selectObjs.value || [];
if (!ids.length) {
useMessage().warning('请先勾选要设置分组的角色');
return;
}
batchGroupLoading.value = true;
try {
await batchUpdateRoleGroup(ids, batchGroupName.value || '');
useMessage().success('分组已更新');
showBatchGroupDialog.value = false;
batchGroupName.value = '';
getDataList();
} catch (err: any) {
useMessage().error(err?.msg || '操作失败');
} finally {
batchGroupLoading.value = false;
}
};
</script>
<style scoped lang="scss">
.role-group-name {
font-weight: 600;
color: var(--el-text-color-primary);
}
.mb12 { margin-bottom: 12px; }
.mb8 { margin-bottom: 8px; }
.mt8 { margin-top: 8px; }
.assign-user-tip { color: var(--el-text-color-secondary); font-size: 13px; }
.dept-row { font-weight: 600; color: var(--el-text-color-primary); }
</style>

View File

@@ -204,7 +204,8 @@ const dataRules = ref({
{ required: true, message: '学院不能为空', trigger: 'change' }
],
classCode: [
{ required: true, message: '班级代码不能为空', trigger: 'blur' }
{ required: true, message: '班级代码不能为空', trigger: 'blur' },
{ min: 4, message: '班级代码至少4位班号取后4位', trigger: 'blur' }
],
classNo: [
{ required: true, message: '班号不能为空', trigger: 'blur' }
@@ -406,11 +407,11 @@ watch(() => form.enterDate, (newVal) => {
}
})
// 监听班级代码,取后四位为班号
// 监听班级代码,取后四位为班号班级代码至少4位
watch(() => form.classCode, (newVal) => {
if (newVal) {
const length = newVal.length
if (length > 4) {
if (length >= 4) {
// 只在新增模式下自动填充班号,编辑模式下如果班号为空才填充
if (!form.id || !form.classNo) {
form.classNo = newVal.substring(length - 4, length)

View File

@@ -196,7 +196,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="200">
<el-table-column label="操作" align="center" fixed="right" width="260">
<template #default="scope">
<el-button
icon="View"

View File

@@ -262,13 +262,25 @@
<template #default="scope" v-if="col.prop === 'gender'">
<GenderTag :gender="scope.row.gender" />
</template>
<template #default="scope" v-else-if="col.prop === 'education'">
<el-tag v-if="getEducationLabel(scope.row.education)" size="small" type="info" effect="plain">
{{ getEducationLabel(scope.row.education) }}
</el-tag>
<span v-else>-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'isDorm'">
<el-tag v-if="scope.row.isDorm === 1 || scope.row.isDorm === '1'" size="small" type="success" effect="plain"></el-tag>
<el-tag v-else-if="scope.row.isDorm === 0 || scope.row.isDorm === '0'" size="small" type="info" effect="plain"></el-tag>
<span v-else>-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'enrollStatus'">
<el-tag v-if="scope.row.enrollStatus" size="small" type="info" effect="plain">{{ scope.row.enrollStatus }}</el-tag>
<el-tag
v-if="getEnrollStatusLabel(scope.row.enrollStatus)"
size="small"
type="info"
effect="plain">
{{ getEnrollStatusLabel(scope.row.enrollStatus) }}
</el-tag>
<span v-else>-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'stuStatus'">
@@ -427,7 +439,6 @@ import {
editIsleader,
updateInout,
updateStuSimpleInfo,
getStuStatus,
prePrint
} from "/@/api/basic/basicstudent";
import { getDeptList, getClassListByRole } from "/@/api/basic/basicclass";
@@ -456,6 +467,7 @@ const deptList = ref<any[]>([])
const classList = ref<any[]>([])
const statusList = ref<any[]>([])
const stuStatusList = ref<any[]>([])
const educationList = ref<any[]>([])
const selectedRows = ref<any[]>([])
const importCertificateDialogVisible = ref(false)
const uploadLoading = ref(false)
@@ -781,9 +793,17 @@ const getClassListData = async () => {
// 获取学籍状态列表
const getStatusListData = async () => {
try {
const res = await getStuStatus()
const res = await getDicts('enroll_status')
if (res.data) {
statusList.value = Array.isArray(res.data) ? res.data : []
if (Array.isArray(res.data)) {
// 确保数据格式统一为 {label, value}
statusList.value = res.data.map((item: any) => ({
label: item.label || item.name || item.dictLabel || item.text || '',
value: String(item.value || item.code || item.dictValue || item.id || '')
})).filter((item: any) => item.label && item.value !== undefined && item.value !== null && item.value !== '')
} else {
statusList.value = []
}
}
} catch (err) {
console.error('获取学籍状态列表失败', err)
@@ -791,6 +811,24 @@ const getStatusListData = async () => {
}
}
// 获取文化程度字典
const getEducationListData = async () => {
try {
const res = await getDicts('pre_school_education')
if (res.data && Array.isArray(res.data)) {
educationList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
}))
} else {
educationList.value = []
}
} catch (err) {
console.error('获取文化程度字典失败', err)
educationList.value = []
}
}
// 获取学生状态列表
const getStuStatusListData = async () => {
try {
@@ -813,6 +851,20 @@ const getStuStatusListData = async () => {
}
}
// 根据学籍状态值获取标签
const getEnrollStatusLabel = (value: any) => {
if (value === undefined || value === null || value === '') return ''
const status = statusList.value.find(item => String(item.value) === String(value))
return status ? status.label : ''
}
// 根据文化程度值获取标签
const getEducationLabel = (value: any) => {
if (value === undefined || value === null || value === '') return ''
const item = educationList.value.find(i => String(i.value) === String(value))
return item ? item.label : ''
}
// 根据学生状态值获取标签
const getStuStatusLabel = (value: any) => {
if (value === undefined || value === null || value === '') return ''
@@ -833,6 +885,7 @@ onMounted(() => {
getDeptListData()
getClassListData()
getStatusListData()
getEducationListData()
getStuStatusListData()
})
</script>

View File

@@ -35,6 +35,14 @@
招标代理管理
</span>
<div class="header-actions">
<el-button
icon="Files"
link
type="primary"
>
代理汇总
</el-button>
<el-button
icon="FolderAdd"
type="primary"
@@ -170,7 +178,7 @@ const handleDelete = async (row: any) => {
}
try {
await delObj(row.id);
await delObj({"id":row.id});
useMessage().success('删除成功');
getDataList();
} catch (err: any) {

View File

@@ -21,12 +21,14 @@
</div>
</template>
<!-- 树形表格 -->
<!-- 树形表格懒加载仅首屏加载根节点展开时再加载子节点 -->
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
stripe
lazy
:load="loadTreeNode"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table"
@@ -88,7 +90,7 @@
<script setup lang="ts" name="PurchasingCategory">
import { ref, reactive, defineAsyncComponent } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { getTree, delObj } from "/@/api/finance/purchasingcategory";
import { getTreeRoots, getTreeChildren, delObj } from "/@/api/finance/purchasingcategory";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { List, Document, DocumentCopy, EditPen } from '@element-plus/icons-vue'
@@ -100,19 +102,40 @@ const tableRef = ref()
const formDialogRef = ref()
/**
* 查询树形数据方法
* @param params - 查询参数
* @returns Promise<any>
* 查询树根节点(懒加载:首屏只加载根节点)
*/
const queryTree = (params?: any) => {
return getTree(params);
const queryTreeRoots = () => {
return getTreeRoots().then((res: any) => {
const list = res?.data ?? [];
return { data: Array.isArray(list) ? list : [] };
});
};
/**
* 懒加载子节点:展开某行时按需请求子节点
* @param row 当前行
* @param treeNode 树节点信息
* @param resolve 回调,传入子节点数组
*/
const loadTreeNode = (row: any, treeNode: any, resolve: (data: any[]) => void) => {
const parentCode = row?.code;
if (!parentCode) {
resolve([]);
return;
}
getTreeChildren(parentCode)
.then((res: any) => {
const list = res?.data ?? [];
resolve(Array.isArray(list) ? list : []);
})
.catch(() => resolve([]));
};
/**
* 定义响应式表格数据
*/
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: queryTree,
pageList: queryTreeRoots,
queryForm: {},
isPage: false, // 树形表格不分页
});

View File

@@ -0,0 +1,317 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="160px" >
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="验收方式" prop="acceptType">
<el-radio-group v-model="form.acceptType" :disabled="readonly">
<el-radio label="1" :disabled="!canFill">填写履约验收评价表</el-radio>
<el-radio label="2">上传履约验收评价表</el-radio>
</el-radio-group>
<div v-if="!canFill" class="el-form-item__tip">金额30万仅支持上传模版</div>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="验收日期" prop="acceptDate">
<el-date-picker
v-model="form.acceptDate"
type="date"
placeholder="请选择"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
:disabled="readonly"
/>
</el-form-item>
</el-col>
<!-- 填报方式验收内容表格 -->
<template v-if="form.acceptType === '1' && canFill">
<el-col :span="12" class="mb20">
<el-form-item label="验收内容" prop="acceptContents">
<el-table :data="form.acceptContents" border size="small" class="accept-content-table">
<el-table-column prop="itemName" label="验收项" >
<template #default="{ row }">
<div>
<span>{{row.itemName}}</span>
<el-input
v-if="row.type === 'input'"
v-model="row.remark"
placeholder="请输入"
size="small"
:disabled="readonly"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="isQualified" label="合格/不合格" align="center">
<template #default="{ row }">
<el-radio-group v-model="row.isQualified" size="small" :disabled="readonly">
<el-radio label="1">合格</el-radio>
<el-radio label="0">不合格</el-radio>
</el-radio-group>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-col>
</template>
<!-- 上传方式 -->
<template v-if="form.acceptType === '2'">
<el-col :span="12">
<el-form-item label="履约验收模版" prop="templateFileIds">
<UploadFile
v-model="templateFileIdsStr"
:limit="1"
:data="{ purchaseId: purchaseId || '', fileType: '110' }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="readonly"
/>
</el-form-item>
</el-col>
</template>
<el-col :span="12" class="mb20">
<el-form-item label="验收地点" prop="acceptAddress">
<el-input v-model="form.acceptAddress" placeholder="请输入验收地点" :disabled="readonly" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="存在问题及改进意见" prop="question">
<el-input
v-model="form.question"
type="textarea"
:rows="2"
placeholder="请输入存在问题及改进意见"
:disabled="readonly"
/>
</el-form-item>
</el-col>
<!-- 验收小组 -->
<el-col :span="12">
<el-form-item label="验收小组" prop="acceptTeam">
<div class="team-list">
<div v-for="(m, idx) in form.acceptTeam" :key="idx" class="team-row">
<el-select
v-model="m.roleType"
placeholder="身份"
size="small"
style="width: 130px; margin-right: 8px"
:disabled="readonly"
@change="(val) => onRoleChange(idx, val as string)"
>
<el-option label="组长(校内)" value="LEADER_IN" />
<el-option label="组长(校外)" value="LEADER_OUT" />
<el-option label="组员(校内)" value="MEMBER_IN" />
<el-option label="组员(校外)" value="MEMBER_OUT" />
</el-select>
<template v-if="m.roleType === 'LEADER_IN' || m.roleType === 'MEMBER_IN'">
<org-selector
v-model:orgList="m.userList"
type="user"
:multiple="false"
@update:orgList="(list: any[]) => onTeamUserChange(idx, list)"
/>
</template>
<template v-else>
<el-input v-model="m.name" placeholder="姓名" size="small" style="width:120px" :disabled="readonly" />
<el-input v-model="m.deptName" placeholder="单位/部门" size="small" style="width:160px" :disabled="readonly" />
</template>
<el-button v-if="!readonly && form.acceptTeam.length > 3" type="danger" link size="small" @click="removeTeam(idx)">删除</el-button>
</div>
<el-button v-if="!readonly" type="primary" link size="small" @click="addTeam">+ 增加成员</el-button>
</div>
<div class="el-form-item__tip">
至少3人且为单数
<template v-if="(previousBatchesTeams || []).length > 0">
<span class="copy-from-inline">
从往期带入
<el-select
v-model="copyFromBatch"
placeholder="同第N期"
clearable
size="small"
style="width: 100px"
@change="onCopyFromBatch"
>
<el-option
v-for="item in (previousBatchesTeams || [])"
:key="item.batch"
:label="`同第${item.batch}期`"
:value="item.batch"
/>
</el-select>
</span>
</template>
</div>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入" :disabled="readonly" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
const props = withDefaults(
defineProps<{
modelValue: Record<string, any>
canFill: boolean
readonly?: boolean
purchaseId?: string
acceptanceItems?: any[]
batchNum?: number
previousBatchesTeams?: { batch: number; team: any[] }[]
}>(),
{ readonly: false, canFill: true, purchaseId: '', batchNum: 1, previousBatchesTeams: () => [] }
)
const emit = defineEmits(['update:modelValue'])
const formRef = ref<FormInstance>()
const templateFileIdsStr = ref('')
const copyFromBatch = ref<number | null>(null)
const form = reactive({
acceptType: '1',
acceptDate: '',
acceptContents: [] as any[],
acceptTeam: [
{ name: '', deptCode: '', deptName: '', roleType: '' },
{ name: '', deptCode: '', deptName: '', roleType: '' },
{ name: '', deptCode: '', deptName: '', roleType: '' },
] as any[],
templateFileIds: [] as string[],
acceptAddress: '',
question: '',
remark: '',
...props.modelValue,
})
watch(() => props.modelValue, (val) => {
Object.assign(form, val || {})
// 金额≥30万时强制为上传方式
if (!props.canFill && form.acceptType === '1') {
form.acceptType = '2'
}
}, { deep: true })
watch(form, () => emit('update:modelValue', { ...form }), { deep: true })
// 金额≥30万时默认选中上传方式
watch(() => props.canFill, (val) => {
if (!val && form.acceptType === '1') {
form.acceptType = '2'
}
}, { immediate: true })
watch(() => props.acceptanceItems, (items) => {
if (items?.length && form.acceptContents.length === 0) {
form.acceptContents = items.map((it: any) => ({
configId: it.id,
itemName: it.itemName,
type: it.type,
isQualified: '1',
remark: '',
}))
}
}, { immediate: true })
watch(templateFileIdsStr, (s) => {
const arr = s ? s.split(',').map((x: string) => x.trim()).filter(Boolean) : []
if (JSON.stringify(form.templateFileIds) !== JSON.stringify(arr)) {
form.templateFileIds = arr
}
})
watch(() => form.templateFileIds, (arr) => {
if (Array.isArray(arr) && arr.length) templateFileIdsStr.value = arr.join(',')
}, { immediate: true, deep: true })
const addTeam = () => {
form.acceptTeam.push({ name: '', deptCode: '', deptName: '', roleType: '' })
}
const removeTeam = (idx: number) => {
form.acceptTeam.splice(idx, 1)
}
const onCopyFromBatch = (n: number | null) => {
if (!n) return
const item = props.previousBatchesTeams?.find((x) => x.batch === n)
if (item?.team?.length) {
form.acceptTeam = item.team.map((m: any) => ({
name: m.name || '',
deptCode: m.deptCode || '',
deptName: m.deptName || '',
roleType: m.roleType || '',
}))
}
copyFromBatch.value = null
}
const rules: FormRules = {
acceptType: [{ required: true, message: '请选择验收方式', trigger: 'change' }],
acceptDate: [{ required: true, message: '请选择验收日期', trigger: 'change' }],
}
const onRoleChange = (idx: number, val: string) => {
const isLeader = val === 'LEADER_IN' || val === 'LEADER_OUT'
if (isLeader) {
const hasOtherLeader = form.acceptTeam.some((m, i) =>
i !== idx && (m.roleType === 'LEADER_IN' || m.roleType === 'LEADER_OUT')
)
if (hasOtherLeader) {
// 只能有一个组长
form.acceptTeam[idx].roleType = ''
return
}
}
}
const onTeamUserChange = (idx: number, list: any[]) => {
const m = form.acceptTeam[idx]
if (!m) return
if (list && list.length) {
const u = list[0]
m.name = u.name || u.realName || ''
m.deptCode = u.deptCode || u.commonDeptCode || ''
m.deptName = u.deptName || u.commonDeptName || ''
m.userList = list
} else {
m.name = ''
m.deptCode = ''
m.deptName = ''
m.userList = []
}
}
const validate = () => formRef.value?.validate()
defineExpose({ validate, form })
</script>
<style scoped>
.mb20 {
margin-bottom: 20px;
}
.copy-from-inline {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 4px;
}
.copy-from-inline :deep(.el-select) {
vertical-align: middle;
}
</style>

View File

@@ -0,0 +1,231 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
<el-row :gutter="24">
<el-col :span="8" class="mb20">
<el-form-item label="项目名称">
<el-input :model-value="projectName || form.projectName" readonly placeholder="-" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="需求部门">
<el-input :model-value="deptName || form.deptName" readonly placeholder="-" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="是否签订合同" prop="hasContract">
<el-radio-group v-model="form.hasContract">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20" v-if="form.hasContract === '1'">
<el-form-item label="合同" prop="contractId">
<el-select
v-model="form.contractId"
placeholder="请选择合同"
clearable
filterable
style="width: 100%"
:loading="contractLoading"
@visible-change="onContractSelectVisibleChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName || item.contractNo || item.id"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="是否分期验收" prop="isInstallment">
<el-radio-group v-model="form.isInstallment">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20" v-if="form.isInstallment === '1'">
<el-form-item label="分期次数" prop="totalPhases">
<el-input-number v-model="form.totalPhases" :min="1" :max="99" placeholder="请输入" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="供应商名称" prop="supplierName">
<el-input v-model="form.supplierName" placeholder="选择合同后自动带出" clearable />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="供应商联系人及电话" prop="supplierContact">
<el-input v-model="form.supplierContact" placeholder="请输入" clearable />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="采购人员" prop="purchaserId">
<org-selector v-model:orgList="purchaserList" type="user" :multiple="false" @update:orgList="onPurchaserChange" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="资产管理员" prop="assetAdminId">
<org-selector v-model:orgList="assetAdminList" type="user" :multiple="false" @update:orgList="onAssetAdminChange" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { getContracts } from '/@/api/finance/purchasingrequisition'
const props = defineProps<{
modelValue: Record<string, any>
projectName?: string
deptName?: string
/** 采购申请ID用于拉取合同列表 */
purchaseId?: string | number
/** 每次打开弹窗时变化,用于强制重置内部 form */
resetKey?: number
}>()
const emit = defineEmits(['update:modelValue'])
const formRef = ref<FormInstance>()
const contractOptions = ref<any[]>([])
const contractLoading = ref(false)
const contractLoaded = ref(false)
const purchaserList = ref<any[]>([])
const assetAdminList = ref<any[]>([])
const form = reactive({
hasContract: '0',
contractId: '',
isInstallment: '0',
totalPhases: 1,
projectName: '',
deptName: '',
supplierName: '',
supplierContact: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
assetAdminName: '',
...props.modelValue,
})
const syncFormFromModel = (val: Record<string, any> | undefined) => {
Object.assign(form, val || {})
// 同步采购人员、资产管理员回 org-selector
if (form.purchaserId && form.purchaserName) {
purchaserList.value = [{ id: form.purchaserId, name: form.purchaserName, type: 'user' }]
} else {
purchaserList.value = []
}
if (form.assetAdminId && form.assetAdminName) {
assetAdminList.value = [{ id: form.assetAdminId, name: form.assetAdminName, type: 'user' }]
} else {
assetAdminList.value = []
}
}
const loadContractOptions = async () => {
if (contractLoaded.value || contractLoading.value) return
contractLoading.value = true
try {
const res = await getContracts(props.purchaseId ? { id: props.purchaseId } : {})
const list = res?.data
contractOptions.value = Array.isArray(list) ? list : []
contractLoaded.value = true
} catch (_) {
contractOptions.value = []
} finally {
contractLoading.value = false
}
}
const onContractSelectVisibleChange = (visible: boolean) => {
if (visible && form.hasContract === '1' && contractOptions.value.length === 0) {
loadContractOptions()
}
}
watch(() => props.modelValue, syncFormFromModel, { deep: true, immediate: true })
// resetKey 变化时强制用 modelValue 覆盖内部 form并重置合同列表以便重新拉取
watch(() => props.resetKey, () => {
syncFormFromModel(props.modelValue)
contractLoaded.value = false
contractOptions.value = []
})
watch(form, () => emit('update:modelValue', { ...form }), { deep: true })
watch(() => form.hasContract, (val) => {
if (val === '1') {
contractLoaded.value = false
loadContractOptions()
} else {
contractOptions.value = []
contractLoaded.value = false
}
})
// 选择合同后,自动带出合同供应商名称
watch(
() => form.contractId,
(val) => {
if (!val) {
form.supplierName = ''
return
}
const c = contractOptions.value.find((it: any) => it.id === val)
if (c && c.supplierName) {
form.supplierName = c.supplierName
}
}
)
onMounted(() => {
if (form.hasContract === '1') {
loadContractOptions()
}
})
const onPurchaserChange = (list: any[]) => {
if (list?.length) {
const u = list[0]
form.purchaserId = u.userId || u.id || ''
form.purchaserName = u.name || u.realName || ''
} else {
form.purchaserId = ''
form.purchaserName = ''
}
}
const onAssetAdminChange = (list: any[]) => {
if (list?.length) {
const u = list[0]
form.assetAdminId = u.userId || u.id || ''
form.assetAdminName = u.name || u.realName || ''
} else {
form.assetAdminId = ''
form.assetAdminName = ''
}
}
const rules: FormRules = {
isInstallment: [{ required: true, message: '请选择是否分期验收', trigger: 'change' }],
totalPhases: [{ required: true, message: '请输入分期次数', trigger: 'blur' }],
}
const validate = () => formRef.value?.validate()
defineExpose({ validate, form })
</script>
<style scoped>
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,575 @@
<template>
<el-dialog
v-model="visible"
title="履约验收"
width="75%"
:close-on-click-modal="false"
destroy-on-close
class="purchasing-accept-modal"
@close="handleClose"
>
<div v-loading="loading" class="modal-body" :key="String(purchaseId)">
<div class="main-tabs">
<div class="main-tab-nav">
<div
class="main-tab-item"
:class="{ active: mainTab === 'common' }"
@click="mainTab = 'common'"
>
公共信息
</div>
<div
class="main-tab-item"
:class="{ active: mainTab === 'batch' }"
@click="mainTab = 'batch'"
>
{{ commonForm?.isInstallment === '0' ? '验收' : '分期验收' }}{{ commonForm?.isInstallment !== '0' && batches.length > 0 ? ` (${batches.length})` : '' }}
</div>
</div>
<div class="main-tab-content">
<div v-show="mainTab === 'common'" class="tab-content">
<AcceptCommonForm
:key="`${purchaseId}-${openToken}`"
:reset-key="openToken"
ref="commonFormRef"
v-model="commonForm"
:purchase-id="purchaseId"
:project-name="applyInfo?.projectName"
:dept-name="applyInfo?.deptName"
/>
</div>
<div v-show="mainTab === 'batch'" class="tab-content">
<div v-if="batches.length > 0">
<div v-show="commonForm?.isInstallment !== '0'" class="batch-tabs">
<div
v-for="b in batches"
:key="b.id"
class="batch-tab-item"
:class="{ active: String(b.batch) === activeTab, disabled: !canEditBatch(b.batch) }"
@click="canEditBatch(b.batch) && (activeTab = String(b.batch))"
>
<span>{{ b.batch }}</span>
<el-tag v-if="isBatchCompleted(b)" type="success" size="small">已填</el-tag>
<el-tag v-else-if="!canEditBatch(b.batch)" type="info" size="small">需先完成上一期</el-tag>
</div>
</div>
<div class="batch-panel">
<AcceptBatchForm
v-for="b in batches"
v-show="String(b.batch) === activeTab"
:key="b.id"
:ref="(el) => setBatchFormRef(b.batch, el)"
v-model="batchForms[b.batch]"
:can-fill="canFillForm"
:readonly="false"
:purchase-id="String(purchaseId)"
:acceptance-items="acceptanceItems"
:batch-num="b.batch"
:previous-batches-teams="getPreviousBatchesTeams(b.batch)"
/>
</div>
</div>
<div v-else class="tip-box">
<el-alert type="info" :closable="false" show-icon>
请先在公共信息中填写并点击保存公共配置系统将按分期次数自动生成验收批次
</el-alert>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<span>
<el-button @click="handleClose"> </el-button>
<!-- 下载履约验收模板按钮 -->
<el-dropdown split-button type="primary" @click="handleDownloadTemplate" @command="handleDownloadTemplateCommand">
下载履约模板
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="current" :disabled="!activeBatchId">
下载当前期({{ activeTab }})
</el-dropdown-item>
<el-dropdown-item command="all" :disabled="batches.length === 0">
下载全部期数
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-if="mainTab === 'common' || batches.length === 0"
type="primary"
@click="saveCommonConfig"
:loading="saving"
>
保存公共配置
</el-button>
<el-button
v-else-if="mainTab === 'batch' && activeBatchId"
type="primary"
@click="saveCurrentBatch"
:loading="saving"
>
保存第{{ activeTab }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { handleBlobFile } from '/@/utils/other'
import {
saveCommonConfig as apiSaveCommonConfig,
getCommonConfigWithBatches,
updateBatch,
canFillForm as apiCanFillForm,
getAcceptanceItems,
getDetail,
downloadPerformanceAcceptanceTemplate,
} from '/@/api/purchase/purchasingAccept'
import AcceptCommonForm from './AcceptCommonForm.vue'
import AcceptBatchForm from './AcceptBatchForm.vue'
const emit = defineEmits(['refresh'])
const visible = ref(false)
const loading = ref(false)
const saving = ref(false)
const purchaseId = ref<string | number>('')
const applyInfo = ref<any>(null)
const rowProjectType = ref<string>('A')
const canFillForm = ref(true)
const acceptanceItems = ref<any[]>([])
const batches = ref<any[]>([])
const mainTab = ref('common')
const activeTab = ref('1')
const commonFormRef = ref()
const batchFormRefMap = ref<Record<number, any>>({})
/** 使用 ref 并在每次打开时替换整个对象,确保子组件能感知引用变化并清空 */
const commonForm = ref<Record<string, any>>({})
/** 每次打开自增,用于强制 AcceptCommonForm 重新挂载,确保公共信息彻底清空 */
const openToken = ref(0)
const batchForms = reactive<Record<number, any>>({})
/** 记录哪些期已保存到服务器,用于控制“下一期可填”:只有上一期已保存才允许填下一期 */
const batchSavedFlags = ref<Record<number, boolean>>({})
const setBatchFormRef = (batch: number, el: any) => {
if (el) batchFormRefMap.value[batch] = el
}
const activeBatchId = computed(() => {
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value)
return b?.id || ''
})
const getPreviousBatchesTeams = (batchNum: number) => {
const list: { batch: number; team: any[] }[] = []
for (let n = 1; n < batchNum; n++) {
const team = batchForms[n]?.acceptTeam
if (Array.isArray(team) && team.length > 0) {
list.push({ batch: n, team })
}
}
return list
}
/** 是否允许编辑该期:第 1 期始终可编辑;第 N 期仅当第 1N-1 期均已保存后才可编辑 */
const canEditBatch = (batch: number) => {
if (batch === 1) return true
for (let i = 1; i < batch; i++) {
if (!batchSavedFlags.value[i]) return false
}
return true
}
/** 该期是否已保存(用于 tab 上显示“已填”标签) */
const isBatchCompleted = (b: any) => {
return !!batchSavedFlags.value[b.batch]
}
const isBatchCompletedByIdx = (batch: number) => {
return !!batchSavedFlags.value[batch]
}
const loadData = async () => {
if (!purchaseId.value) return
const currentId = String(purchaseId.value)
loading.value = true
try {
const [configRes, canFillRes] = await Promise.all([
getCommonConfigWithBatches(currentId),
apiCanFillForm(currentId),
])
// 防止快速切换:若已打开其他申请单,忽略本次结果
if (String(purchaseId.value) !== currentId) return
const config = configRes?.data
canFillForm.value = !!canFillRes?.data
if (config?.common) {
applyInfo.value = config.common
// 仅当存在已保存批次时,才用接口数据回填公共信息;否则保持 open() 中的默认清空值
if (config?.batches?.length) {
Object.assign(commonForm.value, {
hasContract: config.common.hasContract || '0',
contractId: config.common.contractId || '',
isInstallment: config.common.isInstallment || '0',
totalPhases: config.common.totalPhases || 1,
supplierName: config.common.supplierName || '',
supplierContact: config.common.supplierContact || '',
})
}
}
const projectType = applyInfo.value?.projectType || rowProjectType.value || 'A'
const typeMap: Record<string, string> = { A: 'A', B: 'B', C: 'C' }
const at = typeMap[projectType] || 'A'
const itemsRes = await getAcceptanceItems(at)
if (String(purchaseId.value) !== currentId) return
acceptanceItems.value = itemsRes?.data || []
if (config?.batches?.length) {
batches.value = config.batches.sort((a: any, b: any) => (a.batch || 0) - (b.batch || 0))
activeTab.value = String(batches.value[0]?.batch || '1')
mainTab.value = 'batch'
for (const b of batches.value) {
if (!batchForms[b.batch]) batchForms[b.batch] = {}
}
await loadBatchDetails()
if (String(purchaseId.value) !== currentId) return
} else {
batches.value = []
}
} catch (e: any) {
useMessage().error(e?.msg || '加载失败')
} finally {
loading.value = false
}
}
const loadBatchDetails = async () => {
for (const b of batches.value) {
batchSavedFlags.value[b.batch] = false
}
for (const b of batches.value) {
try {
const res = await getDetail(String(purchaseId.value), b.batch)
const d = res?.data
if (d?.accept) {
// 仅当该期在服务端有验收日期(且验收方式)时才视为已保存,避免空结构被当成“已填”
const hasSaved = !!(d.accept.acceptDate && d.accept.acceptType)
batchSavedFlags.value[b.batch] = hasSaved
const itemMap = (acceptanceItems.value || []).reduce((acc: any, it: any) => {
acc[it.id] = it
return acc
}, {})
batchForms[b.batch] = {
acceptType: d.accept.acceptType || '1',
acceptDate: d.accept.acceptDate || '',
acceptAddress: d.accept.acceptAddress || '',
question: d.accept.question || '',
remark: d.accept.remark || '',
templateFileIds: d.accept.templateFileIds || [],
acceptContents: (d.contents || []).map((c: any) => {
const cfg = itemMap[c.configId]
return {
configId: c.configId,
itemName: cfg?.itemName || '',
type: cfg?.type,
isQualified: c.isQualified || '1',
remark: c.remark || '',
}
}),
acceptTeam: (d.team || []).map((t: any) => ({
name: t.name,
deptCode: t.deptCode,
deptName: t.deptName,
roleType: t.roleType || '',
})),
}
if (batchForms[b.batch].acceptTeam.length < 3) {
while (batchForms[b.batch].acceptTeam.length < 3) {
batchForms[b.batch].acceptTeam.push({ name: '', deptCode: '', deptName: '' })
}
}
if (acceptanceItems.value.length && (!batchForms[b.batch].acceptContents || batchForms[b.batch].acceptContents.length === 0)) {
batchForms[b.batch].acceptContents = acceptanceItems.value.map((it: any) => ({
configId: it.id,
itemName: it.itemName,
type: it.type,
isQualified: '1',
remark: '',
}))
}
}
} catch (_) {}
}
}
const saveCommonConfig = async () => {
const formRef = commonFormRef.value
const valid = await formRef?.validate?.().catch(() => false)
if (!valid) return
// 直接从子组件 form 读取,确保拿到用户填写的最新值(避免 v-model 同步延迟)
const form = formRef?.form || commonForm.value
const isInstallment = form.isInstallment === '1' || form.isInstallment === 1
if (isInstallment && (!form.totalPhases || form.totalPhases < 1)) {
useMessage().error('请填写分期次数')
return
}
saving.value = true
try {
await apiSaveCommonConfig({
purchaseId: String(purchaseId.value),
hasContract: form.hasContract ?? '0',
contractId: form.contractId ?? '',
isInstallment: form.isInstallment ?? '0',
totalPhases: isInstallment ? (Number(form.totalPhases) || 1) : 1,
supplierName: String(form.supplierName ?? ''),
supplierContact: String(form.supplierContact ?? ''),
purchaserId: String(form.purchaserId ?? ''),
purchaserName: String(form.purchaserName ?? ''),
assetAdminId: String(form.assetAdminId ?? ''),
assetAdminName: String(form.assetAdminName ?? ''),
})
useMessage().success('保存成功')
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '保存失败')
} finally {
saving.value = false
}
}
const saveCurrentBatch = async () => {
const curBatch = Number(activeTab.value)
const batchFormRef = batchFormRefMap.value[curBatch]
const valid = await batchFormRef?.validate?.().catch(() => false)
if (!valid) return
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value)
if (!b?.id) return
const form = batchForms[curBatch]
if (!form) return
if (!form.acceptType) {
useMessage().error('请选择验收方式')
return
}
if (!form.acceptDate) {
useMessage().error('请选择验收日期')
return
}
const team = (form.acceptTeam || []).filter((m: any) => m?.name)
if (team.length < 3 || team.length % 2 === 0) {
useMessage().error('验收小组至少3人且为单数')
return
}
saving.value = true
try {
await updateBatch({
id: b.id,
purchaseId: String(purchaseId.value),
acceptType: form.acceptType,
acceptDate: form.acceptDate,
acceptAddress: form.acceptAddress,
question: form.question,
remark: form.remark,
templateFileIds: form.templateFileIds || [],
acceptContents: form.acceptContents || [],
acceptTeam: team,
})
useMessage().success('保存成功')
batchSavedFlags.value[curBatch] = true
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '保存失败')
} finally {
saving.value = false
}
}
const handleClose = () => {
visible.value = false
emit('refresh')
}
// 下载履约验收模板
const handleDownloadTemplate = async () => {
if (!purchaseId.value) {
useMessage().error('请先选择采购项目')
return
}
// 默认下载当前期
await downloadTemplateForBatch(Number(activeTab.value))
}
// 处理下拉菜单命令
const handleDownloadTemplateCommand = async (command: string) => {
if (!purchaseId.value) {
useMessage().error('请先选择采购项目')
return
}
if (command === 'current') {
await downloadTemplateForBatch(Number(activeTab.value))
} else if (command === 'all') {
// 下载全部期数的模板
for (const batch of batches.value) {
await downloadTemplateForBatch(batch.batch)
}
useMessage().success(`已触发${batches.value.length}期模板下载`)
}
}
// 为指定批次下载模板
const downloadTemplateForBatch = async (batchNum: number) => {
try {
const response = await downloadPerformanceAcceptanceTemplate(String(purchaseId.value), batchNum)
// 使用项目中现有的工具函数处理文件下载
const fileName = `履约验收表-${purchaseId.value}-第${batchNum}期-${new Date().getTime()}.docx`;
handleBlobFile(response, fileName)
useMessage().success(`${batchNum}期履约模板下载成功`)
} catch (error: any) {
console.error('下载模板失败:', error)
useMessage().error(error?.msg || '下载履约模板失败')
}
}
const DEFAULT_COMMON_FORM = {
hasContract: '0',
contractId: '',
isInstallment: '0',
totalPhases: 1,
supplierName: '',
supplierContact: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
assetAdminName: '',
}
/** 将弹窗内所有内容恢复为初始空值(替换整个对象以确保引用变化) */
const resetAllToDefault = () => {
openToken.value++
commonForm.value = { ...DEFAULT_COMMON_FORM }
applyInfo.value = null
mainTab.value = 'common'
activeTab.value = '1'
batchFormRefMap.value = {}
batches.value = []
acceptanceItems.value = []
canFillForm.value = true
Object.keys(batchForms).forEach((k) => delete batchForms[Number(k)])
batchSavedFlags.value = {}
}
const open = async (row: any) => {
purchaseId.value = row?.id ?? ''
rowProjectType.value = row?.projectType || 'A'
// 1. 先将弹窗内所有内容恢复为初始空值
resetAllToDefault()
// 2. 显示弹窗并开启 loading避免接口返回前展示旧数据
visible.value = true
loading.value = true
// 3. 等待 Vue 完成渲染,确保子组件已接收并展示空值
await nextTick()
await nextTick()
// 4. 再进行接口查询并覆盖
await loadData()
}
defineExpose({ open })
</script>
<style scoped>
.modal-body {
padding: 0;
max-height: 70vh;
overflow-y: auto;
overflow-x: hidden;
}
.main-tab-nav {
display: flex;
gap: 4px;
margin-bottom: 16px;
border-bottom: 1px solid var(--el-border-color);
}
.main-tab-item {
padding: 12px 20px;
cursor: pointer;
color: var(--el-text-color-regular);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s;
}
.main-tab-item:hover {
color: var(--el-color-primary);
}
.main-tab-item.active {
color: var(--el-color-primary);
font-weight: 600;
border-bottom-color: var(--el-color-primary);
}
.main-tab-content {
padding-top: 4px;
}
.tab-content {
min-height: 200px;
display: block;
}
.tip-box {
padding: 20px 0;
}
.batch-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.batch-tab-item {
padding: 8px 16px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.batch-tab-item:hover:not(.disabled) {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
.batch-tab-item.active {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
color: #fff;
}
.batch-tab-item.disabled {
cursor: not-allowed;
opacity: 0.6;
}
.batch-panel {
min-height: 200px;
}
</style>
<style>
/* 弹窗横向滚动修复,需非 scoped 以影响 el-dialog */
.purchasing-accept-modal .el-dialog__body {
overflow-x: hidden;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,355 @@
<template>
<div class="implement-page">
<div class="implement-form">
<el-form-item label="实施采购方式" required>
<el-radio-group v-model="implementType">
<el-radio label="1">自行组织采购</el-radio>
<el-radio label="2">委托代理采购</el-radio>
</el-radio-group>
</el-form-item>
<!-- 采购文件版本列表保留原文件多版本分别显示 -->
<el-divider content-position="left">采购文件版本</el-divider>
<div v-if="purchaseFileVersions.length" class="file-versions mb-2">
<el-table :data="purchaseFileVersions" border size="small" max-height="280">
<el-table-column type="index" label="版本" width="70" align="center">
<template #default="{ $index }">V{{ $index + 1 }}</template>
</el-table-column>
<el-table-column prop="fileTitle" label="文件名" min-width="180" show-overflow-tooltip />
<el-table-column prop="createBy" label="上传人" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="createTime" label="上传时间" width="165" align="center">
<template #default="{ row }">{{ formatCreateTime(row.createTime) }}</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleDownloadVersion(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="mb-2">可继续上传新版本保留原文件格式 doc/docx/pdf单文件不超过 5MB</div>
<upload-file
v-model="implementFileIds"
:limit="5"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: PURCHASE_FILE_TYPE }"
upload-file-url="/purchase/purchasingfiles/upload"
/>
<!-- 仅部门审核角色显示采购代表相关 -->
<template v-if="isDeptAuditRole">
<el-divider content-position="left">采购代表</el-divider>
<div class="mb-2">需求部门初审需指定采购代表人请选择一种方式</div>
<el-radio-group v-model="representorMode" class="mb-2">
<el-radio label="single">指定采购代表人单人</el-radio>
<el-radio label="multi">部门多人由系统自动抽取</el-radio>
</el-radio-group>
<el-form-item v-if="representorMode === 'single'" label="采购代表人">
<el-select v-model="representorTeacherNo" placeholder="请选择" clearable filterable style="width: 100%">
<el-option v-for="m in deptMembers" :key="m.userId || m.teacherNo || m.id" :label="m.realName || m.name || m.teacherNo" :value="m.userId || m.teacherNo || m.id" />
</el-select>
</el-form-item>
<el-form-item v-else label="部门多人">
<el-select v-model="representorsMulti" placeholder="请选择多人,系统将自动抽取一人" clearable filterable multiple style="width: 100%">
<el-option v-for="m in deptMembers" :key="m.userId || m.teacherNo || m.id" :label="m.realName || m.name || m.teacherNo" :value="m.userId || m.teacherNo || m.id" />
</el-select>
</el-form-item>
</template>
</div>
<div class="implement-footer">
<el-button @click="handleClose">取消</el-button>
<template v-if="implementHasPurchaseFiles && !applyRow?.fileFlowInstId">
<el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">保存实施采购</el-button>
<el-button v-if="canStartFileFlow" type="success" :loading="startFileFlowSubmitting" @click="handleStartFileFlow">发起采购文件审批</el-button>
</template>
<template v-else>
<el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">确定</el-button>
</template>
</div>
</div>
</template>
<script setup lang="ts" name="PurchasingImplement">
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { implementApply, getApplyFiles, startFileFlow, getDeptMembers, getObj } from '/@/api/finance/purchasingrequisition'
import { useMessage } from '/@/hooks/message'
import other from '/@/utils/other'
import UploadFile from '/@/components/Upload/index.vue'
import { Session } from '/@/utils/storage'
import * as orderVue from '/@/api/order/order-key-vue'
/** 部门审核角色编码:仅该角色下显示采购代表相关页面和功能,流转至部门审核时需填写采购代表 */
const PURCHASE_DEPT_AUDIT_ROLE_CODE = 'PURCHASE_DEPT_AUDIT'
/** 采购中心角色编码:可保存/发起实施采购,但不出现采购代表相关内容和接口 */
const PURCHASE_CENTER_ROLE_CODE = 'PURCHASE_CENTER'
const roleCode = computed(() => Session.getRoleCode() || '')
const isDeptAuditRole = computed(() => roleCode.value === PURCHASE_DEPT_AUDIT_ROLE_CODE)
const isPurchaseCenterRole = computed(() => roleCode.value === PURCHASE_CENTER_ROLE_CODE)
/** 可发起采购文件审批:部门审核(需填采购代表)、采购中心(不填采购代表) */
const canStartFileFlow = computed(() => isDeptAuditRole.value || isPurchaseCenterRole.value)
// 与编辑界面一致:支持流程 dynamic-link 传入 currJob/currElTab申请单 ID 优先取 currJob.orderId
const props = defineProps({
currJob: { type: Object, default: null },
currElTab: { type: Object, default: null }
})
const emit = defineEmits(['handleJob'])
/** 是否被流程 handle 页面通过 dynamic-link 嵌入 */
const isFlowEmbed = computed(() => !!props.currJob)
const route = useRoute()
const PURCHASE_FILE_TYPE = '130'
/** 申请单 ID数值用于 getObj 等):与 add 一致,优先流程 currJob.orderId否则 route.query.id */
const applyId = computed(() => {
const raw = applyIdRaw.value
if (raw == null || raw === '') return null
const n = Number(raw)
return Number.isNaN(n) ? null : n
})
/** 申请单 ID 原始字符串(用于 getApplyFiles 的 purchaseId与编辑页一致避免类型/精度问题) */
const applyIdRaw = computed(() => {
if (props.currJob?.orderId != null && props.currJob?.orderId !== '') {
return String(props.currJob.orderId)
}
const id = route.query.id
return id != null && id !== '' ? String(id) : ''
})
const applyRow = ref<any>(null)
/** 已有采购文件版本列表(按 createTime 排序,用于展示与提交时一并关联) */
const purchaseFileVersions = ref<{ id: string; fileTitle?: string; createBy?: string; createTime?: string; remark?: string }[]>([])
/** 本次新上传的采购文件(仅新版本,不与已有版本混在一起) */
const implementFileIds = ref<string | string[]>([])
const implementType = ref<string>('1')
const implementSubmitting = ref(false)
const representorMode = ref<'single' | 'multi'>('single')
const representorTeacherNo = ref<string>('')
const representorsMulti = ref<string[]>([])
const deptMembers = ref<any[]>([])
const startFileFlowSubmitting = ref(false)
const implementHasPurchaseFiles = computed(() => {
if (purchaseFileVersions.value.length > 0) return true
const raw = implementFileIds.value
if (Array.isArray(raw)) return raw.length > 0
return !!raw
})
function formatCreateTime(t?: string) {
if (!t) return '-'
const d = new Date(t)
return isNaN(d.getTime()) ? t : d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
}
function handleDownloadVersion(file: { remark?: string; fileTitle?: string }) {
if (!file?.remark) {
useMessage().warning('无法获取文件路径')
return
}
const url = `/purchase/purchasingfiles/download?fileName=${encodeURIComponent(file.remark)}&fileTitle=${encodeURIComponent(file.fileTitle || '采购文件')}`
other.downBlobFile(url, {}, file.fileTitle || '采购文件')
}
const isInIframe = () => typeof window !== 'undefined' && window.self !== window.top
const postMessage = (type: string, payload?: any) => {
if (typeof window !== 'undefined' && window.parent) {
window.parent.postMessage({ type, ...payload }, '*')
}
}
const loadData = async () => {
const id = applyId.value
if (!id) {
useMessage().warning('缺少申请单ID')
return
}
const needDeptMembers = isDeptAuditRole.value
try {
const idStr = applyIdRaw.value || String(id)
const requests: [ReturnType<typeof getObj>, ReturnType<typeof getApplyFiles>, ReturnType<typeof getDeptMembers>?] = [
getObj(id),
getApplyFiles(idStr)
]
if (needDeptMembers) requests.push(getDeptMembers())
const results = await Promise.all(requests)
const detailRes = results[0]
const filesRes = results[1]
const membersRes = needDeptMembers ? results[2] : null
applyRow.value = detailRes?.data ? { ...detailRes.data, id: detailRes.data.id ?? id } : { id }
const row = applyRow.value
if (row?.implementType) implementType.value = row.implementType
// 回显需求部门初审-采购代表人方式与人员(与发起采购文件审批时保存的一致)
if (row?.representorTeacherNo) {
representorMode.value = 'single'
representorTeacherNo.value = row.representorTeacherNo ?? ''
representorsMulti.value = []
} else if (row?.representors) {
representorMode.value = 'multi'
representorTeacherNo.value = ''
const parts = typeof row.representors === 'string' ? row.representors.split(',') : []
representorsMulti.value = parts.map((s: string) => s.trim()).filter(Boolean)
} else {
representorTeacherNo.value = ''
representorsMulti.value = []
}
const list = filesRes?.data || []
const purchaseFiles = list.filter((f: any) => f.fileType === PURCHASE_FILE_TYPE)
purchaseFileVersions.value = purchaseFiles.map((f: any) => ({
id: String(f.id),
fileTitle: f.fileTitle || f.file_title || '采购文件',
createBy: f.createBy ?? f.create_by ?? '-',
createTime: f.createTime || f.create_time,
remark: f.remark
}))
deptMembers.value = needDeptMembers && membersRes?.data ? membersRes.data : []
} catch (_) {
applyRow.value = { id }
purchaseFileVersions.value = []
deptMembers.value = []
}
}
const handleClose = () => {
postMessage('purchasingimplement:close')
if (!isInIframe()) {
window.history.back()
}
}
const handleImplementSubmit = async () => {
const row = applyRow.value
if (!row?.id && !applyId.value) return
const id = row?.id ?? applyId.value
if (!id) return
if (!implementType.value) {
useMessage().warning('请选择实施采购方式')
return
}
const existingIds = purchaseFileVersions.value.map((f) => f.id)
const raw = implementFileIds.value
const newIds: string[] = Array.isArray(raw)
? raw.map((x: any) => (typeof x === 'object' && x?.id ? x.id : x)).filter(Boolean)
: raw ? [String(raw)] : []
const fileIds = [...existingIds, ...newIds]
if (fileIds.length === 0) {
useMessage().warning('请至少上传一个采购文件')
return
}
// 仅部门审核角色提交采购代表;采购中心保存时不传采购代表
const single = isDeptAuditRole.value && representorMode.value === 'single' ? representorTeacherNo.value : undefined
const multi = isDeptAuditRole.value && representorMode.value === 'multi' && representorsMulti.value?.length ? representorsMulti.value.join(',') : undefined
implementSubmitting.value = true
try {
await implementApply(id, fileIds, implementType.value, single, multi)
useMessage().success('实施采购已保存')
implementFileIds.value = []
await loadData()
postMessage('purchasingimplement:saved')
// 流程嵌入场景:通知流程当前 Tab 已保存,避免审批时提示“未保存”
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsSave(props.currJob, props.currElTab.id, true, emit)
}
} catch (err: any) {
useMessage().error(err?.msg || '实施采购失败')
} finally {
implementSubmitting.value = false
}
}
const handleStartFileFlow = async () => {
const row = applyRow.value
const id = row?.id ?? applyId.value
if (!id) return
// 部门审核角色必须填写采购代表;采购中心不填采购代表
if (isDeptAuditRole.value) {
if (representorMode.value === 'single') {
if (!representorTeacherNo.value) {
useMessage().warning('请选择采购代表人')
return
}
} else {
if (!representorsMulti.value?.length) {
useMessage().warning('请选择部门多人')
return
}
}
}
startFileFlowSubmitting.value = true
try {
const single = isDeptAuditRole.value && representorMode.value === 'single' ? representorTeacherNo.value : undefined
const multi = isDeptAuditRole.value && representorMode.value === 'multi' ? representorsMulti.value.join(',') : undefined
await startFileFlow(id, single, multi)
useMessage().success('已发起采购文件审批流程')
postMessage('purchasingimplement:submitSuccess')
await loadData()
} catch (err: any) {
useMessage().error(err?.msg || '发起失败')
} finally {
startFileFlowSubmitting.value = false
}
}
/** 流程嵌入时供 handle.vue 调用的“保存”回调:与页面按钮保存逻辑保持一致 */
async function flowSubmitForm() {
await handleImplementSubmit()
}
// 流程切换工单时重新加载数据(与 add 编辑页一致)
watch(
() => props.currJob?.orderId ?? props.currJob?.id,
(newVal, oldVal) => {
if (newVal !== oldVal && applyId.value) {
loadData()
}
}
)
onMounted(async () => {
await loadData()
if (isInIframe()) {
document.documentElement.classList.add('iframe-mode')
document.body.classList.add('iframe-mode')
}
// 流程嵌入:注册 tab 保存回调,供审批页调用(与采购申请编辑页保持一致)
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsExist(props.currJob, props.currElTab.id)
await orderVue.currElTabIsView({}, props.currJob, props.currElTab.id, flowSubmitForm)
}
})
</script>
<style scoped lang="scss">
.implement-page {
padding: 20px;
min-height: 100%;
display: flex;
flex-direction: column;
}
.implement-form {
flex: 1;
.mb-2 {
margin-bottom: 8px;
}
}
.implement-form-tip {
margin-top: 12px;
padding: 8px 0;
}
.implement-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
display: flex;
gap: 12px;
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<el-dialog
v-model="visible"
title="实施采购"
width="50%"
:close-on-click-modal="false"
destroy-on-close
class="implement-iframe-dialog"
@close="handleClose"
>
<div class="implement-iframe-content">
<iframe
ref="iframeRef"
:src="iframeSrc"
frameborder="0"
class="implement-iframe"
/>
</div>
</el-dialog>
</template>
<script setup lang="ts" name="ImplementForm">
import { ref, computed, watch } from 'vue'
const emit = defineEmits<{
(e: 'refresh'): void
}>()
const visible = ref(false)
const iframeRef = ref<HTMLIFrameElement>()
const applyId = ref<string | number>('')
const iframeSrc = computed(() => {
const baseUrl = window.location.origin + window.location.pathname
return `${baseUrl}#/finance/purchasingrequisition/implement?id=${applyId.value}`
})
const handleClose = () => {
visible.value = false
window.removeEventListener('message', handleMessage)
}
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'purchasingimplement:submitSuccess') {
handleClose()
emit('refresh')
} else if (event.data?.type === 'purchasingimplement:close') {
handleClose()
} else if (event.data?.type === 'purchasingimplement:saved') {
emit('refresh')
}
}
const openDialog = (row: { id: string | number }) => {
applyId.value = row?.id ?? ''
visible.value = true
window.addEventListener('message', handleMessage)
}
watch(visible, (val) => {
if (!val) {
window.removeEventListener('message', handleMessage)
}
})
defineExpose({
openDialog,
})
</script>
<style scoped lang="scss">
.implement-iframe-content {
width: 100%;
height: 65vh;
min-height: 480px;
max-height: calc(100vh - 180px);
position: relative;
overflow: hidden;
.implement-iframe {
width: 100%;
height: 100%;
min-height: 480px;
border: none;
display: block;
}
}
</style>
<style>
.implement-iframe-dialog .el-dialog__body {
padding: 16px;
overflow: hidden;
}
</style>

View File

@@ -43,12 +43,11 @@
placeholder="请选择状态"
clearable
style="width: 200px">
<el-option label="撤回" value="-2" />
<el-option label="暂存" value="-1" />
<el-option label="运行中" value="0" />
<el-option label="完成" value="1" />
<el-option label="作废" value="2" />
<el-option label="终止" value="3" />
</el-select>
</el-form-item>
<el-form-item label="是否集采" prop="isCentralized">
@@ -77,6 +76,14 @@
采购申请管理
</span>
<div class="header-actions">
<el-button
icon="Files"
link
type="primary"
>
采购申请汇总
</el-button>
<el-button
icon="FolderAdd"
type="primary"
@@ -102,7 +109,7 @@
<el-icon><List /></el-icon>
</template>
</el-table-column>
<el-table-column prop="code" label="申请单编号" min-width="140" show-overflow-tooltip>
<el-table-column prop="purchaseNo" label="申请单编号" min-width="140" show-overflow-tooltip>
<template #header>
<el-icon><DocumentCopy /></el-icon>
<span style="margin-left: 4px">申请单编号</span>
@@ -160,8 +167,10 @@
<span style="margin-left: 4px">是否特殊</span>
</template>
<template #default="scope">
<el-tag v-if="scope.row.isSpecial === '1' || scope.row.isSpecial === 1" type="warning"></el-tag>
<el-tag v-else-if="scope.row.isSpecial === '0' || scope.row.isSpecial === 0" type="info"></el-tag>
<el-tag v-if="String(scope.row.isSpecial) === '1'" type="warning">紧急</el-tag>
<el-tag v-else-if="String(scope.row.isSpecial) === '2'" type="danger">单一</el-tag>
<el-tag v-else-if="String(scope.row.isSpecial) === '3'" type="info">进口</el-tag>
<el-tag v-else-if="String(scope.row.isSpecial) === '0'" type="info"></el-tag>
<span v-else>-</span>
</template>
</el-table-column>
@@ -171,8 +180,8 @@
<span style="margin-left: 4px">是否集采</span>
</template>
<template #default="scope">
<el-tag v-if="scope.row.isCentralized === '1'" type="success"></el-tag>
<el-tag v-else-if="scope.row.isCentralized === '0'" type="info"></el-tag>
<el-tag v-if="String(scope.row.isCentralized) === '1'" type="success"></el-tag>
<el-tag v-else-if="String(scope.row.isCentralized) === '0'" type="info"></el-tag>
<span v-else>-</span>
</template>
</el-table-column>
@@ -182,40 +191,111 @@
<span style="margin-left: 4px">审核状态</span>
</template>
<template #default="scope">
<el-tag v-if="scope.row.status === '-2'" type="info">撤回</el-tag>
<el-tag v-else-if="scope.row.status === '-1'" type="warning">暂存</el-tag>
<el-tag v-else-if="scope.row.status === '0'" type="primary">运行中</el-tag>
<el-tag v-else-if="scope.row.status === '1'" type="success">完成</el-tag>
<el-tag v-else-if="scope.row.status === '2'" type="danger">作废</el-tag>
<el-tag v-else-if="scope.row.status === '3'" type="info">终止</el-tag>
<span v-else>-</span>
<el-tooltip v-if="scope.row.flowInstId" content="点击查看审批过程" placement="top">
<el-tag
v-if="scope.row.status === '-2'"
type="info"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">撤回</el-tag>
<el-tag
v-else-if="scope.row.status === '-1'"
type="warning"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">暂存</el-tag>
<el-tag
v-else-if="scope.row.status === '0'"
type="primary"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">运行中</el-tag>
<el-tag
v-else-if="scope.row.status === '1'"
type="success"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">完成</el-tag>
<el-tag
v-else-if="scope.row.status === '2'"
type="danger"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">作废</el-tag>
<el-tag
v-else-if="scope.row.status === '3'"
type="info"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">终止</el-tag>
<span v-else>-</span>
</el-tooltip>
<template v-else>
<el-tag v-if="scope.row.status === '-2'" type="info">撤回</el-tag>
<el-tag v-else-if="scope.row.status === '-1'" type="warning">暂存</el-tag>
<el-tag v-else-if="scope.row.status === '0'" type="primary">运行中</el-tag>
<el-tag v-else-if="scope.row.status === '1'" type="success">完成</el-tag>
<el-tag v-else-if="scope.row.status === '2'" type="danger">作废</el-tag>
<el-tag v-else-if="scope.row.status === '3'" type="info">终止</el-tag>
<span v-else>-</span>
</template>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="300">
<el-table-column prop="fileFlowStatus" label="文件审批状态" width="110" align="center">
<template #header>
<el-icon><DocumentChecked /></el-icon>
<span style="margin-left: 4px">文件审批状态</span>
</template>
<template #default="scope">
<el-button
icon="View"
link
type="primary"
@click="formDialogRef.openDialog('view', scope.row)">
查看
</el-button>
<el-button
v-if="scope.row.status === '-1'"
icon="Edit"
link
type="primary"
@click="formDialogRef.openDialog('edit', scope.row)">
编辑
</el-button>
<el-button
v-if="scope.row.status === '-1'"
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
<template v-if="scope.row.fileFlowInstId">
<el-tooltip content="点击查看审批过程" placement="top">
<el-tag
v-if="scope.row.fileFlowStatus === '-2'"
type="info"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">撤回</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '-1'"
type="warning"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">暂存</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '0'"
type="primary"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">运行中</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '1'"
type="success"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">完成</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '2'"
type="danger"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">作废</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '3'"
type="info"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">终止</el-tag>
<span v-else class="status-tag-clickable" @click="handleShowFileFlowComment(scope.row)">-</span>
</el-tooltip>
</template>
<template v-else>
<span style="color: #909399;"></span>
</template>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="150">
<template #default="scope">
<div class="op-cell">
<el-button
type="primary"
link
icon="View"
@click="handleView(scope.row)">
查看
</el-button>
<ActionDropdown
:items="getActionMenuItems(scope.row)"
@command="(command) => handleMoreCommand(command, scope.row)"
/>
</div>
</template>
</el-table-column>
</el-table>
@@ -226,52 +306,56 @@
:total="state.pagination.total"
:current="state.pagination.current"
:size="state.pagination.size"
@pagination="getDataList"
@sizeChange="sizeChangeHandle"
@currentChange="currentChangeHandle"
/>
</el-card>
</div>
<!-- 新增页面 iframe 对话框 -->
<el-dialog
v-model="showAddIframe"
title="新增采购申请"
width="90%"
:style="{ maxWidth: '1600px' }"
:close-on-click-modal="false"
:close-on-press-escape="true"
destroy-on-close
class="iframe-dialog"
@close="closeAddIframe">
<div class="iframe-dialog-content">
<iframe
ref="addIframeRef"
:src="addIframeSrc"
frameborder="0"
class="add-iframe"
/>
</div>
</el-dialog>
<!-- 编辑新增表单对话框 -->
<!-- 新增/编辑/查看统一使用 form.vue 弹窗iframe 引入 add.vue -->
<FormDialog
ref="formDialogRef"
:dict-data="dictData"
@refresh="getDataList" />
<!-- 履约验收弹窗 -->
<PurchasingAcceptModal ref="acceptModalRef" @refresh="getDataList" />
<!-- 查看审批过程申请单审批 / 文件审批 -->
<el-dialog
v-model="showFlowComment"
v-if="showFlowComment"
:title="currFlowCommentType === 'file' ? '查看文件审批过程' : '查看审批过程'"
top="20px"
width="90%"
append-to-body
destroy-on-close
@close="currFlowJob = null; currFlowCommentType = 'apply'">
<FlowCommentTimeline v-if="currFlowJob" :key="String(currFlowJob.flowInstId) + currFlowCommentType" :curr-job="currFlowJob" />
</el-dialog>
<!-- 实施采购iframe 嵌入 implement.vue供列表与流程页面使用 -->
<ImplementForm ref="implementFormRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PurchasingRequisition">
import { ref, reactive, defineAsyncComponent, onUnmounted, onMounted } from 'vue'
import { ref, reactive, defineAsyncComponent, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { getPage, delObj } from "/@/api/finance/purchasingrequisition";
import { getPage, delObj, submitObj, getArchiveDownloadUrl, getApplyTemplateDownloadUrl, getFileApplyTemplateDownloadUrl } from "/@/api/finance/purchasingrequisition";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { getDicts } from '/@/api/admin/dict';
import { getTree } from '/@/api/finance/purchasingcategory';
import { List, Document, DocumentCopy, Search, Collection, Money, CircleCheck, InfoFilled, Calendar, OfficeBuilding, Warning } from '@element-plus/icons-vue'
import { List, Document, DocumentCopy, Search, Collection, Money, CircleCheck, InfoFilled, Calendar, OfficeBuilding, Warning, DocumentChecked, Edit, Delete, Upload, FolderOpened, Download } from '@element-plus/icons-vue'
import other from '/@/utils/other'
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const ImplementForm = defineAsyncComponent(() => import('./implementForm.vue'));
const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue'));
const PurchasingAcceptModal = defineAsyncComponent(() => import('./accept/PurchasingAcceptModal.vue'));
const FlowCommentTimeline = defineAsyncComponent(() => import('/@/views/jsonflow/comment/timeline.vue'));
// 字典数据和品目树数据
const dictData = ref({
@@ -288,11 +372,15 @@ const dictData = ref({
const router = useRouter()
const tableRef = ref()
const formDialogRef = ref()
const acceptModalRef = ref()
const searchFormRef = ref()
const showSearch = ref(true)
const showAddIframe = ref(false)
const addIframeRef = ref<HTMLIFrameElement>()
const addIframeSrc = ref('')
/** 审批过程弹窗:是否显示、当前行对应的流程 job供 Comment 组件用)、类型(申请单/文件) */
const showFlowComment = ref(false)
const currFlowJob = ref<{ id?: number; flowInstId?: number } | null>(null)
const currFlowCommentType = ref<'apply' | 'file'>('apply')
const implementFormRef = ref()
/**
* 定义响应式表格数据
@@ -312,7 +400,7 @@ const state: BasicTableProps = reactive<BasicTableProps>({
/**
* 使用 useTable 定义表格相关操作
*/
const { getDataList, tableStyle } = useTable(state);
const { getDataList, tableStyle, sizeChangeHandle, currentChangeHandle } = useTable(state);
/**
* 重置搜索表单
@@ -323,42 +411,67 @@ const handleReset = () => {
};
/**
* 新增采购申请 - 在 iframe 中展示
* 新增采购申请 - 统一通过 form.vue 弹窗iframe 引入 add.vue
*/
const handleAdd = () => {
// 构建 iframe 的 src使用当前页面的 hash 路由
const baseUrl = window.location.origin + window.location.pathname
addIframeSrc.value = `${baseUrl}#/finance/purchasingrequisition/add`
showAddIframe.value = true
// 监听来自 iframe 的消息
window.addEventListener('message', handleIframeMessage)
formDialogRef.value?.openDialog('add')
};
/**
* 关闭新增 iframe
*/
const closeAddIframe = () => {
showAddIframe.value = false
// 移除消息监听器
window.removeEventListener('message', handleIframeMessage)
};
/**
* 处理 iframe 发送的消息
* 点击审核状态:若有流程实例则打开「查看审批过程」弹窗(参考 hi-job.vue
* @param row - 当前行数据(需含 flowInstId
*/
const handleIframeMessage = (event: MessageEvent) => {
// 验证消息来源(可选,根据实际需求)
// if (event.origin !== window.location.origin) return
if (event.data && event.data.type === 'purchasingrequisition:submitSuccess') {
// 提交成功,关闭 iframe 并刷新列表
closeAddIframe()
getDataList()
useMessage().success('提交成功')
} else if (event.data && event.data.type === 'purchasingrequisition:close') {
// 关闭 iframe
closeAddIframe()
/** 点击审核状态:打开申请单审批过程 */
const handleShowFlowComment = (row: any) => {
if (!row?.flowInstId) {
useMessage().info('暂存状态无审批过程');
return;
}
currFlowCommentType.value = 'apply';
currFlowJob.value = { id: row.id, flowInstId: row.flowInstId };
showFlowComment.value = true;
};
/** 点击文件审批状态:打开文件审批过程 */
const handleShowFileFlowComment = (row: any) => {
if (!row?.fileFlowInstId) {
useMessage().info('未发起文件审批流程');
return;
}
currFlowCommentType.value = 'file';
const flowInstId = typeof row.fileFlowInstId === 'string' ? parseInt(row.fileFlowInstId, 10) : row.fileFlowInstId;
currFlowJob.value = { id: row.id, flowInstId: Number.isNaN(flowInstId) ? row.fileFlowInstId : flowInstId };
showFlowComment.value = true;
};
/**
* 打开查看对话框
* @param row - 当前行数据
*/
const handleView = (row: any) => {
formDialogRef.value?.openDialog('view', row);
};
/**
* 打开编辑对话框
* @param row - 当前行数据
*/
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row);
};
/**
* 履约验收
* @param row - 当前行数据
*/
const handleAccept = (row: any) => {
acceptModalRef.value?.open(row);
};
/** 打开实施采购(仅暂存状态可点;通过 iframe 嵌入 implement.vue */
const handleImplement = (row: any) => {
implementFormRef.value?.openDialog(row);
};
/**
@@ -381,6 +494,142 @@ const handleDelete = async (row: any) => {
}
};
/** 暂存状态下提交采购申请(启动流程) */
const handleSubmit = async (row: any) => {
try {
await useMessageBox().confirm('确定要提交该采购申请并启动流程吗?');
} catch {
return;
}
try {
await submitObj({ id: row.id });
useMessage().success('提交成功');
getDataList();
} catch (err: any) {
useMessage().error(err?.msg || '提交失败');
}
};
/** 操作栏「更多」菜单项配置 */
const getActionMenuItems = (row: any) => {
const isTemp = row?.status === '-1';
return [
{
command: 'edit',
label: '编辑',
icon: Edit,
visible: () => isTemp,
},
{
command: 'submit',
label: '提交',
icon: Upload,
visible: () => isTemp,
},
{
command: 'delete',
label: '删除',
icon: Delete,
visible: () => isTemp,
},
{
command: 'accept',
label: '履约验收',
icon: DocumentChecked,
visible: () => true,
},
{
command: 'implement',
label: '实施采购',
icon: Upload
},
{
command: 'archive',
label: '文件归档',
icon: FolderOpened,
visible: () => true,
},
{
command: 'downloadApply',
label: '下载审批表',
icon: Download,
visible: () => true,
},
{
command: 'downloadFileApply',
label: '下载文件审批表',
icon: Download,
visible: () => true,
},
];
};
/** 处理更多操作下拉菜单命令 */
const handleMoreCommand = (command: string, row: any) => {
switch (command) {
case 'edit':
handleEdit(row);
break;
case 'submit':
handleSubmit(row);
break;
case 'delete':
handleDelete(row);
break;
case 'accept':
handleAccept(row);
break;
case 'implement':
handleImplement(row);
break;
case 'archive':
handleArchive(row);
break;
case 'downloadApply':
handleDownloadApply(row);
break;
case 'downloadFileApply':
handleDownloadFileApply(row);
break;
}
};
/** 下载审批表 */
const handleDownloadApply = (row: any) => {
const id = row?.id ?? row?.purchaseId;
if (id == null || id === '') {
useMessage().warning('无法获取申请单ID');
return;
}
const url = getApplyTemplateDownloadUrl(id);
const fileName = `审批表_${row?.purchaseNo || id}.docx`;
other.downBlobFile(url, {}, fileName);
};
/** 下载文件审批表 */
const handleDownloadFileApply = (row: any) => {
const id = row?.id ?? row?.purchaseId;
if (id == null || id === '') {
useMessage().warning('无法获取申请单ID');
return;
}
const url = getFileApplyTemplateDownloadUrl(id);
const fileName = `文件审批表_${row?.purchaseNo || id}.docx`;
other.downBlobFile(url, {}, fileName);
};
/** 文件归档:按文件类型打包下载该申请单下所有附件 */
const handleArchive = (row: any) => {
const id = row?.id ?? row?.purchaseId;
if (id == null || id === '') {
useMessage().warning('无法获取申请单ID');
return;
}
const url = getArchiveDownloadUrl(id);
const fileName = `归档_${row?.purchaseNo || id}.zip`;
other.downBlobFile(url, {}, fileName);
};
// 获取字典数据和品目树数据
const loadDictData = async () => {
try {
@@ -526,53 +775,19 @@ onMounted(() => {
loadDictData();
});
// 组件卸载时清理事件监听器
onUnmounted(() => {
window.removeEventListener('message', handleIframeMessage)
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.iframe-dialog-content {
width: 100%;
height: 70vh;
min-height: 500px;
max-height: calc(100vh - 200px);
position: relative;
overflow: hidden;
.add-iframe {
width: 100%;
height: 100%;
min-height: 500px;
border: none;
display: block;
}
.op-cell {
display: flex;
align-items: center;
justify-content: center;
}
:deep(.iframe-dialog) {
.el-dialog {
display: flex;
flex-direction: column;
max-height: 90vh;
margin-top: 5vh !important;
}
.el-dialog__header {
flex-shrink: 0;
padding: 20px 20px 10px;
}
.el-dialog__body {
padding: 20px;
overflow-y: auto;
overflow-x: hidden;
flex: 1;
min-height: 0;
max-height: calc(100vh - 200px);
}
.status-tag-clickable {
cursor: pointer;
}
</style>

View File

@@ -161,14 +161,14 @@
</el-timeline-item>
</el-timeline>
</div>
<footer class="el-dialog__footer" style="text-align: center;">
<span class="dialog-footer">
<el-button type="primary" @click="printForm">{{
t('jfI18n.print')
}}
</el-button>
</span>
</footer>
<!-- <footer class="el-dialog__footer" style="text-align: center;">-->
<!-- <span class="dialog-footer">-->
<!-- <el-button type="primary" @click="printForm">{{-->
<!-- t('jfI18n.print')-->
<!-- }}-->
<!-- </el-button>-->
<!-- </span>-->
<!-- </footer>-->
<el-drawer
v-if="data.showHiJob"

View File

@@ -0,0 +1,121 @@
<template>
<el-dialog v-model="visible" :title="title" width="600" append-to-body>
<!-- <el-alert-->
<!-- type="warning"-->
<!-- :closable="false"-->
<!-- show-icon-->
<!-- style="margin-bottom: 20px;">-->
<!-- <template #title>-->
<!-- <span>下载模板</span>-->
<!-- </template>-->
<!-- </el-alert>-->
<div style="text-align: center; margin-bottom: 20px">
<el-button type="success" :icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
</div>
<el-upload
ref="uploadRef"
class="upload-demo"
:action="uploadUrl"
:headers="headers"
:accept="'.xls,.xlsx'"
:on-success="handleUploadSuccess"
:on-error="handleAvatarError"
:limit="1"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 .xls .xlsx 格式的 Excel 文件</div>
</template>
</el-upload>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue';
import { ElNotification } from 'element-plus';
import { Download, UploadFilled } from '@element-plus/icons-vue';
import { Session } from '/@/utils/storage';
import { downBlobFile } from '/@/utils/other';
const title = ref('');
// 响应式数据
const visible = ref(false);
// 计算属性
const headers = computed(() => {
return {
Authorization: 'Bearer ' + Session.getToken(),
TENANT_ID: Session.getTenant()
};
});
const uploadUrl = ref('')
const currentType = ref('')
const uploadRef = ref<{ clearFiles?: () => void }>()
const titleMap: Record<string, string> = {
titleRelation: '职称信息导入',
quaRelation: '职业资格信息导入',
cerRelation: '教师资格证信息导入',
eduDegree: '学历学位信息导入',
partyChange: '党组织变动信息导入',
honor: '综合表彰信息导入'
}
// 方法
const init = (type: any) => {
currentType.value = type
uploadUrl.value = '/professional/file/importTeacherOtherInfo?type=' + type
title.value = titleMap[type] || '信息导入'
visible.value = true
nextTick(() => {
uploadRef.value?.clearFiles()
})
}
// Emits
const emit = defineEmits<{
(e: 'refreshData'): void
}>()
const handleUploadSuccess = () => {
visible.value = false;
ElNotification({
title: '成功',
message: '导入成功',
type: 'success',
});
emit('refreshData')
};
const handleAvatarError = (err: any) => {
const result = JSON.parse(err.message);
if (result.code == '1') {
ElNotification.error({
title: '错误',
message: result.msg,
duration: 30000,
});
}
};
const handleDownloadTemplate = () => {
downBlobFile('/professional/file/exportTeacherInfoTemplate', { type: currentType.value || 'titleRelation' }, title.value+'模板.xlsx')
}
// 暴露方法给父组件
defineExpose({
init,
});
</script>
<style scoped lang="scss">
.upload-demo {
:deep(.el-upload-dragger) {
width: 100%;
}
}
</style>

View File

@@ -37,6 +37,20 @@
</template>
</search-form>
<!-- 操作按钮 -->
<el-row>
<div class="mb15" style="width: 100%;">
<el-button
v-auth="'professional_teacherinfo_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入信息</el-button>
</div>
</el-row>
<!-- 表格 -->
<el-table
ref="tableRef"
@@ -77,6 +91,9 @@
@current-change="currentChangeHandle"
@size-change="sizeChangeHandle"
/>
<!-- 子组件 -->
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</div>
</template>
@@ -88,6 +105,7 @@ import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList } from '/@/api/professional/professionaluser/professionalpartychange'
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
const ImportTeacherOtherInfo = defineAsyncComponent(() => import('/@/views/professional/common/import-teacher-other-info.vue'))
// 表格引用
const tableRef = ref()
@@ -100,6 +118,10 @@ const search = reactive({
realName: ''
})
// 导入加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: async (params: any) => {
@@ -130,6 +152,11 @@ const resetQuery = () => {
getDataList()
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('partyChange')
}
// 表格数据由 useTablecreatedIsNeed 默认 true在挂载时自动请求
</script>

View File

@@ -73,6 +73,14 @@
@click="handleDownLoadWord"
:loading="exportLoading"
>导出信息</el-button>
<el-button
v-auth="'professional_teacherinfo_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入信息</el-button>
</div>
<div class="header-right">
<RightToolbar
@@ -207,6 +215,7 @@
<!-- 子组件 -->
<DataForm ref="dataFormRef" @refreshData="handleFilter" />
<ProfessionalBackResaon ref="backReasonRef" @refreshData="handleFilter" />
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</template>
@@ -220,11 +229,10 @@ import { useDict } from '/@/hooks/dict'
import {
fetchList,
examObj,
delObj,
exportExcel
} from '/@/api/professional/professionaluser/professionalqualificationrelation'
delObj} from '/@/api/professional/professionaluser/professionalqualificationrelation'
import { getLevelList } from '/@/api/professional/rsbase/professionalqualificationconfig'
import { getWorkTypeList } from '/@/api/professional/rsbase/professionalworktype'
import { makeExportTeacherInfoByTypeTask } from '/@/api/professional/professionalfile';
import { PROFESSIONAL_AUDIT_STATE_OPTIONS } from '/@/config/global'
import { defineAsyncComponent } from 'vue'
import { Medal } from '@element-plus/icons-vue'
@@ -234,6 +242,7 @@ const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/i
const DataForm = defineAsyncComponent(() => import('./form.vue'))
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
const ImportTeacherOtherInfo = defineAsyncComponent(() => import('/@/views/professional/common/import-teacher-other-info.vue'))
// 审核状态选项
const auditStateOptions = PROFESSIONAL_AUDIT_STATE_OPTIONS
@@ -265,8 +274,9 @@ const search = reactive({
// 材料预览
const imgUrl = ref<Array<{ title: string; url: string }>>([])
// 导出加载状态
// 导出/导入加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 资格等级和工种列表
const qualificationLevelList = ref<any[]>([])
@@ -380,27 +390,15 @@ const handleDel = (row: any) => {
// 导出
const handleDownLoadWord = async () => {
exportLoading.value = true
try {
const response: any = await exportExcel(search)
const blob = new Blob([response as BlobPart])
const fileName = '职业资格信息.xls'
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
message.success('导出成功')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
}
}
exportLoading.value = true;
let params = Object.assign(search, { type: 'P20003' });
makeExportTeacherInfoByTypeTask(params).then((res: any) => {
message.success('后台下载进行中,请稍后查看任务列表');
});
setTimeout(() => {
exportLoading.value = false;
}, 3000); // 5分钟后自动关闭
};
// 获取资格等级名称
const getQualificationLevelName = (id: string | number) => {
@@ -414,6 +412,11 @@ const getWorkTypeName = (id: string | number) => {
return item ? item.workName : '-'
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('quaRelation')
}
// 加载字典数据
const loadDictData = async () => {
try {

View File

@@ -84,10 +84,10 @@
/>
</el-form-item>
<el-form-item label="证书编" prop="certificateNumber">
<el-form-item label="证书编" prop="certificateNumber">
<el-input
v-model="dataForm.certificateNumber"
placeholder="请输入证书编(仅支持英文和数字)"
placeholder="请输入证书编(仅支持英文和数字)"
clearable
show-word-limit
maxlength="100"
@@ -215,8 +215,8 @@ const formRules = computed(() => {
{ required: true, message: '请输入所学专业', trigger: 'blur' }
],
certificateNumber: [
{ required: true, message: '请输入证书编', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9]+$/, message: '证书编只能包含英文和数字', trigger: 'blur' }
{ required: true, message: '请输入证书编', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9]+$/, message: '证书编只能包含英文和数字', trigger: 'blur' }
]
}

View File

@@ -69,6 +69,14 @@
@click="handleDownLoadWord"
:loading="exportLoading">导出信息
</el-button>
<el-button
v-auth="'professional_teacherinfo_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog">导入信息
</el-button>
</div>
</el-row>
@@ -218,6 +226,7 @@
<!-- 子组件 -->
<DataForm ref="dataFormRef" @refreshData="handleFilter" />
<ProfessionalBackResaon ref="backReasonRef" @refreshData="handleFilter" />
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</div>
</template>
@@ -232,19 +241,19 @@ import { useDict } from '/@/hooks/dict'
import {
fetchList,
examObj,
delObj,
exportExcel
} from '/@/api/professional/professionaluser/professionalteacheracademicrelation'
delObj} from '/@/api/professional/professionaluser/professionalteacheracademicrelation'
import { getDegreeList } from '/@/api/professional/rsbase/professionalacademicdegreeconfig'
import { getQualificationList } from '/@/api/professional/rsbase/academicqualificationsconfig'
import { getAllTypeList } from '/@/api/professional/rsbase/professionalacademiceducationtypeconfig'
import { PROFESSIONAL_AUDIT_STATE_OPTIONS } from '/@/config/global'
import { defineAsyncComponent } from 'vue'
import {makeExportTeacherInfoByTypeTask} from "/@/api/professional/professionalfile";
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue'))
const DataForm = defineAsyncComponent(() => import('./form.vue'))
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
const ImportTeacherOtherInfo = defineAsyncComponent(() => import('/@/views/professional/common/import-teacher-other-info.vue'))
// 审核状态选项
const auditStateOptions = PROFESSIONAL_AUDIT_STATE_OPTIONS
@@ -279,6 +288,7 @@ const imgUrl = ref<Array<{ title: string; url: string }>>([])
// 导出加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 学位、学历和教育类型列表
const degreeList = ref<any[]>([])
@@ -401,26 +411,14 @@ const handleDel = (row: any) => {
// 导出
const handleDownLoadWord = async () => {
exportLoading.value = true
try {
const response: any = await exportExcel(search)
const blob = new Blob([response as BlobPart])
const fileName = '学历学位信息.xls'
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
message.success('导出成功')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
}
exportLoading.value = true;
let params = Object.assign(search, { type: 'P20005' });
makeExportTeacherInfoByTypeTask(params).then((res: any) => {
message.success('后台下载进行中,请稍后查看任务列表');
});
setTimeout(() => {
exportLoading.value = false;
}, 3000); // 5分钟后自动关闭
}
// 获取学位名称
@@ -442,6 +440,11 @@ const getEducationTypeName = (id: string | number | undefined) => {
return item ? item.name : '-'
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('eduDegree')
}
// 加载字典数据
const loadDictData = async () => {
try {

View File

@@ -69,6 +69,14 @@
@click="handleDownLoadWord"
:loading="exportLoading">导出信息
</el-button>
<el-button
v-auth="'professional_teacherinfo_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog">导入信息
</el-button>
</div>
</el-row>
@@ -188,6 +196,7 @@
<!-- 子组件 -->
<DataForm ref="dataFormRef" @refreshData="handleFilter" />
<ProfessionalBackResaon ref="backReasonRef" @refreshData="handleFilter" />
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</div>
</template>
@@ -203,9 +212,10 @@ import { getTeacherCertificateList } from '/@/api/professional/rsbase/profession
import {
fetchList,
examObj,
delObj,
exportExcel
delObj
} from '/@/api/professional/professionaluser/professionalteachercertificaterelation'
import { makeExportTeacherInfoByTypeTask } from '/@/api/professional/professionalfile';
import { PROFESSIONAL_AUDIT_STATE_OPTIONS } from '/@/config/global'
import { defineAsyncComponent } from 'vue'
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
@@ -213,6 +223,7 @@ const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/i
const DataForm = defineAsyncComponent(() => import('./form.vue'))
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
const ImportTeacherOtherInfo = defineAsyncComponent(() => import('/@/views/professional/common/import-teacher-other-info.vue'))
// 审核状态选项(独立定义,防止其他页面修改时被波及)
import type { StateOption } from '/@/components/AuditState/index.vue'
@@ -247,6 +258,7 @@ const imgUrl = ref<Array<{ title: string; url: string }>>([])
// 导出加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 证书列表
const certificateList = ref<any[]>([])
@@ -357,27 +369,15 @@ const handleDel = (row: any) => {
// 导出
const handleDownLoadWord = async () => {
exportLoading.value = true
try {
const response: any = await exportExcel(search)
const blob = new Blob([response as BlobPart])
const fileName = '教师资格证信息.xls'
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
message.success('导出成功')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
}
}
exportLoading.value = true;
let params = Object.assign(search, { type: 'P20004' });
makeExportTeacherInfoByTypeTask(params).then((res: any) => {
message.success('后台下载进行中,请稍后查看任务列表');
});
setTimeout(() => {
exportLoading.value = false;
}, 3000); // 5分钟后自动关闭
};
// 获取证书名称
const getCertificateName = (id: string | number) => {
@@ -385,6 +385,11 @@ const getCertificateName = (id: string | number) => {
return item ? item.cretificateName : '-'
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('cerRelation')
}
// 加载字典数据
const loadDictData = async () => {
try {

View File

@@ -68,6 +68,14 @@
@click="handleDownLoadWord"
:loading="exportLoading">导出信息
</el-button>
<el-button
v-auth="'professional_teacherinfo_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog">导入信息
</el-button>
</div>
</el-row>
@@ -216,6 +224,7 @@
<!-- 子组件 -->
<ProfessionalBackResaon ref="backReasonRef" @refreshData="handleFilter" />
<DataForm ref="dataFormRef" @refreshData="handleFilter" />
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</div>
</template>
@@ -234,6 +243,7 @@ import {
} from '/@/api/professional/professionaluser/professionalteacherhonor'
import { PROFESSIONAL_AUDIT_STATE_OPTIONS, getStatusConfig } from '/@/config/global'
import { defineAsyncComponent } from 'vue'
import {makeExportTeacherInfoByTypeTask} from "/@/api/professional/professionalfile";
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue'))
const ClickableTag = defineAsyncComponent(() => import('/@/components/ClickableTag/index.vue'))
@@ -241,6 +251,7 @@ const DetailPopover = defineAsyncComponent(() => import('/@/components/DetailPop
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const DataForm = defineAsyncComponent(() => import('./form.vue'))
const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
const ImportTeacherOtherInfo = defineAsyncComponent(() => import('/@/views/professional/common/import-teacher-other-info.vue'))
// 消息提示 hooks
const message = useMessage()
@@ -281,6 +292,7 @@ const imgUrl = ref<Array<{ title: string; url: string }>>([])
// 导出加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
@@ -382,29 +394,19 @@ const handleDel = (row: any) => {
// 导出
const handleDownLoadWord = async () => {
exportLoading.value = true
try {
const response = await fetchList({
current: 1,
size: 999999,
...search
})
const data = response.data.records || []
const tHeader = ['工号', '姓名', '荣誉', '表彰单位', '年份']
const filterVal = ['teacherNo', 'teacherName', 'honor', 'honorCompany', 'year']
// 动态导入导出工具
const { export_json_to_excel } = await import('/@/excel/Export2Excel.js')
const exportData = data.map((v: any) => filterVal.map((j: string) => v[j]))
export_json_to_excel(tHeader, exportData, '综合表彰.xls')
message.success('导出成功')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
}
exportLoading.value = true;
let params = Object.assign(search, { type: 'P20006' });
makeExportTeacherInfoByTypeTask(params).then((res: any) => {
message.success('后台下载进行中,请稍后查看任务列表');
});
setTimeout(() => {
exportLoading.value = false;
}, 3000); // 5分钟后自动关闭
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('honor')
}
// 表格数据由 useTablecreatedIsNeed 默认 true在挂载时自动请求

View File

@@ -6,23 +6,25 @@
show-icon
style="margin-bottom: 20px;">
<template #title>
<span>导入前请先下载字典文件部分字段需严格按照字典值填写</span>
<span> 可先导出教职工信息,按照导出信息的模板填入职工数据,再执行导入</span>
</template>
</el-alert>
<div style="text-align: center; margin-bottom: 20px;">
<a href="excel/dictlist.xlsx" rel="external nofollow" download="职工信息字典下载" style="text-decoration: none;">
<el-button type="success" :icon="Download">下载字典文件</el-button>
</a>
</div>
<!-- <div style="text-align: center; margin-bottom: 20px;">-->
<!-- <a href="excel/dictlist.xlsx" rel="external nofollow" download="职工信息字典下载" style="text-decoration: none;">-->
<!-- <el-button type="success" :icon="Download">下载字典文件</el-button>-->
<!-- </a>-->
<!-- </div>-->
<el-upload
ref="uploadRef"
class="upload-demo"
action="/professional/file/makeImportTeacherInfoSimpleTask"
:headers="headers"
:accept="'.xls,.xlsx'"
:on-success="handleUploadSuccess"
:on-error="handleAvatarError"
:limit="1"
drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
@@ -38,7 +40,7 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, nextTick } from 'vue'
import { ElNotification } from 'element-plus'
import { Download, UploadFilled } from '@element-plus/icons-vue'
import { Session } from '/@/utils/storage'
@@ -53,9 +55,14 @@
}
})
const uploadRef = ref<{ clearFiles?: () => void }>()
// 方法
const init = () => {
visible.value = true
nextTick(() => {
uploadRef.value?.clearFiles?.()
})
}
const handleUploadSuccess = () => {

View File

@@ -1024,7 +1024,7 @@
</el-table-column>
<el-table-column prop="graduateSchool" label="毕业学校" min-width="180" align="center" show-overflow-tooltip />
<el-table-column prop="major" label="所学专业" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="certificateNumber" label="证书编" min-width="120" align="center" />
<el-table-column prop="certificateNumber" label="证书编" min-width="120" align="center" />
<el-table-column label="学历证书" min-width="150" align="center">
<template #default="scope">
<el-button

View File

@@ -1,225 +0,0 @@
<template>
<div>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="120px"
v-loading="loading"
:disabled="operType === 'view'"
>
<el-row :gutter="24">
<el-col :span="12" class="mb20" v-if="!hiddenFields.projectName">
<el-form-item label="采购项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入采购项目名称" :disabled="disabledFields.projectName" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.projectType">
<el-form-item label="项目类别" prop="projectType">
<el-select v-model="form.projectType" placeholder="请选择" :disabled="disabledFields.projectType" clearable style="width: 100%">
<el-option label="货物" value="A" />
<el-option label="工程" value="B" />
<el-option label="服务" value="C" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20" v-if="!hiddenFields.projectContent">
<el-form-item label="采购内容" prop="projectContent">
<el-input v-model="form.projectContent" type="textarea" :rows="3" placeholder="请输入采购内容" :disabled="disabledFields.projectContent" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.applyDate">
<el-form-item label="填报日期" prop="applyDate">
<el-date-picker
v-model="form.applyDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
:disabled="disabledFields.applyDate"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.fundSource">
<el-form-item label="资金来源" prop="fundSource">
<el-input v-model="form.fundSource" placeholder="资金来源" :disabled="disabledFields.fundSource" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.budget">
<el-form-item label="预算金额(元)" prop="budget">
<el-input-number v-model="form.budget" :min="0" :precision="2" :disabled="disabledFields.budget" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.isCentralized">
<el-form-item label="是否集采" prop="isCentralized">
<el-select v-model="form.isCentralized" placeholder="请选择" :disabled="disabledFields.isCentralized" clearable style="width: 100%">
<el-option label="否" value="0" />
<el-option label="是" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.isSpecial">
<el-form-item label="是否特殊情况" prop="isSpecial">
<el-select v-model="form.isSpecial" placeholder="请选择" :disabled="disabledFields.isSpecial" clearable style="width: 100%">
<el-option label="否" value="0" />
<el-option label="紧急" value="1" />
<el-option label="单一" value="2" />
<el-option label="进口" value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.purchaseMode">
<el-form-item label="采购形式" prop="purchaseMode">
<el-input v-model="form.purchaseMode" placeholder="采购形式" :disabled="disabledFields.purchaseMode" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.purchaseType">
<el-form-item label="采购方式" prop="purchaseType">
<el-input v-model="form.purchaseType" placeholder="采购方式" :disabled="disabledFields.purchaseType" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20" v-if="!hiddenFields.remark">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注" :disabled="disabledFields.remark" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template v-if="data.submitBtn">
<footer class="el-dialog__footer">
<span class="dialog-footer">
<el-button type="primary" @click="submitForm" :disabled="loading">提交</el-button>
</span>
</footer>
</template>
<template v-else>
<footer class="el-dialog__footer">
<span class="dialog-footer" />
</footer>
</template>
</div>
</template>
<script setup lang="ts" name="PurchaseApplyFlow">
import { useMessage } from '/@/hooks/message'
import * as purchaseApply from '/@/api/order/purchase-apply'
import * as orderVue from '/@/api/order/order-key-vue'
import { handleCustomFormPerm, handleFormPrint } from '/@/flow/utils/form-perm'
import { deepClone } from '/@/utils/other'
import { initCustomFormMethods } from '/@/views/order/index'
const emits = defineEmits(['handleJob'])
const dataFormRef = ref()
const loading = ref(false)
const operType = ref<'view' | 'flow'>('flow')
const props = defineProps({
currJob: { type: Object, default: null },
currElTab: { type: Object, default: () => ({}) }
})
const form = reactive<Record<string, any>>({
id: null,
code: '',
flowKey: 'PURCHASE_APPLY',
flowInstId: null,
purchaseNo: '',
projectName: '',
projectType: '',
projectContent: '',
applyDate: '',
fundSource: '',
budget: null,
isCentralized: '',
isSpecial: '',
purchaseMode: '',
purchaseSchool: '',
purchaseType: '',
categoryCode: '',
fileIds: [],
remark: '',
runJobId: '',
flowVarUser: null
})
const dataRules = ref({
projectContent: [{ required: true, message: '请输入采购内容', trigger: 'blur' }],
budget: [{ required: true, message: '请输入预算金额', trigger: 'blur' }]
})
const fieldsPerm = {
projectName: false,
projectType: false,
projectContent: false,
applyDate: false,
fundSource: false,
budget: false,
isCentralized: false,
isSpecial: false,
purchaseMode: false,
purchaseType: false,
remark: false
}
const hiddenFields = reactive({ ...fieldsPerm })
const disabledFields = reactive(deepClone(fieldsPerm))
const data = reactive({
submitBtn: true,
elTab: null as any
})
const methods = initCustomFormMethods(data, disabledFields, operType)
function initJobData() {
if (props.currJob?.orderId) handleGetObj(props.currJob.orderId)
}
function handleGetObj(id: string | number) {
purchaseApply.getObj(id).then(async (resp: any) => {
const formData = resp?.data ?? {}
Object.assign(form, formData)
form.runJobId = props.currJob?.id ?? ''
await initFormPermPrint()
})
}
async function initFormPermPrint() {
const elTab = orderVue.currElTabIsExist(props.currJob, props.currElTab?.id)
const res = await handleCustomFormPerm(props, hiddenFields, disabledFields, elTab)
await orderVue.currElTabIsView(methods, props.currJob, props.currElTab?.id, submitForm, res?.callback)
await handleFormPrint(form, elTab?.type, elTab?.id, '1')
data.elTab = elTab
}
async function submitForm() {
try {
loading.value = true
await purchaseApply.putObj(form)
orderVue.currElTabIsSave(props.currJob, props.currElTab?.id, true, emits)
useMessage().success(form.id ? '修改成功' : '保存成功')
} catch (err: any) {
useMessage().error(err?.msg ?? '操作失败')
} finally {
loading.value = false
}
}
watch(
() => props.currJob?.id,
() => { initJobData() }
)
onMounted(() => {
initJobData()
})
</script>
<style lang="scss" scoped>
.el-dialog__footer {
text-align: center;
.dialog-footer {
text-align: center;
}
}
</style>

View File

@@ -11,6 +11,26 @@
:rules="dataRules"
label-width="120px"
v-loading="loading">
<!-- 新增时先选取用户选后自动带出姓名工号 -->
<el-form-item label="选取用户" prop="userId">
<org-selector
v-model:orgList="userList"
type="user"
:multiple="false"
@update:orgList="handleUserChange" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input
v-model="form.name"
placeholder="选择用户后自动填充"
readonly />
</el-form-item>
<el-form-item label="用户工号" prop="username">
<el-input
v-model="form.username"
placeholder="选择用户后自动填充"
readonly />
</el-form-item>
<el-form-item label="部门" prop="deptId">
<org-selector
v-model:orgList="deptList"
@@ -21,26 +41,7 @@
<el-form-item label="部门名称" prop="deptName">
<el-input
v-model="form.deptName"
placeholder="选择部门后自动填充"
readonly />
</el-form-item>
<el-form-item label="分管负责人" prop="userId">
<org-selector
v-model:orgList="userList"
type="user"
:multiple="false"
@update:orgList="handleUserChange" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input
v-model="form.name"
placeholder="请选择用户后自动填充"
readonly />
</el-form-item>
<el-form-item label="用户工号" prop="username">
<el-input
v-model="form.username"
placeholder="请选择用户后自动填充"
placeholder="选择部门后自动填充"
readonly />
</el-form-item>
<el-form-item label="备注" prop="remark">
@@ -74,7 +75,7 @@ const emit = defineEmits(['refresh']);
const dataFormRef = ref();
const deptList = ref<any[]>([]);
const userList = ref<any[]>([]);
const dataForm = reactive({
const form = reactive({
id: '',
deptId: '',
deptName: '',
@@ -87,17 +88,17 @@ const visible = ref(false);
const loading = ref(false);
const dataRules = ref({
userId: [
{ required: true, message: '请选取用户(分管负责人)', trigger: 'change' }
],
deptId: [
{ required: true, message: '请选择部门', trigger: 'change' }
],
userId: [
{ required: true, message: '请选择分管负责人', trigger: 'change' }
],
name: [
{ required: true, message: '姓名不能为空', trigger: 'blur' }
{ required: true, message: '请先选取用户', trigger: 'blur' }
],
username: [
{ required: true, message: '用户工号不能为空', trigger: 'blur' }
{ required: true, message: '请先选取用户', trigger: 'blur' }
],
});
@@ -105,38 +106,38 @@ const dataRules = ref({
const handleDeptChange = (list: any[]) => {
if (list && list.length > 0) {
const dept = list[0];
dataForm.deptId = dept.deptId || dept.id || '';
dataForm.deptName = dept.name || dept.deptName || '';
form.deptId = dept.deptId || dept.id || '';
form.deptName = dept.name || dept.deptName || '';
} else {
dataForm.deptId = '';
dataForm.deptName = '';
form.deptId = '';
form.deptName = '';
}
};
// 处理用户选择变化
// 处理用户选择变化(选取用户后自动带出姓名、工号)
const handleUserChange = (list: any[]) => {
if (list && list.length > 0) {
const user = list[0];
dataForm.userId = user.userId || user.id || '';
dataForm.username = user.username || user.userName || '';
dataForm.name = user.name || user.realName || '';
form.userId = user.userId || user.id || '';
form.username = user.username || user.userName || '';
form.name = user.name || user.realName || '';
} else {
dataForm.userId = '';
dataForm.username = '';
dataForm.name = '';
form.userId = '';
form.username = '';
form.name = '';
}
};
// 打开弹窗
const openDialog = async (id?: string) => {
visible.value = true;
dataForm.id = '';
dataForm.deptId = '';
dataForm.deptName = '';
dataForm.userId = '';
dataForm.username = '';
dataForm.name = '';
dataForm.remark = '';
form.id = '';
form.deptId = '';
form.deptName = '';
form.userId = '';
form.username = '';
form.name = '';
form.remark = '';
deptList.value = [];
userList.value = [];
@@ -148,7 +149,7 @@ const openDialog = async (id?: string) => {
getObj({ id }).then((res: any) => {
if (res.data && res.data.length > 0) {
const data = res.data[0];
Object.assign(dataForm, {
Object.assign(form, {
id: data.id || '',
deptId: data.deptId || '',
deptName: data.deptName || '',
@@ -201,17 +202,17 @@ const onSubmit = async () => {
return false;
}
if (dataForm.id) {
await putObj(dataForm);
if (form.id) {
await putObj(form);
useMessage().success('编辑成功');
} else {
await addObj(dataForm);
await addObj(form);
useMessage().success('新增成功');
}
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg || (dataForm.id ? '编辑失败' : '新增失败'));
useMessage().error(err.msg || (form.id ? '编辑失败' : '新增失败'));
} finally {
loading.value = false;
}

View File

@@ -46,7 +46,7 @@
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
业务分管管理
业务分管部门及人员
</span>
<div class="header-actions">
<el-button
@@ -56,15 +56,15 @@
v-auth="'purchase_purchasingBusinessDept_add'">
新增
</el-button>
<el-button
plain
icon="UploadFilled"
type="primary"
class="ml10"
@click="excelUploadRef.show()"
v-auth="'purchase_purchasingBusinessDept_add'">
导入
</el-button>
<!-- <el-button -->
<!-- plain -->
<!-- icon="UploadFilled" -->
<!-- type="primary" -->
<!-- class="ml10" -->
<!-- @click="excelUploadRef.show()" -->
<!-- v-auth="'purchase_purchasingBusinessDept_add'">-->
<!-- 导入-->
<!-- </el-button>-->
<el-button
plain
:disabled="multiple"
@@ -134,14 +134,14 @@
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="150">
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
v-auth="'purchase_purchasingBusinessDept_edit'"
@click="formDialogRef.openDialog(scope.row.id)">
编辑
</el-button>
<!-- <el-button -->
<!-- icon="Edit" -->
<!-- link -->
<!-- type="primary" -->
<!-- v-auth="'purchase_purchasingBusinessDept_edit'"-->
<!-- @click="formDialogRef.openDialog(scope.row.id)">-->
<!-- 编辑-->
<!-- </el-button>-->
<el-button
icon="Delete"
link

View File

@@ -93,13 +93,13 @@
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="150">
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="formDialogRef.openDialog('edit', scope.row)">
编辑
</el-button>
<!-- <el-button -->
<!-- icon="Edit" -->
<!-- link -->
<!-- type="primary" -->
<!-- @click="formDialogRef.openDialog('edit', scope.row)">-->
<!-- 编辑-->
<!-- </el-button>-->
<el-button
icon="Delete"
link

View File

@@ -0,0 +1,120 @@
<template>
<el-dialog v-model="visible" :title="title" width="600" append-to-body>
<div style="text-align: center; margin-bottom: 20px" v-if="currentType!='R10003'">
<el-button type="success" :icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
</div>
<el-alert
v-if="currentType=='R10003'"
type="warning"
:closable="false"
show-icon
style="margin-bottom: 20px;">
<template #title>
<span> 请从中招平台导出数据后导入</span>
</template>
</el-alert>
<el-upload
ref="uploadRef"
class="upload-demo"
:action="uploadUrl"
:headers="headers"
:accept="'.xls,.xlsx'"
:on-success="handleUploadSuccess"
:on-error="handleAvatarError"
:limit="1"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 .xls .xlsx 格式的 Excel 文件</div>
</template>
</el-upload>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue';
import { ElNotification } from 'element-plus';
import { Download, UploadFilled } from '@element-plus/icons-vue';
import { Session } from '/@/utils/storage';
import { downBlobFile } from '/@/utils/other';
const title = ref('');
// 响应式数据
const visible = ref(false);
// 计算属性
const headers = computed(() => {
return {
Authorization: 'Bearer ' + Session.getToken(),
TENANT_ID: Session.getTenant()
};
});
const uploadUrl = ref('')
const currentType = ref('')
const uploadRef = ref<{ clearFiles?: () => void }>()
const titleMap: Record<string, string> = {
R10001: '计划专业导入',
R10002: '地区分数导入',
R10003: '中招平台数据导入'
}
// 方法
const init = (type: any) => {
currentType.value = type
uploadUrl.value = '/api/recruit/file/importRecruitInfo?type=' + type
title.value = titleMap[type] || '信息导入'
visible.value = true
nextTick(() => {
uploadRef.value?.clearFiles()
})
}
// Emits
const emit = defineEmits<{
(e: 'refreshDataList'): void
}>()
const handleUploadSuccess = () => {
visible.value = false;
ElNotification({
title: '成功',
message: '导入成功',
type: 'success',
});
emit('refreshDataList')
};
const handleAvatarError = (err: any) => {
const result = JSON.parse(err.message);
if (result.code == '1') {
ElNotification.error({
title: '错误',
message: result.msg,
duration: 30000,
});
}
};
const handleDownloadTemplate = () => {
downBlobFile('/recruit/file/exportRecruitTemplate', { type: currentType.value || 'planMajor' }, title.value+'模板.xlsx')
}
// 暴露方法给父组件
defineExpose({
init
});
</script>
<style scoped lang="scss">
.upload-demo {
:deep(.el-upload-dragger) {
width: 100%;
}
}
</style>

View File

@@ -16,303 +16,279 @@
-->
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<!-- 搜索表单 -->
<el-form :model="queryForm" inline ref="searchFormRef">
<el-form-item label="招生计划" prop="groupId">
<el-select v-model="queryForm.groupId" filterable clearable placeholder="请选择招生计划">
<el-option
v-for="item in planList"
:key="item.id"
:label="item.groupName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="学院" prop="deptCode">
<el-select v-model="queryForm.deptCode" filterable clearable placeholder="请选择">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode"
/>
</el-select>
</el-form-item>
<el-form-item label="专业序号" prop="majorCode">
<el-input v-model="queryForm.majorCode" placeholder="专业序号" />
</el-form-item>
<el-form-item label="专业名称" prop="majorName">
<el-input v-model="queryForm.majorName" placeholder="专业名称" />
</el-form-item>
<el-form-item label="学制" prop="learnYear">
<el-input v-model="queryForm.learnYear" placeholder="学制" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getDataList">查询</el-button>
<el-button plain icon="Refresh" class="ml10" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<!-- 搜索表单 -->
<el-form :model="queryForm" inline ref="searchFormRef">
<el-form-item label="招生计划" prop="groupId">
<el-select v-model="queryForm.groupId" filterable clearable placeholder="请选择招生计划">
<el-option v-for="item in planList" :key="item.id" :label="item.groupName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="学院" prop="deptCode">
<el-select v-model="queryForm.deptCode" filterable clearable placeholder="请选择">
<el-option v-for="item in deptList" :key="item.deptCode" :label="item.deptName" :value="item.deptCode" />
</el-select>
</el-form-item>
<el-form-item label="专业序号" prop="majorCode">
<el-input v-model="queryForm.majorCode" placeholder="专业序号" />
</el-form-item>
<el-form-item label="专业名称" prop="majorName">
<el-input v-model="queryForm.majorName" placeholder="专业名称" />
</el-form-item>
<el-form-item label="学制" prop="learnYear">
<el-input v-model="queryForm.learnYear" placeholder="学制" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getDataList">查询</el-button>
<el-button plain icon="Refresh" class="ml10" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<div class="mb15">
<el-button
v-if="hasAuth('recruit_recruitplanmajor_add')"
type="primary"
icon="FolderAdd"
@click="addOrUpdateHandle"
>
</el-button>
</div>
<!-- 操作按钮 -->
<div class="mb15">
<el-button v-if="hasAuth('recruit_recruitplanmajor_add')" type="primary" icon="FolderAdd" @click="addOrUpdateHandle"> </el-button>
<!-- 表格 -->
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
border
stripe
row-key="id"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="groupId" label="招生计划名称" align="center" min-width="140" show-overflow-tooltip>
<template #default="scope">
{{ getPlanName(scope.row.groupId) }}
</template>
</el-table-column>
<el-table-column prop="majorCode" label="专业序号" align="center" show-overflow-tooltip />
<el-table-column prop="majorName" label="专业名称" align="center" show-overflow-tooltip />
<el-table-column prop="deptCode" label="学院" align="center" show-overflow-tooltip>
<template #default="scope">
{{ getDeptName(scope.row.deptCode) }}
</template>
</el-table-column>
<el-table-column prop="learnYear" label="学制" align="center" width="80" show-overflow-tooltip />
<el-table-column prop="majorLevel" label="层次" align="center" show-overflow-tooltip />
<el-table-column prop="isOrder" label="订单班|中德班|联院班" align="center" width="180" show-overflow-tooltip>
<template #default="scope">
{{ getYesNoLabel(scope.row.isOrder) }}{{ getYesNoLabel(scope.row.isZd) }}{{ getYesNoLabel(scope.row.isUnion) }}
</template>
</el-table-column>
<el-table-column prop="sm" label="色盲不可录" align="center" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.sm"
active-text=""
inactive-text=""
active-value="1"
inactive-value="0"
@change="changeSm(scope.row)"
/>
</template>
</el-table-column>
<el-table-column prop="stuworkMajorCode" label="正式专业代码" align="center" show-overflow-tooltip>
<template #default="scope">
{{ getMajorCodeName(scope.row.stuworkMajorCode) }}
</template>
</el-table-column>
<!-- <el-table-column prop="cityPlanId" label="市平台招生计划" align="center" show-overflow-tooltip>
<el-button
v-auth="'recruit_major_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入信息
</el-button>
</div>
<!-- 表格 -->
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
border
stripe
row-key="id"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="groupId" label="招生计划名称" align="center" min-width="140" show-overflow-tooltip>
<template #default="scope">
{{ getPlanName(scope.row.groupId) }}
</template>
</el-table-column>
<el-table-column prop="majorCode" label="专业序号" align="center" show-overflow-tooltip />
<el-table-column prop="majorName" label="专业名称" align="center" show-overflow-tooltip />
<el-table-column prop="deptCode" label="学院" align="center" show-overflow-tooltip>
<template #default="scope">
{{ getDeptName(scope.row.deptCode) }}
</template>
</el-table-column>
<el-table-column prop="learnYear" label="学制" align="center" width="80" show-overflow-tooltip />
<el-table-column prop="majorLevel" label="层次" align="center" show-overflow-tooltip />
<el-table-column prop="isOrder" label="订单班|中德班|联院班" align="center" width="180" show-overflow-tooltip>
<template #default="scope">
{{ getYesNoLabel(scope.row.isOrder) }}{{ getYesNoLabel(scope.row.isZd) }}{{ getYesNoLabel(scope.row.isUnion) }}
</template>
</el-table-column>
<el-table-column prop="sm" label="色盲不可录" align="center" width="120">
<template #default="scope">
<el-switch v-model="scope.row.sm" active-text="是" inactive-text="否" active-value="1" inactive-value="0" @change="changeSm(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="stuworkMajorCode" label="正式专业代码" align="center" show-overflow-tooltip>
<template #default="scope">
{{ getMajorCodeName(scope.row.stuworkMajorCode) }}
</template>
</el-table-column>
<!-- <el-table-column prop="cityPlanId" label="市平台招生计划" align="center" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.cityPlanName != undefined ? (scope.row.cityPlanName + '|' + scope.row.cityPlanYear) : '' }}
</template>
</el-table-column> -->
<!-- <el-table-column prop="sort" label="排序" align="center" show-overflow-tooltip /> -->
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
icon="EditPen"
@click="addOrUpdateHandle(scope.row)"
>
修改
</el-button>
<el-button
v-if="hasAuth('recruit_recruitplanmajor_del')"
type="danger"
link
icon="Delete"
@click="deleteHandle(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- <el-table-column prop="sort" label="排序" align="center" show-overflow-tooltip /> -->
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link icon="EditPen" @click="addOrUpdateHandle(scope.row)"> 修改 </el-button>
<el-button v-if="hasAuth('recruit_recruitplanmajor_del')" type="danger" link icon="Delete" @click="deleteHandle(scope.row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-bind="state.pagination"
@current-change="currentChangeHandle"
@size-change="sizeChangeHandle"
/>
<!-- 分页 -->
<pagination v-bind="state.pagination" @current-change="currentChangeHandle" @size-change="sizeChangeHandle" />
<!-- 弹窗, 新增 / 修改 -->
<table-form ref="addOrUpdateRef" @refreshDataList="getDataList" />
</div>
</div>
<!-- 弹窗, 新增 / 修改 -->
<table-form ref="addOrUpdateRef" @refreshDataList="getDataList" />
<import-recruit-info ref="ImportRecruitInfoRef" @refreshDataList="getDataList"></import-recruit-info>
</div>
</div>
</template>
<script setup lang="ts" name="recruitplanmajor">
import { ref, reactive, onMounted, nextTick, defineAsyncComponent } from 'vue'
import { useAuth } from '/@/hooks/auth'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { useDict } from '/@/hooks/dict'
import { getList } from '/@/api/recruit/recruitstudentplangroup'
import { fetchList, delObj, editQuickField } from '/@/api/recruit/recruitstudentplan'
import { getDeptList } from '/@/api/basic/basicclass'
import { getMajorNameList } from '/@/api/basic/major'
import { ref, reactive, onMounted, nextTick, defineAsyncComponent } from 'vue';
import { useAuth } from '/@/hooks/auth';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { getList } from '/@/api/recruit/recruitstudentplangroup';
import { fetchList, delObj, editQuickField } from '/@/api/recruit/recruitstudentplan';
import { getDeptList } from '/@/api/basic/basicclass';
import { getMajorNameList } from '/@/api/basic/major';
const TableForm = defineAsyncComponent(() => import('./detaiform.vue'))
const { hasAuth } = useAuth()
const TableForm = defineAsyncComponent(() => import('./detaiform.vue'));
const ImportRecruitInfo = defineAsyncComponent(() => import('/@/views/recruit/common/import-recruit-info.vue'));
const ImportRecruitInfoRef=ref<any>();
const { hasAuth } = useAuth();
// 消息提示 hooks
const message = useMessage()
const messageBox = useMessageBox()
const message = useMessage();
const messageBox = useMessageBox();
// 表格引用
const tableRef = ref()
const searchFormRef = ref()
const addOrUpdateRef = ref()
const tableRef = ref();
const searchFormRef = ref();
const addOrUpdateRef = ref();
// 字典数据
const { yes_no_type } = useDict('yes_no_type')
const { yes_no_type } = useDict('yes_no_type');
// 数据
const planList = ref<any[]>([])
const deptList = ref<any[]>([])
const offcialZydmList = ref<any[]>([])
const planList = ref<any[]>([]);
const deptList = ref<any[]>([]);
const offcialZydmList = ref<any[]>([]);
// 查询表单
const queryForm = reactive({
groupId: '',
deptCode: '',
majorCode: '',
majorName: '',
learnYear: ''
})
groupId: '',
deptCode: '',
majorCode: '',
majorName: '',
learnYear: '',
});
// 获取计划名称
const getPlanName = (groupId: string) => {
const item = planList.value.find(item => item.id === groupId)
return item ? item.groupName : ''
}
const item = planList.value.find((item) => item.id === groupId);
return item ? item.groupName : '';
};
// 获取学院名称
const getDeptName = (deptCode: string) => {
const item = deptList.value.find(item => item.deptCode === deptCode)
return item ? item.deptName : ''
}
const item = deptList.value.find((item) => item.deptCode === deptCode);
return item ? item.deptName : '';
};
// 获取是/否标签
const getYesNoLabel = (value: string) => {
const item = yes_no_type.value.find((item: any) => item.value === value)
return item ? item.label : ''
}
const item = yes_no_type.value.find((item: any) => item.value === value);
return item ? item.label : '';
};
// 获取专业代码名称
const getMajorCodeName = (majorCode: string) => {
const item = offcialZydmList.value.find(item => item.majorCode === majorCode)
return item ? item.majorCodeAndName : ''
}
const item = offcialZydmList.value.find((item) => item.majorCode === majorCode);
return item ? item.majorCodeAndName : '';
};
// 表格状态
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: queryForm,
pageList: async (params: any) => {
const response = await fetchList(params)
return {
data: {
records: response.data.records,
total: response.data.total
}
}
},
createdIsNeed: false
})
queryForm: queryForm,
pageList: async (params: any) => {
const response = await fetchList(params);
return {
data: {
records: response.data.records,
total: response.data.total,
},
};
},
createdIsNeed: false,
});
// 使用 table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
// 初始化
const init = async () => {
try {
// 查询二级学院信息
const deptData = await getDeptList()
deptList.value = deptData.data || []
// 获取招生计划列表
const planData = await getList()
planList.value = planData.data || []
if (planList.value.length > 0) {
queryForm.groupId = planList.value[0].id
}
// 获取专业名称列表
const majorData = await getMajorNameList()
offcialZydmList.value = majorData.data || []
getDataList()
} catch (error) {
console.log(error)
}
}
try {
// 查询二级学院信息
const deptData = await getDeptList();
deptList.value = deptData.data || [];
// 获取招生计划列表
const planData = await getList();
planList.value = planData.data || [];
if (planList.value.length > 0) {
queryForm.groupId = planList.value[0].id;
}
// 获取专业名称列表
const majorData = await getMajorNameList();
offcialZydmList.value = majorData.data || [];
getDataList();
} catch (error) {
console.log(error);
}
};
// 修改开关
const changeSm = async (row: any) => {
try {
let parmas={id:row.id,sm:row.sm}
await editQuickField(parmas)
message.success('修改成功')
} catch (error: any) {
console.log(error)
}
}
try {
let parmas = { id: row.id, sm: row.sm };
await editQuickField(parmas);
message.success('修改成功');
} catch (error: any) {
console.log(error);
}
};
// 新增 / 修改
const addOrUpdateHandle = (row?: any) => {
nextTick(() => {
addOrUpdateRef.value?.init(row.id || null)
})
}
nextTick(() => {
addOrUpdateRef.value?.init(row.id || null);
});
};
// 删除
const deleteHandle = async (id: string) => {
try {
await messageBox.confirm('是否确认删除本条数据?请谨慎操作')
await delObj(id)
message.success('删除成功')
getDataList()
} catch {
// 用户取消
}
}
try {
await messageBox.confirm('是否确认删除本条数据?请谨慎操作');
await delObj(id);
message.success('删除成功');
getDataList();
} catch {
// 用户取消
}
};
// 重置查询
const resetQuery = () => {
searchFormRef.value?.resetFields()
queryForm.groupId = ''
queryForm.deptCode = ''
queryForm.majorCode = ''
queryForm.majorName = ''
queryForm.learnYear = ''
if (planList.value.length > 0) {
queryForm.groupId = planList.value[0].id
}
getDataList()
}
searchFormRef.value?.resetFields();
queryForm.groupId = '';
queryForm.deptCode = '';
queryForm.majorCode = '';
queryForm.majorName = '';
queryForm.learnYear = '';
if (planList.value.length > 0) {
queryForm.groupId = planList.value[0].id;
}
getDataList();
};
const exportLoading = ref(false);
const handleImportDialog = () => {
ImportRecruitInfoRef.value?.init("R10001");
};
onMounted(() => {
init()
})
init();
});
</script>
<style lang="scss" scoped>
</style>
<style lang="scss" scoped></style>

View File

@@ -29,6 +29,15 @@
>
</el-button>
<el-button
v-auth="'recruit_areascore_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入信息
</el-button>
</div>
<!-- 表格 -->
@@ -83,6 +92,9 @@
<!-- 弹窗, 新增 / 修改 -->
<table-form ref="addOrUpdateRef" @refreshDataList="getDataList" />
<import-recruit-info ref="ImportRecruitInfoRef" @refreshDataList="getDataList"></import-recruit-info>
</div>
</div>
</template>
@@ -94,6 +106,8 @@ import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { getList } from '/@/api/recruit/recruitstudentplangroup'
import { fetchList, delObj } from '/@/api/recruit/recruitstudentplancorrectscoreconfig'
const ImportRecruitInfo = defineAsyncComponent(() => import('/@/views/recruit/common/import-recruit-info.vue'));
const ImportRecruitInfoRef=ref<any>();
const TableForm = defineAsyncComponent(() => import('./detaiform.vue'))
const { hasAuth } = useAuth()
@@ -183,6 +197,12 @@ const resetQuery = () => {
getDataList()
}
const exportLoading = ref(false);
const handleImportDialog = () => {
ImportRecruitInfoRef.value?.init("R10002");
};
onMounted(() => {
init()
})

View File

@@ -281,19 +281,28 @@
@click="handleAddData">新增
</el-button>
<el-button
v-if="hasAuth('zipExport')"
type="warning"
plain
icon="Download"
@click="downZip()">招生名单打包导出
</el-button>
<el-button
class="ml10"
type="warning"
plain
icon="Download"
@click="handleExport()">名单导出
v-auth="'recruit_zzpt_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入中招平台数据
</el-button>
<!-- <el-button-->
<!-- v-if="hasAuth('zipExport')"-->
<!-- type="warning"-->
<!-- plain-->
<!-- icon="Download"-->
<!-- @click="downZip()">招生名单打包导出-->
<!-- </el-button>-->
<!-- <el-button -->
<!-- class="ml10"-->
<!-- type="warning"-->
<!-- plain-->
<!-- icon="Download"-->
<!-- @click="handleExport()">名单导出-->
<!-- </el-button>-->
</div>
</el-row>
@@ -634,7 +643,9 @@
<AdmissionNoticeDialog ref="admissionNoticeDialogRef" @refresh="getDataList"></AdmissionNoticeDialog>
<InterviewForm ref="interviewFormRef" @refresh="getDataList"></InterviewForm>
</div>
<import-recruit-info ref="ImportRecruitInfoRef" @refreshDataList="getDataList"></import-recruit-info>
</div>
</div>
</template>
@@ -681,6 +692,8 @@ const InterviewForm = defineAsyncComponent(() => import('/@/views/recruit/recrui
const PayQrcodeDialog = defineAsyncComponent(() => import('./PayQrcodeDialog.vue'))
const AdmissionNoticeDialog = defineAsyncComponent(() => import('./AdmissionNoticeDialog.vue'))
const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue'))
const ImportRecruitInfo = defineAsyncComponent(() => import('/@/views/recruit/common/import-recruit-info.vue'));
const ImportRecruitInfoRef=ref<any>();
const { hasAuth } = useAuth()
// 消息提示 hooks
const message = useMessage()
@@ -1131,6 +1144,10 @@ watch(() => dataForm.groupId, () => {
}
})
const handleImportDialog = () => {
ImportRecruitInfoRef.value?.init("R10003");
};
onMounted(() => {
init()
})

View File

@@ -1,28 +1,59 @@
<template>
<el-dialog
title="查看详情"
v-model="visible"
:close-on-click-modal="false"
<el-dialog
title="查看详情"
v-model="visible"
:close-on-click-modal="false"
draggable
width="800px">
<div v-loading="loading" class="detail-container" v-if="detailData">
<el-descriptions :column="2" border>
width="960px">
<div class="detail-container">
<!-- 活动主信息来自列表行 -->
<el-descriptions v-if="mainInfo.activityTheme" :column="2" border class="mb16">
<el-descriptions-item label="活动主题" :span="2">
{{ detailData.activityTheme || '-' }}
{{ mainInfo.activityTheme || '-' }}
</el-descriptions-item>
<el-descriptions-item label="活动说明" :span="2">
{{ detailData.remarks || '-' }}
{{ mainInfo.remarks || '-' }}
</el-descriptions-item>
<el-descriptions-item label="活动兼报数">
{{ detailData.maxSub !== undefined && detailData.maxSub !== null ? detailData.maxSub : '-' }}
{{ mainInfo.maxSub !== undefined && mainInfo.maxSub !== null ? mainInfo.maxSub : '-' }}
</el-descriptions-item>
<el-descriptions-item label="开始时间">
{{ parseTime(detailData.startTime, '{y}-{m}-{d}') }}
{{ mainInfo.startTime ? parseTime(mainInfo.startTime, '{y}-{m}-{d}') : '-' }}
</el-descriptions-item>
<el-descriptions-item label="结束时间">
{{ parseTime(detailData.endTime, '{y}-{m}-{d}') }}
{{ mainInfo.endTime ? parseTime(mainInfo.endTime, '{y}-{m}-{d}') : '-' }}
</el-descriptions-item>
</el-descriptions>
<!-- 子项目列表接口getActivityInfoSubList -->
<div class="sub-title">子项目列表</div>
<el-table
:data="subList"
v-loading="loading"
border
size="small"
max-height="400"
style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="subTitle" label="子项目名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="deptName" label="学院" width="110" show-overflow-tooltip />
<el-table-column prop="classNo" label="班号" width="80" align="center" />
<el-table-column prop="classMasterName" label="班主任" width="90" show-overflow-tooltip />
<el-table-column prop="startTime" label="开始时间" width="155" align="center">
<template #default="scope">
{{ scope.row.startTime ? parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}') : '-' }}
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" width="155" align="center">
<template #default="scope">
{{ scope.row.endTime ? parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}') : '-' }}
</template>
</el-table-column>
<el-table-column prop="maxNum" label="人数限制" width="88" align="center" />
<el-table-column prop="applyNums" label="已报名" width="78" align="center" />
<el-table-column prop="position" label="地点" min-width="100" show-overflow-tooltip />
<el-table-column prop="projectDescription" label="项目描述" min-width="180" show-overflow-tooltip />
</el-table>
<el-empty v-if="!loading && subList.length === 0" description="暂无子项目" :image-size="80" />
</div>
<template #footer>
<span class="dialog-footer">
@@ -33,47 +64,40 @@
</template>
<script setup lang="ts" name="ActivityInfoDetailDialog">
import { ref } from 'vue'
import { getDetail } from '/@/api/stuwork/activityinfo'
import { ref, reactive } from 'vue'
import { getActivityInfoSubList } from '/@/api/stuwork/activityinfosub'
import { parseTime } from '/@/utils/formatTime'
import { useMessage } from '/@/hooks/message'
// 定义变量内容
const visible = ref(false)
const loading = ref(false)
const detailData = ref<any>({})
const mainInfo = reactive<Record<string, any>>({})
const subList = ref<any[]>([])
// 打开弹窗
const openDialog = async (id: string) => {
/**
* 打开弹窗:使用接口 getActivityInfoSubList(activityInfoId) 获取详情子项目列表
* @param activityInfoId 活动信息ID
* @param row 列表行数据,用于展示活动主题等主信息
*/
const openDialog = async (activityInfoId: string, row?: any) => {
visible.value = true
loading.value = true
detailData.value = {}
subList.value = []
Object.keys(mainInfo).forEach((k) => delete mainInfo[k])
if (row && typeof row === 'object') {
Object.assign(mainInfo, row)
}
try {
const res = await getDetail(id)
if (res.data) {
// 根据接口文档,返回的数据可能是 { records: [...], total: ... } 格式
// 如果是列表格式,取第一条;如果是对象,直接使用
if (res.data.records && Array.isArray(res.data.records) && res.data.records.length > 0) {
detailData.value = res.data.records[0]
} else if (res.data.records && Array.isArray(res.data.records)) {
// 列表为空
useMessage().warning('未找到详情数据')
visible.value = false
} else {
// 直接是对象
detailData.value = res.data
}
}
} catch (err: any) {
useMessage().error(err.msg || '获取详情失败')
visible.value = false
const res = await getActivityInfoSubList(activityInfoId)
const data = res.data
subList.value = Array.isArray(data) ? data : []
} catch (_err) {
subList.value = []
} finally {
loading.value = false
}
}
// 暴露方法
defineExpose({
openDialog
})
@@ -81,7 +105,15 @@ defineExpose({
<style scoped lang="scss">
.detail-container {
padding: 20px 0;
padding: 8px 0;
}
.mb16 {
margin-bottom: 16px;
}
.sub-title {
margin-bottom: 8px;
font-weight: 600;
color: #303133;
}
</style>

View File

@@ -250,9 +250,9 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// 查看详情
// 查看详情接口getActivityInfoSubList传入活动ID与行数据用于展示
const handleView = (row: any) => {
detailDialogRef.value?.openDialog(row.id)
detailDialogRef.value?.openDialog(row.id, row)
}
// 编辑

View File

@@ -48,6 +48,17 @@
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="默认扣分值" prop="score">
<el-input-number
v-model="form.score"
:min="0"
:max="999"
:precision="0"
placeholder="请输入默认扣分值"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
@@ -86,12 +97,13 @@ const loading = ref(false)
const operType = ref('add')
const categoryList = ref<any[]>([])
// 提交表单数据
// 提交表单数据与接口文档一致edit 含 score 默认扣分值)
const form = reactive({
id: '',
categortyId: '',
pointName: '',
standard: '',
score: undefined as number | undefined,
remarks: ''
})
@@ -117,6 +129,7 @@ const openDialog = async (type: string = 'add', row?: any) => {
form.categortyId = ''
form.pointName = ''
form.standard = ''
form.score = undefined
form.remarks = ''
// 编辑时填充数据
@@ -125,20 +138,21 @@ const openDialog = async (type: string = 'add', row?: any) => {
form.categortyId = row.categortyId || ''
form.pointName = row.pointName || ''
form.standard = row.standard || ''
form.score = row.score !== undefined && row.score !== null ? Number(row.score) : undefined
form.remarks = row.remarks || ''
// 如果需要获取详情
if (row.id && (!row.pointName || !row.standard)) {
// 如果需要获取详情(含 score
if (row.id && (!row.pointName || row.standard === undefined || row.score === undefined)) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.categortyId = res.data.categortyId || ''
form.pointName = res.data.pointName || ''
form.standard = res.data.standard || ''
form.score = res.data.score !== undefined && res.data.score !== null ? Number(res.data.score) : undefined
form.remarks = res.data.remarks || ''
}
}).catch((err) => {
}).finally(() => {
}).catch(() => {}).finally(() => {
loading.value = false
})
}
@@ -155,10 +169,12 @@ const onSubmit = async () => {
loading.value = true
try {
// 与接口文档一致categortyId, pointName, standard, score, remarks
const submitData = {
categortyId: form.categortyId,
pointName: form.pointName,
standard: form.standard || '',
score: form.score !== undefined && form.score !== null ? Number(form.score) : 0,
remarks: form.remarks || ''
}

View File

@@ -17,6 +17,21 @@
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="考核项" prop="categortyId">
<el-select
v-model="state.queryForm.categortyId"
placeholder="请选择考核项"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in categoryList"
:key="item.id"
:label="item.category"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="指标名称" prop="pointName">
<el-input
v-model="state.queryForm.pointName"
@@ -155,9 +170,10 @@
</template>
<script setup lang="ts" name="AssessmentPoint">
import { reactive, ref } from 'vue'
import { reactive, ref, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/assessmentpoint";
import { getList as getAssessmentCategoryList } from "/@/api/stuwork/assessmentcategory";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue'
@@ -169,6 +185,7 @@ const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref()
const showSearch = ref(true)
const categoryList = ref<any[]>([])
// 表格列配置
const tableColumns = [
@@ -194,9 +211,10 @@ const tableStyle = {
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }
}
// 配置 useTable
// 配置 useTable(分页接口支持 categortyId、pointName与考核项对应
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
categortyId: '',
pointName: ''
},
pageList: fetchList,
@@ -215,13 +233,28 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// 获取考核项列表与表单考核项对应接口assessmentcategory/list
const getCategoryList = async () => {
try {
const res = await getAssessmentCategoryList()
categoryList.value = res.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
categoryList.value = []
}
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.categortyId = ''
state.queryForm.pointName = ''
getDataList()
}
onMounted(() => {
getCategoryList()
})
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)

View File

@@ -69,8 +69,7 @@
v-model="form.score"
:precision="0"
:step="1"
:min="0"
placeholder="请输入分数"
placeholder="请输入分数(可为负数)"
style="width: 100%" />
</el-form-item>
</el-col>

View File

@@ -12,6 +12,53 @@
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="form.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="form.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 100%">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="班号" prop="classCode">
<el-select
v-model="form.classCode"
placeholder="请选择班号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classCode" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="标题" prop="title">
<el-input
@@ -52,9 +99,12 @@
</template>
<script setup lang="ts" name="ClassPlanFormDialog">
import { ref, reactive, nextTick } from 'vue'
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/classplan'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { getDicts } from '/@/api/admin/dict'
import Editor from '/@/components/Editor/index.vue'
const emit = defineEmits(['refresh'])
@@ -64,10 +114,16 @@ const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const classList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
schoolYear: '',
schoolTerm: '',
classCode: '',
title: '',
content: '',
remarks: ''
@@ -75,6 +131,15 @@ const form = reactive({
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
classCode: [
{ required: true, message: '请选择班号', trigger: 'change' }
],
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
@@ -92,6 +157,9 @@ const openDialog = async (type: string = 'add', row?: any) => {
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.schoolYear = ''
form.schoolTerm = ''
form.classCode = ''
form.title = ''
form.content = ''
form.remarks = ''
@@ -99,6 +167,9 @@ const openDialog = async (type: string = 'add', row?: any) => {
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.schoolYear = row.schoolYear || ''
form.schoolTerm = row.schoolTerm || ''
form.classCode = row.classCode || ''
form.title = row.title || ''
form.content = row.content || ''
form.remarks = row.remarks || ''
@@ -108,6 +179,9 @@ const openDialog = async (type: string = 'add', row?: any) => {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.schoolYear = res.data.schoolYear || form.schoolYear
form.schoolTerm = res.data.schoolTerm || form.schoolTerm
form.classCode = res.data.classCode || form.classCode
form.content = res.data.content || ''
form.remarks = res.data.remarks || ''
}
@@ -119,6 +193,43 @@ const openDialog = async (type: string = 'add', row?: any) => {
})
}
// 学年列表(班级管理-学年接口)
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
schoolYearList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
schoolYearList.value = []
}
}
// 学期字典(系统通用)
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
if (res?.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label ?? item.dictLabel ?? item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 班级列表(班级管理目录下)
const getClassListData = async () => {
try {
const res = await getClassListByRole()
classList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
classList.value = []
}
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
@@ -129,6 +240,9 @@ const onSubmit = async () => {
loading.value = true
try {
const submitData = {
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
classCode: form.classCode,
title: form.title,
content: form.content,
remarks: form.remarks
@@ -154,6 +268,13 @@ const onSubmit = async () => {
})
}
// 初始化:加载学年、学期、班级
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
getClassListData()
})
// 暴露方法
defineExpose({
openDialog

View File

@@ -79,7 +79,7 @@
<script setup lang="ts" name="ClassroomBaseArrangeDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editObj } from '/@/api/stuwork/classroombase'
import { addClassRoomAssign } from '/@/api/stuwork/teachclassroomassign'
import { queryAllClass } from '/@/api/basic/basicclass'
import { getClassRoomList } from '/@/api/stuwork/teachclassroom'
import { getBuildingList } from '/@/api/stuwork/teachbuilding'
@@ -150,17 +150,16 @@ const onSubmit = async () => {
loading.value = true
try {
await editObj({
id: form.id,
await addClassRoomAssign({
buildingNo: form.buildingNo,
classCode: form.classCode,
position: form.position
position: form.position,
classCode: form.classCode
})
useMessage().success('教室安排成功')
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || '教室安排失败')
} catch (_err) {
// 错误由 request 拦截器统一提示
} finally {
loading.value = false
}

View File

@@ -0,0 +1,327 @@
<template>
<el-dialog
title="教室公物编辑"
v-model="visible"
:width="800"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="120px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="楼号" prop="buildingNo">
<el-input
v-model="form.buildingNo"
placeholder="楼号"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="学院" prop="deptName">
<el-input
v-model="form.deptName"
placeholder="学院"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="班级" prop="classCode">
<el-input
v-model="form.classNo"
placeholder="班级"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="教室位置" prop="position">
<el-input
v-model="form.position"
placeholder="教室位置"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="讲台类型" prop="platformType">
<el-select
v-model="form.platformType"
placeholder="请选择讲台类型"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in platformTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="投影类型" prop="tyType">
<el-select
v-model="form.tyType"
placeholder="请选择投影类型"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in tyTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="电视机" prop="tvType">
<el-select
v-model="form.tvType"
placeholder="请选择电视机"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in tvTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="方凳数量" prop="chairCnt">
<el-input-number
v-model="form.chairCnt"
:min="0"
placeholder="请输入方凳数量"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="课桌数量" prop="tableCnt">
<el-input-number
v-model="form.tableCnt"
:min="0"
placeholder="请输入课桌数量"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="ClassroomAssetsDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editAssets } from '/@/api/stuwork/classassets'
import { getDicts } from '/@/api/admin/dict'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const platformTypeList = ref<any[]>([])
const tyTypeList = ref<any[]>([])
const tvTypeList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
buildingNo: '',
deptName: '',
deptCode: '',
classCode: '',
classNo: '',
position: '',
platformType: '',
tyType: '',
tvType: '',
chairCnt: 0,
tableCnt: 0,
remarks: ''
})
// 定义校验规则
const dataRules = {
platformType: [
{ required: true, message: '请选择讲台类型', trigger: 'change' }
],
tyType: [
{ required: true, message: '请选择投影类型', trigger: 'change' }
],
tvType: [
{ required: true, message: '请选择电视机', trigger: 'change' }
],
chairCnt: [
{ required: true, message: '请填写方凳数量', trigger: 'blur' }
],
tableCnt: [
{ required: true, message: '请填写课桌数量', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (row: any) => {
visible.value = true
resetForm()
// 填充现有数据
if (row) {
form.id = row.id || ''
form.buildingNo = row.buildingNo || ''
form.deptName = row.deptName || ''
form.deptCode = row.deptCode || ''
form.classCode = row.classCode || ''
form.classNo = row.classNo || ''
form.position = row.position || ''
form.platformType = row.platformType || ''
form.tyType = row.tyType || ''
form.tvType = row.tvType || ''
form.chairCnt = row.chairCnt || 0
form.tableCnt = row.tableCnt || 0
form.remarks = row.remarks || ''
}
nextTick(() => {
dataFormRef.value?.clearValidate()
})
}
// 重置表单
const resetForm = () => {
form.id = ''
form.buildingNo = ''
form.deptName = ''
form.deptCode = ''
form.classCode = ''
form.classNo = ''
form.position = ''
form.platformType = ''
form.tyType = ''
form.tvType = ''
form.chairCnt = 0
form.tableCnt = 0
form.remarks = ''
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
id: form.id,
buildingNo: form.buildingNo,
deptName: form.deptName,
deptCode: form.deptCode,
classCode: form.classCode,
classNo: form.classNo,
position: form.position,
platformType: form.platformType,
tyType: form.tyType,
tvType: form.tvType,
chairCnt: form.chairCnt,
tableCnt: form.tableCnt,
remarks: form.remarks
}
await editAssets(submitData)
useMessage().success('保存成功')
visible.value = false
emit('refresh')
} catch (_err) {
// 错误由 request 拦截器统一提示
} finally {
loading.value = false
}
})
}
// 获取讲台类型字典
const getPlatformTypeDict = async () => {
try {
const res = await getDicts('platform_type')
if (res.data) {
platformTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
platformTypeList.value = []
}
}
// 获取投影类型字典
const getTyTypeDict = async () => {
try {
const res = await getDicts('ty_type')
if (res.data) {
tyTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
tyTypeList.value = []
}
}
// 获取电视机类型字典
const getTvTypeDict = async () => {
try {
const res = await getDicts('tv_type')
if (res.data) {
tvTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
tvTypeList.value = []
}
}
// 初始化
onMounted(() => {
getPlatformTypeDict()
getTyTypeDict()
getTvTypeDict()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -181,19 +181,43 @@
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="120" align="center" fixed="right">
<el-table-column label="操作" width="380" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
v-if="!scope.row.classCode || !scope.row.position"
icon="Setting"
link
type="primary"
@click="handleArrange(scope.row)">
教室安排
</el-button>
<el-button
icon="Close"
link
type="danger"
@click="handleCancelArrange(scope.row)">
取消教室安排
</el-button>
<el-button
icon="Collection"
link
type="success"
@click="handleAssets(scope.row)"
class="ml10">
教室公物
</el-button>
<el-button
icon="Lock"
link
type="warning"
@click="handlePassword(scope.row)"
class="ml10">
门锁密码
</el-button>
</template>
</el-table-column>
<template #empty>
@@ -213,6 +237,10 @@
<!-- 教室安排表单弹窗 -->
<arrange-dialog ref="arrangeDialogRef" @refresh="getDataList" />
<!-- 教室公物编辑弹窗 -->
<assets-dialog ref="assetsDialogRef" @refresh="getDataList" />
<!-- 门锁密码编辑弹窗 -->
<password-dialog ref="passwordDialogRef" @refresh="getDataList" />
</div>
</template>
@@ -226,7 +254,10 @@ import { getDicts } from "/@/api/admin/dict";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import ArrangeDialog from './arrange.vue'
import { List, OfficeBuilding, CircleCheck, Location, UserFilled, Collection, Setting, Menu, Calendar, Search, Document } from '@element-plus/icons-vue'
import AssetsDialog from './assets.vue'
import PasswordDialog from './password.vue'
import { delClassRoomAssign } from '/@/api/stuwork/teachclassroomassign'
import { List, OfficeBuilding, CircleCheck, Location, UserFilled, Collection, Setting, Menu, Calendar, Search, Document, Close, Lock } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量内容
@@ -240,6 +271,8 @@ const platformTypeList = ref<any[]>([])
const tyTypeList = ref<any[]>([])
const tvTypeList = ref<any[]>([])
const arrangeDialogRef = ref()
const assetsDialogRef = ref()
const passwordDialogRef = ref()
// 表格列配置
const tableColumns = [
@@ -416,6 +449,33 @@ const handleArrange = (row: any) => {
arrangeDialogRef.value?.openDialog(row)
}
// 取消教室安排
const handleCancelArrange = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('确定要取消教室安排吗?')
await delClassRoomAssign(row)
useMessage().success('取消成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
if (!err?._messageShown) {
useMessage().error(err?.msg || '取消失败')
}
}
}
}
// 教室公物
const handleAssets = (row: any) => {
assetsDialogRef.value?.openDialog(row)
}
// 门锁密码
const handlePassword = (row: any) => {
passwordDialogRef.value?.openDialog(row)
}
// 获取系部列表
const getDeptListData = async () => {
try {

View File

@@ -0,0 +1,156 @@
<template>
<el-dialog
title="门锁密码"
v-model="visible"
:width="500"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="120px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="楼号">
<el-input
v-model="form.buildingNo"
placeholder="楼号"
disabled />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="教室位置">
<el-input
v-model="form.position"
placeholder="教室位置"
disabled />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="门锁密码" prop="password">
<el-input
v-model="form.password"
type="password"
show-password
placeholder="请输入门锁密码"
clearable />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="ClassroomPasswordDialog">
import { ref, reactive, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editAssets } from '/@/api/stuwork/classassets'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
// 提交表单数据
const form = reactive({
id: '',
buildingNo: '',
deptName: '',
deptCode: '',
classCode: '',
classNo: '',
position: '',
password: ''
})
// 定义校验规则
const dataRules = {
password: [
{ required: true, message: '请输入门锁密码', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (row: any) => {
visible.value = true
resetForm()
// 填充现有数据
if (row) {
form.id = row.id || ''
form.buildingNo = row.buildingNo || ''
form.deptName = row.deptName || ''
form.deptCode = row.deptCode || ''
form.classCode = row.classCode || ''
form.classNo = row.classNo || ''
form.position = row.position || ''
form.password = row.password || ''
}
nextTick(() => {
dataFormRef.value?.clearValidate()
})
}
// 重置表单
const resetForm = () => {
form.id = ''
form.buildingNo = ''
form.deptName = ''
form.deptCode = ''
form.classCode = ''
form.classNo = ''
form.position = ''
form.password = ''
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
// 只提交必要的字段用于修改门锁密码
const submitData = {
id: form.id,
buildingNo: form.buildingNo,
deptName: form.deptName,
deptCode: form.deptCode,
classCode: form.classCode,
classNo: form.classNo,
position: form.position,
password: form.password
}
await editAssets(submitData)
useMessage().success('保存成功')
visible.value = false
emit('refresh')
} catch (_err) {
// 错误由 request 拦截器统一提示
} finally {
loading.value = false
}
})
}
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -495,7 +495,9 @@ const confirmCheck = async () => {
checkDialogVisible.value = false
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '考核失败')
if (!err?._messageShown) {
useMessage().error(err?.msg || '考核失败')
}
}
}
@@ -512,7 +514,9 @@ const handleDelete = async (ids: string[]) => {
getDataList()
useMessage().success('删除成功')
} catch (err: any) {
useMessage().error(err.msg || '删除失败')
if (!err?._messageShown) {
useMessage().error(err?.msg || '删除失败')
}
}
}

View File

@@ -45,40 +45,6 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="form.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="班级" prop="classNo">
<el-select
v-model="form.classNo"
placeholder="请选择班级"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classNo">
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
@@ -95,8 +61,6 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { initObj } from '/@/api/stuwork/classtheme'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { getDeptList } from '/@/api/basic/basicclass'
import { getDicts } from '/@/api/admin/dict'
const emit = defineEmits(['refresh'])
@@ -107,15 +71,11 @@ const visible = ref(false)
const loading = ref(false)
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const deptList = ref<any[]>([])
const classList = ref<any[]>([])
// 提交表单数据
const form = reactive({
schoolYear: '',
schoolTerm: '',
deptCode: '',
classNo: ''
schoolTerm: ''
})
// 定义校验规则
@@ -125,12 +85,6 @@ const dataRules = {
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
deptCode: [
{ required: true, message: '请选择学院', trigger: 'change' }
],
classNo: [
{ required: true, message: '请选择班级', trigger: 'change' }
]
}
@@ -141,8 +95,6 @@ const openDialog = () => {
dataFormRef.value?.resetFields()
form.schoolYear = ''
form.schoolTerm = ''
form.deptCode = ''
form.classNo = ''
})
}
@@ -192,34 +144,10 @@ const getSchoolTermDict = async () => {
}
}
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
if (res.data && Array.isArray(res.data)) {
deptList.value = res.data
}
} catch (err) {
}
}
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
if (res.data && Array.isArray(res.data)) {
classList.value = res.data
}
} catch (err) {
}
}
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
getDeptListData()
getClassListData()
})
// 暴露方法

View File

@@ -17,7 +17,19 @@
</el-select>
</el-form-item>
<el-form-item label="宿舍号" prop="roomNo">
<el-input v-model="form.roomNo" placeholder="请输入宿舍号" />
<el-select
v-model="form.roomNo"
placeholder="请选择宿舍号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in roomList"
:key="item.roomNo"
:label="item.roomNo"
:value="item.roomNo">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="记录日期" prop="recordDate">
<el-date-picker
@@ -50,6 +62,7 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, putObj, getObj } from '/@/api/stuwork/dormhygienedaily'
import { getBuildingList } from '/@/api/stuwork/dormbuilding'
import { getRoomList } from '/@/api/stuwork/dormroom'
const emit = defineEmits(['refresh'])
@@ -59,6 +72,7 @@ const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const buildingList = ref<any[]>([])
const roomList = ref<any[]>([])
// 提交表单数据
const form = reactive({
@@ -75,7 +89,7 @@ const dataRules = {
{ required: true, message: '请选择楼号', trigger: 'change' }
],
roomNo: [
{ required: true, message: '请输入宿舍号', trigger: 'blur' }
{ required: true, message: '请选择宿舍号', trigger: 'change' }
],
recordDate: [
{ required: true, message: '请选择记录日期', trigger: 'change' }
@@ -159,9 +173,22 @@ const getBuildingListData = async () => {
}
}
// 获取宿舍号列表
const getRoomListData = async () => {
try {
const res = await getRoomList()
if (res.data) {
roomList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
roomList.value = []
}
}
// 初始化
onMounted(() => {
getBuildingListData()
getRoomListData()
})
// 暴露方法给父组件

View File

@@ -2,7 +2,19 @@
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="100px" v-loading="loading">
<el-form-item label="房间号" prop="roomNo">
<el-input v-model="form.roomNo" placeholder="请输入房间号" />
<el-select
v-model="form.roomNo"
placeholder="请选择房间号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in roomList"
:key="item.roomNo"
:label="item.roomNo"
:value="item.roomNo">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="整改时间" prop="reformDate">
<el-date-picker
@@ -31,9 +43,10 @@
</template>
<script setup lang="ts" name="DormReformFormDialog">
import { ref, reactive, nextTick } from 'vue'
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, putObj } from '/@/api/stuwork/dormreform'
import { getRoomList } from '/@/api/stuwork/dormroom'
const emit = defineEmits(['refresh'])
@@ -42,6 +55,7 @@ const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const roomList = ref<any[]>([])
// 提交表单数据
const form = reactive({
@@ -54,7 +68,7 @@ const form = reactive({
// 定义校验规则
const dataRules = {
roomNo: [
{ required: true, message: '请输入房间号', trigger: 'blur' }
{ required: true, message: '请选择房间号', trigger: 'change' }
],
reformDate: [
{ required: true, message: '请选择整改时间', trigger: 'change' }
@@ -90,6 +104,23 @@ const openDialog = (type: string = 'add', row?: any) => {
})
}
// 获取房间号列表
const getRoomListData = async () => {
try {
const res = await getRoomList()
if (res.data) {
roomList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
roomList.value = []
}
}
// 初始化
onMounted(() => {
getRoomListData()
})
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return

View File

@@ -89,9 +89,11 @@
style="width: 100%">
<el-option
v-for="item in bedNoList"
:key="item"
:label="item"
:value="item">
:key="item.bedNo"
:value="item.bedNo">
<span :class="{ 'bed-option-occupied': item.haveStudent }">
{{ item.bedNo }}{{ item.haveStudent ? ' (有人)' : '' }}
</span>
</el-option>
</el-select>
</el-form-item>
@@ -138,7 +140,8 @@ const loading = ref(false)
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const roomList = ref<any[]>([])
const bedNoList = ref<string[]>([])
// 床位列表:支持 haveStudent 标记true=有人false=无人)
const bedNoList = ref<Array<{ bedNo: string; haveStudent: boolean }>>([])
// 提交表单数据
const form = reactive({
@@ -206,28 +209,33 @@ const handleStudentChange = (stuNo: string) => {
}
}
// 房间号变化时获取床位号列表
// 房间号变化时获取床位号列表(支持 haveStudenttrue=有人false=无人)
const handleRoomChange = async (roomNo: string) => {
if (!roomNo) {
bedNoList.value = []
form.bedNo = ''
return
}
const toBedItem = (item: any): { bedNo: string; haveStudent: boolean } => {
if (typeof item === 'number' || typeof item === 'string') {
return { bedNo: String(item), haveStudent: false }
}
return {
bedNo: String(item?.bedNo ?? item?.value ?? item ?? ''),
haveStudent: !!(item && item.haveStudent === true)
}
}
try {
const res = await fearchRoomStuNum(roomNo)
if (res.data) {
// 根据返回的数据结构处理床位号列表
// 假设返回的是数字数组或对象数组
if (Array.isArray(res.data)) {
bedNoList.value = res.data.map((item: any) => {
if (typeof item === 'number' || typeof item === 'string') {
return String(item)
}
return String(item.bedNo || item.value || item)
})
bedNoList.value = res.data.map(toBedItem).filter((b) => b.bedNo)
} else if (res.data.bedNos && Array.isArray(res.data.bedNos)) {
bedNoList.value = res.data.bedNos.map((item: any) => String(item))
bedNoList.value = res.data.bedNos.map((item: any) =>
toBedItem(typeof item === 'object' ? item : { bedNo: String(item), haveStudent: false })
)
} else {
bedNoList.value = []
}
@@ -321,3 +329,8 @@ defineExpose({
})
</script>
<style scoped lang="scss">
.bed-option-occupied {
color: var(--el-color-warning);
}
</style>

View File

@@ -211,9 +211,13 @@
<el-icon><component :is="columnConfigMap[col.prop || '']?.icon || OfficeBuilding" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 床位号列特殊模板 -->
<!-- 床位号列haveStudent true 时变色标记有人 -->
<template v-if="col.prop === 'bedNo'" #default="scope">
<el-tag v-if="scope.row.bedNo" size="small" type="info" effect="plain">
<el-tag
v-if="scope.row.bedNo"
size="small"
:type="scope.row.haveStudent ? 'warning' : 'info'"
effect="plain">
{{ scope.row.bedNo }}
</el-tag>
<span v-else>-</span>
@@ -274,6 +278,12 @@
<!-- 转宿弹窗 -->
<TransferDialog ref="transferDialogRef" @refresh="getDataList" />
<!-- 宿舍互换弹窗 -->
<SwapDialog ref="swapDialogRef" @refresh="getDataList" />
<!-- 打印宿舍卡弹窗 -->
<PrintCardDialog ref="printCardDialogRef" />
</div>
</template>
@@ -281,7 +291,7 @@
import { reactive, ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObjs } from "/@/api/stuwork/dormroomstudent";
import { fetchList, delObjs, exportEmptyPeopleRoomExcel } from "/@/api/stuwork/dormroomstudent";
import { getDeptList } from "/@/api/basic/basicclass";
import { getBuildingList } from "/@/api/stuwork/dormbuilding";
import { fetchDormRoomTreeList } from "/@/api/stuwork/dormroom";
@@ -289,6 +299,8 @@ import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue';
import TransferDialog from './transfer.vue';
import SwapDialog from './swap.vue';
import PrintCardDialog from './printCard.vue';
import TreeSelect from '/@/components/TreeSelect/index.vue';
import { List, OfficeBuilding, House, Grid, UserFilled, Phone, CreditCard, Avatar, User, Setting, Menu, Search, Document } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
@@ -301,6 +313,8 @@ const route = useRoute()
const formDialogRef = ref()
const columnControlRef = ref<any>()
const transferDialogRef = ref()
const swapDialogRef = ref()
const printCardDialogRef = ref()
const searchFormRef = ref()
const showSearch = ref(true)
const deptList = ref<any[]>([])
@@ -425,29 +439,91 @@ const handleDormDataTypeChange = (dormdataType: string) => {
getDormRoomTreeListData(dormdataType)
}
// 打印宿舍卡
// 打印宿舍卡(按房间号查询后打印)
const handlePrintCard = () => {
useMessage().warning('功能开发中')
printCardDialogRef.value?.openDialog()
}
// 宿舍互换
// 宿舍互换(两名学生互换宿舍)
const handleRoomSwap = () => {
useMessage().warning('功能开发中')
const query = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
swapDialogRef.value?.openDialog(query)
}
// 导出
const handleExport = () => {
useMessage().warning('功能开发中')
// 导出:空 n 人宿舍导出(按当前筛选条件传参)
const handleExport = async () => {
try {
const params = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
dormdataType: searchForm.dormdataType,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
const res = await exportEmptyPeopleRoomExcel(params)
const blob = res instanceof Blob ? res : new Blob([res as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `空宿舍导出_${Date.now()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err?.msg || '导出失败')
}
}
// 名单导出
const handleExportList = () => {
useMessage().warning('功能开发中')
// 名单导出:与导出共用空 n 人宿舍导出接口,文件名区分
const handleExportList = async () => {
try {
const params = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
dormdataType: searchForm.dormdataType,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
const res = await exportEmptyPeopleRoomExcel(params)
const blob = res instanceof Blob ? res : new Blob([res as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `住宿学生名单_${Date.now()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err?.msg || '导出失败')
}
}
// 编辑(与转宿共用接口 edit修改房间/床位/是否舍长)
const handleEdit = (row: any) => {
transferDialogRef.value?.openDialog(row)
}
// 转宿
const handleTransfer = (row: any) => {
transferDialogRef.value.openDialog(row)
transferDialogRef.value?.openDialog(row)
}
// 退宿

View File

@@ -0,0 +1,152 @@
<template>
<el-dialog
title="打印宿舍卡"
v-model="visible"
:close-on-click-modal="false"
width="640px"
destroy-on-close
@closed="onClosed">
<div v-loading="loading">
<el-form :inline="true" class="mb16">
<el-form-item label="房间号">
<el-input v-model="roomNo" placeholder="请输入房间号" clearable style="width: 180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFetch">查询并预览</el-button>
</el-form-item>
</el-form>
<div v-if="printData.length" ref="printAreaRef" class="print-area">
<div v-for="(room, idx) in printData" :key="idx" class="room-card mb16">
<div class="room-title">房间号{{ room.roomNo }}</div>
<table class="print-table">
<thead>
<tr>
<th>姓名</th>
<th>学号</th>
<th>床位</th>
<th>是否舍长</th>
<th>班级</th>
</tr>
</thead>
<tbody>
<tr v-for="(s, i) in (room.dormRoomStudentVOList || [])" :key="i">
<td>{{ s.realName }}</td>
<td>{{ s.stuNo }}</td>
<td>{{ s.bedNo }}</td>
<td>{{ s.isLeader === '1' ? '是' : '否' }}</td>
<td>{{ s.className || s.classNo }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handlePrint" :disabled="!printData.length"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormRoomStudentPrintCard">
import { ref } from 'vue'
import { useMessage } from '/@/hooks/message'
import { printDormRoomData } from '/@/api/stuwork/dormroomstudent'
const visible = ref(false)
const loading = ref(false)
const roomNo = ref('')
const printData = ref<any[]>([])
const printAreaRef = ref<HTMLElement>()
const openDialog = (initialRoomNo?: string) => {
visible.value = true
roomNo.value = initialRoomNo || ''
printData.value = []
}
const onClosed = () => {
printData.value = []
}
const handleFetch = async () => {
const no = (roomNo.value || '').trim()
if (!no) {
useMessage().warning('请输入房间号')
return
}
loading.value = true
try {
const res = await printDormRoomData(no)
const data = res?.data ?? res
printData.value = Array.isArray(data) ? data : (data ? [data] : [])
if (!printData.value.length) {
useMessage().info('该房间暂无数据')
}
} catch (err: any) {
useMessage().error(err?.msg || '查询失败')
printData.value = []
} finally {
loading.value = false
}
}
const handlePrint = () => {
if (!printData.value.length) return
const win = window.open('', '_blank')
if (!win) {
useMessage().error('无法打开打印窗口')
return
}
const el = printAreaRef.value
if (el) {
win.document.write(`
<!DOCTYPE html><html><head><meta charset="utf-8"><title>宿舍卡</title>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #333; padding: 6px 8px; text-align: center; }
.room-title { font-weight: bold; margin-bottom: 8px; }
</style>
</head><body>${el.innerHTML}</body></html>
`)
win.document.close()
win.focus()
setTimeout(() => {
win.print()
win.close()
}, 300)
}
}
defineExpose({ openDialog })
</script>
<style scoped lang="scss">
.print-area {
max-height: 60vh;
overflow: auto;
}
.room-card {
padding: 12px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
}
.room-title {
margin-bottom: 8px;
}
.print-table {
width: 100%;
border-collapse: collapse;
th,
td {
border: 1px solid var(--el-border-color);
padding: 6px 8px;
text-align: center;
}
}
.mb16 {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<el-dialog
title="宿舍互换"
v-model="visible"
:close-on-click-modal="false"
draggable
width="520px">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
:validate-on-rule-change="false"
v-loading="loading">
<el-form-item label="学生一" prop="sourceSutNo">
<el-select
v-model="form.sourceSutNo"
placeholder="请选择学生(学号)"
clearable
filterable
style="width: 100%"
@change="onSourceChange">
<el-option
v-for="item in studentOptions"
:key="item.stuNo"
:label="`${item.realName}${item.stuNo}${item.roomNo || ''}`"
:value="item.stuNo" />
</el-select>
</el-form-item>
<el-form-item label="学生二" prop="targetStuNO">
<el-select
v-model="form.targetStuNO"
placeholder="请选择学生(学号)"
clearable
filterable
style="width: 100%"
@change="onTargetChange">
<el-option
v-for="item in studentOptions"
:key="item.stuNo"
:label="`${item.realName}${item.stuNo}${item.roomNo || ''}`"
:value="item.stuNo" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :loading="submitting"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormRoomStudentSwapDialog">
import { ref, reactive } from 'vue'
import { useMessage } from '/@/hooks/message'
import { exchangeRoom } from '/@/api/stuwork/dormroomstudent'
import { fetchList } from '/@/api/stuwork/dormroomstudent'
const emit = defineEmits(['refresh'])
const formRef = ref()
const visible = ref(false)
const loading = ref(false)
const submitting = ref(false)
const studentOptions = ref<any[]>([])
const form = reactive({
sourceSutNo: '',
targetStuNO: ''
})
const rules = {
sourceSutNo: [{ required: true, message: '请选择学生一', trigger: 'change' }],
targetStuNO: [{ required: true, message: '请选择学生二', trigger: 'change' }]
}
const onSourceChange = () => {
if (form.sourceSutNo && form.targetStuNO && form.sourceSutNo === form.targetStuNO) {
form.targetStuNO = ''
}
}
const onTargetChange = () => {
if (form.sourceSutNo && form.targetStuNO && form.sourceSutNo === form.targetStuNO) {
form.sourceSutNo = ''
}
}
const openDialog = async (queryParams?: any) => {
visible.value = true
form.sourceSutNo = ''
form.targetStuNO = ''
loading.value = true
try {
const res = await fetchList({
current: 1,
size: 500,
...queryParams
})
const list = res?.data?.records ?? res?.records ?? []
studentOptions.value = Array.isArray(list) ? list : []
} catch (err) {
studentOptions.value = []
} finally {
loading.value = false
}
}
const onSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
if (form.sourceSutNo === form.targetStuNO) {
useMessage().warning('请选择两名不同学生')
return
}
submitting.value = true
try {
await exchangeRoom({
sourceSutNo: form.sourceSutNo,
targetStuNO: form.targetStuNO
})
useMessage().success('互换成功')
visible.value = false
emit('refresh')
} catch (err: any) {
console.error(err)
} finally {
submitting.value = false
}
})
}
defineExpose({ openDialog })
</script>

View File

@@ -41,9 +41,11 @@
style="width: 100%">
<el-option
v-for="item in bedNoList"
:key="item"
:label="item"
:value="item">
:key="item.bedNo"
:value="item.bedNo">
<span :class="{ 'bed-option-occupied': item.haveStudent }">
{{ item.bedNo }}{{ item.haveStudent ? ' (有人)' : '' }}
</span>
</el-option>
</el-select>
</el-form-item>
@@ -95,7 +97,8 @@ const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const roomList = ref<any[]>([])
const bedNoList = ref<string[]>([])
// 床位列表:支持 haveStudent 标记true=有人false=无人)
const bedNoList = ref<Array<{ bedNo: string; haveStudent: boolean }>>([])
// 提交表单数据
const form = reactive({
@@ -119,26 +122,33 @@ const dataRules = {
]
}
// 房间号变化时获取床位号列表
// 房间号变化时获取床位号列表(支持 haveStudenttrue=有人false=无人)
const handleRoomChange = async (roomNo: string) => {
if (!roomNo) {
bedNoList.value = []
form.bedNo = ''
return
}
const toBedItem = (item: any): { bedNo: string; haveStudent: boolean } => {
if (typeof item === 'number' || typeof item === 'string') {
return { bedNo: String(item), haveStudent: false }
}
return {
bedNo: String(item?.bedNo ?? item?.value ?? item ?? ''),
haveStudent: !!(item && item.haveStudent === true)
}
}
try {
const res = await fearchRoomStuNum(roomNo)
if (res.data) {
if (Array.isArray(res.data)) {
bedNoList.value = res.data.map((item: any) => {
if (typeof item === 'number' || typeof item === 'string') {
return String(item)
}
return String(item.bedNo || item.value || item)
})
bedNoList.value = res.data.map(toBedItem).filter((b) => b.bedNo)
} else if (res.data.bedNos && Array.isArray(res.data.bedNos)) {
bedNoList.value = res.data.bedNos.map((item: any) => String(item))
bedNoList.value = res.data.bedNos.map((item: any) =>
toBedItem(typeof item === 'object' ? item : { bedNo: String(item), haveStudent: false })
)
} else {
bedNoList.value = []
}
@@ -157,20 +167,20 @@ const openDialog = async (row: any) => {
visible.value = true
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = row.id || ''
form.roomNo = row.roomNo || ''
await nextTick()
dataFormRef.value?.resetFields()
form.id = row.id || ''
form.roomNo = row.roomNo || ''
form.bedNo = row.bedNo || ''
form.stuNo = row.stuNo || ''
form.isLeader = row.isLeader || '0'
bedNoList.value = []
// 如果有房间号,先拉取床位列表再回填床位号(避免 handleRoomChange 清空 bedNo
if (form.roomNo) {
await handleRoomChange(form.roomNo)
form.bedNo = row.bedNo || ''
form.stuNo = row.stuNo || ''
form.isLeader = row.isLeader || '0'
bedNoList.value = []
// 如果有房间号,获取床位号列表
if (form.roomNo) {
handleRoomChange(form.roomNo)
}
})
}
}
// 提交表单
@@ -193,7 +203,8 @@ const onSubmit = async () => {
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || '转宿失败')
// 统一交给全局拦截器处理错误提示,避免在这里重复弹出一次
console.error('转宿失败', err)
} finally {
loading.value = false
}
@@ -223,3 +234,8 @@ defineExpose({
})
</script>
<style scoped lang="scss">
.bed-option-occupied {
color: var(--el-color-warning);
}
</style>

View File

@@ -0,0 +1,216 @@
<template>
<el-dialog
title="新增宿舍点名"
v-model="visible"
:width="600"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="点名类型" prop="type">
<el-select
v-model="form.type"
placeholder="请选择点名类型"
clearable
style="width: 100%">
<el-option label="普通住宿点名" value="1" />
<el-option label="留宿点名" value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="楼号" prop="buildId">
<el-select
v-model="form.buildId"
placeholder="请选择楼号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in buildingList"
:key="item.buildingNo"
:label="item.buildingNo"
:value="item.buildingNo" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="房间号" prop="roomNo">
<el-input
v-model="form.roomNo"
placeholder="请输入房间号"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="学生列表" prop="list">
<el-input
v-model="studentNosInput"
type="textarea"
:rows="5"
placeholder="请输入学号,多个学号用逗号或换行分隔"
style="width: 100%" />
<div class="form-tip">提示多个学号可用逗号或换行分隔</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormSignRecordFormDialog">
import { ref, reactive, nextTick, computed, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj } from '/@/api/stuwork/dormsignrecord'
import { getBuildingList } from '/@/api/stuwork/dormbuilding'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const buildingList = ref<any[]>([])
// 提交表单数据
const form = reactive({
type: '',
buildId: '',
roomNo: '',
list: [] as Array<{ stuNo: string }>
})
// 学生学号输入(用于显示和编辑)
const studentNosInput = ref('')
// 定义校验规则
const dataRules = {
type: [
{ required: true, message: '请选择点名类型', trigger: 'change' }
],
buildId: [
{ required: true, message: '请选择楼号', trigger: 'change' }
],
roomNo: [
{ required: true, message: '请输入房间号', trigger: 'blur' }
],
list: [
{ required: true, message: '请输入至少一个学号', trigger: 'change' },
{
validator: (rule: any, value: any, callback: any) => {
if (!value || value.length === 0) {
callback(new Error('请输入至少一个学号'))
} else {
callback()
}
},
trigger: 'change'
}
]
}
// 打开弹窗
const openDialog = async () => {
visible.value = true
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.type = ''
form.buildId = ''
form.roomNo = ''
form.list = []
studentNosInput.value = ''
})
}
// 解析学号输入(支持逗号和换行分隔)
const parseStudentNos = (input: string): string[] => {
if (!input || !input.trim()) return []
return input
.split(/[,\n]/)
.map(s => s.trim())
.filter(s => s.length > 0)
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
// 解析学号列表
const stuNos = parseStudentNos(studentNosInput.value)
if (stuNos.length === 0) {
useMessage().warning('请输入至少一个学号')
return
}
form.list = stuNos.map(stuNo => ({ stuNo }))
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
await addObj({
type: form.type,
buildId: form.buildId,
roomNo: form.roomNo,
list: form.list
})
useMessage().success('新增成功')
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || '新增失败')
}
} finally {
loading.value = false
}
})
}
// 获取楼号列表
const getBuildingListData = async () => {
try {
const res = await getBuildingList()
buildingList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
buildingList.value = []
}
}
// 初始化
onMounted(() => {
getBuildingListData()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,367 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 搜索表单卡片 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="state.queryForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="state.queryForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="楼号" prop="buildNo">
<el-select
v-model="state.queryForm.buildNo"
placeholder="请选择楼号"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in buildingList"
:key="item.buildingNo"
:label="item.buildingNo"
:value="item.buildingNo">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="房间号" prop="roomNo">
<el-input
v-model="state.queryForm.roomNo"
placeholder="请输入房间号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="state.queryForm.stuNo"
placeholder="请输入学号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="点名类型" prop="type">
<el-select
v-model="state.queryForm.type"
placeholder="请选择点名类型"
clearable
style="width: 200px">
<el-option label="普通住宿点名" value="1" />
<el-option label="留宿点名" value="2" />
</el-select>
</el-form-item>
<el-form-item label="考勤状态" prop="sign">
<el-select
v-model="state.queryForm.sign"
placeholder="请选择考勤状态"
clearable
style="width: 200px">
<el-option label="未到" :value="0" />
<el-option label="已到" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="考勤日期" prop="date">
<el-date-picker
v-model="state.queryForm.date"
type="date"
placeholder="请选择考勤日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
宿舍点名管理列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
新增点名
</el-button>
<el-button
icon="RefreshLeft"
type="warning"
class="ml10"
@click="handleInit">
初始化
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:width="col.width"
:min-width="col.minWidth"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
:align="col.align || 'center'">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<!-- 点名类型列特殊模板 -->
<template v-if="col.prop === 'type'" #default="scope">
<el-tag size="small" :type="scope.row.type === '1' ? 'primary' : 'success'" effect="plain">
{{ scope.row.type === '1' ? '普通住宿点名' : '留宿点名' }}
</el-tag>
</template>
<!-- 考勤状态列特殊模板 -->
<template v-else-if="col.prop === 'sign'" #default="scope">
<el-tag size="small" :type="scope.row.sign === '1' || scope.row.sign === 1 ? 'success' : 'danger'" effect="plain">
{{ scope.row.sign === '1' || scope.row.sign === 1 ? '已到' : '未到' }}
</el-tag>
</template>
<!-- 是否扫过脸列特殊模板 -->
<template v-else-if="col.prop === 'isFace'" #default="scope">
<el-tag size="small" :type="scope.row.isFace === '1' || scope.row.isFace === 1 ? 'success' : 'info'" effect="plain">
{{ scope.row.isFace === '1' || scope.row.isFace === 1 ? '是' : '否' }}
</el-tag>
</template>
<!-- 是否请假列特殊模板 -->
<template v-else-if="col.prop === 'isApply'" #default="scope">
<el-tag size="small" :type="scope.row.isApply === '1' || scope.row.isApply === 1 ? 'warning' : 'info'" effect="plain">
{{ scope.row.isApply === '1' || scope.row.isApply === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增点名弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="DormSignRecord">
import { reactive, ref, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, initDormStuInfoForAttendance } from "/@/api/stuwork/dormsignrecord";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { getDeptList } from "/@/api/basic/basicclass";
import { getBuildingList } from "/@/api/stuwork/dormbuilding";
import FormDialog from './form.vue'
import { Search, Document, List, Menu, RefreshLeft, OfficeBuilding, Grid, User, CreditCard, House, Check, Calendar, UserFilled } from '@element-plus/icons-vue'
import RightToolbar from '/@/components/RightToolbar/index.vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
import { useRoute } from 'vue-router'
const route = useRoute()
const formDialogRef = ref()
const showSearch = ref(true)
const searchFormRef = ref()
const columnControlRef = ref<any>()
const deptList = ref<any[]>([])
const buildingList = ref<any[]>([])
// 表格列配置
const tableColumns = [
{ prop: 'deptName', label: '学院', minWidth: 120, icon: OfficeBuilding },
{ prop: 'className', label: '班级', minWidth: 120, icon: Grid },
{ prop: 'stuName', label: '姓名', width: 100, icon: User },
{ prop: 'stuNo', label: '学号', width: 120, icon: CreditCard },
{ prop: 'roomNo', label: '房间号', width: 100, icon: House },
{ prop: 'buildId', label: '楼号', width: 80, icon: OfficeBuilding },
{ prop: 'type', label: '点名类型', width: 120, icon: Document },
{ prop: 'sign', label: '考勤状态', width: 100, icon: Check },
{ prop: 'date', label: '考勤日期', width: 120, icon: Calendar },
{ prop: 'isFace', label: '是否扫过脸', width: 100, icon: UserFilled },
{ prop: 'isApply', label: '是否请假', width: 100, icon: Document }
]
// 使用表格列控制
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange,
loadSavedConfig
} = useTableColumnControl(tableColumns, { storageKey: route.path })
// 立即加载配置
loadSavedConfig()
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
deptCode: '',
buildNo: '',
roomNo: '',
stuNo: '',
type: '',
sign: '',
date: ''
},
pageList: async (params: any) => {
const res = await fetchList(params)
const data = res?.data
return {
data: {
records: data?.records ?? [],
total: data?.total ?? 0
}
}
},
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true,
isPage: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.deptCode = ''
state.queryForm.buildNo = ''
state.queryForm.roomNo = ''
state.queryForm.stuNo = ''
state.queryForm.type = ''
state.queryForm.sign = ''
state.queryForm.date = ''
getDataList()
}
// 初始化
const handleInit = async () => {
const { confirm } = useMessageBox()
try {
await confirm('确定要初始化宿舍学生信息用于考勤吗?')
await initDormStuInfoForAttendance()
useMessage().success('初始化成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
if (!err?._messageShown) {
useMessage().error(err?.msg || '初始化失败')
}
}
}
}
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
deptList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
deptList.value = []
}
}
// 获取楼号列表
const getBuildingListData = async () => {
try {
const res = await getBuildingList()
buildingList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
buildingList.value = []
}
}
// 初始化
onMounted(() => {
getDeptListData()
getBuildingListData()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -184,11 +184,24 @@ const TimeRuleTableComponent = defineComponent({
default: ({ row }: any) => {
const timeList = getTimeList(row)
return h('div', { class: 'time-slots-container' },
timeList.map((timeSlot: any, index: number) =>
h('div', { key: index, class: 'time-slot-item' }, [
timeList.map((timeSlot: any, index: number) => {
// 通过数组索引更新,确保响应式
const updateStartTime = (val: string) => {
const list = getTimeList(row)
if (list[index]) {
list[index].startTime = val || ''
}
}
const updateEndTime = (val: string) => {
const list = getTimeList(row)
if (list[index]) {
list[index].endTime = val || ''
}
}
return h('div', { key: index, class: 'time-slot-item' }, [
h(ElTimePicker, {
modelValue: timeSlot.startTime,
'onUpdate:modelValue': (val: string) => { timeSlot.startTime = val },
modelValue: timeSlot.startTime || '',
'onUpdate:modelValue': updateStartTime,
format: 'HH:mm',
valueFormat: 'HH:mm',
placeholder: '开始时间',
@@ -196,8 +209,8 @@ const TimeRuleTableComponent = defineComponent({
}),
h('span', { style: { margin: '0 10px' } }, '至'),
h(ElTimePicker, {
modelValue: timeSlot.endTime,
'onUpdate:modelValue': (val: string) => { timeSlot.endTime = val },
modelValue: timeSlot.endTime || '',
'onUpdate:modelValue': updateEndTime,
format: 'HH:mm',
valueFormat: 'HH:mm',
placeholder: '结束时间',
@@ -213,7 +226,7 @@ const TimeRuleTableComponent = defineComponent({
style: { marginLeft: '10px' }
})
])
)
})
)
}
}),

View File

@@ -0,0 +1,144 @@
<template>
<el-dialog
:title="form.id ? '编辑文件夹' : '新建文件夹'"
v-model="visible"
:width="500"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="文件夹名称" prop="classification">
<el-input
v-model="form.classification"
placeholder="请输入文件夹名称"
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="FileManagerFolderDialog">
import { ref, reactive, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editFile, getDetail } from '/@/api/stuwork/filemanager'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
// 提交表单数据
const form = reactive({
id: '',
classification: '',
parentId: null as string | null
})
// 定义校验规则
const dataRules = {
classification: [
{ required: true, message: '请输入文件夹名称', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.classification = ''
form.parentId = row?.parentId ?? null
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.classification = row.classification || ''
form.parentId = row.parentId || null
// 如果需要获取详情
if (row.id && !row.classification) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.classification = res.data.classification || ''
form.parentId = res.data.parentId || form.parentId
}
}).finally(() => {
loading.value = false
})
}
}
})
}
// 提交表单(新增文件夹需要通过新增文件接口,但只传 classification 和 parentId
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
if (operType.value === 'add') {
// 新增文件夹:使用 addObj但只传 classification 和 parentId
const { addObj } = await import('/@/api/stuwork/filemanager')
await addObj({
classification: form.classification,
parentId: form.parentId || '-1',
level: '0' // 文件夹层级为 0
})
useMessage().success('新建成功')
} else {
// 编辑文件夹:使用 editFile 接口
await editFile({
id: form.id,
parentId: form.parentId || '-1',
classification: form.classification
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || (operType.value === 'add' ? '新建失败' : '编辑失败'))
}
} finally {
loading.value = false
}
})
}
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,181 @@
<template>
<el-dialog
:title="form.id ? '编辑文件' : '上传文件'"
v-model="visible"
:width="600"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="文件名称" prop="fileName">
<el-input
v-model="form.fileName"
placeholder="请输入文件名称"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="文件上传" prop="fileUrl">
<Upload
v-model="form.fileUrl"
:limit="1"
uploadFileUrl="/stuwork/file/upload"
type="default" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注"
:maxlength="250"
show-word-limit
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="FileManagerFormDialog">
import { ref, reactive, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/filemanager'
import Upload from '/@/components/Upload/index.vue'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
// 提交表单数据
const form = reactive({
id: '',
fileName: '',
fileUrl: '',
remarks: '',
parentId: null as string | null,
level: '1'
})
// 定义校验规则
const dataRules = {
fileName: [
{ required: true, message: '请输入文件名称', trigger: 'blur' }
],
fileUrl: [
{ required: true, message: '请上传文件', trigger: 'change' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.fileName = ''
form.fileUrl = ''
form.remarks = ''
form.parentId = row?.parentId ?? null
form.level = row?.level || '1'
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.fileName = row.fileName || ''
form.fileUrl = row.fileUrl || ''
form.remarks = row.remarks || ''
form.parentId = row.parentId || null
form.level = row.level || '1'
// 如果需要获取详情
if (row.id && !row.fileUrl) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.fileName = res.data.fileName || form.fileName
form.fileUrl = res.data.fileUrl || ''
form.remarks = res.data.remarks || ''
form.parentId = res.data.parentId || form.parentId
form.level = res.data.level || form.level
}
}).finally(() => {
loading.value = false
})
}
}
})
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
fileName: form.fileName,
fileUrl: form.fileUrl,
remarks: form.remarks,
parentId: form.parentId || '-1',
level: form.level
}
if (operType.value === 'add') {
await addObj(submitData)
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
...submitData
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
}
} finally {
loading.value = false
}
})
}
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,345 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
学工文件管理
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="handleAddFolder">
新建文件夹
</el-button>
<el-button
icon="FolderAdd"
type="success"
class="ml10"
@click="handleAddFile">
上传文件
</el-button>
<el-button
icon="Refresh"
class="ml10"
@click="getDataList">
刷新
</el-button>
</div>
</div>
</template>
<!-- 面包屑导航 -->
<div class="breadcrumb-wrapper mb16" v-if="breadcrumbList.length > 0">
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<el-button link @click="handleBreadcrumbClick(null)">
<el-icon><Folder /></el-icon>
根目录
</el-button>
</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.id">
<el-button
link
@click="handleBreadcrumbClick(item)"
:disabled="index === breadcrumbList.length - 1">
{{ item.classification || item.fileName }}
</el-button>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column prop="classification" label="文件夹名称" min-width="200" show-overflow-tooltip>
<template #header>
<el-icon><Folder /></el-icon>
<span style="margin-left: 4px">文件夹名称</span>
</template>
<template #default="scope">
<span v-if="scope.row.classification">{{ scope.row.classification }}</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column prop="fileName" label="文件名称" min-width="200" show-overflow-tooltip>
<template #header>
<el-icon><Document /></el-icon>
<span style="margin-left: 4px">文件名称</span>
</template>
<template #default="scope">
<span v-if="scope.row.fileName">{{ scope.row.fileName }}</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column prop="level" label="层级" width="80" align="center">
<template #header>
<el-icon><Grid /></el-icon>
<span style="margin-left: 4px">层级</span>
</template>
</el-table-column>
<el-table-column prop="remarks" label="备注" min-width="150" show-overflow-tooltip>
<template #header>
<el-icon><EditPen /></el-icon>
<span style="margin-left: 4px">备注</span>
</template>
<template #default="scope">
<span v-if="scope.row.remarks">{{ scope.row.remarks }}</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" align="center">
<template #header>
<el-icon><Clock /></el-icon>
<span style="margin-left: 4px">创建时间</span>
</template>
</el-table-column>
<el-table-column label="操作" width="250" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<!-- 文件夹进入编辑删除 -->
<template v-if="scope.row.classification">
<el-button
icon="FolderOpened"
link
type="primary"
@click="handleEnterFolder(scope.row)">
进入
</el-button>
<el-button
icon="Edit"
link
type="primary"
@click="handleEditFolder(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
<!-- 文件下载编辑删除 -->
<template v-else>
<el-button
icon="Download"
link
type="primary"
@click="handleDownload(scope.row)">
下载
</el-button>
<el-button
icon="Edit"
link
type="primary"
@click="handleEditFile(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 新增/编辑文件弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
<!-- 新增/编辑文件夹弹窗 -->
<folder-dialog ref="folderDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="FileManager">
import { reactive, ref, onMounted, computed } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/filemanager";
import { useMessage, useMessageBox } from "/@/hooks/message";
import FormDialog from './form.vue'
import FolderDialog from './folder.vue'
import { List, Document, Folder, Grid, EditPen, Clock, Setting, FolderOpened, Download } from '@element-plus/icons-vue'
// 定义变量内容
const formDialogRef = ref()
const folderDialogRef = ref()
const currentParentId = ref<string | null>(null)
const breadcrumbList = ref<any[]>([])
// 配置 useTable接口返回数组非分页结构
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
parentId: null as string | null
},
pageList: async (params: any) => {
const res = await fetchList(params)
// 数据链是 data.data.records需要正确解析
let list: any[] = []
if (res?.data) {
// 如果 res.data 是数组,直接使用
if (Array.isArray(res.data)) {
list = res.data
}
// 如果 res.data.data 存在且是对象,尝试取 records
else if (res.data.data && res.data.data.records && Array.isArray(res.data.data.records)) {
list = res.data.data.records
}
// 如果 res.data.records 存在且是数组,直接使用
else if (res.data.records && Array.isArray(res.data.records)) {
list = res.data.records
}
}
return {
data: list // isPage: false 时,直接返回数组
}
},
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true,
isPage: false // 不分页
})
// table hook
const {
getDataList,
tableStyle
} = useTable(state)
// 进入文件夹
const handleEnterFolder = (row: any) => {
currentParentId.value = row.id
breadcrumbList.value.push(row)
state.queryForm.parentId = row.id
getDataList()
}
// 面包屑点击
const handleBreadcrumbClick = (item: any | null) => {
if (item === null) {
// 返回根目录
currentParentId.value = null
breadcrumbList.value = []
state.queryForm.parentId = null
} else {
// 返回指定目录
const index = breadcrumbList.value.findIndex(b => b.id === item.id)
if (index >= 0) {
breadcrumbList.value = breadcrumbList.value.slice(0, index + 1)
currentParentId.value = item.id
state.queryForm.parentId = item.id
}
}
getDataList()
}
// 新增文件夹
const handleAddFolder = () => {
folderDialogRef.value?.openDialog('add', { parentId: currentParentId.value })
}
// 新增文件
const handleAddFile = () => {
formDialogRef.value?.openDialog('add', { parentId: currentParentId.value })
}
// 编辑文件夹
const handleEditFolder = (row: any) => {
folderDialogRef.value?.openDialog('edit', row)
}
// 编辑文件
const handleEditFile = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// 下载文件
const handleDownload = (row: any) => {
if (!row.fileUrl) {
useMessage().warning('文件地址无效')
return
}
// fileUrl 可能是完整 URL 或相对路径
if (row.fileUrl.startsWith('http://') || row.fileUrl.startsWith('https://')) {
window.open(row.fileUrl, '_blank')
} else {
// 相对路径,需要拼接 baseURL
const baseURL = import.meta.env.VITE_API_URL || ''
const url = baseURL + (row.fileUrl.startsWith('/') ? row.fileUrl : '/' + row.fileUrl)
window.open(url, '_blank')
}
}
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
const name = row.classification || row.fileName || '该项'
await confirm(`确定要删除${name}吗?`)
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err?.msg || '删除失败')
}
}
}
// 初始化
onMounted(() => {
getDataList()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.breadcrumb-wrapper {
padding: 12px 16px;
background: #f5f7fa;
border-radius: 4px;
:deep(.el-breadcrumb) {
.el-breadcrumb__item {
.el-button {
padding: 0;
font-size: 14px;
}
}
}
}
.mb16 {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,227 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选 -->
<el-card class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
统计条件
</span>
</div>
</template>
<el-form :model="queryForm" :inline="true" class="search-form">
<el-form-item label="毕业年份" prop="graduYear">
<el-select
v-model="queryForm.graduYear"
placeholder="请选择毕业年份"
clearable
style="width: 160px">
<el-option
v-for="y in graduYearOptions"
:key="y"
:label="y + '年'"
:value="y" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="loadStatistics">查询统计</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 汇总卡片 -->
<el-row :gutter="16" class="summary-row">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-label">毕业生总数</div>
<div class="stat-value">{{ summary.total }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-success">
<div class="stat-label">确认毕业</div>
<div class="stat-value">{{ summary.confirmed }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-warning">
<div class="stat-label">待确认</div>
<div class="stat-value">{{ summary.pending }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-danger">
<div class="stat-label">不可毕业</div>
<div class="stat-value">{{ summary.rejected }}</div>
</el-card>
</el-col>
</el-row>
<!-- 按学院统计表格 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
按学院统计
</span>
</div>
</template>
<el-table
:data="deptStats"
v-loading="loading"
stripe
border
class="modern-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="deptName" label="学院" min-width="140" show-overflow-tooltip />
<el-table-column prop="total" label="应毕业人数" width="110" align="center" />
<el-table-column prop="pending" label="待确认" width="90" align="center" />
<el-table-column prop="confirmed" label="确认毕业" width="100" align="center" />
<el-table-column prop="rejected" label="不可毕业" width="100" align="center" />
<el-table-column prop="completionRate" label="完成率" width="100" align="center">
<template #default="scope">
<span :class="completionRateClass(scope.row.completionRate)">
{{ scope.row.completionRate }}
</span>
</template>
</el-table-column>
</el-table>
<template #empty>
<el-empty description="请选择毕业年份并点击「查询统计」" :image-size="100" />
</template>
</el-card>
</div>
</div>
</template>
<script setup lang="ts" name="GradustuAnalyse">
import { reactive, ref, computed, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { fetchListForAnalyse } from '/@/api/stuwork/gradustu'
import { Search, Document } from '@element-plus/icons-vue'
const queryForm = reactive({
graduYear: ''
})
const loading = ref(false)
const rawList = ref<any[]>([])
const graduYearOptions = computed(() => {
const y = new Date().getFullYear()
return Array.from({ length: 11 }, (_, i) => y - 5 + i)
})
// 汇总:总人数、确认毕业、待确认、不可毕业
const summary = computed(() => {
const list = rawList.value
let pending = 0
let confirmed = 0
let rejected = 0
list.forEach((item: any) => {
const s = String(item.status ?? '')
if (s === '0') pending++
else if (s === '1') confirmed++
else if (s === '-1') rejected++
})
return {
total: list.length,
pending,
confirmed,
rejected
}
})
// 按学院聚合
const deptStats = computed(() => {
const list = rawList.value
const map: Record<string, { deptCode: string; deptName: string; total: number; pending: number; confirmed: number; rejected: number }> = {}
list.forEach((item: any) => {
const code = item.deptCode || '未知'
const name = item.deptName || item.deptCode || '未知'
if (!map[code]) {
map[code] = { deptCode: code, deptName: name, total: 0, pending: 0, confirmed: 0, rejected: 0 }
}
const row = map[code]
row.total++
const s = String(item.status ?? '')
if (s === '0') row.pending++
else if (s === '1') row.confirmed++
else if (s === '-1') row.rejected++
})
return Object.values(map).map((row) => ({
...row,
completionRate: row.total > 0 ? ((row.confirmed / row.total) * 100).toFixed(1) + '%' : '0%'
}))
})
const completionRateClass = (rate: string) => {
const num = parseFloat(rate)
if (num >= 100) return 'rate-high'
if (num >= 80) return 'rate-mid'
return 'rate-low'
}
const loadStatistics = async () => {
if (!queryForm.graduYear) {
useMessage().warning('请选择毕业年份')
return
}
loading.value = true
try {
const list = await fetchListForAnalyse(queryForm.graduYear)
rawList.value = list
} catch (err: any) {
useMessage().error(err.msg || '获取数据失败')
rawList.value = []
} finally {
loading.value = false
}
}
onMounted(() => {
queryForm.graduYear = String(new Date().getFullYear())
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.summary-row {
margin-bottom: 16px;
}
.stat-card {
text-align: center;
.stat-label {
font-size: 14px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 24px;
font-weight: 600;
margin-top: 8px;
}
&.stat-success .stat-value {
color: var(--el-color-success);
}
&.stat-warning .stat-value {
color: var(--el-color-warning);
}
&.stat-danger .stat-value {
color: var(--el-color-danger);
}
}
.rate-high {
color: var(--el-color-success);
font-weight: 500;
}
.rate-mid {
color: var(--el-color-warning);
}
.rate-low {
color: var(--el-color-danger);
}
</style>

View File

@@ -0,0 +1,342 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="handleSearch"
class="search-form">
<el-form-item label="毕业年份" prop="graduYear">
<el-select
v-model="searchForm.graduYear"
placeholder="请选择毕业年份"
clearable
filterable
style="width: 140px">
<el-option
v-for="y in graduYearOptions"
:key="y"
:label="y + '年'"
:value="y" />
</el-select>
</el-form-item>
<el-form-item label="毕业状态" prop="status">
<el-select
v-model="searchForm.status"
placeholder="请选择"
clearable
style="width: 140px">
<el-option label="待确认" value="0" />
<el-option label="确认毕业" value="1" />
<el-option label="不可毕业" value="-1" />
</el-select>
</el-form-item>
<el-form-item label="毕业类型" prop="type">
<el-select
v-model="searchForm.type"
placeholder="请选择"
clearable
style="width: 120px">
<el-option label="段段清" value="1" />
<el-option label="正常毕业" value="2" />
</el-select>
</el-form-item>
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="searchForm.stuNo"
placeholder="请输入学号"
clearable
style="width: 140px" />
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input
v-model="searchForm.realName"
placeholder="请输入姓名"
clearable
style="width: 120px" />
</el-form-item>
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="searchForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 160px">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode" />
</el-select>
</el-form-item>
<el-form-item label="班号" prop="classNo">
<el-input
v-model="searchForm.classNo"
placeholder="请输入班号"
clearable
style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
学生毕业处理列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
:loading="makeLoading"
@click="handleMakeGraduStu">
生成毕业生信息
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<template v-if="col.prop === 'status'" #default="scope">
<el-tag :type="statusTagType(scope.row.status)" size="small">
{{ formatStatus(scope.row.status) }}
</el-tag>
</template>
<template v-else-if="col.prop === 'type'" #default="scope">
{{ scope.row.type === '1' ? '段段清' : scope.row.type === '2' ? '正常毕业' : scope.row.type || '-' }}
</template>
</el-table-column>
</template>
<template #empty>
<el-empty description="暂无数据,请选择条件查询或先生成毕业生信息" :image-size="120" />
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts" name="Gradustu">
import { reactive, ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList, makeGraduStu } from '/@/api/stuwork/gradustu'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { getDeptList } from '/@/api/basic/basicclass'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import {
List,
Calendar,
UserFilled,
Document,
Setting,
Menu,
Search,
Grid
} from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
const makeLoading = ref(false)
const deptList = ref<any[]>([])
// 毕业年份:当前年前后各 5 年
const graduYearOptions = computed(() => {
const y = new Date().getFullYear()
return Array.from({ length: 11 }, (_, i) => y - 5 + i)
})
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'stuNo', label: '学号', icon: UserFilled },
{ prop: 'realName', label: '姓名', icon: UserFilled },
{ prop: 'classNo', label: '班号', icon: Grid },
{ prop: 'majorName', label: '专业名称', icon: Document, minWidth: 140 },
{ prop: 'graduYear', label: '毕业年份', icon: Calendar },
{ prop: 'status', label: '毕业状态', icon: Document },
{ prop: 'type', label: '毕业类型', icon: Document },
{ prop: 'scoreCondition', label: '学分情况', icon: Document, minWidth: 100 },
{ prop: 'skillCondition', label: '技能情况', icon: Document, minWidth: 100 },
{ prop: 'conductCondition', label: '操行情况', icon: Document, minWidth: 100 },
{ prop: 'specialGradu', label: '特殊毕业', icon: Document, minWidth: 100 }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单
const searchForm = reactive({
graduYear: '',
status: '',
type: '',
stuNo: '',
realName: '',
deptCode: '',
classNo: ''
})
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
const formatStatus = (status: string) => {
const map: Record<string, string> = { '0': '待确认', '1': '确认毕业', '-1': '不可毕业' }
return map[status] || status || '-'
}
const statusTagType = (status: string) => {
const map: Record<string, string> = { '0': 'warning', '1': 'success', '-1': 'danger' }
return map[status] || 'info'
}
const handleSearch = () => {
getDataList()
}
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.graduYear = ''
searchForm.status = ''
searchForm.type = ''
searchForm.stuNo = ''
searchForm.realName = ''
searchForm.deptCode = ''
searchForm.classNo = ''
getDataList()
}
const handleMakeGraduStu = async () => {
try {
await useMessageBox().confirm('确定要生成毕业生信息吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
makeLoading.value = true
try {
await makeGraduStu({ type: '0' })
useMessage().success('生成成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '生成失败')
} finally {
makeLoading.value = false
}
}
const loadDeptList = async () => {
try {
const res = await getDeptList()
deptList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
deptList.value = []
}
}
onMounted(() => {
loadDeptList()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,253 @@
<template>
<el-dialog
:title="form.id ? '编辑' : '新增'"
v-model="visible"
:width="800"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="form.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="form.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 100%">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="标题" prop="title">
<el-input
v-model="form.title"
placeholder="请输入标题"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="作者" prop="author">
<el-input
v-model="form.author"
placeholder="请输入作者"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="内容" prop="content">
<Editor
v-model:getHtml="form.content"
:height="'400'"
placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="MoralPlanFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/moralplan'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getDicts } from '/@/api/admin/dict'
import Editor from '/@/components/Editor/index.vue'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
schoolYear: '',
schoolTerm: '',
title: '',
content: '',
author: ''
})
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入内容', trigger: 'blur' }
],
author: [
{ required: true, message: '请输入作者', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.schoolYear = ''
form.schoolTerm = ''
form.title = ''
form.content = ''
form.author = ''
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.schoolYear = row.schoolYear || ''
form.schoolTerm = row.schoolTerm || ''
form.title = row.title || ''
form.content = row.content || ''
form.author = row.author || ''
// 如果需要获取详情
if (row.id && !row.content) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.schoolYear = res.data.schoolYear || form.schoolYear
form.schoolTerm = res.data.schoolTerm || form.schoolTerm
form.title = res.data.title || ''
form.content = res.data.content || ''
form.author = res.data.author || ''
}
}).finally(() => {
loading.value = false
})
}
}
})
}
// 学年列表(班级管理-学年接口)
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
schoolYearList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
schoolYearList.value = []
}
}
// 学期字典(系统通用)
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
if (res?.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label ?? item.dictLabel ?? item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
title: form.title,
content: form.content,
author: form.author
}
if (operType.value === 'add') {
await addObj(submitData)
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
...submitData
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
}
} finally {
loading.value = false
}
})
}
// 初始化:加载学年、学期
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,356 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 搜索表单卡片 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="state.queryForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="state.queryForm.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="state.queryForm.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input
v-model="state.queryForm.title"
placeholder="请输入标题"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input
v-model="state.queryForm.content"
placeholder="请输入内容"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input
v-model="state.queryForm.author"
placeholder="请输入作者"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
德育计划列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:width="col.width"
:min-width="col.minWidth"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
:align="col.align || 'center'">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<!-- 学期列特殊模板 -->
<template v-if="col.prop === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ formatSchoolTerm(scope.row.schoolTerm) }}
</el-tag>
</template>
<!-- 内容列特殊模板可能较长截断显示 -->
<template v-else-if="col.prop === 'content'" #default="scope">
<span :title="scope.row.content">{{ scope.row.content ? (scope.row.content.length > 50 ? scope.row.content.substring(0, 50) + '...' : scope.row.content) : '-' }}</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑表单弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="MoralPlan">
import { reactive, ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/moralplan";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { getDicts } from "/@/api/admin/dict";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue'
import { List, Calendar, Clock, Document, User, Setting, Menu, Search } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量内容
const route = useRoute()
const formDialogRef = ref()
const columnControlRef = ref<any>()
const searchFormRef = ref()
const showSearch = ref(true)
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '学年' },
{ prop: 'schoolTerm', label: '学期' },
{ prop: 'title', label: '标题', minWidth: 200 },
{ prop: 'content', label: '内容', minWidth: 250 },
{ prop: 'author', label: '作者' },
{ prop: 'createTime', label: '创建时间', width: 180 },
{ prop: 'updateTime', label: '更新时间', width: 180 }
]
// 列配置映射(用于图标)
const columnConfigMap: Record<string, { icon: any }> = {
schoolYear: { icon: Calendar },
schoolTerm: { icon: Clock },
title: { icon: Document },
content: { icon: Document },
author: { icon: User },
createTime: { icon: Clock },
updateTime: { icon: Clock }
}
// 使用表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, { storageKey: route.path })
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
schoolYear: '',
schoolTerm: '',
title: '',
content: '',
author: ''
},
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
}
const dictItem = schoolTermList.value.find(item => item.value == value)
return dictItem ? dictItem.label : value
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.schoolYear = ''
state.queryForm.schoolTerm = ''
state.queryForm.title = ''
state.queryForm.content = ''
state.queryForm.author = ''
getDataList()
}
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('确定要删除该德育计划吗?')
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败')
}
}
}
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
if (res.data && Array.isArray(res.data)) {
schoolYearList.value = res.data
} else {
schoolYearList.value = []
}
} catch (err) {
schoolYearList.value = []
}
}
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
if (res.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,270 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="120px" v-loading="loading">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="form.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="类别编码" prop="categoryCode">
<el-select
v-model="form.categoryCode"
placeholder="请选择类别"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in categoryList"
:key="item.categoryCode"
:label="item.categoryName"
:value="item.categoryCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="封面" prop="bookImg">
<upload-img
v-model="form.bookImg"
:width="'150px'"
:height="'200px'"
upload-file-url="/admin/sys-file/upload" />
</el-form-item>
<el-form-item label="书名" prop="bookName">
<el-input
v-model="form.bookName"
placeholder="请输入书名"
clearable
style="width: 100%" />
</el-form-item>
<el-form-item label="作者" prop="bookAuthor">
<el-input
v-model="form.bookAuthor"
placeholder="请输入作者"
clearable
style="width: 100%" />
</el-form-item>
<el-form-item label="链接" prop="webUrl">
<el-input
v-model="form.webUrl"
placeholder="请输入链接例如https://www.example.com"
clearable
style="width: 100%" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="250"
show-word-limit
style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="OnlineBooksFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/onlinebooks'
import request from '/@/utils/request'
import UploadImg from '/@/components/Upload/Image.vue'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const deptList = ref<any[]>([])
const categoryList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
deptCode: '',
categoryCode: '',
bookImg: '',
bookName: '',
bookAuthor: '',
webUrl: '',
remarks: ''
})
// URL验证函数
const validateUrl = (rule: any, value: any, callback: any) => {
if (!value) {
callback(new Error('链接不能为空'))
return
}
const urlPattern = /^(https?:\/\/)?([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?$/i
if (urlPattern.test(value)) {
callback()
} else {
callback(new Error('请输入合法的网址例如https://www.baidu.com'))
}
}
// 定义校验规则
const dataRules = {
deptCode: [
{ required: true, message: '请选择学院', trigger: 'change' }
],
categoryCode: [
{ required: true, message: '请选择类别', trigger: 'change' }
],
bookName: [
{ required: true, message: '请输入书名', trigger: 'blur' }
],
bookAuthor: [
{ required: true, message: '请输入作者', trigger: 'blur' }
],
webUrl: [
{ validator: validateUrl, trigger: 'blur' }
],
remarks: [
{ max: 250, message: '备注不能超过250个字符', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.deptCode = ''
form.categoryCode = ''
form.bookImg = ''
form.bookName = ''
form.bookAuthor = ''
form.webUrl = ''
form.remarks = ''
// 编辑时填充数据
if (type === 'edit' && row) {
loading.value = true
getDetail(row.id)
.then((res: any) => {
if (res.data) {
form.id = res.data.id || row.id
form.deptCode = res.data.deptCode || ''
form.categoryCode = res.data.categoryCode || ''
form.bookImg = res.data.bookImg || ''
form.bookName = res.data.bookName || ''
form.bookAuthor = res.data.bookAuthor || ''
form.webUrl = res.data.webUrl || ''
form.remarks = res.data.remarks || ''
}
})
.catch((err: any) => {
useMessage().error(err.msg || '获取详情失败')
visible.value = false
})
.finally(() => {
loading.value = false
})
}
})
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData: any = {
deptCode: form.deptCode,
categoryCode: form.categoryCode,
bookImg: form.bookImg,
bookName: form.bookName,
bookAuthor: form.bookAuthor,
webUrl: form.webUrl,
remarks: form.remarks
}
if (operType.value === 'add') {
await addObj(submitData)
useMessage().success('新增成功')
} else {
submitData.id = form.id
await editObj(submitData)
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
} finally {
loading.value = false
}
})
}
// 获取学院列表
const getDeptList = async () => {
try {
const res = await request({
url: '/basic/basicdept/getDeptList',
method: 'get',
params: { secondFlag: 1 }
})
if (res.data) {
deptList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
deptList.value = []
}
}
// 获取类别列表
const getCategoryList = async () => {
try {
const res = await request({
url: '/stuwork/onlinebookscategory/list',
method: 'get'
})
if (res.data) {
categoryList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
categoryList.value = []
}
}
// 初始化
onMounted(() => {
getDeptList()
getCategoryList()
})
// 暴露方法给父组件
defineExpose({
openDialog
})
</script>

View File

@@ -0,0 +1,347 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
查询条件
</span>
</div>
</template>
<el-form :model="searchForm" ref="searchFormRef" :inline="true" @keyup.enter="handleSearch" class="search-form">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="searchForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="类别编码" prop="categoryCode">
<el-select
v-model="searchForm.categoryCode"
placeholder="请选择类别"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in categoryList"
:key="item.categoryCode"
:label="item.categoryName"
:value="item.categoryCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="书名" prop="bookName">
<el-input
v-model="searchForm.bookName"
placeholder="请输入书名"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
在线书列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table"
@sort-change="sortChangeHandle">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
align="center">
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Document" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 封面图片 -->
<template v-if="col.prop === 'bookImg'" #default="scope">
<el-image
v-if="scope.row.bookImg"
:src="scope.row.bookImg"
:preview-src-list="[scope.row.bookImg]"
fit="cover"
style="width: 60px; height: 80px; cursor: pointer; border-radius: 4px;"
preview-teleported>
</el-image>
<span v-else>-</span>
</template>
<!-- 链接 -->
<template v-else-if="col.prop === 'webUrl'" #default="scope">
<el-link
v-if="scope.row.webUrl"
:href="scope.row.webUrl"
target="_blank"
type="primary">
{{ scope.row.webUrl }}
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑对话框 -->
<FormDialog ref="formDialogRef" @refresh="getDataList()" />
</div>
</template>
<script setup lang="ts" name="OnlineBooks">
import { reactive, ref, onMounted, computed, nextTick } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/onlinebooks";
import { useMessage, useMessageBox } from "/@/hooks/message";
import request from '/@/utils/request';
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue';
import { List, Document, Picture, Link, User, Collection, Setting, Menu, Search } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量
const searchFormRef = ref()
const columnControlRef = ref<any>()
const formDialogRef = ref()
const showSearch = ref(true)
const deptList = ref<any[]>([])
const categoryList = ref<any[]>([])
// 表格列配置
const tableColumns = [
{ prop: 'deptCode', label: '学院' },
{ prop: 'categoryCode', label: '类别编码' },
{ prop: 'bookImg', label: '封面' },
{ prop: 'bookName', label: '书名' },
{ prop: 'bookAuthor', label: '作者' },
{ prop: 'webUrl', label: '链接' },
{ prop: 'remarks', label: '备注' }
]
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
deptCode: { icon: Collection },
categoryCode: { icon: Collection },
bookImg: { icon: Picture },
bookName: { icon: Document },
bookAuthor: { icon: User },
webUrl: { icon: Link },
remarks: { icon: Document }
}
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns)
// 搜索表单
const searchForm = reactive({
deptCode: '',
categoryCode: '',
bookName: ''
})
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
tableStyle
} = useTable(state)
// 查询
const handleSearch = () => {
getDataList()
}
// 重置
const handleReset = () => {
searchFormRef.value?.formRef?.resetFields()
searchForm.deptCode = ''
searchForm.categoryCode = ''
searchForm.bookName = ''
getDataList()
}
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value.openDialog('edit', row)
}
// 删除
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该在线书吗?')
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败')
}
}
}
// 获取学院列表
const getDeptList = async () => {
try {
const res = await request({
url: '/basic/basicdept/getDeptList',
method: 'get',
params: { secondFlag: 1 }
})
if (res.data) {
deptList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
deptList.value = []
}
}
// 获取类别列表
const getCategoryList = async () => {
try {
const res = await request({
url: '/stuwork/onlinebookscategory/list',
method: 'get'
})
if (res.data) {
categoryList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
categoryList.value = []
}
}
// 初始化
onMounted(() => {
getDeptList()
getCategoryList()
nextTick(() => {
if (visibleColumns.value.length === 0) {
}
})
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,257 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="120px" v-loading="loading">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="form.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="用户类型" prop="userType">
<el-select
v-model="form.userType"
placeholder="请选择用户类型"
clearable
style="width: 100%">
<el-option
v-for="item in userTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="用户名" prop="userName">
<el-input
v-model="form.userName"
placeholder="请输入用户名"
clearable
style="width: 100%" />
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input
v-model="form.realName"
placeholder="请输入姓名"
clearable
style="width: 100%" />
</el-form-item>
<el-form-item label="图书" prop="bookId">
<el-select
v-model="form.bookId"
placeholder="请选择图书"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in bookList"
:key="item.id"
:label="item.bookName"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="浏览时间" prop="readTime">
<el-date-picker
v-model="form.readTime"
type="datetime"
placeholder="请选择浏览时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注"
style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="OnlineBooksBrowsingHistoryFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/onlinebooksbrowsinghistory'
import { getList as getBookList } from '/@/api/stuwork/onlinebooks'
import request from '/@/utils/request'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const deptList = ref<any[]>([])
const bookList = ref<any[]>([])
// 用户类型列表
const userTypeList = [
{ label: '学生', value: '0' },
{ label: '老师', value: '1' }
]
// 提交表单数据
const form = reactive({
id: '',
deptCode: '',
userType: '',
userName: '',
realName: '',
bookId: '',
readTime: '',
remarks: ''
})
// 定义校验规则
const dataRules = {
deptCode: [
{ required: true, message: '请选择学院', trigger: 'change' }
],
userType: [
{ required: true, message: '请选择用户类型', trigger: 'change' }
],
bookId: [
{ required: true, message: '请选择图书', trigger: 'change' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.deptCode = ''
form.userType = ''
form.userName = ''
form.realName = ''
form.bookId = ''
form.readTime = ''
form.remarks = ''
// 编辑时填充数据
if (type === 'edit' && row) {
loading.value = true
getDetail(row.id)
.then((res: any) => {
if (res.data) {
form.id = res.data.id || row.id
form.deptCode = res.data.deptCode || ''
form.userType = res.data.userType || ''
form.userName = res.data.userName || ''
form.realName = res.data.realName || ''
form.bookId = res.data.bookId || ''
form.readTime = res.data.readTime || ''
form.remarks = res.data.remarks || ''
}
})
.catch((err: any) => {
useMessage().error(err.msg || '获取详情失败')
visible.value = false
})
.finally(() => {
loading.value = false
})
}
})
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData: any = {
deptCode: form.deptCode,
userType: form.userType,
userName: form.userName,
realName: form.realName,
bookId: form.bookId,
readTime: form.readTime,
remarks: form.remarks
}
if (operType.value === 'add') {
await addObj(submitData)
useMessage().success('新增成功')
} else {
submitData.id = form.id
await editObj(submitData)
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
} finally {
loading.value = false
}
})
}
// 获取学院列表
const getDeptList = async () => {
try {
const res = await request({
url: '/basic/basicdept/getDeptList',
method: 'get',
params: { secondFlag: 1 }
})
if (res.data) {
deptList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
deptList.value = []
}
}
// 获取图书列表
const getBookListData = async () => {
try {
const res = await getBookList()
if (res.data) {
bookList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
bookList.value = []
}
}
// 初始化
onMounted(() => {
getDeptList()
getBookListData()
})
// 暴露方法给父组件
defineExpose({
openDialog
})
</script>

View File

@@ -0,0 +1,381 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
查询条件
</span>
</div>
</template>
<el-form :model="searchForm" ref="searchFormRef" :inline="true" @keyup.enter="handleSearch" class="search-form">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="searchForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="用户类型" prop="userType">
<el-select
v-model="searchForm.userType"
placeholder="请选择用户类型"
clearable
style="width: 200px">
<el-option
v-for="item in userTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="用户名" prop="userName">
<el-input
v-model="searchForm.userName"
placeholder="请输入用户名"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input
v-model="searchForm.realName"
placeholder="请输入姓名"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="图书" prop="bookId">
<el-select
v-model="searchForm.bookId"
placeholder="请选择图书"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in bookList"
:key="item.id"
:label="item.bookName"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
在线书浏览记录列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table"
@sort-change="sortChangeHandle">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
align="center">
<template #header>
<el-icon><component :is="col.prop ? (columnConfigMap[col.prop]?.icon || Document) : Document" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 用户类型格式化 -->
<template v-if="col.prop === 'userType'" #default="scope">
<el-tag v-if="String(scope.row.userType) === '0'" type="primary" size="small">学生</el-tag>
<el-tag v-else-if="String(scope.row.userType) === '1'" type="success" size="small">老师</el-tag>
<span v-else>-</span>
</template>
<!-- 浏览时间格式化 -->
<template v-else-if="col.prop === 'readTime'" #default="scope">
<span v-if="scope.row.readTime">{{ formatDateTime(scope.row.readTime) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑对话框 -->
<FormDialog ref="formDialogRef" @refresh="getDataList()" />
</div>
</template>
<script setup lang="ts" name="OnlineBooksBrowsingHistory">
import { reactive, ref, onMounted, computed, nextTick } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/onlinebooksbrowsinghistory";
import { getList as getBookList } from "/@/api/stuwork/onlinebooks";
import { useMessage, useMessageBox } from "/@/hooks/message";
import request from '/@/utils/request';
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue';
import { List, Document, User, Avatar, Collection, Clock, Setting, Menu, Search } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量
const searchFormRef = ref()
const columnControlRef = ref<any>()
const formDialogRef = ref()
const showSearch = ref(true)
const deptList = ref<any[]>([])
const bookList = ref<any[]>([])
// 用户类型列表
const userTypeList = [
{ label: '学生', value: '0' },
{ label: '老师', value: '1' }
]
// 表格列配置
const tableColumns = [
{ prop: 'deptCode', label: '学院' },
{ prop: 'userType', label: '用户类型' },
{ prop: 'userName', label: '用户名' },
{ prop: 'realName', label: '姓名' },
{ prop: 'bookId', label: '图书' },
{ prop: 'readTime', label: '浏览时间' },
{ prop: 'remarks', label: '备注' }
]
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
deptCode: { icon: Collection },
userType: { icon: User },
userName: { icon: User },
realName: { icon: Avatar },
bookId: { icon: Document },
readTime: { icon: Clock },
remarks: { icon: Document }
}
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns)
// 搜索表单
const searchForm = reactive({
deptCode: '',
userType: '',
userName: '',
realName: '',
bookId: ''
})
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
tableStyle
} = useTable(state)
// 查询
const handleSearch = () => {
getDataList()
}
// 重置
const handleReset = () => {
searchFormRef.value?.formRef?.resetFields()
searchForm.deptCode = ''
searchForm.userType = ''
searchForm.userName = ''
searchForm.realName = ''
searchForm.bookId = ''
getDataList()
}
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value.openDialog('edit', row)
}
// 删除
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该浏览记录吗?')
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败')
}
}
}
// 格式化日期时间
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
try {
const date = new Date(dateTime)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} catch (e) {
return dateTime
}
}
// 获取学院列表
const getDeptList = async () => {
try {
const res = await request({
url: '/basic/basicdept/getDeptList',
method: 'get',
params: { secondFlag: 1 }
})
if (res.data) {
deptList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
deptList.value = []
}
}
// 获取图书列表
const getBookListData = async () => {
try {
const res = await getBookList()
if (res.data) {
bookList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
bookList.value = []
}
}
// 初始化
onMounted(() => {
getDeptList()
getBookListData()
nextTick(() => {
if (visibleColumns.value.length === 0) {
}
})
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,153 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="120px" v-loading="loading">
<el-form-item label="类别名称" prop="categoryName">
<el-input
v-model="form.categoryName"
placeholder="请输入类别名称"
clearable
maxlength="30"
show-word-limit
style="width: 100%" />
</el-form-item>
<el-form-item label="类别编码" prop="categoryCode">
<el-input
v-model="form.categoryCode"
placeholder="请输入类别编码"
clearable
maxlength="30"
show-word-limit
:disabled="!!form.id"
style="width: 100%" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="250"
show-word-limit
style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="OnlineBooksCategoryFormDialog">
import { ref, reactive, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/onlinebookscategory'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
// 提交表单数据
const form = reactive({
id: '',
categoryName: '',
categoryCode: '',
remarks: ''
})
// 定义校验规则
const dataRules = {
categoryName: [
{ required: true, message: '请输入类别名称', trigger: 'blur' },
{ max: 30, message: '类别名称不能超过30个字符', trigger: 'blur' }
],
categoryCode: [
{ required: true, message: '请输入类别编码', trigger: 'blur' },
{ max: 30, message: '类别编码不能超过30个字符', trigger: 'blur' }
],
remarks: [
{ max: 250, message: '备注不能超过250个字符', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.categoryName = ''
form.categoryCode = ''
form.remarks = ''
// 编辑时填充数据
if (type === 'edit' && row) {
loading.value = true
getDetail(row.id)
.then((res: any) => {
if (res.data) {
form.id = res.data.id || row.id
form.categoryName = res.data.categoryName || ''
form.categoryCode = res.data.categoryCode || ''
form.remarks = res.data.remarks || ''
}
})
.catch((err: any) => {
useMessage().error(err.msg || '获取详情失败')
visible.value = false
})
.finally(() => {
loading.value = false
})
}
})
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData: any = {
categoryName: form.categoryName,
categoryCode: form.categoryCode,
remarks: form.remarks
}
if (operType.value === 'add') {
await addObj(submitData)
useMessage().success('新增成功')
} else {
submitData.id = form.id
await editObj(submitData)
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
} finally {
loading.value = false
}
})
}
// 暴露方法给父组件
defineExpose({
openDialog
})
</script>

View File

@@ -0,0 +1,269 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
查询条件
</span>
</div>
</template>
<el-form :model="searchForm" ref="searchFormRef" :inline="true" @keyup.enter="handleSearch" class="search-form">
<el-form-item label="类别名称" prop="categoryName">
<el-input
v-model="searchForm.categoryName"
placeholder="请输入类别名称"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="类别编码" prop="categoryCode">
<el-input
v-model="searchForm.categoryCode"
placeholder="请输入类别编码"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
在线书类别列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table"
@sort-change="sortChangeHandle">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
align="center">
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Document" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 二维码图片 -->
<template v-if="col.prop === 'qrCode'" #default="scope">
<el-image
v-if="scope.row.qrCode"
:src="scope.row.qrCode"
:preview-src-list="[scope.row.qrCode]"
fit="cover"
style="width: 80px; height: 80px; cursor: pointer; border-radius: 4px;"
preview-teleported>
</el-image>
<span v-else>-</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑对话框 -->
<FormDialog ref="formDialogRef" @refresh="getDataList()" />
</div>
</template>
<script setup lang="ts" name="OnlineBooksCategory">
import { reactive, ref, onMounted, computed, nextTick } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/onlinebookscategory";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue';
import { List, Document, Picture, Setting, Menu, Search } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量
const searchFormRef = ref()
const columnControlRef = ref<any>()
const formDialogRef = ref()
const showSearch = ref(true)
// 表格列配置
const tableColumns = [
{ prop: 'categoryName', label: '类别名称' },
{ prop: 'categoryCode', label: '类别编码' },
{ prop: 'qrCode', label: '二维码' },
{ prop: 'remarks', label: '备注' }
]
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
categoryName: { icon: Document },
categoryCode: { icon: Document },
qrCode: { icon: Picture },
remarks: { icon: Document }
}
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns)
// 搜索表单
const searchForm = reactive({
categoryName: '',
categoryCode: ''
})
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
tableStyle
} = useTable(state)
// 查询
const handleSearch = () => {
getDataList()
}
// 重置
const handleReset = () => {
searchFormRef.value?.formRef?.resetFields()
searchForm.categoryName = ''
searchForm.categoryCode = ''
getDataList()
}
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value.openDialog('edit', row)
}
// 删除
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该类别吗?')
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败')
}
}
}
// 初始化
onMounted(() => {
nextTick(() => {
if (visibleColumns.value.length === 0) {
}
})
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,154 @@
<template>
<el-dialog
title="新增排班"
v-model="visible"
:width="500"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-form-item label="值班日期" prop="date">
<el-date-picker
v-model="form.date"
type="date"
placeholder="选择值班日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled-date="disabledDate"
style="width: 100%" />
</el-form-item>
<el-form-item label="预约师" prop="teacherUserName">
<el-select
v-model="form.teacherUserName"
placeholder="请选择预约师"
filterable
clearable
style="width: 100%">
<el-option
v-for="item in teacherList"
:key="item.userName"
:label="(item.realName || '') + (item.userName ? ` (${item.userName})` : '')"
:value="item.userName" />
</el-select>
</el-form-item>
<el-form-item label="周类型" prop="weekType">
<el-select
v-model="form.weekType"
placeholder="请选择"
clearable
style="width: 100%">
<el-option label="单周" value="single" />
<el-option label="双周" value="double" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="PsychologicalCounselingDutyFormDialog">
import { ref, reactive, nextTick, watch } from 'vue'
import { useMessage } from '/@/hooks/message'
import { saveDuty } from '/@/api/stuwork/psychologicalcounselingduty'
import { getList } from '/@/api/stuwork/psychologicalcounselingteacher'
const props = defineProps<{
year?: number
month?: number
}>()
const emit = defineEmits(['refresh'])
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const teacherList = ref<any[]>([])
const form = reactive({
date: '',
teacherUserName: '',
weekType: 'single'
})
const dataRules = {
date: [{ required: true, message: '请选择值班日期', trigger: 'change' }],
teacherUserName: [{ required: true, message: '请选择预约师', trigger: 'change' }]
}
// 限制只能选当前年月的日期
const disabledDate = (time: Date) => {
if (!props.year || !props.month) return false
const y = time.getFullYear()
const m = time.getMonth() + 1
if (y !== props.year) return true
if (m !== props.month) return true
return false
}
const loadTeacherList = async () => {
try {
const res = await getList()
if (res.data && Array.isArray(res.data)) {
teacherList.value = res.data
} else {
teacherList.value = []
}
} catch (err) {
teacherList.value = []
}
}
const openDialog = () => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
form.date = ''
form.teacherUserName = ''
form.weekType = 'single'
})
}
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
await saveDuty([
{
date: form.date,
teacherUserName: form.teacherUserName,
weekType: form.weekType || 'single'
}
])
useMessage().success('排班成功')
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || '排班失败')
} finally {
loading.value = false
}
})
}
watch(visible, (v) => {
if (v) loadTeacherList()
})
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,310 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="年份" prop="year">
<el-select
v-model="searchForm.year"
placeholder="请选择年份"
clearable
style="width: 120px">
<el-option
v-for="y in yearOptions"
:key="y"
:label="y + '年'"
:value="y" />
</el-select>
</el-form-item>
<el-form-item label="月份" prop="month">
<el-select
v-model="searchForm.month"
placeholder="请选择月份"
clearable
style="width: 120px">
<el-option
v-for="m in 12"
:key="m"
:label="m + '月'"
:value="m" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Calendar /></el-icon>
值班管理列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef?.openDialog()">
新增排班
</el-button>
<el-button
icon="Delete"
type="danger"
plain
:disabled="!dataList.length"
@click="handleClearMonth">
一键清空本月
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="dataList"
v-loading="loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<template v-if="col.prop === 'reservation'" #default="scope">
<el-tag :type="scope.row.reservation === '1' ? 'success' : 'info'" size="small">
{{ scope.row.reservation === '1' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Delete"
link
type="danger"
@click="handleClearOne(scope.row)">
清除
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无值班数据,请选择年月查询或新增排班" :image-size="120">
<el-button type="primary" icon="FolderAdd" @click="formDialogRef?.openDialog()">新增排班</el-button>
</el-empty>
</template>
</el-table>
</el-card>
</div>
<!-- 新增排班弹窗 -->
<FormDialog
ref="formDialogRef"
:year="searchForm.year"
:month="searchForm.month"
@refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PsychologicalCounselingDuty">
import { reactive, ref, computed, onMounted, defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { getDutyByMonth, clearDuty, clearOneDuty } from '/@/api/stuwork/psychologicalcounselingduty'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { List, Calendar, UserFilled, Phone, Document, Setting, Menu, Search } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const FormDialog = defineAsyncComponent(() => import('./form.vue'))
const route = useRoute()
const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
const loading = ref(false)
const dataList = ref<any[]>([])
// 年份选项:当前年前后各 5 年
const yearOptions = computed(() => {
const y = new Date().getFullYear()
return Array.from({ length: 11 }, (_, i) => y - 5 + i)
})
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'date', label: '值班日期', icon: Calendar },
{ prop: 'dutyTime', label: '值班时间', icon: Calendar },
{ prop: 'realName', label: '教师姓名', icon: UserFilled },
{ prop: 'userName', label: '用户名', icon: UserFilled },
{ prop: 'phone', label: '联系方式', icon: Phone, minWidth: 120 },
{ prop: 'reservation', label: '是否预约', icon: Document }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 表格样式(与 useTable 一致)
const tableStyle = {
cellStyle: {},
headerCellStyle: {}
}
// 搜索表单
const searchForm = reactive({
year: new Date().getFullYear(),
month: new Date().getMonth() + 1
})
const getDataList = async () => {
if (!searchForm.year || !searchForm.month) {
useMessage().warning('请选择年份和月份')
return
}
loading.value = true
try {
const res = await getDutyByMonth({
year: searchForm.year,
month: searchForm.month
})
dataList.value = Array.isArray(res.data) ? res.data : []
} catch (err: any) {
useMessage().error(err.msg || '获取值班表失败')
dataList.value = []
} finally {
loading.value = false
}
}
const handleReset = () => {
const now = new Date()
searchForm.year = now.getFullYear()
searchForm.month = now.getMonth() + 1
getDataList()
}
const handleClearMonth = async () => {
if (!searchForm.year || !searchForm.month) {
useMessage().warning('请先选择年份和月份')
return
}
try {
await useMessageBox().confirm(
`确定要清空 ${searchForm.year}${searchForm.month}月 的全部值班吗?`,
'提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return
}
try {
await clearDuty({
year: Number(searchForm.year),
month: Number(searchForm.month)
})
useMessage().success('清空成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '清空失败')
}
}
const handleClearOne = async (row: any) => {
const dateStr = row.date || row.dutyTime || ''
if (!dateStr) {
useMessage().warning('无法获取日期')
return
}
const day = dateStr.split(' ')[0] || dateStr
try {
await useMessageBox().confirm(`确定要清除 ${day} 的值班吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
try {
await clearOneDuty({ days: day })
useMessage().success('清除成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '清除失败')
}
}
onMounted(() => {
getDataList()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,309 @@
<template>
<el-dialog
:title="form.id ? '编辑' : '新增'"
v-model="visible"
:width="600"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-form-item label="值班教师" prop="teacherNo">
<el-select
v-model="form.teacherNo"
placeholder="请选择值班教师"
filterable
clearable
style="width: 100%"
:disabled="!!form.id"
@change="handleTeacherChange">
<el-option
v-for="item in teacherList"
:key="item.userName"
:label="(item.realName || '') + (item.userName ? ` (${item.userName})` : '')"
:value="item.userName" />
</el-select>
</el-form-item>
<el-form-item label="预约时间" prop="reservationTime">
<el-date-picker
v-model="form.reservationTime"
type="datetime"
placeholder="选择预约时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
:disabled="!!form.id" />
</el-form-item>
<el-form-item label="班号" prop="classCode">
<el-select
v-model="form.classCode"
placeholder="请选择班号"
clearable
filterable
style="width: 100%"
:disabled="!!form.id"
@change="handleClassChange">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classCode" />
</el-select>
</el-form-item>
<el-form-item label="学号" prop="stuNo">
<el-select
v-model="form.stuNo"
:placeholder="form.classCode ? (studentLoading ? '加载中...' : '请选择学生') : '请先选择班号'"
clearable
filterable
style="width: 100%"
:disabled="!form.classCode || !!form.id || studentLoading"
:loading="studentLoading"
@change="handleStudentChange">
<el-option
v-for="item in studentList"
:key="item.stuNo"
:label="`${item.realName || item.stuName || ''} (${item.stuNo})`"
:value="item.stuNo" />
</el-select>
</el-form-item>
<el-form-item label="学生姓名" prop="stuName">
<el-input v-model="form.stuName" placeholder="选学生后自动带出" disabled />
</el-form-item>
<el-form-item label="联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="选学生后自动带出" disabled />
</el-form-item>
<el-form-item label="学生留言" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入学生留言"
maxlength="250"
show-word-limit
:disabled="!!form.id" />
</el-form-item>
<template v-if="form.id">
<el-form-item label="是否处理" prop="isHandle">
<el-select
v-model="form.isHandle"
placeholder="请选择"
clearable
style="width: 100%">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item label="老师备注" prop="teaRemark">
<el-input
v-model="form.teaRemark"
type="textarea"
:rows="3"
placeholder="请输入老师备注"
maxlength="250"
show-word-limit />
</el-form-item>
</template>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="PsychologicalCounselingReservationFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/psychologicalcounselingreservation'
import { getList } from '/@/api/stuwork/psychologicalcounselingteacher'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { queryAllStudentByClassCode } from '/@/api/basic/basicstudent'
const emit = defineEmits(['refresh'])
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const studentLoading = ref(false)
const operType = ref('add')
const teacherList = ref<any[]>([])
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const form = reactive({
id: '',
teacherNo: '',
realName: '',
reservationTime: '',
classCode: '',
classNo: '',
stuNo: '',
stuName: '',
phone: '',
remarks: '',
isHandle: '0',
teaRemark: ''
})
const dataRules = {
teacherNo: [{ required: true, message: '请选择值班教师', trigger: 'change' }],
reservationTime: [{ required: true, message: '请选择预约时间', trigger: 'change' }],
classCode: [{ required: true, message: '请选择班号', trigger: 'change' }],
stuNo: [{ required: true, message: '请选择学生', trigger: 'change' }]
}
const handleTeacherChange = (val: string) => {
const item = teacherList.value.find((t: any) => t.userName === val)
form.realName = item ? (item.realName || '') : ''
}
const handleClassChange = async () => {
form.stuNo = ''
form.stuName = ''
form.phone = ''
studentList.value = []
if (!form.classCode) return
studentLoading.value = true
try {
const res = await queryAllStudentByClassCode(form.classCode)
studentList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
studentList.value = []
} finally {
studentLoading.value = false
}
}
const handleStudentChange = (val: string) => {
const item = studentList.value.find((s: any) => s.stuNo === val)
if (item) {
form.stuName = item.realName || item.stuName || ''
form.phone = item.phone || item.mobile || ''
} else {
form.stuName = ''
form.phone = ''
}
}
const openDialog = (type: string = 'add', row?: any) => {
operType.value = type
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.teacherNo = ''
form.realName = ''
form.reservationTime = ''
form.classCode = ''
form.classNo = ''
form.stuNo = ''
form.stuName = ''
form.phone = ''
form.remarks = ''
form.isHandle = '0'
form.teaRemark = ''
studentList.value = []
if (type === 'edit' && row) {
form.id = row.id
form.teacherNo = row.teacherNo || ''
form.realName = row.realName || ''
form.reservationTime = row.reservationTime || ''
form.classNo = row.classNo || ''
const cls = classList.value.find((c: any) => c.classNo === row.classNo)
form.classCode = cls ? cls.classCode : (row.classCode || '')
form.stuNo = row.stuNo || ''
form.stuName = row.stuName || ''
form.phone = row.phone || ''
form.remarks = row.remarks || ''
form.isHandle = row.isHandle !== undefined && row.isHandle !== null ? String(row.isHandle) : '0'
form.teaRemark = row.teaRemark || ''
}
})
}
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
const selectedClass = classList.value.find((c: any) => c.classCode === form.classCode)
const classNo = selectedClass ? selectedClass.classNo : form.classNo || form.classCode
loading.value = true
try {
if (operType.value === 'add') {
await addObj({
teacherNo: form.teacherNo,
realName: form.realName,
reservationTime: form.reservationTime ? (form.reservationTime.length === 10 ? form.reservationTime + ' 00:00:00' : form.reservationTime) : '',
classNo,
stuNo: form.stuNo,
stuName: form.stuName,
phone: form.phone,
remarks: form.remarks
})
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
teacherNo: form.teacherNo,
realName: form.realName,
reservationTime: form.reservationTime,
classNo: form.classNo || classNo,
stuNo: form.stuNo,
stuName: form.stuName,
phone: form.phone,
remarks: form.remarks,
isHandle: form.isHandle,
teaRemark: form.teaRemark || ''
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
} finally {
loading.value = false
}
})
}
const loadTeacherList = async () => {
try {
const res = await getList()
teacherList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
teacherList.value = []
}
}
const loadClassList = async () => {
try {
const res = await getClassListByRole()
classList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
classList.value = []
}
}
onMounted(() => {
loadTeacherList()
loadClassList()
})
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,296 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="handleSearch"
class="search-form">
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="searchForm.stuNo"
placeholder="请输入学号"
clearable
style="width: 160px" />
</el-form-item>
<el-form-item label="班号" prop="classNo">
<el-input
v-model="searchForm.classNo"
placeholder="请输入班号"
clearable
style="width: 160px" />
</el-form-item>
<el-form-item label="预约时间" prop="reservationTime">
<el-date-picker
v-model="searchForm.reservationTime"
type="date"
placeholder="选择预约时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 180px" />
</el-form-item>
<el-form-item label="是否处理" prop="isHandle">
<el-select
v-model="searchForm.isHandle"
placeholder="请选择"
clearable
style="width: 120px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
预约记录列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef?.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<template v-if="col.prop === 'isHandle'" #default="scope">
<el-tag :type="scope.row.isHandle === '1' ? 'success' : 'info'" size="small">
{{ scope.row.isHandle === '1' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" :image-size="120">
<el-button type="primary" icon="FolderAdd" @click="formDialogRef?.openDialog()">新增预约记录</el-button>
</el-empty>
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<FormDialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PsychologicalCounselingReservation">
import { reactive, ref, onMounted, defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList, delObj } from '/@/api/stuwork/psychologicalcounselingreservation'
import { useMessage, useMessageBox } from '/@/hooks/message'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import {
List,
Calendar,
UserFilled,
Phone,
EditPen,
Setting,
Menu,
Search,
Document,
FolderAdd
} from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const FormDialog = defineAsyncComponent(() => import('./form.vue'))
const route = useRoute()
const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'reservationTime', label: '预约时间', icon: Calendar, minWidth: 160 },
{ prop: 'realName', label: '值班老师', icon: UserFilled },
{ prop: 'classNo', label: '班号', icon: Document },
{ prop: 'stuNo', label: '学号', icon: UserFilled },
{ prop: 'stuName', label: '学生姓名', icon: UserFilled },
{ prop: 'phone', label: '联系方式', icon: Phone, minWidth: 120 },
{ prop: 'isHandle', label: '是否处理', icon: Document },
{ prop: 'teaRemark', label: '老师备注', icon: EditPen, minWidth: 120 },
{ prop: 'remarks', label: '学生留言', icon: EditPen, minWidth: 120 }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单
const searchForm = reactive({
stuNo: '',
classNo: '',
reservationTime: '',
isHandle: ''
})
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
const handleSearch = () => {
getDataList()
}
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.stuNo = ''
searchForm.classNo = ''
searchForm.reservationTime = ''
searchForm.isHandle = ''
getDataList()
}
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该预约记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
try {
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '删除失败')
}
}
onMounted(() => {})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,186 @@
<template>
<el-dialog
:title="form.id ? '编辑' : '新增'"
v-model="visible"
:width="600"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-form-item label="教师姓名" prop="userName">
<el-select
v-model="form.userName"
placeholder="请选择教师"
filterable
clearable
style="width: 100%"
:disabled="!!form.id"
@change="handleTeacherChange">
<el-option
v-for="item in teacherList"
:key="item.teacherNo || item.userName"
:label="(item.realName || item.name) + (item.teacherNo ? ` (${item.teacherNo})` : '')"
:value="item.teacherNo || item.userName">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="真实姓名" prop="realName">
<el-input
v-model="form.realName"
placeholder="选教师后自动带出,可修改"
clearable />
</el-form-item>
<el-form-item label="联系方式" prop="phone">
<el-input
v-model="form.phone"
placeholder="请输入联系方式"
clearable />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="4"
placeholder="请输入备注"
maxlength="250"
show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="PsychologicalCounselingTeacherFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/psychologicalcounselingteacher'
import { getTeacherBaseList } from '/@/api/professional/professionaluser/teacherbase'
const emit = defineEmits(['refresh'])
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add')
const teacherList = ref<any[]>([])
const form = reactive({
id: '',
userName: '',
realName: '',
phone: '',
remarks: ''
})
const dataRules = {
userName: [
{ required: true, message: '请选择教师', trigger: 'change' }
]
}
const handleTeacherChange = (val: string) => {
const item = teacherList.value.find(
(t: any) => (t.teacherNo || t.userName) === val
)
if (item) {
form.realName = item.realName || item.name || ''
form.phone = item.phone || item.tel || ''
} else {
form.realName = ''
form.phone = ''
}
}
const openDialog = (type: string = 'add', row?: any) => {
operType.value = type
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.userName = ''
form.realName = ''
form.phone = ''
form.remarks = ''
if (type === 'edit' && row) {
form.id = row.id
form.userName = row.userName || ''
form.realName = row.realName || ''
form.phone = row.phone || ''
form.remarks = row.remarks || ''
}
})
}
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
if (operType.value === 'add') {
await addObj({
userName: form.userName,
realName: form.realName,
phone: form.phone,
remarks: form.remarks
})
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
userName: form.userName,
realName: form.realName,
phone: form.phone,
remarks: form.remarks
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
} finally {
loading.value = false
}
})
}
const loadTeacherList = async () => {
try {
const res = await getTeacherBaseList()
if (res.data && Array.isArray(res.data)) {
teacherList.value = res.data
} else {
teacherList.value = []
}
} catch (err) {
teacherList.value = []
}
}
onMounted(() => {
loadTeacherList()
})
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,244 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="handleSearch"
class="search-form">
<el-form-item label="教师姓名" prop="realName">
<el-input
v-model="searchForm.realName"
placeholder="请输入教师姓名"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
预约师管理列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef?.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" :image-size="120">
<el-button type="primary" icon="FolderAdd" @click="formDialogRef?.openDialog()">新增预约师</el-button>
</el-empty>
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑表单弹窗 -->
<FormDialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PsychologicalCounselingTeacher">
import { reactive, ref, onMounted, defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList, delObj } from '/@/api/stuwork/psychologicalcounselingteacher'
import { useMessage, useMessageBox } from '/@/hooks/message'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { List, UserFilled, Phone, EditPen, Setting, Menu, Search, Document, FolderAdd } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const FormDialog = defineAsyncComponent(() => import('./form.vue'))
const route = useRoute()
const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'userName', label: '用户名', icon: UserFilled },
{ prop: 'realName', label: '教师姓名', icon: UserFilled },
{ prop: 'phone', label: '联系方式', icon: Phone, minWidth: 120 },
{ prop: 'remarks', label: '备注', icon: EditPen, minWidth: 150 }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单
const searchForm = reactive({
realName: ''
})
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
const handleSearch = () => {
getDataList()
}
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.realName = ''
getDataList()
}
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该预约师吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
try {
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '删除失败')
}
}
onMounted(() => {})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -31,12 +31,20 @@
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="奖项名称" prop="ruleName">
<el-input
v-model="form.ruleName"
placeholder="请输入奖项名称"
<el-form-item label="奖项名称" prop="ruleId">
<el-select
v-model="form.ruleId"
placeholder="请选择奖项名称"
clearable
style="width: 100%" />
filterable
style="width: 100%"
@change="handleRewardRuleChange">
<el-option
v-for="item in rewardRuleList"
:key="item.id"
:label="item.ruleName"
:value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
@@ -67,6 +75,7 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/rewardclass'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { getList as getRewardRuleList } from '/@/api/stuwork/rewardrule'
const emit = defineEmits(['refresh'])
@@ -76,6 +85,8 @@ const visible = ref(false)
const loading = ref(false)
const operType = ref('add')
const classList = ref<any[]>([])
/** 奖项列表(来自接口 /stuwork/rewardrule/list用于班级的奖项类型可传 ruleType 筛选) */
const rewardRuleList = ref<any[]>([])
// 提交表单数据
const form = reactive({
@@ -91,8 +102,8 @@ const dataRules = {
classCode: [
{ required: true, message: '请选择班号', trigger: 'change' }
],
ruleName: [
{ required: true, message: '请输入奖项名称', trigger: 'blur' }
ruleId: [
{ required: true, message: '请选择奖项名称', trigger: 'change' }
]
}
@@ -117,6 +128,8 @@ const openDialog = async (type: string = 'add', row?: any) => {
form.ruleId = row.ruleId || ''
form.ruleName = row.ruleName || ''
form.remarks = row.remarks || ''
// 选择奖项后同步 ruleName
handleRewardRuleChange(form.ruleId)
// 如果需要获取详情
if (row.id && (!row.ruleName || !row.remarks)) {
@@ -138,8 +151,12 @@ const openDialog = async (type: string = 'add', row?: any) => {
}
// 班级变化
const handleClassChange = () => {
// 可以根据班级获取相关信息,这里暂时不实现
const handleClassChange = () => {}
// 选择奖项时同步 ruleName提交时需要
const handleRewardRuleChange = (id: string) => {
const item = rewardRuleList.value.find((r) => r.id === id)
form.ruleName = item ? item.ruleName : ''
}
// 提交表单
@@ -192,9 +209,21 @@ const getClassListData = async () => {
}
}
// 获取奖项列表接口GET /stuwork/rewardrule/list
const getRewardRuleData = async () => {
try {
const res = await getRewardRuleList()
const list = res.data && Array.isArray(res.data) ? res.data : []
rewardRuleList.value = list
} catch (err) {
rewardRuleList.value = []
}
}
// 初始化
onMounted(() => {
getClassListData()
getRewardRuleData()
})
// 暴露方法

View File

@@ -12,6 +12,39 @@
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="form.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="form.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 100%">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="宿舍号" prop="roomNo">
<el-select
@@ -30,12 +63,20 @@
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="奖项名称" prop="ruleName">
<el-input
v-model="form.ruleName"
placeholder="请输入奖项名称"
<el-form-item label="奖项名称" prop="ruleId">
<el-select
v-model="form.ruleId"
placeholder="请选择奖项名称"
clearable
style="width: 100%" />
filterable
style="width: 100%"
@change="handleRewardRuleChange">
<el-option
v-for="item in rewardRuleList"
:key="item.id"
:label="item.ruleName"
:value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
@@ -66,6 +107,9 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/rewarddorm'
import { dormRoomList } from '/@/api/stuwork/dormroom'
import { getList as getRewardRuleList } from '/@/api/stuwork/rewardrule'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getDicts } from '/@/api/admin/dict'
const emit = defineEmits(['refresh'])
@@ -75,10 +119,16 @@ const visible = ref(false)
const loading = ref(false)
const operType = ref('add')
const roomList = ref<any[]>([])
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
/** 奖项列表(来自接口 /stuwork/rewardrule/list */
const rewardRuleList = ref<any[]>([])
// 提交表单数据
// 提交表单数据与接口文档一致roomNo, ruleId, ruleName, schoolYear, schoolTerm, remarks
const form = reactive({
id: '',
schoolYear: '',
schoolTerm: '',
roomNo: '',
ruleId: '',
ruleName: '',
@@ -87,11 +137,17 @@ const form = reactive({
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
roomNo: [
{ required: true, message: '请选择宿舍号', trigger: 'change' }
],
ruleName: [
{ required: true, message: '请输入奖项名称', trigger: 'blur' }
ruleId: [
{ required: true, message: '请选择奖项名称', trigger: 'change' }
]
}
@@ -104,6 +160,8 @@ const openDialog = async (type: string = 'add', row?: any) => {
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.schoolYear = ''
form.schoolTerm = ''
form.roomNo = ''
form.ruleId = ''
form.ruleName = ''
@@ -112,23 +170,27 @@ const openDialog = async (type: string = 'add', row?: any) => {
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.schoolYear = row.schoolYear || ''
form.schoolTerm = row.schoolTerm || ''
form.roomNo = row.roomNo || ''
form.ruleId = row.ruleId || ''
form.ruleName = row.ruleName || ''
form.remarks = row.remarks || ''
handleRewardRuleChange(form.ruleId)
// 如果需要获取详情
if (row.id && (!row.ruleName || !row.remarks)) {
if (row.id && (!row.ruleName || !row.schoolYear)) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.schoolYear = res.data.schoolYear || ''
form.schoolTerm = res.data.schoolTerm || ''
form.roomNo = res.data.roomNo || ''
form.ruleId = res.data.ruleId || ''
form.ruleName = res.data.ruleName || ''
form.remarks = res.data.remarks || ''
}
}).catch((err) => {
}).finally(() => {
}).catch(() => {}).finally(() => {
loading.value = false
})
}
@@ -136,6 +198,12 @@ const openDialog = async (type: string = 'add', row?: any) => {
})
}
// 选择奖项时同步 ruleName提交时需要
const handleRewardRuleChange = (id: string) => {
const item = rewardRuleList.value.find((r) => r.id === id)
form.ruleName = item ? item.ruleName : ''
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
@@ -145,10 +213,13 @@ const onSubmit = async () => {
loading.value = true
try {
// 与接口文档一致roomNo, ruleId, ruleName, schoolYear, schoolTerm, remarks
const submitData = {
roomNo: form.roomNo,
ruleId: form.ruleId || '', // 如果没有ruleId传空字符串
ruleId: form.ruleId != null && form.ruleId !== '' ? String(form.ruleId) : '',
ruleName: form.ruleName,
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
remarks: form.remarks || ''
}
@@ -164,8 +235,8 @@ const onSubmit = async () => {
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
} catch (_err) {
// 不再在此处弹 errorrequest 拦截器已统一弹过,避免重复两个 error 弹窗
} finally {
loading.value = false
}
@@ -186,9 +257,50 @@ const getRoomListData = async () => {
}
}
// 获取奖项列表接口GET /stuwork/rewardrule/list
const getRewardRuleData = async () => {
try {
const res = await getRewardRuleList()
const list = res.data && Array.isArray(res.data) ? res.data : []
rewardRuleList.value = list
} catch (err) {
rewardRuleList.value = []
}
}
// 获取学年列表
const getSchoolYearData = async () => {
try {
const res = await queryAllSchoolYear()
schoolYearList.value = res.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
schoolYearList.value = []
}
}
// 获取学期字典
const getSchoolTermData = async () => {
try {
const res = await getDicts('school_term')
if (res.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 初始化
onMounted(() => {
getRoomListData()
getRewardRuleData()
getSchoolYearData()
getSchoolTermData()
})
// 暴露方法

View File

@@ -1,21 +1,21 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ?????? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
????
查询条件
</span>
</div>
</template>
<el-form :model="state.queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="??" prop="schoolYear">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="state.queryForm.schoolYear"
placeholder="?????"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
@@ -27,10 +27,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="schoolTerm">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="state.queryForm.schoolTerm"
placeholder="?????"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
@@ -41,48 +41,48 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="???" prop="roomNo">
<el-form-item label="宿舍号" prop="roomNo">
<el-input
v-model="state.queryForm.roomNo"
placeholder="??????"
placeholder="请输入宿舍号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="????" prop="ruleName">
<el-form-item label="奖项名称" prop="ruleName">
<el-input
v-model="state.queryForm.ruleName"
placeholder="???????"
placeholder="请输入奖项名称"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">??</el-button>
<el-button icon="Refresh" @click="handleReset">??</el-button>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
????????
文明宿舍列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
@click="formDialogRef.openDialog()">
??
新增
</el-button>
<el-button
icon="Upload"
type="success"
class="ml10"
@click="handleImport">
??
导入
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -98,7 +98,7 @@
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="???" placement="top">
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
@@ -110,7 +110,7 @@
</div>
</template>
<!-- ?? -->
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
@@ -118,7 +118,7 @@
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="??" width="70" align="center">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
@@ -128,7 +128,7 @@
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '??'"
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
:width="col.width"
@@ -152,10 +152,10 @@
</template>
</el-table-column>
</template>
<el-table-column label="??" width="150" align="center" fixed="right">
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">??</span>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
@@ -163,20 +163,20 @@
link
type="primary"
@click="handleEdit(scope.row)">
??
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
??
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ?? -->
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@@ -186,12 +186,12 @@
</el-card>
</div>
<!-- ??/?????? -->
<!-- 新增/编辑对话框 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
<!-- ???? -->
<!-- 导入对话框 -->
<el-dialog
title="??????"
title="导入数据"
v-model="importDialogVisible"
:width="500"
:close-on-click-modal="false"
@@ -201,7 +201,7 @@
icon="Download"
type="success"
@click="handleDownloadTemplate">
????
下载模板
</el-button>
</div>
<el-upload
@@ -213,18 +213,18 @@
drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
????????<em>????</em>
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
???? xlsx/xls ??
只能上传 xlsx/xls 文件
</div>
</template>
</el-upload>
<template #footer>
<span class="dialog-footer">
<el-button @click="importDialogVisible = false">??</el-button>
<el-button type="primary" @click="handleImportSubmit" :disabled="!importFile || importLoading">??</el-button>
<el-button @click="importDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleImportSubmit" :disabled="!importFile || importLoading">确认</el-button>
</span>
</template>
</el-dialog>
@@ -245,7 +245,7 @@ import { useTableColumnControl } from '/@/hooks/tableColumn'
import FormDialog from './form.vue'
// ????
// 定义变量
const route = useRoute()
const formDialogRef = ref()
const uploadRef = ref()
@@ -258,31 +258,31 @@ const importDialogVisible = ref(false)
const importFile = ref<File | null>(null)
const importLoading = ref(false)
// ?????
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '??', icon: Calendar },
{ prop: 'schoolTerm', label: '??', icon: Clock },
{ prop: 'roomNo', label: '???', icon: House },
{ prop: 'ruleName', label: '????', icon: Trophy, minWidth: 150 },
{ prop: 'remarks', label: '??', icon: EditPen, minWidth: 200 }
{ prop: 'schoolYear', label: '学年', icon: Calendar },
{ prop: 'schoolTerm', label: '学期', icon: Clock },
{ prop: 'roomNo', label: '宿舍号', icon: House },
{ prop: 'ruleName', label: '奖项名称', icon: Trophy, minWidth: 150 },
{ prop: 'remarks', label: '备注', icon: EditPen, minWidth: 200 }
]
// ??????? hook
// 表格列控制 hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
} = useTableColumnControl(tableColumns)
// ????
// 表格样式
const tableStyle = {
cellStyle: { padding: '8px 0' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }
}
// ?? useTable
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
schoolYear: '',
@@ -295,7 +295,7 @@ const state: BasicTableProps = reactive<BasicTableProps>({
item: 'records',
totalCount: 'total'
},
createdIsNeed: true // ?????????
createdIsNeed: true // 创建时是否需要请求数据
})
// table hook
@@ -306,7 +306,7 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// ?????
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -315,7 +315,7 @@ const formatSchoolTerm = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.schoolYear = ''
@@ -325,22 +325,22 @@ const handleReset = () => {
getDataList()
}
// ????
// 导入
const handleImport = () => {
importDialogVisible.value = true
importFile.value = null
uploadRef.value?.clearFiles()
}
// ????
// 下载模板
const handleDownloadTemplate = async () => {
try {
const fileName = '????????.xlsx'
// ?????? views/stuwork/rewarddorm ? assets/file ?
const fileName = '文明宿舍导入模板.xlsx'
// 模板文件位于 views/stuwork/rewarddorm 下的 assets/file 目录
const fileUrl = new URL(`../../../assets/file/${fileName}`, import.meta.url).href
const response = await fetch(fileUrl)
if (!response.ok) {
throw new Error('??????')
throw new Error('下载失败')
}
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
@@ -351,60 +351,60 @@ const handleDownloadTemplate = async () => {
link.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(link)
useMessage().success('??????')
useMessage().success('下载成功')
} catch (error) {
useMessage().error('??????')
useMessage().error('下载失败')
}
}
// ????
// 文件变化
const handleFileChange = (file: any) => {
importFile.value = file.raw
}
// ????
// 提交导入
const handleImportSubmit = async () => {
if (!importFile.value) {
useMessage().warning('?????????')
useMessage().warning('请先选择要上传的文件')
return
}
importLoading.value = true
try {
await importExcel(importFile.value)
useMessage().success('????')
useMessage().success('导入成功')
importDialogVisible.value = false
importFile.value = null
uploadRef.value?.clearFiles()
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '导入失败')
} finally {
importLoading.value = false
}
}
// ??
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// ??
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('???????????')
await confirm('确定要删除该记录吗?')
await delObj([row.id])
useMessage().success('????')
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '删除失败')
}
}
}
// ??????
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
@@ -418,7 +418,7 @@ const getSchoolYearList = async () => {
}
}
// ??????
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
@@ -435,7 +435,7 @@ const getSchoolTermDict = async () => {
}
}
// ???
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()

View File

@@ -1,21 +1,21 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ???? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
????
查询条件
</span>
</div>
</template>
<el-form :model="state.queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="????" prop="ruleType">
<el-form-item label="奖项类型" prop="ruleType">
<el-select
v-model="state.queryForm.ruleType"
placeholder="请输入奖项名称"
placeholder="请选择奖项类型"
clearable
style="width: 200px">
<el-option
@@ -27,26 +27,26 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">??</el-button>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
????????
评优评先奖项列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
@click="formDialogRef.openDialog()">
??
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -92,7 +92,7 @@
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
:width="col.width"
@@ -150,7 +150,7 @@
</el-card>
</div>
<!-- ??/?????? -->
<!-- 新增/编辑对话框 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
@@ -168,7 +168,7 @@ import { List, Trophy, Collection, EditPen, Setting, Menu, Search, Document } fr
import { useTableColumnControl } from '/@/hooks/tableColumn'
// ??
// 定义变量
const route = useRoute()
const formDialogRef = ref()
const searchFormRef = ref()
@@ -176,29 +176,29 @@ const columnControlRef = ref()
const showSearch = ref(true)
const ruleTypeList = ref<any[]>([])
// ?????
// 表格列配置
const tableColumns = [
{ prop: 'ruleName', label: '????', icon: Trophy, minWidth: 200 },
{ prop: 'ruleType', label: '????', icon: Collection },
{ prop: 'remarks', label: '??', icon: EditPen, minWidth: 200 }
{ prop: 'ruleName', label: '奖项名称', icon: Trophy, minWidth: 200 },
{ prop: 'ruleType', label: '奖项类型', icon: Collection },
{ prop: 'remarks', label: '备注', icon: EditPen, minWidth: 200 }
]
// ??????? hook
// 表格列控制 hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
} = useTableColumnControl(tableColumns)
// ????
// 表格样式
const tableStyle = {
cellStyle: { padding: '8px 0' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }
}
// ?? useTable
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
ruleType: ''
@@ -208,7 +208,7 @@ const state: BasicTableProps = reactive<BasicTableProps>({
item: 'records',
totalCount: 'total'
},
createdIsNeed: true // ???????????
createdIsNeed: true // 创建时是否需要请求数据
})
// table hook
@@ -219,7 +219,7 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// ???????
// 格式化规则类型
const formatRuleType = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -228,34 +228,34 @@ const formatRuleType = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.ruleType = ''
getDataList()
}
// ??
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// ??
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('??????????????')
await confirm('确定要删除该记录吗?')
await delObj([row.id])
useMessage().success('????')
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '删除失败')
}
}
}
// ????????
// 获取规则类型字典
const getRuleTypeDict = async () => {
try {
const res = await getDicts('rule_type')
@@ -272,7 +272,7 @@ const getRuleTypeDict = async () => {
}
}
// ???
// 初始化
onMounted(() => {
getRuleTypeDict()
})

View File

@@ -1,21 +1,21 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ?????? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
????
查询条件
</span>
</div>
</template>
<el-form :model="queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="??" prop="deptCode">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="queryForm.deptCode"
placeholder="?????"
placeholder="请选择学院"
clearable
filterable
style="width: 200px">
@@ -27,10 +27,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="classCode">
<el-form-item label="班级" prop="classCode">
<el-select
v-model="queryForm.classCode"
placeholder="?????"
placeholder="请选择班级"
clearable
filterable
style="width: 200px">
@@ -42,10 +42,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="schoolYear">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="queryForm.schoolYear"
placeholder="?????"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
@@ -57,10 +57,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="schoolTerm">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="queryForm.schoolTerm"
placeholder="?????"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
@@ -72,19 +72,19 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">??</el-button>
<el-button icon="Refresh" @click="handleReset">??</el-button>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
????
奖励学生列表
</span>
<div class="header-actions">
<el-button
@@ -92,7 +92,7 @@
type="success"
class="ml10"
@click="handleExport">
??
导出
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -108,7 +108,7 @@
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="???" placement="top">
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
@@ -120,7 +120,7 @@
</div>
</template>
<!-- ?? -->
<!-- 表格 -->
<el-table
:data="dataList"
v-loading="loading"
@@ -128,7 +128,7 @@
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="??" width="70" align="center">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
@@ -138,7 +138,7 @@
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '??'"
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
@@ -149,28 +149,28 @@
<el-icon><component :is="columnConfigMap[col.prop]?.icon || OfficeBuilding" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- ?????????? -->
<!-- 平均操行分格式化 -->
<template v-if="col.prop === 'averageConduct'" #default="scope">
<el-tag v-if="scope.row.averageConduct !== null && scope.row.averageConduct !== undefined" size="small" type="success" effect="plain">
{{ scope.row.averageConduct.toFixed(2) }}
</el-tag>
<span v-else>-</span>
</template>
<!-- ???????????? -->
<!-- 平均学习成绩格式化 -->
<template v-else-if="col.prop === 'averageScore'" #default="scope">
<el-tag v-if="scope.row.averageScore !== null && scope.row.averageScore !== undefined" size="small" type="primary" effect="plain">
{{ scope.row.averageScore.toFixed(2) }}
</el-tag>
<span v-else>-</span>
</template>
<!-- ????????? -->
<!-- 规则名称格式化 -->
<template v-else-if="col.prop === 'ruleName'" #default="scope">
<el-tag v-if="scope.row.ruleName && Array.isArray(scope.row.ruleName) && scope.row.ruleName.length > 0" size="small" type="warning" effect="light">
{{ scope.row.ruleName.join('?') }}
{{ scope.row.ruleName.join('') }}
</el-tag>
<span v-else>-</span>
</template>
<!-- ????????? -->
<!-- 更新时间格式化 -->
<template v-else-if="col.prop === 'upDateTime'" #default="scope">
<span>{{ parseTime(scope.row.upDateTime, dateTimeFormat) }}</span>
</template>
@@ -198,7 +198,7 @@ import { useTableColumnControl } from '/@/hooks/tableColumn'
const dateTimeFormat = '{y}-{m}-{d} {h}:{i}:{s}'
// ????
// 定义变量
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
@@ -210,20 +210,20 @@ const schoolTermList = ref<any[]>([])
const deptList = ref<any[]>([])
const classList = ref<any[]>([])
// ?????
// 表格列配置
const tableColumns = [
{ prop: 'departName', label: '????' },
{ prop: 'classNo', label: '??' },
{ prop: 'stuNo', label: '??' },
{ prop: 'realName', label: '??' },
{ prop: 'averageConduct', label: '?????' },
{ prop: 'averageScore', label: '???????' },
{ prop: 'violation', label: '????', minWidth: 150 },
{ prop: 'ruleName', label: '????', minWidth: 200 },
{ prop: 'upDateTime', label: '????', width: 180 }
{ prop: 'departName', label: '学院名称' },
{ prop: 'classNo', label: '班级' },
{ prop: 'stuNo', label: '学号' },
{ prop: 'realName', label: '姓名' },
{ prop: 'averageConduct', label: '平均操行分' },
{ prop: 'averageScore', label: '平均学习成绩' },
{ prop: 'violation', label: '违规情况', minWidth: 150 },
{ prop: 'ruleName', label: '奖励规则', minWidth: 200 },
{ prop: 'upDateTime', label: '更新时间', width: 180 }
]
// ?????????????
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
departName: { icon: OfficeBuilding },
classNo: { icon: Grid },
@@ -236,7 +236,7 @@ const columnConfigMap: Record<string, { icon: any }> = {
upDateTime: { icon: Clock }
}
// ???????hook
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
@@ -245,7 +245,7 @@ const {
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// ????
// 查询表单
const queryForm = reactive({
deptCode: '',
classCode: '',
@@ -253,13 +253,13 @@ const queryForm = reactive({
schoolTerm: ''
})
// ????
// 表格样式
const tableStyle = {
cellStyle: { padding: '8px 0' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }
}
// ?????
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -268,7 +268,7 @@ const formatSchoolTerm = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ????
// 获取数据列表
const getDataList = async () => {
loading.value = true
try {
@@ -279,14 +279,14 @@ const getDataList = async () => {
dataList.value = []
}
} catch (err: any) {
useMessage().error(err.msg || '??????')
useMessage().error(err.msg || '查询失败')
dataList.value = []
} finally {
loading.value = false
}
}
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
queryForm.deptCode = ''
@@ -296,43 +296,43 @@ const handleReset = () => {
getDataList()
}
// ??
// 导出
const handleExport = async () => {
try {
loading.value = true
const res = await exportExcel(queryForm)
// ?? blob
// 创建 blob
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
// ??????
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// ???
const fileName = `??????_${new Date().getTime()}.xlsx`
// 设置文件名
const fileName = `奖励学生_${new Date().getTime()}.xlsx`
link.setAttribute('download', fileName)
// ????
// 触发下载
document.body.appendChild(link)
link.click()
// ??
// 清理
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('????')
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '导出失败')
} finally {
loading.value = false
}
}
// ??????
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
@@ -346,7 +346,7 @@ const getSchoolYearList = async () => {
}
}
// ??????
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
@@ -363,7 +363,7 @@ const getSchoolTermDict = async () => {
}
}
// ??????
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
@@ -377,7 +377,7 @@ const getDeptListData = async () => {
}
}
// ??????
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
@@ -391,7 +391,7 @@ const getClassListData = async () => {
}
}
// ????
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
@@ -400,7 +400,6 @@ onMounted(() => {
getDataList()
nextTick(() => {
if (visibleColumns.value.length === 0) {
loadSavedConfig()
}
})
})

View File

@@ -1,21 +1,21 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ?????? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
????
查询条件
</span>
</div>
</template>
<el-form :model="state.queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="??" prop="schoolYear">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="state.queryForm.schoolYear"
placeholder="?????"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
@@ -27,10 +27,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="schoolTerm">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="state.queryForm.schoolTerm"
placeholder="?????"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
@@ -41,10 +41,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="deptCode">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="state.queryForm.deptCode"
placeholder="?????"
placeholder="请选择学院"
clearable
filterable
style="width: 200px">
@@ -56,10 +56,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="classCode">
<el-form-item label="班级" prop="classCode">
<el-select
v-model="state.queryForm.classCode"
placeholder="?????"
placeholder="请选择班级"
clearable
filterable
style="width: 200px">
@@ -71,41 +71,41 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="realName">
<el-form-item label="姓名" prop="realName">
<el-input
v-model="state.queryForm.realName"
placeholder="?????"
placeholder="请输入姓名"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="??" prop="stuNo">
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="state.queryForm.stuNo"
placeholder="?????"
placeholder="请输入学号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">??</el-button>
<el-button icon="Refresh" @click="handleReset">??</el-button>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
????????
学生关怀记录列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
@click="formDialogRef.openDialog()">
??
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -121,7 +121,7 @@
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="???" placement="top">
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
@@ -133,7 +133,7 @@
</div>
</template>
<!-- ?? -->
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
@@ -141,7 +141,7 @@
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="??" width="70" align="center">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
@@ -151,7 +151,7 @@
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '??'"
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
@@ -162,32 +162,32 @@
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- ??????? -->
<!-- 学期格式化 -->
<template v-if="col.prop === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ formatSchoolTerm(scope.row.schoolTerm) }}
</el-tag>
</template>
<!-- ?????????? -->
<!-- 关怀类型格式化 -->
<template v-else-if="col.prop === 'careType'" #default="scope">
<el-tag size="small" type="danger" effect="plain">
{{ formatCareType(scope.row.careType) }}
</el-tag>
</template>
<!-- ????????? -->
<!-- 记录日期格式化 -->
<template v-else-if="col.prop === 'recordDate'" #default="scope">
<span>{{ scope.row.recordDate || '-' }}</span>
</template>
<!-- ????????? -->
<!-- 处理结果格式化 -->
<template v-else-if="col.prop === 'result'" #default="scope">
<span>{{ scope.row.result || '-' }}</span>
</template>
</el-table-column>
</template>
<el-table-column label="??" width="250" align="center" fixed="right">
<el-table-column label="操作" width="250" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">??</span>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
@@ -195,27 +195,27 @@
link
type="primary"
@click="handleEdit(scope.row)">
??
编辑
</el-button>
<el-button
icon="Document"
link
type="success"
@click="handleResult(scope.row)">
????
处理结果
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
??
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ?? -->
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@@ -225,10 +225,10 @@
</el-card>
</div>
<!-- ??/?????? -->
<!-- 新增/编辑对话框 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
<!-- ?????? -->
<!-- 处理结果对话框 -->
<result-dialog ref="resultDialogRef" @refresh="getDataList" />
</div>
</template>
@@ -250,7 +250,7 @@ import { List, Calendar, Clock, OfficeBuilding, Grid, Avatar, UserFilled, Phone,
import { useTableColumnControl } from '/@/hooks/tableColumn'
// ??????
// 定义变量
const route = useRoute()
const formDialogRef = ref()
const resultDialogRef = ref()
@@ -263,22 +263,22 @@ const deptList = ref<any[]>([])
const classList = ref<any[]>([])
const careTypeList = ref<any[]>([])
// ?????
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '??' },
{ prop: 'schoolTerm', label: '??' },
{ prop: 'deptName', label: '??' },
{ prop: 'classNo', label: '??' },
{ prop: 'realName', label: '??' },
{ prop: 'teacherRealName', label: '?????' },
{ prop: 'teacherTel', label: '?????' },
{ prop: 'careType', label: '?????' },
{ prop: 'present', label: '????', minWidth: 150 },
{ prop: 'recordDate', label: '????', width: 120 },
{ prop: 'result', label: '????', minWidth: 150 }
{ prop: 'schoolYear', label: '学年' },
{ prop: 'schoolTerm', label: '学期' },
{ prop: 'deptName', label: '学院' },
{ prop: 'classNo', label: '班级' },
{ prop: 'realName', label: '姓名' },
{ prop: 'teacherRealName', label: '班主任姓名' },
{ prop: 'teacherTel', label: '班主任电话' },
{ prop: 'careType', label: '关怀类型' },
{ prop: 'present', label: '现状描述', minWidth: 150 },
{ prop: 'recordDate', label: '记录日期', width: 120 },
{ prop: 'result', label: '处理结果', minWidth: 150 }
]
// ?????????????
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
schoolYear: { icon: Calendar },
schoolTerm: { icon: Clock },
@@ -293,7 +293,7 @@ const columnConfigMap: Record<string, { icon: any }> = {
result: { icon: CircleCheck }
}
// ???????hook
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
@@ -302,7 +302,7 @@ const {
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// ?? useTable
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
schoolYear: '',
@@ -328,7 +328,7 @@ const {
tableStyle
} = useTable(state)
// ?????
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -337,14 +337,14 @@ const formatSchoolTerm = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ????????
// 格式化关怀类型
const formatCareType = (value: string) => {
if (!value) return '-'
const item = careTypeList.value.find((item: any) => item.value === value)
return item ? item.label : value
}
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.schoolYear = ''
@@ -356,32 +356,32 @@ const handleReset = () => {
getDataList()
}
// ??
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// ????
// 处理结果
const handleResult = (row: any) => {
resultDialogRef.value?.openDialog(row)
}
// ??
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('??????????????')
await confirm('确定要删除该记录吗?')
await delObj([row.id])
useMessage().success('????')
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '删除失败')
}
}
}
// ??????
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
@@ -395,7 +395,7 @@ const getSchoolYearList = async () => {
}
}
// ??????
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
@@ -412,7 +412,7 @@ const getSchoolTermDict = async () => {
}
}
// ??????
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
@@ -426,7 +426,7 @@ const getDeptListData = async () => {
}
}
// ??????
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
@@ -440,7 +440,7 @@ const getClassListData = async () => {
}
}
// ?????????
// 获取关怀类型字典
const getCareTypeDict = async () => {
try {
const res = await getDicts('stu_care_type')
@@ -457,7 +457,7 @@ const getCareTypeDict = async () => {
}
}
// ???
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()

View File

@@ -1,21 +1,21 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ?????? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
????
查询条件
</span>
</div>
</template>
<el-form :model="state.queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="??" prop="schoolYear">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="state.queryForm.schoolYear"
placeholder="?????"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
@@ -27,10 +27,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="schoolTerm">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="state.queryForm.schoolTerm"
placeholder="?????"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
@@ -41,10 +41,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="deptCode">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="state.queryForm.deptCode"
placeholder="?????"
placeholder="请选择学院"
clearable
filterable
style="width: 200px"
@@ -57,10 +57,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="classCode">
<el-form-item label="班级" prop="classCode">
<el-select
v-model="state.queryForm.classCode"
placeholder="?????"
placeholder="请选择班级"
clearable
filterable
style="width: 200px">
@@ -72,17 +72,17 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="realName">
<el-form-item label="姓名" prop="realName">
<el-input
v-model="state.queryForm.realName"
placeholder="?????"
placeholder="请输入姓名"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="??" prop="conductType">
<el-form-item label="类型" prop="conductType">
<el-select
v-model="state.queryForm.conductType"
placeholder="?????"
placeholder="请选择类型"
clearable
style="width: 200px">
<el-option
@@ -94,33 +94,33 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">??</el-button>
<el-button icon="Refresh" @click="handleReset">??</el-button>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
????????
学生行为记录列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
@click="formDialogRef.openDialog()">
??
新增
</el-button>
<el-button
icon="Upload"
type="success"
class="ml10"
@click="handleImport">
??
导入
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -136,7 +136,7 @@
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="???" placement="top">
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
@@ -148,7 +148,7 @@
</div>
</template>
<!-- ?? -->
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
@@ -156,7 +156,7 @@
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="??" width="70" align="center">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
@@ -166,7 +166,7 @@
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '??'"
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
@@ -177,23 +177,23 @@
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- ??????? -->
<!-- 学期格式化 -->
<template v-if="col.prop === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ formatSchoolTerm(scope.row.schoolTerm) }}
</el-tag>
</template>
<!-- ??????? -->
<!-- 类型格式化 -->
<template v-else-if="col.prop === 'conductType'" #default="scope">
<el-tag size="small" type="warning" effect="plain">
{{ formatType(scope.row.conductType) }}
</el-tag>
</template>
<!-- ????????? -->
<!-- 记录日期格式化 -->
<template v-else-if="col.prop === 'recordDate'" #default="scope">
<span>{{ scope.row.recordDate || '-' }}</span>
</template>
<!-- ??????? -->
<!-- 附件格式化 -->
<template v-else-if="col.prop === 'attachment'" #default="scope">
<el-button
v-if="scope.row.attachment"
@@ -202,16 +202,16 @@
type="primary"
size="small"
@click="handleViewAttachment(scope.row)">
??
查看
</el-button>
<span v-else>-</span>
</template>
</el-table-column>
</template>
<el-table-column label="??" width="150" align="center" fixed="right">
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">??</span>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
@@ -219,20 +219,20 @@
link
type="primary"
@click="handleEdit(scope.row)">
??
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
??
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ?? -->
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@@ -242,12 +242,12 @@
</el-card>
</div>
<!-- ??/?????? -->
<!-- 新增/编辑对话框 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
<!-- ???? -->
<!-- 导入对话框 -->
<el-dialog
title="??????"
title="导入数据"
v-model="importDialogVisible"
:width="500"
:close-on-click-modal="false"
@@ -261,18 +261,18 @@
drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
????????<em>????</em>
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
???? xlsx/xls ??
只能上传 xlsx/xls 文件
</div>
</template>
</el-upload>
<template #footer>
<span class="dialog-footer">
<el-button @click="importDialogVisible = false">??</el-button>
<el-button type="primary" @click="handleImportSubmit" :disabled="!importFile || importLoading">??</el-button>
<el-button @click="importDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleImportSubmit" :disabled="!importFile || importLoading">确认</el-button>
</span>
</template>
</el-dialog>
@@ -295,7 +295,7 @@ import { useTableColumnControl } from '/@/hooks/tableColumn'
import FormDialog from './form.vue'
// ??????
// 定义变量
const route = useRoute()
const formDialogRef = ref()
const columnControlRef = ref<any>()
@@ -311,21 +311,21 @@ const importDialogVisible = ref(false)
const importFile = ref<File | null>(null)
const importLoading = ref(false)
// ?????
// 表格列配置
const tableColumns = [
{ prop: 'stuNo', label: '??' },
{ prop: 'schoolYear', label: '??' },
{ prop: 'schoolTerm', label: '??' },
{ prop: 'deptName', label: '??' },
{ prop: 'classNo', label: '??' },
{ prop: 'realName', label: '??' },
{ prop: 'conductType', label: '??' },
{ prop: 'recordDate', label: '????', width: 120 },
{ prop: 'description', label: '????', minWidth: 150 },
{ prop: 'attachment', label: '??', width: 100 }
{ prop: 'stuNo', label: '学号' },
{ prop: 'schoolYear', label: '学年' },
{ prop: 'schoolTerm', label: '学期' },
{ prop: 'deptName', label: '学院' },
{ prop: 'classNo', label: '班级' },
{ prop: 'realName', label: '姓名' },
{ prop: 'conductType', label: '类型' },
{ prop: 'recordDate', label: '记录日期', width: 120 },
{ prop: 'description', label: '描述说明', minWidth: 150 },
{ prop: 'attachment', label: '附件', width: 100 }
]
// ????????????
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
stuNo: { icon: CreditCard },
schoolYear: { icon: Calendar },
@@ -339,7 +339,7 @@ const columnConfigMap: Record<string, { icon: any }> = {
attachment: { icon: Document }
}
// ???????hook
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
@@ -348,7 +348,7 @@ const {
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// ?? useTable
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
schoolYear: '',
@@ -374,7 +374,7 @@ const {
tableStyle
} = useTable(state)
// ?????
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -383,19 +383,19 @@ const formatSchoolTerm = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ?????
// 格式化类型
const formatType = (value: string) => {
if (!value) return '-'
const item = typeList.value.find((item: any) => item.value === value)
return item ? item.label : value
}
// ????
// 学院变化
const handleDeptChange = () => {
// ??????????????????
// 学院变化时清空班级选择
}
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.schoolYear = ''
@@ -407,7 +407,7 @@ const handleReset = () => {
getDataList()
}
// ????
// 查看附件
const handleViewAttachment = (row: any) => {
if (row.attachment) {
const urls = typeof row.attachment === 'string' ? row.attachment.split(',') : [row.attachment]
@@ -419,61 +419,61 @@ const handleViewAttachment = (row: any) => {
}
}
// ??
// 导入
const handleImport = () => {
importDialogVisible.value = true
importFile.value = null
uploadRef.value?.clearFiles()
}
// ????
// 文件变化
const handleFileChange = (file: any) => {
importFile.value = file.raw
}
// ????
// 提交导入
const handleImportSubmit = async () => {
if (!importFile.value) {
useMessage().warning('?????????')
useMessage().warning('请先选择要上传的文件')
return
}
importLoading.value = true
try {
await importExcel(importFile.value)
useMessage().success('????')
useMessage().success('导入成功')
importDialogVisible.value = false
importFile.value = null
uploadRef.value?.clearFiles()
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '导入失败')
} finally {
importLoading.value = false
}
}
// ??
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// ??
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('??????????????')
await confirm('确定要删除该记录吗?')
await delObj([row.id])
useMessage().success('????')
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '删除失败')
}
}
}
// ??????
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
@@ -487,7 +487,7 @@ const getSchoolYearList = async () => {
}
}
// ??????
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
@@ -504,7 +504,7 @@ const getSchoolTermDict = async () => {
}
}
// ??????
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
@@ -518,7 +518,7 @@ const getDeptListData = async () => {
}
}
// ??????
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
@@ -532,7 +532,7 @@ const getClassListData = async () => {
}
}
// ??????
// 获取类型字典
const getTypeDict = async () => {
try {
const res = await getDicts('conduct_type')
@@ -549,7 +549,7 @@ const getTypeDict = async () => {
}
}
// ???
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()

View File

@@ -104,12 +104,62 @@
</el-table>
</el-row>
</div>
<!-- 查看详情弹窗接口queryDataByStuNo 通过学年学号查看详情按当前学期筛选 -->
<el-dialog
v-model="viewDialogVisible"
title="学期操行考核详情"
width="800px"
destroy-on-close
@close="viewDetailList = []">
<div v-if="viewRow" class="view-summary">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="学号">{{ viewRow.stuNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewRow.realName }}</el-descriptions-item>
<el-descriptions-item label="学年">{{ queryForm.schoolYear }}</el-descriptions-item>
<el-descriptions-item label="学期">{{ formatSchoolTerm(queryForm.schoolTerm) }}</el-descriptions-item>
<el-descriptions-item label="学期总评" :span="2">
{{ viewRow.score != null && viewRow.score !== undefined ? Number(viewRow.score).toFixed(2) : '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<div class="view-detail-title">考核记录</div>
<el-table
:data="viewDetailList"
v-loading="viewLoading"
border
size="small"
max-height="400"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="schoolTerm" label="学期" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="recordDate" label="考核日期" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="conductType" label="类型" width="80" align="center">
<template #default="scope">
<el-tag :type="scope.row.conductType === '1' ? 'success' : 'danger'" size="small">
{{ scope.row.conductType === '1' ? '加分' : scope.row.conductType === '0' ? '扣分' : scope.row.conductType || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="score" label="分数" width="80" align="center">
<template #default="scope">
{{ scope.row.score != null && scope.row.score !== undefined ? Number(scope.row.score) : '-' }}
</template>
</el-table-column>
<el-table-column prop="description" label="情况记录" min-width="140" show-overflow-tooltip />
<el-table-column prop="remarks" label="备注" min-width="100" show-overflow-tooltip />
</el-table>
<template v-if="viewDetailList.length === 0 && !viewLoading">
<el-empty description="暂无考核记录" :image-size="80" />
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="StuConductTerm">
import { reactive, ref, onMounted, computed } from 'vue'
import { getStuConductTerm } from "/@/api/stuwork/stuconduct";
import { getStuConductTerm, queryDataByStuNo } from "/@/api/stuwork/stuconduct";
import { getClassListByRole } from "/@/api/basic/basicclass";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { getDicts } from "/@/api/admin/dict";
@@ -129,6 +179,10 @@ const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const viewDialogVisible = ref(false)
const viewLoading = ref(false)
const viewRow = ref<any>(null)
const viewDetailList = ref<any[]>([])
// 查询表单
const queryForm = reactive({
@@ -263,14 +317,20 @@ const getDataList = async () => {
} else {
studentList.value = []
}
} catch (err: any) {
useMessage().error(err.msg || '获取数据列表失败')
} catch (_err) {
studentList.value = []
} finally {
loading.value = false
}
}
// 格式化学期显示
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') return '-'
const dictItem = schoolTermList.value.find((item: any) => item.value == value)
return dictItem ? dictItem.label : value
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
@@ -280,10 +340,29 @@ const handleReset = () => {
studentList.value = []
}
// 查看详情
const handleView = (row: any) => {
// 可以跳转到详情页面或打开详情弹窗
useMessage().info('查看详情功能待实现')
// 查看详情接口GET /stuwork/stuconduct/queryDataByStuNo按当前学年+学号拉取后筛本学期记录)
const handleView = async (row: any) => {
if (!queryForm.schoolYear || !row.stuNo) {
useMessage().warning('缺少学年或学号')
return
}
viewRow.value = row
viewDialogVisible.value = true
viewDetailList.value = []
viewLoading.value = true
try {
const res = await queryDataByStuNo({
schoolYear: queryForm.schoolYear,
stuNo: row.stuNo
})
const list = Array.isArray(res.data) ? res.data : []
const term = queryForm.schoolTerm
viewDetailList.value = term ? list.filter((r: any) => String(r.schoolTerm) === String(term)) : list
} catch (_err) {
viewDetailList.value = []
} finally {
viewLoading.value = false
}
}
// 获取学年列表
@@ -350,6 +429,15 @@ onMounted(() => {
}
}
.view-summary {
margin-bottom: 16px;
}
.view-detail-title {
margin-bottom: 8px;
font-weight: 600;
color: #303133;
}
// 确保页面可以滚动
.layout-padding {
height: 100%;

View File

@@ -90,12 +90,61 @@
</el-table>
</el-row>
</div>
<!-- 查看详情弹窗接口queryDataByStuNo 通过学年学号查看详情 -->
<el-dialog
v-model="viewDialogVisible"
title="学年操行考核详情"
width="800px"
destroy-on-close
@close="viewDetailList = []">
<div v-if="viewRow" class="view-summary">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="学号">{{ viewRow.stuNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewRow.realName }}</el-descriptions-item>
<el-descriptions-item label="学年">{{ queryForm.schoolYear }}</el-descriptions-item>
<el-descriptions-item label="学年总评">
{{ viewRow.score != null && viewRow.score !== undefined ? Number(viewRow.score).toFixed(2) : '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<div class="view-detail-title">考核记录</div>
<el-table
:data="viewDetailList"
v-loading="viewLoading"
border
size="small"
max-height="400"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="schoolTerm" label="学期" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="recordDate" label="考核日期" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="conductType" label="类型" width="80" align="center">
<template #default="scope">
<el-tag :type="scope.row.conductType === '1' ? 'success' : 'danger'" size="small">
{{ scope.row.conductType === '1' ? '加分' : scope.row.conductType === '0' ? '扣分' : scope.row.conductType || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="score" label="分数" width="80" align="center">
<template #default="scope">
{{ scope.row.score != null && scope.row.score !== undefined ? Number(scope.row.score) : '-' }}
</template>
</el-table-column>
<el-table-column prop="description" label="情况记录" min-width="140" show-overflow-tooltip />
<el-table-column prop="remarks" label="备注" min-width="100" show-overflow-tooltip />
</el-table>
<template v-if="viewDetailList.length === 0 && !viewLoading">
<el-empty description="暂无考核记录" :image-size="80" />
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="StuConductYear">
import { reactive, ref, onMounted, computed } from 'vue'
import { getStuConductYear } from "/@/api/stuwork/stuconduct";
import { getStuConductYear, queryDataByStuNo } from "/@/api/stuwork/stuconduct";
import { getClassListByRole } from "/@/api/basic/basicclass";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { useMessage } from "/@/hooks/message";
@@ -113,6 +162,10 @@ const loading = ref(false)
const schoolYearList = ref<any[]>([])
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const viewDialogVisible = ref(false)
const viewLoading = ref(false)
const viewRow = ref<any>(null)
const viewDetailList = ref<any[]>([])
// 查询表单
const queryForm = reactive({
@@ -245,8 +298,7 @@ const getDataList = async () => {
} else {
studentList.value = []
}
} catch (err: any) {
useMessage().error(err.msg || '获取数据列表失败')
} catch (_err) {
studentList.value = []
} finally {
loading.value = false
@@ -261,10 +313,27 @@ const handleReset = () => {
studentList.value = []
}
// 查看详情
const handleView = (row: any) => {
// 可以跳转到详情页面或打开详情弹窗
useMessage().info('查看详情功能待实现')
// 查看详情接口GET /stuwork/stuconduct/queryDataByStuNo通过学年学号查看详情
const handleView = async (row: any) => {
if (!queryForm.schoolYear || !row.stuNo) {
useMessage().warning('缺少学年或学号')
return
}
viewRow.value = row
viewDialogVisible.value = true
viewDetailList.value = []
viewLoading.value = true
try {
const res = await queryDataByStuNo({
schoolYear: queryForm.schoolYear,
stuNo: row.stuNo
})
viewDetailList.value = Array.isArray(res.data) ? res.data : []
} catch (_err) {
viewDetailList.value = []
} finally {
viewLoading.value = false
}
}
// 获取学年列表
@@ -313,6 +382,15 @@ onMounted(() => {
}
}
.view-summary {
margin-bottom: 16px;
}
.view-detail-title {
margin-bottom: 8px;
font-weight: 600;
color: #303133;
}
// 确保页面可以滚动
.layout-padding {
height: 100%;

View File

@@ -0,0 +1,309 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="handleSearch"
class="search-form">
<el-form-item label="毕业年份" prop="graduYear">
<el-select
v-model="searchForm.graduYear"
placeholder="请选择毕业年份"
clearable
filterable
style="width: 140px">
<el-option
v-for="y in graduYearOptions"
:key="y"
:label="y + '年'"
:value="y" />
</el-select>
</el-form-item>
<el-form-item label="毕业状态" prop="status">
<el-select
v-model="searchForm.status"
placeholder="请选择"
clearable
style="width: 140px">
<el-option label="待确认" value="0" />
<el-option label="确认毕业" value="1" />
<el-option label="不可毕业" value="-1" />
</el-select>
</el-form-item>
<el-form-item label="毕业类型" prop="type">
<el-select
v-model="searchForm.type"
placeholder="请选择"
clearable
style="width: 120px">
<el-option label="段段清" value="1" />
<el-option label="正常毕业" value="2" />
</el-select>
</el-form-item>
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="searchForm.stuNo"
placeholder="请输入学号"
clearable
style="width: 140px" />
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input
v-model="searchForm.realName"
placeholder="请输入姓名"
clearable
style="width: 120px" />
</el-form-item>
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="searchForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 160px">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode" />
</el-select>
</el-form-item>
<el-form-item label="班号" prop="classNo">
<el-input
v-model="searchForm.classNo"
placeholder="请输入班号"
clearable
style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
毕业学生名单
</span>
<div class="header-actions">
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<template v-if="col.prop === 'status'" #default="scope">
<el-tag :type="statusTagType(scope.row.status)" size="small">
{{ formatStatus(scope.row.status) }}
</el-tag>
</template>
<template v-else-if="col.prop === 'type'" #default="scope">
{{ scope.row.type === '1' ? '段段清' : scope.row.type === '2' ? '正常毕业' : scope.row.type || '-' }}
</template>
</el-table-column>
</template>
<template #empty>
<el-empty description="暂无数据,请选择条件查询" :image-size="120" />
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts" name="Stugraducheck">
import { reactive, ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList } from '/@/api/stuwork/stugraducheck'
import { getDeptList } from '/@/api/basic/basicclass'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import {
List,
Calendar,
UserFilled,
Document,
Menu,
Search,
Grid
} from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
const deptList = ref<any[]>([])
// 毕业年份:当前年前后各 5 年
const graduYearOptions = computed(() => {
const y = new Date().getFullYear()
return Array.from({ length: 11 }, (_, i) => y - 5 + i)
})
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'stuNo', label: '学号', icon: UserFilled },
{ prop: 'realName', label: '姓名', icon: UserFilled },
{ prop: 'classNo', label: '班号', icon: Grid },
{ prop: 'majorName', label: '专业名称', icon: Document, minWidth: 140 },
{ prop: 'graduYear', label: '毕业年份', icon: Calendar },
{ prop: 'status', label: '毕业状态', icon: Document },
{ prop: 'type', label: '毕业类型', icon: Document },
{ prop: 'scoreCondition', label: '学分情况', icon: Document, minWidth: 100 },
{ prop: 'skillCondition', label: '技能情况', icon: Document, minWidth: 100 },
{ prop: 'conductCondition', label: '操行情况', icon: Document, minWidth: 100 }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单与接口文档一致graduYear, status, type, stuNo, realName, deptCode, classCode 等)
const searchForm = reactive({
graduYear: '',
status: '',
type: '',
stuNo: '',
realName: '',
deptCode: '',
classNo: ''
})
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
const formatStatus = (status: string) => {
const map: Record<string, string> = { '0': '待确认', '1': '确认毕业', '-1': '不可毕业' }
return map[status] || status || '-'
}
const statusTagType = (status: string) => {
const map: Record<string, string> = { '0': 'warning', '1': 'success', '-1': 'danger' }
return map[status] || 'info'
}
const handleSearch = () => {
getDataList()
}
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.graduYear = ''
searchForm.status = ''
searchForm.type = ''
searchForm.stuNo = ''
searchForm.realName = ''
searchForm.deptCode = ''
searchForm.classNo = ''
getDataList()
}
const loadDeptList = async () => {
try {
const res = await getDeptList()
deptList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
deptList.value = []
}
}
onMounted(() => {
loadDeptList()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -188,9 +188,13 @@ const handleClassChange = async (classCode: string) => {
}
try {
const res = await queryStudentListByClass({ classCode, current: 1, size: 10000 })
if (res.data && Array.isArray(res.data.records)) {
studentList.value = res.data.records
// 接口文档GET 仅 classCode返回 data 为数组
const res = await queryStudentListByClass({ classCode })
const data = res?.data
if (Array.isArray(data)) {
studentList.value = data
} else if (data?.records && Array.isArray(data.records)) {
studentList.value = data.records
} else {
studentList.value = []
}

View File

@@ -181,9 +181,13 @@ const handleClassChange = async (classCode: string) => {
}
try {
const res = await queryStudentListByClass({ classCode, current: 1, size: 10000 })
if (res.data && Array.isArray(res.data.records)) {
studentList.value = res.data.records
// 接口文档GET仅 classCode返回 data 为数组,非 data.records
const res = await queryStudentListByClass({ classCode })
const data = res?.data
if (Array.isArray(data)) {
studentList.value = data
} else if (data?.records && Array.isArray(data.records)) {
studentList.value = data.records
} else {
studentList.value = []
}

View File

@@ -1,21 +1,21 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ???? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
????
查询条件
</span>
</div>
</template>
<el-form :model="searchForm" ref="searchFormRef" :inline="true" @keyup.enter="handleSearch" class="search-form">
<el-form-item label="??" prop="schoolYear">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="searchForm.schoolYear"
placeholder="?????"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
@@ -27,10 +27,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="schoolTerm">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="searchForm.schoolTerm"
placeholder="?????"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
@@ -41,10 +41,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="deptCode">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="searchForm.deptCode"
placeholder="?????"
placeholder="请选择学院"
clearable
filterable
style="width: 200px">
@@ -56,10 +56,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="classCode">
<el-form-item label="班级" prop="classCode">
<el-select
v-model="searchForm.classCode"
placeholder="?????"
placeholder="请选择班级"
clearable
filterable
style="width: 200px">
@@ -71,34 +71,34 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="realName">
<el-form-item label="姓名" prop="realName">
<el-input
v-model="searchForm.realName"
placeholder="?????"
placeholder="请输入姓名"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="??" prop="stuNo">
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="searchForm.stuNo"
placeholder="?????"
placeholder="请输入学号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="????" prop="punlishMonth">
<el-form-item label="处分月份" prop="punlishMonth">
<el-date-picker
v-model="searchForm.punlishMonth"
type="month"
placeholder="??????"
placeholder="请选择处分月份"
format="YYYY-MM"
value-format="YYYY-MM"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="????" prop="punlishLevel">
<el-form-item label="处分等级" prop="punlishLevel">
<el-select
v-model="searchForm.punlishLevel"
placeholder="???????"
placeholder="请选择处分等级"
clearable
style="width: 200px">
<el-option
@@ -110,33 +110,33 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="handleSearch">??</el-button>
<el-button icon="Refresh" @click="handleReset">??</el-button>
<el-button type="primary" plain icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
??????
学生处分列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
??
新增
</el-button>
<el-button
icon="Download"
type="success"
class="ml10"
@click="handleExport">
??
导出
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -152,7 +152,7 @@
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="???" placement="top">
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
@@ -164,7 +164,7 @@
</div>
</template>
<!-- ?? -->
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
@@ -172,7 +172,7 @@
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="??" width="70" align="center">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
@@ -182,7 +182,7 @@
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '??'"
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
@@ -193,40 +193,40 @@
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- ???? -->
<!-- 学期 -->
<template v-if="col.prop === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ formatSchoolTerm(scope.row.schoolTerm) }}
</el-tag>
</template>
<!-- ?????? -->
<!-- 处分开始日期 -->
<template v-else-if="col.prop === 'punlishStartDate'" #default="scope">
<span>{{ scope.row.punlishStartDate ? formatDate(scope.row.punlishStartDate) : '-' }}</span>
</template>
<!-- ?????? -->
<!-- 处分结束日期 -->
<template v-else-if="col.prop === 'punlishEndDate'" #default="scope">
<span>{{ scope.row.punlishEndDate ? formatDate(scope.row.punlishEndDate) : '-' }}</span>
</template>
<!-- ?????? -->
<!-- 处分等级 -->
<template v-else-if="col.prop === 'punlishLevel'" #default="scope">
<el-tag size="small" type="danger" effect="plain">
{{ formatPunlishLevel(scope.row.punlishLevel) }}
</el-tag>
</template>
<!-- ???? -->
<!-- 状态 -->
<template v-else-if="col.prop === 'publishStatus'" #default="scope">
<StatusTag
:value="scope.row.publishStatus"
:options="[{ label: '??', value: '1' }, { label: '???', value: '2' }, { label: '???', value: '3' }]"
:options="[{ label: '已发布', value: '1' }, { label: '已撤销', value: '2' }, { label: '已取消', value: '3' }]"
:type-map="{ '1': { type: 'danger', effect: 'light' }, '2': { type: 'success', effect: 'light' }, '3': { type: 'info', effect: 'light' } }"
/>
</template>
</el-table-column>
</template>
<el-table-column label="??" width="250" align="center" fixed="right">
<el-table-column label="操作" width="250" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">??</span>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
@@ -234,34 +234,34 @@
link
type="primary"
@click="handleReport(scope.row)">
????
处分报告
</el-button>
<el-button
icon="CircleClose"
link
type="warning"
@click="handleCancel(scope.row)">
????
撤销处分
</el-button>
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
??
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
??
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ?? -->
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@@ -271,7 +271,7 @@
</el-card>
</div>
<!-- ??/???? -->
<!-- 新增/编辑 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
@@ -294,7 +294,7 @@ import { useTableColumnControl } from '/@/hooks/tableColumn'
import { defineAsyncComponent } from 'vue'
const StatusTag = defineAsyncComponent(() => import('/@/components/StatusTag/index.vue'))
// ???
// 定义变量
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
@@ -307,22 +307,22 @@ const punlishLevelList = ref<any[]>([])
const publishStatusList = ref<any[]>([])
const formDialogRef = ref()
// ???
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '??' },
{ prop: 'schoolTerm', label: '??' },
{ prop: 'deptName', label: '??' },
{ prop: 'classNo', label: '??' },
{ prop: 'stuRealName', label: '??' },
{ prop: 'stuNo', label: '??' },
{ prop: 'punlishStartDate', label: '??????', width: 180 },
{ prop: 'punlishEndDate', label: '???', width: 180 },
{ prop: 'punlishLevel', label: '????' },
{ prop: 'punlishContent', label: '????', minWidth: 150 },
{ prop: 'publishStatus', label: '????' }
{ prop: 'schoolYear', label: '学年' },
{ prop: 'schoolTerm', label: '学期' },
{ prop: 'deptName', label: '学院' },
{ prop: 'classNo', label: '班级' },
{ prop: 'stuRealName', label: '姓名' },
{ prop: 'stuNo', label: '学号' },
{ prop: 'punlishStartDate', label: '处分开始日期', width: 180 },
{ prop: 'punlishEndDate', label: '结束日期', width: 180 },
{ prop: 'punlishLevel', label: '处分等级' },
{ prop: 'punlishContent', label: '处分内容', minWidth: 150 },
{ prop: 'publishStatus', label: '发布状态' }
]
// ?????
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
schoolYear: { icon: Calendar },
schoolTerm: { icon: Clock },
@@ -337,16 +337,16 @@ const columnConfigMap: Record<string, { icon: any }> = {
publishStatus: { icon: CircleCheck }
}
// ????? hook
// 表格列控制 hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
} = useTableColumnControl(tableColumns)
// ????
// 搜索表单
const searchForm = reactive({
schoolYear: '',
schoolTerm: '',
@@ -359,11 +359,11 @@ const searchForm = reactive({
punlishMonthArray: [] as string[]
})
// ?? useTable
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: async (params: any) => {
// ???????? API
// 处理处分月份参数 API
const queryParams = { ...params }
if (queryParams.punlishMonth) {
queryParams.punlishMonth = [queryParams.punlishMonth]
@@ -388,7 +388,7 @@ const {
tableStyle
} = useTable(state)
// ?????
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -397,36 +397,36 @@ const formatSchoolTerm = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ?????
// 格式化日期
const formatDate = (dateStr: string) => {
if (!dateStr) return '-'
// ??????????
// 处理日期时间格式
if (dateStr.includes(' ')) {
return dateStr.split(' ')[0]
}
return dateStr
}
// ???????
// 格式化处分等级
const formatPunlishLevel = (value: string) => {
if (!value) return '-'
const item = punlishLevelList.value.find((item: any) => item.value === value)
return item ? item.label : value
}
// ???????
// 格式化发布状态
const formatPublishStatus = (value: string) => {
if (!value) return '-'
const item = publishStatusList.value.find((item: any) => item.value === value)
return item ? item.label : value
}
// ??
// 查询
const handleSearch = () => {
getDataList()
}
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.schoolYear = ''
@@ -440,65 +440,65 @@ const handleReset = () => {
getDataList()
}
// ??
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// ??
// 删除
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('???????????')
await useMessageBox().confirm('确定要删除该记录吗?')
await delObj([row.id])
useMessage().success('????')
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '删除失败')
}
}
}
// ????
// 处分报告
const handleReport = (row: any) => {
useMessage().info('?????????')
// TODO: ??????
useMessage().info('功能开发中')
// TODO: 实现处分报告
}
// ????
// 撤销处分
const handleCancel = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('??????????')
await confirm('确定要撤销该处分吗?')
// ??????
// 获取详情数据
const res = await getDetail(row.id)
if (!res.data) {
useMessage().error('????????')
useMessage().error('获取数据失败')
return
}
// ? publishStatus ?? "0" ????
// publishStatus 设置为 "0" 表示撤销
const updateData = {
...res.data,
publishStatus: '0'
}
await revokePunishment(updateData)
useMessage().success('??????')
useMessage().success('撤销成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '??????')
useMessage().error(err.msg || '撤销失败')
}
}
}
// ??
// 导出
const handleExport = async () => {
try {
const params: any = { ...searchForm }
// ???????? API
// 处理处分月份参数 API
if (params.punlishMonth) {
params.punlishMonth = [params.punlishMonth]
} else {
@@ -506,23 +506,23 @@ const handleExport = async () => {
}
delete params.punlishMonthArray
const res = await exportData(params)
// ????
// 下载文件
const blob = new Blob([res.data])
const elink = document.createElement('a')
elink.download = '????.xlsx'
elink.download = '学生处分.xlsx'
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
useMessage().success('????')
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '导出失败')
}
}
// ??????
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
@@ -534,7 +534,7 @@ const getSchoolYearList = async () => {
}
}
// ??????
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
@@ -549,7 +549,7 @@ const getSchoolTermDict = async () => {
}
}
// ??????
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptListByLevelTwo()
@@ -561,7 +561,7 @@ const getDeptListData = async () => {
}
}
// ??????
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
@@ -573,7 +573,7 @@ const getClassListData = async () => {
}
}
// ????????
// 获取处分等级字典
const getPunlishLevelDict = async () => {
try {
const res = await getDicts('punlish_level')
@@ -588,7 +588,7 @@ const getPunlishLevelDict = async () => {
}
}
// ????????
// 获取发布状态字典
const getPublishStatusDict = async () => {
try {
const res = await getDicts('publish_status')

View File

@@ -305,13 +305,18 @@ const getClassListData = async () => {
}
}
// 获取学生列表
// 获取学生列表接口文档GET 仅 classCode返回 data 为数组)
const getStudentListData = async () => {
if (!form.classCode) return
try {
const res = await queryStudentListByClass(form.classCode)
if (res.data) {
studentList.value = Array.isArray(res.data) ? res.data : []
const res = await queryStudentListByClass({ classCode: form.classCode })
const data = res?.data
if (Array.isArray(data)) {
studentList.value = data
} else if (data?.records && Array.isArray(data.records)) {
studentList.value = data.records
} else {
studentList.value = []
}
} catch (err) {
studentList.value = []

View File

@@ -10,7 +10,8 @@
:model="form"
:rules="dataRules"
label-width="120px"
v-loading="loading">
v-loading="loading"
:key="form.turnoverType">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="学年" prop="schoolYear">
@@ -47,7 +48,7 @@
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-col :span="12" class="mb20" v-if="!isDropoutType">
<el-form-item label="原班级" prop="oldClassCode">
<el-select
v-model="form.oldClassCode"
@@ -66,7 +67,7 @@
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-col :span="12" class="mb20" v-if="!isDropoutType">
<el-form-item label="现班级" prop="newClassCode">
<el-select
v-model="form.newClassCode"
@@ -90,7 +91,8 @@
v-model="form.turnoverType"
placeholder="请选择异动类型"
clearable
style="width: 100%">
style="width: 100%"
@change="handleTurnoverTypeChange">
<el-option
v-for="item in turnoverTypeList"
:key="item.value"
@@ -299,30 +301,52 @@ const selectedStudentsText = computed(() => {
return `已选择 ${form.selectedStudents.length} 名学生`
})
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
oldClassCode: [
{ required: true, message: '请选择原班级', trigger: 'change' }
],
newClassCode: [
{ required: true, message: '请选择现班级', trigger: 'change' }
],
turnoverType: [
{ required: true, message: '请选择异动类型', trigger: 'change' }
],
turnoverDate: [
{ required: true, message: '请选择异动时间', trigger: 'change' }
],
selectedStudents: [
{ required: true, message: '请至少选择一个学生', trigger: 'change', type: 'array', min: 1 }
]
}
// 判断是否为退学类型
const isDropoutType = computed(() => {
if (!form.turnoverType) return false
// 查找异动类型字典中 label 包含"退学"的项
const dropoutItem = turnoverTypeList.value.find((item: any) => {
const label = item.label || item.dictLabel || item.name || ''
return label.includes('退学')
})
if (dropoutItem) {
return dropoutItem.value === form.turnoverType || dropoutItem.dictValue === form.turnoverType || dropoutItem.code === form.turnoverType
}
return false
})
// 定义校验规则(动态)
const dataRules = computed(() => {
const rules: any = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
turnoverType: [
{ required: true, message: '请选择异动类型', trigger: 'change' }
],
turnoverDate: [
{ required: true, message: '请选择异动时间', trigger: 'change' }
],
selectedStudents: [
{ required: true, message: '请至少选择一个学生', trigger: 'change', type: 'array', min: 1 }
]
}
// 如果不是退学类型,原班级和现班级为必填
if (!isDropoutType.value) {
rules.oldClassCode = [
{ required: true, message: '请选择原班级', trigger: 'change' }
]
rules.newClassCode = [
{ required: true, message: '请选择现班级', trigger: 'change' }
]
}
return rules
})
// 原班级变化时,更新学生搜索条件
const handleOldClassChange = () => {
@@ -331,14 +355,30 @@ const handleOldClassChange = () => {
form.selectedStudents = []
}
// 异动类型变化时处理
const handleTurnoverTypeChange = () => {
// 如果切换为退学类型,清空原班级和现班级
if (isDropoutType.value) {
form.oldClassCode = ''
form.newClassCode = ''
// 清空已选学生(因为退学不需要原班级,学生选择逻辑会受影响)
form.selectedStudents = []
}
// 重新验证表单
nextTick(() => {
dataFormRef.value?.clearValidate(['oldClassCode', 'newClassCode'])
})
}
// 打开学生选择弹窗
const openStudentDialog = () => {
if (!form.oldClassCode) {
// 如果是退学类型,不需要原班级
if (!isDropoutType.value && !form.oldClassCode) {
useMessage().warning('请先选择原班级')
return
}
studentDialogVisible.value = true
studentSearchForm.classCode = form.oldClassCode
studentSearchForm.classCode = form.oldClassCode || ''
studentSearchForm.classNo = ''
studentSearchForm.stuNo = ''
studentSearchForm.realName = ''
@@ -354,10 +394,13 @@ const handleStudentSearch = async () => {
const params: any = {
current: studentPagination.currentPage,
size: studentPagination.pageSize,
classCode: studentSearchForm.classCode || undefined,
stuNo: studentSearchForm.stuNo || undefined,
realName: studentSearchForm.realName || undefined
}
// 如果不是退学类型且有原班级,才添加 classCode 条件
if (!isDropoutType.value && studentSearchForm.classCode) {
params.classCode = studentSearchForm.classCode
}
const res = await getStudentList(params)
if (res.data && res.data.records) {
studentTableData.value = res.data.records
@@ -494,8 +537,6 @@ const onSubmit = async () => {
const submitData: any = {
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
oldClassCode: form.oldClassCode,
newClassCode: form.newClassCode,
turnoverType: form.turnoverType,
turnYear: form.turnYear || '',
turnoverDate: form.turnoverDate,
@@ -506,6 +547,12 @@ const onSubmit = async () => {
}))
}
// 如果不是退学类型,才添加原班级和现班级
if (!isDropoutType.value) {
submitData.oldClassCode = form.oldClassCode
submitData.newClassCode = form.newClassCode
}
// 编辑时需要包含id和单个学生信息
if (operType.value === 'edit') {
submitData.id = form.id

View File

@@ -207,7 +207,7 @@
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
@@ -220,6 +220,13 @@
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="RefreshLeft"
link
type="warning"
@click="handleCancel(scope.row)">
异动撤销
</el-button>
<el-button
icon="Delete"
link
@@ -250,7 +257,7 @@
import { reactive, ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj, exportData } from "/@/api/stuwork/stuturnover";
import { fetchList, delObj, exportData, cancelObj } from "/@/api/stuwork/stuturnover";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { getDicts } from "/@/api/admin/dict";
import { getDeptListByLevelTwo } from "/@/api/basic/basicdept";
@@ -258,7 +265,7 @@ import { list as getClassList } from "/@/api/basic/basicclass";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue'
import { List, Calendar, Clock, OfficeBuilding, Grid, CreditCard, Avatar, Collection, Document, Setting, Menu, Search } from '@element-plus/icons-vue'
import { List, Calendar, Clock, OfficeBuilding, Grid, CreditCard, Avatar, Collection, Document, Setting, Menu, Search, RefreshLeft } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
@@ -400,6 +407,20 @@ const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// 异动撤销
const handleCancel = async (row: any) => {
try {
await useMessageBox().confirm('确定要撤销该条学籍异动吗?')
await cancelObj([row.id])
useMessage().success('撤销成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err?.msg || '撤销失败')
}
}
}
// 删除
const handleDelete = async (row: any) => {
try {

View File

@@ -1,13 +1,13 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ?????? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
????
查询条件
</span>
</div>
</template>
@@ -15,7 +15,7 @@
<el-form-item label="班级" prop="classCode">
<el-select
v-model="state.queryForm.classCode"
placeholder="请选择学年"
placeholder="请选择班级"
clearable
filterable
style="width: 200px">
@@ -27,10 +27,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="classNo">
<el-form-item label="班级编号" prop="classNo">
<el-input
v-model="state.queryForm.classNo"
placeholder="请输入姓名"
placeholder="请输入班级编号"
clearable
style="width: 200px" />
</el-form-item>
@@ -41,17 +41,17 @@
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="????" prop="serNo">
<el-form-item label="序列号" prop="serNo">
<el-input
v-model="state.queryForm.serNo"
placeholder="请输入奖项名称"
placeholder="请输入序列号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="入学年份" prop="grade">
<el-select
v-model="state.queryForm.grade"
placeholder="请输入奖项名称"
placeholder="请选择入学年份"
clearable
style="width: 200px">
<el-option
@@ -63,40 +63,40 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">??</el-button>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
??????
学生团组织列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
@click="formDialogRef.openDialog()">
??
新增
</el-button>
<el-button
icon="Upload"
type="success"
class="ml10"
@click="handleImport">
??
导入
</el-button>
<el-button
icon="Download"
type="warning"
class="ml10"
@click="handleExport">
??
导出
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -153,11 +153,11 @@
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- ????????? -->
<!-- 入团时间格式化 -->
<template v-if="col.prop === 'enterTime'" #default="scope">
<span>{{ parseTime(scope.row.enterTime, '{y}-{m}-{d}') }}</span>
</template>
<!-- ????????? -->
<!-- 职务格式化 -->
<template v-else-if="col.prop === 'position'" #default="scope">
<el-tag v-if="scope.row.position" size="small" type="primary" effect="plain">
{{ scope.row.position }}
@@ -200,12 +200,12 @@
</el-card>
</div>
<!-- ??/?????? -->
<!-- 新增/编辑对话框 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
<!-- ???? -->
<!-- 导入对话框 -->
<el-dialog
title="????"
title="导入数据"
v-model="importDialogVisible"
:close-on-click-modal="false"
width="500px">
@@ -220,18 +220,18 @@
drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
????????<em>????</em>
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
???? xlsx/xls ??
只能上传 xlsx/xls 文件
</div>
</template>
</el-upload>
<template #footer>
<span class="dialog-footer">
<el-button @click="importDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleImportSubmit" :loading="importLoading">??</el-button>
<el-button type="primary" @click="handleImportSubmit" :loading="importLoading">确认</el-button>
</span>
</template>
</el-dialog>
@@ -254,7 +254,7 @@ import { useTableColumnControl } from '/@/hooks/tableColumn'
import type { UploadFile, UploadFiles } from 'element-plus';
import FormDialog from './form.vue'
// ??????
// 定义变量
const route = useRoute()
const formDialogRef = ref()
const columnControlRef = ref<any>()
@@ -267,19 +267,19 @@ const importDialogVisible = ref(false)
const importLoading = ref(false)
const fileList = ref<UploadFile[]>([])
// ?????
// 表格列配置
const tableColumns = [
{ prop: 'deptName', label: '????', minWidth: 150 },
{ prop: 'classNo', label: '??', width: 120 },
{ prop: 'stuNo', label: '??', width: 120 },
{ prop: 'realName', label: '??', width: 100 },
{ prop: 'phone', label: '???', width: 120 },
{ prop: 'enterTime', label: '????', width: 120 },
{ prop: 'serNo', label: '????', width: 120 },
{ prop: 'position', label: '????', width: 120 }
{ prop: 'deptName', label: '学院名称', minWidth: 150 },
{ prop: 'classNo', label: '班级', width: 120 },
{ prop: 'stuNo', label: '学号', width: 120 },
{ prop: 'realName', label: '姓名', width: 100 },
{ prop: 'phone', label: '手机号', width: 120 },
{ prop: 'enterTime', label: '入团时间', width: 120 },
{ prop: 'serNo', label: '序列号', width: 120 },
{ prop: 'position', label: '职务名称', width: 120 }
]
// ?????????????
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
deptName: { icon: OfficeBuilding },
classNo: { icon: Grid },
@@ -291,22 +291,22 @@ const columnConfigMap: Record<string, { icon: any }> = {
position: { icon: Briefcase }
}
// ???????hook
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
} = useTableColumnControl(tableColumns)
// ????
// 表格样式
const tableStyle = {
cellStyle: { padding: '8px 0' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }
}
// ?? useTable
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
classCode: '',
@@ -331,7 +331,7 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.classCode = ''
@@ -342,70 +342,70 @@ const handleReset = () => {
getDataList()
}
// ??
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// ??
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('???????????')
await confirm('确定要删除该记录吗?')
await delObj([row.id])
useMessage().success('????')
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '删除失败')
}
}
}
// ??
// 导入
const handleImport = () => {
importDialogVisible.value = true
fileList.value = []
}
// ????
// 文件变化
const handleFileChange = (file: UploadFile, files: UploadFiles) => {
fileList.value = [file]
}
// ??????
// 文件超出限制
const handleExceed = () => {
useMessage().warning('????????')
useMessage().warning('文件数量超出限制')
}
// ????
// 提交导入
const handleImportSubmit = async () => {
if (fileList.value.length === 0) {
useMessage().warning('?????????')
useMessage().warning('请先选择要上传的文件')
return
}
const file = fileList.value[0].raw
if (!file) {
useMessage().warning('?????')
useMessage().warning('文件无效')
return
}
importLoading.value = true
try {
await importExcel(file as File)
useMessage().success('????')
useMessage().success('导入成功')
importDialogVisible.value = false
fileList.value = []
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '导入失败')
} finally {
importLoading.value = false
}
}
// ??
// 导出
const handleExport = async () => {
try {
const res = await exportExcel(state.queryForm)
@@ -413,18 +413,18 @@ const handleExport = async () => {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `??_${parseTime(new Date(), '{y}{m}{d}{h}{i}{s}')}.xlsx`
link.download = `学生团组织_${parseTime(new Date(), '{y}{m}{d}{h}{i}{s}')}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('????')
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '导出失败')
}
}
// ??????
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
@@ -438,7 +438,7 @@ const getClassListData = async () => {
}
}
// ????????
// 获取入学年份列表
const getGradeListData = async () => {
try {
const res = await getGradeList()

View File

@@ -58,6 +58,7 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj } from '/@/api/stuwork/stuworkstudyalternate'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { queryAllStudentByClassCode } from '/@/api/basic/basicstudent'
const emit = defineEmits(['refresh'])
@@ -94,7 +95,7 @@ const openDialog = async () => {
})
}
// 提交表单
// 提交表单(接口文档要求 classCode、dateRange、stuNoList
const onSubmit = async () => {
if (!dataFormRef.value) return
@@ -108,15 +109,27 @@ const onSubmit = async () => {
loading.value = true
try {
// 按班级获取学生列表,提取学号作为 stuNoList接口必传
const stuRes = await queryAllStudentByClassCode(form.classCode)
const list = stuRes?.data && Array.isArray(stuRes.data) ? stuRes.data : []
const stuNoList = list.map((s: any) => s.stuNo).filter(Boolean)
if (stuNoList.length === 0) {
useMessage().warning('该班级暂无学生,无法新增工学交替')
loading.value = false
return
}
await addObj({
classCode: form.classCode,
dateRange: form.dateRange
dateRange: form.dateRange,
stuNoList
})
useMessage().success('新增成功')
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || '新增失败')
if (!err?._messageShown) {
useMessage().error(err?.msg || '新增失败')
}
} finally {
loading.value = false
}

View File

@@ -170,7 +170,7 @@
:min-width="col.minWidth"
:width="col.width">
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<el-icon><component :is="columnConfigMap[col.prop || '']?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 学期列特殊模板 -->
@@ -290,7 +290,7 @@ const {
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
} = useTableColumnControl(tableColumns, { storageKey: route.path })
// 搜索表单
const searchForm = reactive({
@@ -303,10 +303,20 @@ const searchForm = reactive({
dateRange: null as [string, string] | null
})
// 配置 useTable
// 配置 useTable(接口返回 data.tableData.records / data.tableData.total需包装以适配 hook 的 res.data[props.item] 取数)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
pageList: async (params: any) => {
const res = await fetchList(params)
const data = res?.data
const tableData = data?.tableData
return {
data: {
'tableData.records': tableData?.records ?? [],
'tableData.total': tableData?.total ?? data?.total ?? 0
}
}
},
props: {
item: 'tableData.records',
totalCount: 'tableData.total'
@@ -397,13 +407,25 @@ const getSchoolTermList = async () => {
try {
const res = await getDicts('school_term')
if (res.data && Array.isArray(res.data)) {
schoolTermList.value = res.data
schoolTermList.value = res.data.map((item: any) => ({
label: item.label ?? item.dictLabel ?? item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 学期列展示:按字典转成文案
const formatSchoolTerm = (value: string | number) => {
if (value === '' || value == null) return '-'
const item = schoolTermList.value.find((t: any) => String(t.value) === String(value))
return item ? item.label : value
}
// 初始化
onMounted(() => {
getDeptListData()

View File

@@ -0,0 +1,253 @@
<template>
<el-dialog
:title="form.id ? '编辑' : '新增'"
v-model="visible"
:width="800"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="form.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="form.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 100%">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="标题" prop="title">
<el-input
v-model="form.title"
placeholder="请输入标题"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="作者" prop="author">
<el-input
v-model="form.author"
placeholder="请输入作者"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="内容" prop="content">
<Editor
v-model:getHtml="form.content"
:height="'400'"
placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="TermActivityFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/termactivity'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getDicts } from '/@/api/admin/dict'
import Editor from '/@/components/Editor/index.vue'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
schoolYear: '',
schoolTerm: '',
title: '',
content: '',
author: ''
})
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入内容', trigger: 'blur' }
],
author: [
{ required: true, message: '请输入作者', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.schoolYear = ''
form.schoolTerm = ''
form.title = ''
form.content = ''
form.author = ''
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.schoolYear = row.schoolYear || ''
form.schoolTerm = row.schoolTerm || ''
form.title = row.title || ''
form.content = row.content || ''
form.author = row.author || ''
// 如果需要获取详情
if (row.id && !row.content) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.schoolYear = res.data.schoolYear || form.schoolYear
form.schoolTerm = res.data.schoolTerm || form.schoolTerm
form.title = res.data.title || ''
form.content = res.data.content || ''
form.author = res.data.author || ''
}
}).finally(() => {
loading.value = false
})
}
}
})
}
// 学年列表(班级管理-学年接口)
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
schoolYearList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
schoolYearList.value = []
}
}
// 学期字典(系统通用)
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
if (res?.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label ?? item.dictLabel ?? item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
title: form.title,
content: form.content,
author: form.author
}
if (operType.value === 'add') {
await addObj(submitData)
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
...submitData
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
}
} finally {
loading.value = false
}
})
}
// 初始化:加载学年、学期
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,366 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 搜索表单卡片 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="state.queryForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="state.queryForm.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="state.queryForm.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input
v-model="state.queryForm.title"
placeholder="请输入标题"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input
v-model="state.queryForm.author"
placeholder="请输入作者"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
学期活动列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:width="col.width"
:min-width="col.minWidth"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
:align="col.align || 'center'">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<!-- 学期列特殊模板 -->
<template v-if="col.prop === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ formatSchoolTerm(scope.row.schoolTerm) }}
</el-tag>
</template>
<!-- 内容列特殊模板截断显示 -->
<template v-else-if="col.prop === 'content'" #default="scope">
<el-tooltip
v-if="scope.row.content"
:content="scope.row.content.replace(/<[^>]*>/g, '')"
placement="top"
effect="dark">
<div class="content-preview" v-html="getContentPreview(scope.row.content)"></div>
</el-tooltip>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="TermActivity">
import { reactive, ref, computed, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/termactivity";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getDicts } from '/@/api/admin/dict'
import FormDialog from './form.vue'
import { Search, Document, List, Setting, Menu, Calendar, EditPen, User, Clock } from '@element-plus/icons-vue'
import RightToolbar from '/@/components/RightToolbar/index.vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
import { useRoute } from 'vue-router'
const route = useRoute()
const formDialogRef = ref()
const showSearch = ref(true)
const searchFormRef = ref()
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '学年', width: 120, icon: Calendar },
{ prop: 'schoolTerm', label: '学期', width: 100, icon: Calendar },
{ prop: 'title', label: '标题', minWidth: 200, icon: Document },
{ prop: 'content', label: '内容', minWidth: 300, icon: EditPen },
{ prop: 'author', label: '作者', width: 120, icon: User },
{ prop: 'createTime', label: '创建时间', width: 180, icon: Clock }
]
// 使用表格列控制
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange,
loadSavedConfig
} = useTableColumnControl(tableColumns, { storageKey: route.path })
// 立即加载配置,确保初始化时列可见
loadSavedConfig()
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
schoolYear: '',
schoolTerm: '',
title: '',
author: ''
},
pageList: async (params: any) => {
const res = await fetchList(params)
const data = res?.data
return {
data: {
records: data?.records ?? [],
total: data?.total ?? 0
}
}
},
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true,
isPage: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
}
const dictItem = schoolTermList.value.find(item => item.value == value)
return dictItem ? dictItem.label : value
}
// 获取内容预览去除HTML标签截断
const getContentPreview = (content: string) => {
if (!content) return '-'
const text = content.replace(/<[^>]*>/g, '')
return text.length > 50 ? text.substring(0, 50) + '...' : text
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.schoolYear = ''
state.queryForm.schoolTerm = ''
state.queryForm.title = ''
state.queryForm.author = ''
getDataList()
}
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm(`确定要删除"${row.title || '该项'}"吗?`)
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err?.msg || '删除失败')
}
}
}
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
schoolYearList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
schoolYearList.value = []
}
}
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
if (res?.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label ?? item.dictLabel ?? item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.content-preview {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
</style>

View File

@@ -1,21 +1,21 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ?????? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
?????
查询条件
</span>
</div>
</template>
<el-form :model="state.queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="??" prop="termId">
<el-form-item label="学期" prop="termId">
<el-select
v-model="state.queryForm.termId"
placeholder="?????"
placeholder="请选择学期"
clearable
filterable
style="width: 200px">
@@ -27,10 +27,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="deptCode">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="state.queryForm.deptCode"
placeholder="?????"
placeholder="请选择学院"
clearable
filterable
style="width: 200px">
@@ -42,10 +42,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="????" prop="grade">
<el-form-item label="入学年份" prop="grade">
<el-select
v-model="state.queryForm.grade"
placeholder="???????"
placeholder="请选择入学年份"
clearable
filterable
style="width: 200px">
@@ -57,19 +57,19 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="gradeCurr">
<el-form-item label="年级" prop="gradeCurr">
<el-input-number
v-model="state.queryForm.gradeCurr"
placeholder="?????"
placeholder="请输入年级"
:min="1"
:max="10"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="??" prop="classCode">
<el-form-item label="班级" prop="classCode">
<el-select
v-model="state.queryForm.classCode"
placeholder="?????"
placeholder="请选择班级"
clearable
filterable
style="width: 200px">
@@ -81,10 +81,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="????" prop="checkStatus">
<el-form-item label="审核状态" prop="checkStatus">
<el-select
v-model="state.queryForm.checkStatus"
placeholder="???????"
placeholder="请选择审核状态"
clearable
style="width: 200px">
<el-option
@@ -96,26 +96,26 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">??</el-button>
<el-button icon="Refresh" @click="handleReset">??</el-button>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
????????
免学费学生列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
@click="handleAdd">
??
新增
</el-button>
<el-button
icon="Download"
@@ -123,7 +123,7 @@
class="ml10"
:loading="exportLoading"
@click="handleExport">
??
导出
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -139,7 +139,7 @@
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="???" placement="top">
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
@@ -151,7 +151,7 @@
</div>
</template>
<!-- ?? -->
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
@@ -159,7 +159,7 @@
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="??" width="70" align="center">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
@@ -169,7 +169,7 @@
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '??'"
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
@@ -180,39 +180,39 @@
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- ????????-->
<!-- 年级格式化-->
<template v-if="col.prop === 'gradeCurr'" #default="scope">
<el-tag v-if="scope.row.gradeCurr !== undefined && scope.row.gradeCurr !== null" size="small" type="primary" effect="plain">
{{ scope.row.gradeCurr }}
</el-tag>
<span v-else>-</span>
</template>
<!-- ????????-->
<!-- 性别格式化-->
<template v-else-if="col.prop === 'gender'" #default="scope">
<GenderTag :sex="scope.row.gender" />
</template>
<!-- ????????-->
<!-- 学历格式化-->
<template v-else-if="col.prop === 'education'" #default="scope">
<el-tag v-if="scope.row.education" size="small" type="info" effect="plain">
{{ formatEducation(scope.row.education) }}
</el-tag>
<span v-else>-</span>
</template>
<!-- ????????-->
<!-- 专业层次格式化-->
<template v-else-if="col.prop === 'majorLevel'" #default="scope">
<el-tag v-if="scope.row.majorLevel" size="small" type="warning" effect="plain">
{{ formatMajorLevel(scope.row.majorLevel) }}
</el-tag>
<span v-else>-</span>
</template>
<!-- ????????-->
<!-- 金额格式化-->
<template v-else-if="col.prop === 'money'" #default="scope">
<el-tag v-if="scope.row.money !== undefined && scope.row.money !== null" size="small" type="success" effect="plain">
?{{ scope.row.money }}
¥{{ scope.row.money }}
</el-tag>
<span v-else>-</span>
</template>
<!-- ????????? -->
<!-- 审核状态格式化 -->
<template v-else-if="col.prop === 'checkStatus'" #default="scope">
<StatusTag
:value="scope.row.checkStatus"
@@ -222,10 +222,10 @@
</template>
</el-table-column>
</template>
<el-table-column label="??" width="150" align="center" fixed="right">
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">??</span>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
@@ -233,20 +233,20 @@
link
type="primary"
@click="handleEdit(scope.row)">
??
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
??
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ?? -->
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@@ -256,7 +256,7 @@
</el-card>
</div>
<!-- ??/?????? -->
<!-- 新增/编辑对话框 -->
<!-- <form-dialog ref="formDialogRef" @refresh="getDataList" /> -->
</div>
</template>
@@ -282,11 +282,11 @@ import {
import { useTableColumnControl } from '/@/hooks/tableColumn'
// ????
// 异步组件
const GenderTag = defineAsyncComponent(() => import('/@/components/GenderTag/index.vue'))
const StatusTag = defineAsyncComponent(() => import('/@/components/StatusTag/index.vue'))
// ??????
// 定义变量
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
@@ -301,29 +301,29 @@ const genderList = ref<any[]>([])
const educationList = ref<any[]>([])
const majorLevelList = ref<any[]>([])
const checkStatusList = ref<any[]>([
{ label: '???', value: '0' },
{ label: '????', value: '1' }
{ label: '未审核', value: '0' },
{ label: '已审核', value: '1' }
])
// ??????
// 表格列配置
const tableColumns = [
{ prop: 'deptName', label: '??', minWidth: 150 },
{ prop: 'majorName', label: '??', minWidth: 150 },
{ prop: 'teacherName', label: '???', width: 100 },
{ prop: 'grade', label: '????', width: 100 },
{ prop: 'gradeCurr', label: '??', width: 80 },
{ prop: 'classNo', label: '??', width: 120 },
{ prop: 'stuNo', label: '??', width: 120 },
{ prop: 'realName', label: '??', width: 100 },
{ prop: 'gender', label: '??', width: 80 },
{ prop: 'education', label: '??', width: 100 },
{ prop: 'majorLevel', label: '??', width: 100 },
{ prop: 'phone', label: '????', width: 120 },
{ prop: 'money', label: '??', width: 100 },
{ prop: 'checkStatus', label: '????', width: 100 }
{ prop: 'deptName', label: '学院', minWidth: 150 },
{ prop: 'majorName', label: '专业', minWidth: 150 },
{ prop: 'teacherName', label: '班主任', width: 100 },
{ prop: 'grade', label: '入学年份', width: 100 },
{ prop: 'gradeCurr', label: '年级', width: 80 },
{ prop: 'classNo', label: '班级', width: 120 },
{ prop: 'stuNo', label: '学号', width: 120 },
{ prop: 'realName', label: '姓名', width: 100 },
{ prop: 'gender', label: '性别', width: 80 },
{ prop: 'education', label: '学历', width: 100 },
{ prop: 'majorLevel', label: '层次', width: 100 },
{ prop: 'phone', label: '联系电话', width: 120 },
{ prop: 'money', label: '金额', width: 100 },
{ prop: 'checkStatus', label: '审核状态', width: 100 }
]
// ????????????
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
deptName: { icon: OfficeBuilding },
majorName: { icon: Reading },
@@ -341,22 +341,22 @@ const columnConfigMap: Record<string, { icon: any }> = {
checkStatus: { icon: CircleCheck }
}
// ???????hook
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
} = useTableColumnControl(tableColumns)
// ????
// 表格样式
const tableStyle = {
cellStyle: { padding: '8px 0' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }
}
// ?? useTable
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
termId: '',
@@ -382,7 +382,7 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// ?????
// 格式化性别
const formatGender = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -391,7 +391,7 @@ const formatGender = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ??????
// 格式化学历
const formatEducation = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -400,7 +400,7 @@ const formatEducation = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ??????
// 格式化层次
const formatMajorLevel = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -409,7 +409,7 @@ const formatMajorLevel = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ????????
// 格式化审核状态
const formatCheckStatus = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -418,7 +418,7 @@ const formatCheckStatus = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.termId = ''
@@ -430,40 +430,40 @@ const handleReset = () => {
getDataList()
}
// ??
// 新增
const handleAdd = () => {
useMessage().info('?????')
useMessage().info('功能开发中')
// formDialogRef.value?.openDialog('add')
}
// ??
// 编辑
const handleEdit = (row: any) => {
useMessage().info('?????')
useMessage().info('功能开发中')
// formDialogRef.value?.openDialog('edit', row.id)
}
// ??
// 删除
const handleDelete = async (row: any) => {
try {
const { confirm } = useMessageBox()
await confirm(`???????? ${row.stuNo} ????????`)
await confirm(`确定要删除学号为 ${row.stuNo} 的学生记录吗?`)
await delObj(row.id)
useMessage().success('????')
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '删除失败')
}
}
}
// ??
// 导出
const handleExport = async () => {
try {
exportLoading.value = true
const res = await exportExcel(state.queryForm)
const blob = new Blob([res.data as BlobPart])
const fileName = `?????_${new Date().getTime()}.xls`
const fileName = `免学费学生_${new Date().getTime()}.xls`
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
@@ -472,15 +472,15 @@ const handleExport = async () => {
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
useMessage().success('????')
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '导出失败')
} finally {
exportLoading.value = false
}
}
// ??????
// 获取学期列表
const getTermList = async () => {
try {
const res = await request({
@@ -497,7 +497,7 @@ const getTermList = async () => {
}
}
// ??????
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
@@ -511,7 +511,7 @@ const getDeptListData = async () => {
}
}
// ??????
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
@@ -525,7 +525,7 @@ const getClassListData = async () => {
}
}
// ????????
// 获取入学年份列表
const getGradeList = async () => {
try {
const res = await queryAllSchoolYear()
@@ -539,7 +539,7 @@ const getGradeList = async () => {
}
}
// ??????
// 获取性别字典
const getGenderDict = async () => {
try {
const res = await getDicts('sexy')
@@ -556,7 +556,7 @@ const getGenderDict = async () => {
}
}
// ??????
// 获取学历字典
const getEducationDict = async () => {
try {
const res = await getDicts('pre_school_education')
@@ -573,7 +573,7 @@ const getEducationDict = async () => {
}
}
// ??????
// 获取层次字典
const getMajorLevelDict = async () => {
try {
const res = await getDicts('basic_major_level')
@@ -590,7 +590,7 @@ const getMajorLevelDict = async () => {
}
}
// ????
// 初始化
onMounted(() => {
getTermList()
getDeptListData()

View File

@@ -1,40 +1,40 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ?????? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
?????
查询条件
</span>
</div>
</template>
<el-form :model="searchForm" ref="searchFormRef" :inline="true" @keyup.enter="handleSearch" class="search-form">
<el-form-item label="????" prop="effectiveMoney">
<el-form-item label="有效金额" prop="effectiveMoney">
<el-input-number
v-model="searchForm.effectiveMoney"
placeholder="???????"
placeholder="请输入有效金额"
:precision="2"
:min="0"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="????" prop="isLiveNum">
<el-form-item label="是否住人" prop="isLiveNum">
<el-select
v-model="searchForm.isLiveNum"
placeholder="?????"
placeholder="请选择"
clearable
style="width: 200px">
<el-option label="??0" :value="true" />
<el-option label="??0" :value="false" />
<el-option label="等于0" :value="true" />
<el-option label="不等于0" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="??" prop="buildNo">
<el-form-item label="楼号" prop="buildNo">
<el-select
v-model="searchForm.buildNo"
placeholder="?????"
placeholder="请选择楼号"
clearable
filterable
style="width: 200px">
@@ -46,48 +46,48 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="???" prop="roomNo">
<el-form-item label="房间号" prop="roomNo">
<el-input
v-model="searchForm.roomNo"
placeholder="??????"
placeholder="请输入房间号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="handleSearch">??</el-button>
<el-button icon="Refresh" @click="handleReset">??</el-button>
<el-button type="primary" plain icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
??????
水费明细列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
????
新增明细
</el-button>
<el-button
icon="Download"
type="success"
class="ml10"
@click="handleExport">
????
导出数据
</el-button>
<el-button
icon="Setting"
type="warning"
class="ml10"
@click="handleInitWaterOrder">
??????????
批量初始化订单
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -103,7 +103,7 @@
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="???" placement="top">
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
@@ -115,7 +115,7 @@
</div>
</template>
<!-- ?? -->
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
@@ -124,7 +124,7 @@
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table"
@sort-change="sortChangeHandle">
<el-table-column type="index" label="??" width="70" align="center">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
@@ -134,7 +134,7 @@
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '??'"
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
@@ -145,53 +145,53 @@
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Money" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- ???????? -->
<!-- 床位号格式化 -->
<template v-if="col.prop === 'bedNum'" #default="scope">
<el-tag v-if="scope.row.bedNum" size="small" type="info" effect="plain">
{{ scope.row.bedNum }}??
{{ scope.row.bedNum }}号床
</el-tag>
<span v-else>-</span>
</template>
<!-- ??????????-->
<!-- 住人数量格式化-->
<template v-else-if="col.prop === 'liveNum'" #default="scope">
<el-tag v-if="scope.row.liveNum !== undefined && scope.row.liveNum !== null" size="small" type="success" effect="plain">
{{ scope.row.liveNum }}
</el-tag>
<span v-else>-</span>
</template>
<!-- ??????????-->
<!-- 期初余额格式化-->
<template v-else-if="col.prop === 'oddbMoney'" #default="scope">
<el-tag v-if="scope.row.oddbMoney" size="small" type="success" effect="plain">
?{{ Number(scope.row.oddbMoney).toFixed(2) }}
¥{{ Number(scope.row.oddbMoney).toFixed(2) }}
</el-tag>
<span v-else>?0.00</span>
<span v-else>¥0.00</span>
</template>
<!-- ????????? -->
<!-- 充值金额格式化 -->
<template v-else-if="col.prop === 'rechargeMoney'" #default="scope">
<el-tag v-if="scope.row.rechargeMoney" size="small" type="primary" effect="plain">
?{{ Number(scope.row.rechargeMoney).toFixed(2) }}
¥{{ Number(scope.row.rechargeMoney).toFixed(2) }}
</el-tag>
<span v-else>?0.00</span>
<span v-else>¥0.00</span>
</template>
<!-- ??????????-->
<!-- 消费金额格式化-->
<template v-else-if="col.prop === 'costMoney'" #default="scope">
<el-tag v-if="scope.row.costMoney" size="small" type="warning" effect="plain">
?{{ Number(scope.row.costMoney).toFixed(2) }}
¥{{ Number(scope.row.costMoney).toFixed(2) }}
</el-tag>
<span v-else>?0.00</span>
<span v-else>¥0.00</span>
</template>
<!-- ??????????-->
<!-- 有效金额格式化-->
<template v-else-if="col.prop === 'effectiveMoney'" #default="scope">
<el-tag size="small" :type="Number(scope.row.effectiveMoney) < 0 ? 'danger' : 'success'" effect="plain">
?{{ scope.row.effectiveMoney ? Number(scope.row.effectiveMoney).toFixed(2) : '0.00' }}
¥{{ scope.row.effectiveMoney ? Number(scope.row.effectiveMoney).toFixed(2) : '0.00' }}
</el-tag>
</template>
</el-table-column>
</template>
<el-table-column label="??" width="200" align="center" fixed="right">
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">??</span>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
@@ -199,27 +199,27 @@
link
type="primary"
@click="handleEdit(scope.row)">
??
编辑
</el-button>
<el-button
icon="View"
link
type="info"
@click="handleViewDetail(scope.row)">
??
详情
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
??
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ?? -->
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@@ -229,28 +229,28 @@
</el-card>
</div>
<!-- ???? -->
<!-- 新增/编辑对话框 -->
<FormDialog ref="formDialogRef" @refresh="getDataList()" />
<!-- ???? -->
<!-- 详情对话框 -->
<DetailDialog ref="detailDialogRef" />
<!-- ??????????-->
<el-dialog v-model="initDialogVisible" title="???????" :width="500" :close-on-click-modal="false" draggable>
<!-- 批量初始化对话框-->
<el-dialog v-model="initDialogVisible" title="批量初始化订单" :width="500" :close-on-click-modal="false" draggable>
<el-form ref="initFormRef" :model="initForm" :rules="initRules" label-width="120px">
<el-form-item label="????" prop="costMoney">
<el-form-item label="消费金额" prop="costMoney">
<el-input-number
v-model="initForm.costMoney"
:precision="2"
:min="0"
placeholder="???????"
placeholder="请输入消费金额"
style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="initDialogVisible = false">??</el-button>
<el-button type="primary" @click="confirmInit" :loading="initLoading">??</el-button>
<el-button @click="initDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmInit" :loading="initLoading">确认</el-button>
</span>
</template>
</el-dialog>
@@ -271,7 +271,7 @@ import { List, OfficeBuilding, House, UserFilled, Money, Setting, Menu, Search,
import { useTableColumnControl } from '/@/hooks/tableColumn'
// ??????
// 定义变量
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
@@ -283,19 +283,19 @@ const buildingList = ref<any[]>([])
const initDialogVisible = ref(false)
const initLoading = ref(false)
// ??????
// 表格列配置
const tableColumns = [
{ prop: 'buildNo', label: '??' },
{ prop: 'roomNo', label: '???' },
{ prop: 'bedNum', label: '???' },
{ prop: 'liveNum', label: '????' },
{ prop: 'oddbMoney', label: '????' },
{ prop: 'rechargeMoney', label: '????' },
{ prop: 'costMoney', label: '????' },
{ prop: 'effectiveMoney', label: '????' }
{ prop: 'buildNo', label: '楼号' },
{ prop: 'roomNo', label: '房间号' },
{ prop: 'bedNum', label: '床位号' },
{ prop: 'liveNum', label: '住人数量' },
{ prop: 'oddbMoney', label: '期初余额' },
{ prop: 'rechargeMoney', label: '充值金额' },
{ prop: 'costMoney', label: '消费金额' },
{ prop: 'effectiveMoney', label: '有效金额' }
]
// ????????????
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
buildNo: { icon: OfficeBuilding },
roomNo: { icon: House },
@@ -307,16 +307,16 @@ const columnConfigMap: Record<string, { icon: any }> = {
effectiveMoney: { icon: Money }
}
// ???????hook
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
} = useTableColumnControl(tableColumns)
// ????
// 搜索表单
const searchForm = reactive({
effectiveMoney: undefined as number | undefined,
isLiveNum: undefined as boolean | undefined,
@@ -324,20 +324,20 @@ const searchForm = reactive({
roomNo: ''
})
// ??????
// 初始化表单
const initForm = reactive({
costMoney: 0
})
// ??????????
// 初始化表单规则
const initRules = {
costMoney: [
{ required: true, message: '???????', trigger: 'blur' },
{ type: 'number', min: 0, message: '??????????0', trigger: 'blur' }
{ required: true, message: '请输入消费金额', trigger: 'blur' },
{ type: 'number', min: 0, message: '消费金额必须大于等于0', trigger: 'blur' }
]
}
// ?? useTable
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
@@ -357,12 +357,12 @@ const {
tableStyle
} = useTable(state)
// ??
// 查询
const handleSearch = () => {
getDataList()
}
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.formRef?.resetFields()
searchForm.effectiveMoney = undefined
@@ -372,43 +372,43 @@ const handleReset = () => {
getDataList()
}
// ??
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value.openDialog('edit', row)
}
// ????
// 查看详情
const handleViewDetail = (row: any) => {
detailDialogRef.value.openDialog(row.roomNo)
}
// ??
// 删除
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('???????????')
await useMessageBox().confirm('确定要删除该记录吗?')
await delObjs([row.id])
useMessage().success('????')
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '删除失败')
}
}
}
// ??
// 导出
const handleExport = () => {
// TODO: ????
useMessage().warning('???????')
// TODO: 实现导出
useMessage().warning('功能开发中')
}
// ??????????
// 批量初始化订单
const handleInitWaterOrder = () => {
initDialogVisible.value = true
initForm.costMoney = 0
}
// ??????
// 确认初始化
const confirmInit = async () => {
if (!initFormRef.value) return
@@ -418,18 +418,18 @@ const confirmInit = async () => {
initLoading.value = true
try {
await initWaterOrder({ costMoney: initForm.costMoney })
useMessage().success('?????')
useMessage().success('初始化成功')
initDialogVisible.value = false
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '?????')
useMessage().error(err.msg || '初始化失败')
} finally {
initLoading.value = false
}
})
}
// ??????
// 获取楼号列表
const getBuildingListData = async () => {
try {
const res = await getBuildingList()
@@ -441,7 +441,7 @@ const getBuildingListData = async () => {
}
}
// ????
// 初始化
onMounted(() => {
getBuildingListData()
nextTick(() => {
@@ -454,4 +454,3 @@ onMounted(() => {
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -1,28 +1,28 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- ?????? -->
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
?????
查询条件
</span>
</div>
</template>
<el-form :model="searchForm" ref="searchFormRef" :inline="true" @keyup.enter="handleSearch" class="search-form">
<el-form-item label="???" prop="roomNo">
<el-form-item label="宿舍号" prop="roomNo">
<el-input
v-model="searchForm.roomNo"
placeholder="??????"
placeholder="请输入宿舍号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="??" prop="year">
<el-form-item label="学年" prop="year">
<el-select
v-model="searchForm.year"
placeholder="?????"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
@@ -34,10 +34,10 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="??" prop="period">
<el-form-item label="学期" prop="period">
<el-select
v-model="searchForm.period"
placeholder="?????"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
@@ -49,26 +49,26 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="handleSearch">??</el-button>
<el-button icon="Refresh" @click="handleReset">??</el-button>
<el-button type="primary" plain icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ???? -->
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
??????
水费订单列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
????
新增订单
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
@@ -84,7 +84,7 @@
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="???" placement="top">
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
@@ -96,7 +96,7 @@
</div>
</template>
<!-- ?? -->
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
@@ -105,7 +105,7 @@
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table"
@sort-change="sortChangeHandle">
<el-table-column type="index" label="??" width="70" align="center">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
@@ -115,7 +115,7 @@
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '??'"
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
@@ -124,40 +124,40 @@
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- ????????-->
<!-- 学期格式化-->
<template v-if="col.prop === 'period'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ formatSchoolTerm(scope.row.period) }}
</el-tag>
</template>
<!-- ????????-->
<!-- 类型格式化-->
<template v-else-if="col.prop === 'type'" #default="scope">
<el-tag size="small" type="info" effect="plain">
{{ formatType(scope.row.type) }}
</el-tag>
</template>
<!-- ????????? -->
<!-- 支付方式格式化 -->
<template v-else-if="col.prop === 'paymentCode'" #default="scope">
<el-tag size="small" type="warning" effect="plain">
{{ formatPaymentType(scope.row.paymentCode) }}
</el-tag>
</template>
<!-- ????????-->
<!-- 金额格式化-->
<template v-else-if="col.prop === 'money'" #default="scope">
<el-tag v-if="scope.row.money" size="small" type="success" effect="plain">
?{{ scope.row.money }}
¥{{ scope.row.money }}
</el-tag>
<span v-else>-</span>
</template>
<!-- ????????? -->
<!-- 充值状态格式化 -->
<template v-else-if="col.prop === 'chargeState'" #default="scope">
<StatusTag
:value="scope.row.chargeState"
:options="[{ label: '??', value: '1' }, { label: '??', value: '0' }]"
:options="[{ label: '已充值', value: '1' }, { label: '未充值', value: '0' }]"
:type-map="{ '1': { type: 'success', effect: 'light' }, '0': { type: 'danger', effect: 'light' } }"
/>
</template>
<!-- ??????? -->
<!-- 订单状态 -->
<template v-else-if="col.prop === 'state'" #default="scope">
<el-tag size="small" type="info" effect="plain">
{{ formatState(scope.row.state) }}
@@ -165,10 +165,10 @@
</template>
</el-table-column>
</template>
<el-table-column label="??" width="150" align="center" fixed="right">
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">??</span>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
@@ -176,20 +176,20 @@
link
type="primary"
@click="handleEdit(scope.row)">
??
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
??
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ?? -->
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@@ -199,7 +199,7 @@
</el-card>
</div>
<!-- ???? -->
<!-- 新增/编辑对话框 -->
<FormDialog ref="formDialogRef" @refresh="getDataList()" />
</div>
</template>
@@ -220,7 +220,7 @@ import { useTableColumnControl } from '/@/hooks/tableColumn'
import { defineAsyncComponent } from 'vue'
const StatusTag = defineAsyncComponent(() => import('/@/components/StatusTag/index.vue'))
// ??????
// 定义变量
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
@@ -233,22 +233,22 @@ const paymentTypeList = ref<any[]>([])
const chargeStateList = ref<any[]>([])
const stateList = ref<any[]>([])
// ??????
// 表格列配置
const tableColumns = [
{ prop: 'roomNo', label: '???' },
{ prop: 'year', label: '??' },
{ prop: 'period', label: '??' },
{ prop: 'orderNum', label: '???' },
{ prop: 'type', label: '??' },
{ prop: 'paymentCode', label: '????' },
{ prop: 'money', label: '??' },
{ prop: 'chargeAccount', label: '??????' },
{ prop: 'chargeRealname', label: '????????' },
{ prop: 'chargeState', label: '????' },
{ prop: 'state', label: '??' }
{ prop: 'roomNo', label: '宿舍号' },
{ prop: 'year', label: '学年' },
{ prop: 'period', label: '学期' },
{ prop: 'orderNum', label: '订单号' },
{ prop: 'type', label: '类型' },
{ prop: 'paymentCode', label: '支付方式' },
{ prop: 'money', label: '金额' },
{ prop: 'chargeAccount', label: '充值账号' },
{ prop: 'chargeRealname', label: '充值人姓名' },
{ prop: 'chargeState', label: '充值状态' },
{ prop: 'state', label: '状态' }
]
// ????????????
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
roomNo: { icon: House },
year: { icon: Calendar },
@@ -263,23 +263,23 @@ const columnConfigMap: Record<string, { icon: any }> = {
state: { icon: CircleCheck }
}
// ???????hook
// 表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
} = useTableColumnControl(tableColumns)
// ????
// 搜索表单
const searchForm = reactive({
roomNo: '',
year: '',
period: ''
})
// ?? useTable
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
@@ -299,12 +299,12 @@ const {
tableStyle
} = useTable(state)
// ??
// 查询
const handleSearch = () => {
getDataList()
}
// ??
// 重置
const handleReset = () => {
searchFormRef.value?.formRef?.resetFields()
searchForm.roomNo = ''
@@ -313,27 +313,27 @@ const handleReset = () => {
getDataList()
}
// ??
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value.openDialog('edit', row)
}
// ??
// 删除
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('??????????')
await useMessageBox().confirm('确定要删除该订单吗?')
const { delObjs } = await import("/@/api/stuwork/waterorder")
await delObjs([row.id])
useMessage().success('????')
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '????')
useMessage().error(err.msg || '删除失败')
}
}
}
// ??????
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -342,7 +342,7 @@ const formatSchoolTerm = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ??????
// 格式化类型
const formatType = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -351,7 +351,7 @@ const formatType = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ????????
// 格式化支付方式
const formatPaymentType = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -360,7 +360,7 @@ const formatPaymentType = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ????????
// 格式化充值状态
const formatChargeState = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -369,7 +369,7 @@ const formatChargeState = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ??????
// 格式化状态
const formatState = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
@@ -378,7 +378,7 @@ const formatState = (value: string | number) => {
return dictItem ? dictItem.label : value
}
// ??????
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
@@ -390,7 +390,7 @@ const getSchoolYearList = async () => {
}
}
// ??????
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
@@ -405,7 +405,7 @@ const getSchoolTermDict = async () => {
}
}
// ??????
// 获取类型字典
const getTypeDict = async () => {
try {
const res = await getDicts('dorm_water_source_type')
@@ -420,7 +420,7 @@ const getTypeDict = async () => {
}
}
// ?????????
// 获取支付方式字典
const getPaymentTypeDict = async () => {
try {
const res = await getDicts('dorm_water_payment_type')
@@ -435,7 +435,7 @@ const getPaymentTypeDict = async () => {
}
}
// ?????????
// 获取充值状态字典
const getChargeStateDict = async () => {
try {
const res = await getDicts('dorm_water_charge_state')
@@ -450,7 +450,7 @@ const getChargeStateDict = async () => {
}
}
// ???????
// 获取状态字典
const getStateDict = async () => {
try {
const res = await getDicts('dorm_water_state')
@@ -465,7 +465,7 @@ const getStateDict = async () => {
}
}
// ????
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()

Some files were not shown because too many files have changed in this diff Show More