260720
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="920"
|
||||
:visible="visible"
|
||||
:title="isUpdate ? '编辑' : '新增'"
|
||||
:confirm-loading="loading"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form ref="formRef" :model="form">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="姓名"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: '请输入姓名' }]"
|
||||
>
|
||||
<a-input v-model:value="form.name" placeholder="请输入姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select
|
||||
v-model:value="form.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择性别"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="民族" name="nation">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:options="nationOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择民族"
|
||||
v-model:value="form.nation"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="政治面貌"
|
||||
name="politics"
|
||||
:rules="[{ required: true, message: '请选择政治面貌' }]"
|
||||
>
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:options="politicsOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择政治面貌"
|
||||
v-model:value="form.politics"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="学院"
|
||||
name="college"
|
||||
:rules="[{ required: true, message: '请选择学院' }]"
|
||||
>
|
||||
<a-input
|
||||
:value="form.college || ''"
|
||||
readonly
|
||||
placeholder="选择班级后自动带出"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="班级"
|
||||
name="className"
|
||||
:rules="[{ required: true, message: '请选择班级' }]"
|
||||
>
|
||||
<class-cascader
|
||||
v-model:value="form.className"
|
||||
mode="class"
|
||||
placeholder="请选择班级"
|
||||
@change="handleClassChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="手机号码" name="phone">
|
||||
<a-input v-model:value="form.phone" placeholder="请输入手机号码" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="团内职务" name="leaguePosition">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:options="leaguePositionOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择团内职务"
|
||||
v-model:value="form.leaguePosition"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="所属团支部" name="branchOrganizationId">
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
tree-default-expand-all
|
||||
:tree-data="organizationOptions"
|
||||
placeholder="请选择所属团支部"
|
||||
:value="form.branchOrganizationId"
|
||||
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
|
||||
@update:value="handleBranchChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="身份证号"
|
||||
name="idCardNo"
|
||||
:rules="[{ required: true, message: '请输入身份证号' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.idCardNo"
|
||||
placeholder="请输入身份证号"
|
||||
@blur="syncBirthDateFromIdCard"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="出生日期" name="birthDate">
|
||||
<a-input
|
||||
:value="form.birthDate || ''"
|
||||
readonly
|
||||
placeholder="将根据身份证号自动读取"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="团籍是否在本组织" name="archiveInCurrentOrg">
|
||||
<a-select
|
||||
v-model:value="form.archiveInCurrentOrg"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<a-select-option value="是">是</a-select-option>
|
||||
<a-select-option value="否">否</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="入团年月" name="joinMonth">
|
||||
<a-date-picker
|
||||
v-model:value="form.joinMonth"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择入团年月"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import { toTreeData } from 'ele-admin-pro/es';
|
||||
import { addTyglForm, updateTyglForm } from '@/api/gxmu/tygl-form';
|
||||
import type { TyglForm } from '@/api/gxmu/tygl-form/model';
|
||||
import { listDictData } from '@/api/system/dict-data';
|
||||
import { listOrganizations } from '@/api/system/organization';
|
||||
import type { Organization } from '@/api/system/organization/model';
|
||||
import ClassCascader from '@/views/gxmu/components/class-cascader.vue';
|
||||
import {
|
||||
useGxmuFormSelectOptions,
|
||||
type ClassCascadeSelection
|
||||
} from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
interface TreeOption {
|
||||
label: string;
|
||||
value: number;
|
||||
children?: TreeOption[];
|
||||
}
|
||||
|
||||
const POLITICS_DICT_ID = 172;
|
||||
const NATION_DICT_ID = 173;
|
||||
const LEAGUE_POSITION_DICT_ID = 174;
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: TyglForm | null;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const nationOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const politicsOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const leaguePositionOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const { genderOptions, loadClassCascaderOptions, findClassCascadeSelection } =
|
||||
useGxmuFormSelectOptions();
|
||||
const organizationOptions = ref<TreeOption[]>([]);
|
||||
const organizationNameMap = new Map<number, string>();
|
||||
const optionsLoaded = ref(false);
|
||||
|
||||
const createDefaultForm = (): TyglForm => ({
|
||||
id: undefined,
|
||||
serialNo: undefined,
|
||||
name: '',
|
||||
gender: '',
|
||||
nation: '',
|
||||
college: '',
|
||||
className: '',
|
||||
politics: '',
|
||||
phone: '',
|
||||
leaguePosition: '',
|
||||
branchOrganizationId: undefined,
|
||||
branchOrganizationName: '',
|
||||
idCardNo: '',
|
||||
birthDate: '',
|
||||
archiveInCurrentOrg: '',
|
||||
joinMonth: '',
|
||||
memberRecords: [],
|
||||
growthArchives: []
|
||||
});
|
||||
|
||||
const form = reactive<TyglForm>(createDefaultForm());
|
||||
|
||||
const filterSelectOption = (input: string, option: any) => {
|
||||
return String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase());
|
||||
};
|
||||
|
||||
const normalizeDictOptions = async (dictId: number) => {
|
||||
const result = await listDictData({ dictId });
|
||||
return result
|
||||
.filter((item) => !!item.dictDataName)
|
||||
.map((item) => ({
|
||||
label: item.dictDataName as string,
|
||||
value: item.dictDataName as string
|
||||
}));
|
||||
};
|
||||
|
||||
const buildOrganizationOptions = (list: Organization[]): TreeOption[] => {
|
||||
return list.map((item) => {
|
||||
if (item.organizationId) {
|
||||
organizationNameMap.set(
|
||||
item.organizationId,
|
||||
item.organizationName || ''
|
||||
);
|
||||
}
|
||||
return {
|
||||
label: item.organizationName || '',
|
||||
value: item.organizationId as number,
|
||||
children: item.children?.length
|
||||
? buildOrganizationOptions(item.children)
|
||||
: undefined
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const loadOrganizations = async () => {
|
||||
const result = await listOrganizations();
|
||||
const tree = toTreeData({
|
||||
data: result,
|
||||
idField: 'organizationId',
|
||||
parentIdField: 'parentId'
|
||||
}) as Organization[];
|
||||
organizationNameMap.clear();
|
||||
organizationOptions.value = buildOrganizationOptions(tree);
|
||||
};
|
||||
|
||||
const ensureOptionsLoaded = async () => {
|
||||
if (optionsLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const [politicsList, nationList, leaguePositionList] = await Promise.all([
|
||||
normalizeDictOptions(POLITICS_DICT_ID),
|
||||
normalizeDictOptions(NATION_DICT_ID),
|
||||
normalizeDictOptions(LEAGUE_POSITION_DICT_ID),
|
||||
loadClassCascaderOptions(),
|
||||
loadOrganizations()
|
||||
]);
|
||||
politicsOptions.value = politicsList;
|
||||
nationOptions.value = nationList;
|
||||
leaguePositionOptions.value = leaguePositionList;
|
||||
optionsLoaded.value = true;
|
||||
};
|
||||
|
||||
const parseBirthDateFromIdCard = (idCardNo?: string) => {
|
||||
const value = String(idCardNo || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
let rawBirth = '';
|
||||
if (/^\d{17}[\dX]$/.test(value)) {
|
||||
rawBirth = value.slice(6, 14);
|
||||
} else if (/^\d{15}$/.test(value)) {
|
||||
rawBirth = `19${value.slice(6, 12)}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
const date = dayjs(rawBirth, 'YYYYMMDD', true);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '';
|
||||
};
|
||||
|
||||
const syncBirthDateFromIdCard = () => {
|
||||
form.birthDate = parseBirthDateFromIdCard(form.idCardNo);
|
||||
};
|
||||
|
||||
const handleBranchChange = (value?: string | number) => {
|
||||
const organizationId = value ? Number(value) : undefined;
|
||||
if (!organizationId) {
|
||||
form.branchOrganizationId = undefined;
|
||||
form.branchOrganizationName = '';
|
||||
return;
|
||||
}
|
||||
form.branchOrganizationId = organizationId;
|
||||
form.branchOrganizationName = organizationNameMap.get(organizationId) || '';
|
||||
};
|
||||
|
||||
const handleClassChange = (value?: ClassCascadeSelection) => {
|
||||
form.college = value?.collegeName || '';
|
||||
};
|
||||
|
||||
watch(
|
||||
() => form.idCardNo,
|
||||
() => {
|
||||
syncBirthDateFromIdCard();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
try {
|
||||
await ensureOptionsLoaded();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '表单选项加载失败');
|
||||
}
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.assign(form, createDefaultForm(), props.data ?? {});
|
||||
if (form.className) {
|
||||
const classSelection = findClassCascadeSelection(
|
||||
form.className,
|
||||
'class'
|
||||
);
|
||||
if (classSelection) {
|
||||
form.college = classSelection.collegeName;
|
||||
}
|
||||
}
|
||||
syncBirthDateFromIdCard();
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
syncBirthDateFromIdCard();
|
||||
if (!form.birthDate) {
|
||||
message.error('身份证号格式不正确,无法识别出生日期');
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateTyglForm : addTyglForm;
|
||||
saveOrUpdate({
|
||||
...form,
|
||||
birthDate: form.birthDate
|
||||
})
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../../form-theme.less';
|
||||
|
||||
.gxmu-field-theme();
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
placement="right"
|
||||
width="760"
|
||||
:visible="visible"
|
||||
title="成长档案"
|
||||
:body-style="{ paddingBottom: '88px' }"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<div class="archive-header">
|
||||
<div class="archive-title">{{ props.data?.name || '未命名团员' }}</div>
|
||||
<div class="archive-subtitle">
|
||||
手机:{{ props.data?.phone || '-' }} | 入团年月:{{ props.data?.joinMonth || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="archive-card">
|
||||
<div
|
||||
v-for="(item, index) in growthArchives"
|
||||
:key="`archive-${index}`"
|
||||
class="archive-item"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="时间">
|
||||
<a-date-picker
|
||||
v-model:value="item.archiveTime"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
placeholder="请选择时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="内容">
|
||||
<a-textarea
|
||||
v-model:value="item.content"
|
||||
:rows="5"
|
||||
placeholder="请输入成长档案内容"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a class="remove-link" @click="removeArchive(index)">删除</a>
|
||||
</div>
|
||||
<a-button block @click="addArchive">新增成长档案</a-button>
|
||||
</div>
|
||||
<div class="drawer-footer">
|
||||
<a-space>
|
||||
<a-button @click="updateVisible(false)">取消</a-button>
|
||||
<a-button type="primary" :loading="loading" @click="save">保存</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { updateTyglGrowthArchives } from '@/api/gxmu/tygl-form';
|
||||
import type { TyglForm, TyglGrowthArchive } from '@/api/gxmu/tygl-form/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: TyglForm | null;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const growthArchives = ref<TyglGrowthArchive[]>([]);
|
||||
|
||||
const createArchive = (): TyglGrowthArchive => ({
|
||||
content: '',
|
||||
archiveTime: ''
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
growthArchives.value = (props.data?.growthArchives || []).map((item) => ({
|
||||
content: item.content || '',
|
||||
archiveTime: item.archiveTime || ''
|
||||
}));
|
||||
} else {
|
||||
growthArchives.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const addArchive = () => {
|
||||
growthArchives.value.push(createArchive());
|
||||
};
|
||||
|
||||
const removeArchive = (index: number) => {
|
||||
growthArchives.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (!props.data?.id) {
|
||||
message.error('请先保存团员基础信息');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
updateTyglGrowthArchives(props.data.id, {
|
||||
growthArchives: growthArchives.value.filter(
|
||||
(item) => (item.content || '').trim() || (item.archiveTime || '').trim()
|
||||
)
|
||||
})
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.archive-header {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #f7fbff 0%, #edf5ff 100%);
|
||||
}
|
||||
|
||||
.archive-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.archive-subtitle {
|
||||
margin-top: 8px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.archive-card {
|
||||
padding: 16px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.archive-item {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.remove-link {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="560"
|
||||
:footer="null"
|
||||
title="导入人员管理"
|
||||
:visible="visible"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<a-space direction="vertical" style="width: 100%" :size="16">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="模板说明"
|
||||
description="请先下载模板,按模板表头填写后上传。人员记录、成长档案不在本次导入范围内。"
|
||||
/>
|
||||
<a-button block @click="downloadTemplate">下载模板</a-button>
|
||||
<a-upload-dragger
|
||||
accept=".xls,.xlsx"
|
||||
:show-upload-list="false"
|
||||
:customRequest="doUpload"
|
||||
style="padding: 24px 0"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<cloud-upload-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-hint">将 Excel 文件拖到此处,或点击上传</p>
|
||||
</a-upload-dragger>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||
import { utils, writeFile, read } from 'xlsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { toTreeData } from 'ele-admin-pro/es';
|
||||
import { addTyglForm } from '@/api/gxmu/tygl-form';
|
||||
import type { TyglForm } from '@/api/gxmu/tygl-form/model';
|
||||
import { listOrganizations } from '@/api/system/organization';
|
||||
import type { Organization } from '@/api/system/organization/model';
|
||||
|
||||
const TEMPLATE_HEADERS = [
|
||||
'姓名',
|
||||
'性别',
|
||||
'民族',
|
||||
'学院',
|
||||
'班级',
|
||||
'政治面貌',
|
||||
'手机号码',
|
||||
'团内职务',
|
||||
'所属团支部',
|
||||
'身份证号',
|
||||
'出生日期',
|
||||
'团籍是否在本组织',
|
||||
'入团年月'
|
||||
];
|
||||
|
||||
const TEMPLATE_SAMPLE = [
|
||||
'张三',
|
||||
'男',
|
||||
'汉族',
|
||||
'第一临床医学院',
|
||||
'2022级临床1班',
|
||||
'共青团员',
|
||||
'13800138000',
|
||||
'团支书',
|
||||
'第一临床医学院团支部',
|
||||
'450101200201011234',
|
||||
'2002-01-01',
|
||||
'是',
|
||||
'2020-05'
|
||||
];
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const readFileBuffer = (file: File) => {
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as ArrayBuffer);
|
||||
reader.onerror = () => reject(new Error('文件读取失败'));
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
};
|
||||
|
||||
const parseBirthDateFromIdCard = (idCardNo?: string) => {
|
||||
const value = String(idCardNo || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
let rawBirth = '';
|
||||
if (/^\d{17}[\dX]$/.test(value)) {
|
||||
rawBirth = value.slice(6, 14);
|
||||
} else if (/^\d{15}$/.test(value)) {
|
||||
rawBirth = `19${value.slice(6, 12)}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
return dayjs(rawBirth, 'YYYYMMDD', true).isValid()
|
||||
? dayjs(rawBirth, 'YYYYMMDD').format('YYYY-MM-DD')
|
||||
: '';
|
||||
};
|
||||
|
||||
const downloadTemplate = () => {
|
||||
const sheet = utils.aoa_to_sheet([
|
||||
TEMPLATE_HEADERS,
|
||||
TEMPLATE_SAMPLE
|
||||
]);
|
||||
sheet['!cols'] = TEMPLATE_HEADERS.map((title) => ({
|
||||
wch: Math.max(title.length + 4, 16)
|
||||
}));
|
||||
writeFile(
|
||||
{
|
||||
SheetNames: ['人员管理导入模板'],
|
||||
Sheets: {
|
||||
人员管理导入模板: sheet
|
||||
}
|
||||
},
|
||||
'人员管理导入模板.xlsx'
|
||||
);
|
||||
};
|
||||
|
||||
const buildOrganizationMap = async () => {
|
||||
const list = await listOrganizations();
|
||||
const tree = toTreeData({
|
||||
data: list,
|
||||
idField: 'organizationId',
|
||||
parentIdField: 'parentId'
|
||||
}) as Organization[];
|
||||
const result = new Map<string, { id?: number; name?: string }>();
|
||||
const walk = (items: Organization[]) => {
|
||||
items.forEach((item) => {
|
||||
const key = String(item.organizationName || '').trim();
|
||||
if (key) {
|
||||
result.set(key, {
|
||||
id: item.organizationId,
|
||||
name: item.organizationName
|
||||
});
|
||||
}
|
||||
if (item.children?.length) {
|
||||
walk(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
walk(tree);
|
||||
return result;
|
||||
};
|
||||
|
||||
const toRowObject = (row: Record<string, any>): TyglForm => {
|
||||
const idCardNo = String(row['身份证号'] || '').trim();
|
||||
const birthDate =
|
||||
String(row['出生日期'] || '').trim() || parseBirthDateFromIdCard(idCardNo);
|
||||
return {
|
||||
name: String(row['姓名'] || '').trim(),
|
||||
gender: String(row['性别'] || '').trim(),
|
||||
nation: String(row['民族'] || '').trim(),
|
||||
college: String(row['学院'] || '').trim(),
|
||||
className: String(row['班级'] || '').trim(),
|
||||
politics: String(row['政治面貌'] || '').trim(),
|
||||
phone: String(row['手机号码'] || '').trim(),
|
||||
leaguePosition: String(row['团内职务'] || '').trim(),
|
||||
branchOrganizationName: String(row['所属团支部'] || '').trim(),
|
||||
idCardNo,
|
||||
birthDate,
|
||||
archiveInCurrentOrg: String(row['团籍是否在本组织'] || '').trim(),
|
||||
joinMonth: String(row['入团年月'] || '').trim(),
|
||||
memberRecords: [],
|
||||
growthArchives: []
|
||||
};
|
||||
};
|
||||
|
||||
const validateRows = (rows: TyglForm[]) => {
|
||||
const errors: string[] = [];
|
||||
rows.forEach((row, index) => {
|
||||
const rowNo = index + 2;
|
||||
if (!row.name) {
|
||||
errors.push(`第 ${rowNo} 行缺少姓名`);
|
||||
}
|
||||
if (!row.college) {
|
||||
errors.push(`第 ${rowNo} 行缺少学院`);
|
||||
}
|
||||
if (!row.className) {
|
||||
errors.push(`第 ${rowNo} 行缺少班级`);
|
||||
}
|
||||
if (!row.politics) {
|
||||
errors.push(`第 ${rowNo} 行缺少政治面貌`);
|
||||
}
|
||||
if (!row.idCardNo) {
|
||||
errors.push(`第 ${rowNo} 行缺少身份证号`);
|
||||
}
|
||||
if (!row.birthDate) {
|
||||
errors.push(`第 ${rowNo} 行身份证号格式不正确,无法识别出生日期`);
|
||||
}
|
||||
// if (
|
||||
// row.branchOrganizationName &&
|
||||
// !String(row.branchOrganizationName).trim()
|
||||
// ) {
|
||||
// errors.push(`第 ${rowNo} 行所属团支部格式不正确`);
|
||||
// }
|
||||
});
|
||||
return errors;
|
||||
};
|
||||
|
||||
const doUpload = async ({ file }: { file: File }) => {
|
||||
if (
|
||||
![
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
].includes(file.type) &&
|
||||
!file.name.endsWith('.xls') &&
|
||||
!file.name.endsWith('.xlsx')
|
||||
) {
|
||||
message.error('只能选择 Excel 文件');
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 10) {
|
||||
message.error('大小不能超过 10MB');
|
||||
return false;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const [buffer, organizationMap] = await Promise.all([
|
||||
readFileBuffer(file),
|
||||
buildOrganizationMap()
|
||||
]);
|
||||
const workbook = read(buffer, { type: 'array' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const rows = utils.sheet_to_json<Record<string, any>>(sheet, {
|
||||
defval: ''
|
||||
});
|
||||
if (!rows.length) {
|
||||
throw new Error('导入文件内容为空');
|
||||
}
|
||||
const data = rows
|
||||
.map(toRowObject)
|
||||
.filter((item) =>
|
||||
[
|
||||
item.name,
|
||||
item.gender,
|
||||
item.nation,
|
||||
item.college,
|
||||
item.className,
|
||||
item.idCardNo
|
||||
].some((value) => !!String(value || '').trim())
|
||||
);
|
||||
if (!data.length) {
|
||||
throw new Error('未识别到有效数据');
|
||||
}
|
||||
const errors = validateRows(data);
|
||||
if (errors.length) {
|
||||
throw new Error(errors.slice(0, 5).join(';'));
|
||||
}
|
||||
for (let index = 0; index < data.length; index += 1) {
|
||||
const item = data[index];
|
||||
const branch = organizationMap.get(item.branchOrganizationName || '');
|
||||
// if (item.branchOrganizationName && !branch?.id) {
|
||||
// throw new Error(
|
||||
// `第 ${index + 2} 行所属团支部“${item.branchOrganizationName}”未匹配到系统组织`
|
||||
// );
|
||||
// }
|
||||
if (branch?.id) {
|
||||
item.branchOrganizationId = branch.id;
|
||||
item.branchOrganizationName = branch.name;
|
||||
}
|
||||
await addTyglForm(item);
|
||||
}
|
||||
message.success(`成功导入 ${data.length} 条人员数据`);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '导入失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
placement="right"
|
||||
width="780"
|
||||
:visible="visible"
|
||||
title="团员记录"
|
||||
:body-style="{ paddingBottom: '88px' }"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<div class="record-header">
|
||||
<div class="record-title">{{ props.data?.name || '未命名团员' }}</div>
|
||||
<div class="record-subtitle">
|
||||
手机:{{ props.data?.phone || '-' }} | 团内职务:{{ props.data?.leaguePosition || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<a-tabs v-model:activeKey="activeKey" type="card" class="gxmu-tag-tabs">
|
||||
<a-tab-pane key="团员转进" tab="团员转进">
|
||||
<div class="section-card">
|
||||
<div
|
||||
v-for="(item, index) in transferInRecords"
|
||||
:key="`transfer-in-${index}`"
|
||||
class="record-item"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="时间">
|
||||
<a-date-picker
|
||||
v-model:value="item.recordTime"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
placeholder="请选择时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="内容">
|
||||
<a-textarea
|
||||
v-model:value="item.content"
|
||||
:rows="4"
|
||||
placeholder="请输入团员转进记录内容"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a class="remove-link" @click="removeRecord('团员转进', index)">删除</a>
|
||||
</div>
|
||||
<a-button block @click="addRecord('团员转进')">新增转进记录</a-button>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="团员转出" tab="团员转出">
|
||||
<div class="section-card">
|
||||
<div
|
||||
v-for="(item, index) in transferOutRecords"
|
||||
:key="`transfer-out-${index}`"
|
||||
class="record-item"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="时间">
|
||||
<a-date-picker
|
||||
v-model:value="item.recordTime"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
placeholder="请选择时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="内容">
|
||||
<a-textarea
|
||||
v-model:value="item.content"
|
||||
:rows="4"
|
||||
placeholder="请输入团员转出记录内容"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a class="remove-link" @click="removeRecord('团员转出', index)">删除</a>
|
||||
</div>
|
||||
<a-button block @click="addRecord('团员转出')">新增转出记录</a-button>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="离校记录" tab="离校记录">
|
||||
<div class="section-card">
|
||||
<div
|
||||
v-for="(item, index) in leaveSchoolRecords"
|
||||
:key="`leave-school-${index}`"
|
||||
class="record-item"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="时间">
|
||||
<a-date-picker
|
||||
v-model:value="item.recordTime"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
placeholder="请选择时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="内容">
|
||||
<a-textarea
|
||||
v-model:value="item.content"
|
||||
:rows="4"
|
||||
placeholder="请输入离校记录内容"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a class="remove-link" @click="removeRecord('离校记录', index)">删除</a>
|
||||
</div>
|
||||
<a-button block @click="addRecord('离校记录')">新增离校记录</a-button>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<div class="drawer-footer">
|
||||
<a-space>
|
||||
<a-button @click="updateVisible(false)">取消</a-button>
|
||||
<a-button type="primary" :loading="loading" @click="save">保存</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { updateTyglMemberRecords } from '@/api/gxmu/tygl-form';
|
||||
import type { TyglForm, TyglMemberRecord } from '@/api/gxmu/tygl-form/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: TyglForm | null;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const activeKey = ref('团员转进');
|
||||
const memberRecords = ref<TyglMemberRecord[]>([]);
|
||||
|
||||
const createRecord = (type: string): TyglMemberRecord => ({
|
||||
type,
|
||||
content: '',
|
||||
recordTime: ''
|
||||
});
|
||||
|
||||
const normalizeRecord = (item?: TyglMemberRecord | null): TyglMemberRecord => ({
|
||||
type: item?.type || '',
|
||||
content: item?.content || '',
|
||||
recordTime: item?.recordTime || ''
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
memberRecords.value = (props.data?.memberRecords || []).map((item) =>
|
||||
normalizeRecord(item)
|
||||
);
|
||||
activeKey.value = '团员转进';
|
||||
} else {
|
||||
memberRecords.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const transferInRecords = computed(() =>
|
||||
memberRecords.value.filter((item) => item.type === '团员转进')
|
||||
);
|
||||
const transferOutRecords = computed(() =>
|
||||
memberRecords.value.filter((item) => item.type === '团员转出')
|
||||
);
|
||||
const leaveSchoolRecords = computed(() =>
|
||||
memberRecords.value.filter((item) => item.type === '离校记录')
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const addRecord = (type: string) => {
|
||||
memberRecords.value.push(createRecord(type));
|
||||
};
|
||||
|
||||
const removeRecord = (type: string, index: number) => {
|
||||
const currentIndexes = memberRecords.value
|
||||
.map((item, currentIndex) => ({ item, currentIndex }))
|
||||
.filter(({ item }) => item.type === type);
|
||||
const target = currentIndexes[index];
|
||||
if (target) {
|
||||
memberRecords.value.splice(target.currentIndex, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (!props.data?.id) {
|
||||
message.error('请先保存团员基础信息');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
updateTyglMemberRecords(props.data.id, {
|
||||
memberRecords: memberRecords.value.filter(
|
||||
(item) =>
|
||||
(item.type || '').trim() ||
|
||||
(item.content || '').trim() ||
|
||||
(item.recordTime || '').trim()
|
||||
)
|
||||
})
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../../tag-tabs.less';
|
||||
|
||||
.gxmu-tag-tabs();
|
||||
|
||||
.record-header {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #f7fbff 0%, #edf5ff 100%);
|
||||
}
|
||||
|
||||
.record-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.record-subtitle {
|
||||
margin-top: 8px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
padding: 16px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.remove-link {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,513 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:scroll="{ x: 1900 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space wrap>
|
||||
<a-button
|
||||
v-if="canShowAddButton"
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('gxmu:TYGLForm:import')"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('gxmu:TYGLForm:import')"
|
||||
class="gxmu-toolbar-btn"
|
||||
:loading="exporting"
|
||||
@click="exportData"
|
||||
>
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
<span>导出</span>
|
||||
</a-button>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入姓名/手机号/学院/班级/身份证号"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-select
|
||||
allow-clear
|
||||
v-model:value="nationFilter"
|
||||
:options="nationOptions"
|
||||
placeholder="全部民族"
|
||||
style="width: 160px"
|
||||
@change="reload"
|
||||
/>
|
||||
<a-select
|
||||
allow-clear
|
||||
v-model:value="leaguePositionFilter"
|
||||
:options="leaguePositionOptions"
|
||||
placeholder="全部团内职务"
|
||||
style="width: 180px"
|
||||
@change="reload"
|
||||
/>
|
||||
<a-button @click="resetFilters">重置筛选</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'currentReviewStatus'">
|
||||
<ReviewStatusTag :review-list="record.reviewList" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<div class="action-links">
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
<a
|
||||
v-if="hasPermission('gxmu:TYGLForm:edit')"
|
||||
@click="openEdit(record)"
|
||||
>
|
||||
编辑
|
||||
</a>
|
||||
<a @click="openMemberRecords(record)">团员记录</a>
|
||||
<a @click="openGrowthArchives(record)">成长档案</a>
|
||||
<a-popconfirm
|
||||
v-if="hasPermission('gxmu:TYGLForm:remove')"
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
<import-modal v-model:visible="showImport" @done="reload" />
|
||||
<member-records-modal
|
||||
v-model:visible="showMemberRecords"
|
||||
:data="recordCurrent"
|
||||
@done="reload"
|
||||
/>
|
||||
<growth-archives-modal
|
||||
v-model:visible="showGrowthArchives"
|
||||
:data="archiveCurrent"
|
||||
@done="reload"
|
||||
/>
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
module="gxmu_tygl_form"
|
||||
module-name="团员管理"
|
||||
:pk="detailCurrent?.id"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
:module="REVIEW_MODULE"
|
||||
:pk="reviewCurrent?.id"
|
||||
:review-list="reviewCurrent?.reviewList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
DownloadOutlined,
|
||||
UploadOutlined,
|
||||
PlusOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import EditModal from './components/edit.vue';
|
||||
import ImportModal from './components/import.vue';
|
||||
import MemberRecordsModal from './components/memberRecords.vue';
|
||||
import GrowthArchivesModal from './components/growthArchives.vue';
|
||||
import ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
exportTyglForm,
|
||||
getTyglForm,
|
||||
pageTyglForm,
|
||||
removeTyglForm
|
||||
} from '@/api/gxmu/tygl-form';
|
||||
import type { TyglForm, TyglFormParam } from '@/api/gxmu/tygl-form/model';
|
||||
import { listDictData } from '@/api/system/dict-data';
|
||||
import { hasAnyRole, hasPermission } from '@/utils/permission';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const userStore = useUserStore();
|
||||
const addRoleCodes = [
|
||||
'superAdmin',
|
||||
'SchoolYouthLeagueCommittee',
|
||||
'PartyCommitteesOfSecondaryColleges',
|
||||
'YouthLeagueCommitteeOfSecondaryColleges',
|
||||
'YouthLeagueBranchesOfSecondaryColleges'
|
||||
];
|
||||
const studentRoleCodes = ['sutdent', 'student'];
|
||||
const searchText = ref('');
|
||||
const nationFilter = ref<string>();
|
||||
const leaguePositionFilter = ref<string>();
|
||||
const nationOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const leaguePositionOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const showEdit = ref(false);
|
||||
const showImport = ref(false);
|
||||
const current = ref<TyglForm>();
|
||||
const showMemberRecords = ref(false);
|
||||
const recordCurrent = ref<TyglForm>();
|
||||
const showGrowthArchives = ref(false);
|
||||
const archiveCurrent = ref<TyglForm>();
|
||||
const showDetail = ref(false);
|
||||
const detailCurrent = ref<TyglForm | null>(null);
|
||||
const REVIEW_MODULE: string | undefined = undefined;
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<TyglForm | null>(null);
|
||||
const exporting = ref(false);
|
||||
const studentRecordCount = ref(0);
|
||||
const isStudentOnlyUser = computed(() => {
|
||||
const roles = (userStore.roles ?? []).filter(
|
||||
(role): role is string => !!role
|
||||
);
|
||||
return roles.length === 1 && studentRoleCodes.includes(roles[0]);
|
||||
});
|
||||
const canShowAddButton = computed(() => {
|
||||
if (hasAnyRole(addRoleCodes)) {
|
||||
return true;
|
||||
}
|
||||
return isStudentOnlyUser.value && studentRecordCount.value === 0;
|
||||
});
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '民族',
|
||||
dataIndex: 'nation',
|
||||
key: 'nation',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '学院',
|
||||
dataIndex: 'college',
|
||||
key: 'college',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '班级',
|
||||
dataIndex: 'className',
|
||||
key: 'className',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '政治面貌',
|
||||
dataIndex: 'politics',
|
||||
key: 'politics',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '手机号码',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '团内职务',
|
||||
dataIndex: 'leaguePosition',
|
||||
key: 'leaguePosition',
|
||||
align: 'center',
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所属团支部',
|
||||
dataIndex: 'branchOrganizationName',
|
||||
key: 'branchOrganizationName',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '出生日期',
|
||||
dataIndex: 'birthDate',
|
||||
key: 'birthDate',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '团籍是否在本组织',
|
||||
dataIndex: 'archiveInCurrentOrg',
|
||||
key: 'archiveInCurrentOrg',
|
||||
align: 'center',
|
||||
width: 170
|
||||
},
|
||||
{
|
||||
title: '入团年月',
|
||||
dataIndex: 'joinMonth',
|
||||
key: 'joinMonth',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 260
|
||||
}
|
||||
]);
|
||||
|
||||
const buildQueryParams = (): TyglFormParam => {
|
||||
return {
|
||||
keywords: searchText.value.trim() || undefined,
|
||||
nation: nationFilter.value,
|
||||
leaguePosition: leaguePositionFilter.value
|
||||
} as TyglFormParam;
|
||||
};
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return pageTyglForm({
|
||||
page,
|
||||
limit,
|
||||
...buildQueryParams()
|
||||
} as TyglFormParam)
|
||||
.then((result) => {
|
||||
if (isStudentOnlyUser.value) {
|
||||
studentRecordCount.value = Number(result?.count ?? 0);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
loadStudentRecordCount();
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const loadStudentRecordCount = async () => {
|
||||
if (!isStudentOnlyUser.value) {
|
||||
studentRecordCount.value = 0;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await pageTyglForm({
|
||||
page: 1,
|
||||
limit: 1
|
||||
} as TyglFormParam);
|
||||
studentRecordCount.value = Number(result?.count ?? 0);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '获取团员数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
const loadDictOptions = async (dictId: number) => {
|
||||
const result = await listDictData({ dictId });
|
||||
return result
|
||||
.filter((item) => !!item.dictDataName)
|
||||
.map((item) => ({
|
||||
label: item.dictDataName as string,
|
||||
value: item.dictDataName as string
|
||||
}));
|
||||
};
|
||||
|
||||
const loadFilterOptions = async () => {
|
||||
try {
|
||||
const [nationList, leaguePositionList] = await Promise.all([
|
||||
loadDictOptions(173),
|
||||
loadDictOptions(174)
|
||||
]);
|
||||
nationOptions.value = nationList;
|
||||
leaguePositionOptions.value = leaguePositionList;
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '筛选字典加载失败');
|
||||
}
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
searchText.value = '';
|
||||
nationFilter.value = undefined;
|
||||
leaguePositionFilter.value = undefined;
|
||||
reload();
|
||||
};
|
||||
|
||||
const resolveExportUrl = (payload: any) => {
|
||||
return String(
|
||||
payload?.data?.url ||
|
||||
payload?.data?.downloadUrl ||
|
||||
payload?.data?.path ||
|
||||
payload?.url ||
|
||||
payload?.downloadUrl ||
|
||||
payload?.path ||
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
const exportData = async () => {
|
||||
if (exporting.value) {
|
||||
return;
|
||||
}
|
||||
exporting.value = true;
|
||||
const hide = message.loading('导出中..', 0);
|
||||
try {
|
||||
const result = await exportTyglForm(buildQueryParams());
|
||||
const url = resolveExportUrl(result);
|
||||
if (!url) {
|
||||
throw new Error('导出地址缺失');
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
hide();
|
||||
message.success('导出成功');
|
||||
} catch (e: any) {
|
||||
hide();
|
||||
message.error(e.message || '导出失败');
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
const openEdit = (record?: TyglForm) => {
|
||||
if (!record && isStudentOnlyUser.value && studentRecordCount.value > 0) {
|
||||
message.warning('当前学生用户已有团员数据,不能重复新增');
|
||||
return;
|
||||
}
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as TyglForm)
|
||||
: undefined;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const loadTyglDetail = (
|
||||
record: TyglForm,
|
||||
callback: (data: TyglForm) => void
|
||||
) => {
|
||||
if (!record.id) {
|
||||
message.error('缺少团员记录ID');
|
||||
return;
|
||||
}
|
||||
const hide = message.loading('加载中..', 0);
|
||||
getTyglForm(record.id)
|
||||
.then((data) => {
|
||||
hide();
|
||||
callback(JSON.parse(JSON.stringify(data)) as TyglForm);
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const openMemberRecords = (record: TyglForm) => {
|
||||
loadTyglDetail(record, (data) => {
|
||||
recordCurrent.value = data;
|
||||
showMemberRecords.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const openGrowthArchives = (record: TyglForm) => {
|
||||
loadTyglDetail(record, (data) => {
|
||||
archiveCurrent.value = data;
|
||||
showGrowthArchives.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const openDetail = (record: TyglForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const openReviewHistory = (record: TyglForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: TyglForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeTyglForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadStudentRecordCount();
|
||||
loadFilterOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TYGLForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
|
||||
.action-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user