260720
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
placement="right"
|
||||
width="78%"
|
||||
:visible="visible"
|
||||
:title="isUpdate ? '编辑基层团支部工作台帐' : '新增基层团支部工作台帐'"
|
||||
:body-style="{ paddingBottom: '88px' }"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-tabs v-model:activeKey="activeKey" type="card" class="gxmu-tag-tabs">
|
||||
<a-tab-pane key="overview" tab="团支部简况">
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="单位">
|
||||
<a-input v-model:value="form.unitName" placeholder="请输入单位" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="团支部书记姓名">
|
||||
<a-input v-model:value="form.branchSecretaryName" placeholder="请输入团支部书记姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="手机号码">
|
||||
<a-input v-model:value="form.branchSecretaryPhone" placeholder="请输入手机号码" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8" v-for="item in overviewFields" :key="item.key">
|
||||
<a-form-item :label="item.label">
|
||||
<a-input v-model:value="form[item.key]" :placeholder="`请输入${item.label}`" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="branchMembers" tab="团支部班子成员名单">
|
||||
<editable-table title="团支部班子成员名单" :rows="form.branchCommitteeMembers" :columns="branchCommitteeColumns" :create-row="createBranchCommitteeMember" :scroll-x="1100" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="leagueMembers" tab="团员登记表">
|
||||
<editable-table title="团员登记表" :rows="form.leagueMemberRegisters" :columns="leagueMemberColumns" :create-row="createLeagueMemberRegister" :scroll-x="1380" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="youths" tab="青年登记表">
|
||||
<editable-table title="青年登记表" :rows="form.youthRegisters" :columns="youthColumns" :create-row="createYouthRegister" :scroll-x="1180" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="honors" tab="荣誉登记表">
|
||||
<editable-table title="荣誉登记表" :rows="form.branchHonorRecords" :columns="branchHonorColumns" :create-row="createBranchHonorRecord" :scroll-x="1120" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="fees" tab="团费收缴登记表">
|
||||
<editable-table title="团员团费收缴登记表" :rows="form.memberFeeRecords" :columns="memberFeeColumns" :create-row="createMemberFeeRecord" :scroll-x="1750" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="meeting" tab="团支部会议记录">
|
||||
<editable-table title="团支部有关会议记录" :rows="form.branchMeetingRecords" :columns="meetingColumns" :create-row="createBranchMeetingRecord" :scroll-x="1400" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="activity" tab="团支部活动记录">
|
||||
<editable-table title="团支部有关活动记录" :rows="form.branchActivityRecords" :columns="activityColumns" :create-row="createBranchActivityRecord" :scroll-x="1320" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="evaluation" tab="教育评议结果">
|
||||
<editable-table title="年度团员教育评议结果汇总表" :rows="form.branchEducationEvaluations" :columns="branchEducationColumns" :create-row="createBranchEducationEvaluation" :scroll-x="1080" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="summary" tab="年度工作总结">
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="编号发展团员智慧团建录入率">
|
||||
<a-input v-model:value="form.smartLeagueEntryRate" placeholder="若无请填无" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="学社衔接率">
|
||||
<a-input v-model:value="form.schoolSocietyConnectionRate" placeholder="若无请填无" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="对标定级等次">
|
||||
<a-input v-model:value="form.standardLevel" placeholder="若无请填无" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="团支部年度工作总结">
|
||||
<a-textarea v-model:value="form.branchAnnualSummary" :rows="18" placeholder="请输入团支部年度工作总结" />
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
<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, reactive, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { leagueBranchLedgerApi } from '@/api/gxmu/work-ledger';
|
||||
import type { WorkLedger } from '@/api/gxmu/work-ledger/model';
|
||||
import EditableTable from '../../workLedger/components/editable-table.vue';
|
||||
import { useGxmuFormSelectOptions } from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: WorkLedger | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const activeKey = ref('overview');
|
||||
const { nationOptions, loadNationOptions } = useGxmuFormSelectOptions();
|
||||
|
||||
const rowKey = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const withKey = <T extends Record<string, any>>(row: T) => ({ ...row, __rowKey: rowKey() });
|
||||
|
||||
const createBranchCommitteeMember = () => withKey({ name: '', gender: '', birthMonth: '', politics: '', leagueDuty: '', partTimeWork: '' });
|
||||
const createLeagueMemberRegister = () => withKey({ name: '', gender: '', birthMonth: '', nation: '', joinMonth: '', leagueDuty: '', workDuty: '', remark: '' });
|
||||
const createYouthRegister = () => withKey({ name: '', gender: '', birthMonth: '', nation: '', workDuty: '', remark: '' });
|
||||
const createBranchHonorRecord = () => withKey({ honorName: '', awardTime: '', winner: '', rewardSituation: '', remark: '' });
|
||||
const createMemberFeeRecord = () => withKey({
|
||||
name: '', januaryAmount: '', februaryAmount: '', marchAmount: '', aprilAmount: '', mayAmount: '', juneAmount: '',
|
||||
julyAmount: '', augustAmount: '', septemberAmount: '', octoberAmount: '', novemberAmount: '', decemberAmount: '', totalAmount: ''
|
||||
});
|
||||
const createBranchMeetingRecord = () => withKey({ meetingName: '', meetingTheme: '', time: '', location: '', host: '', attendees: '', absentees: '', content: '', recorder: '' });
|
||||
const createBranchActivityRecord = () => withKey({ activityName: '', activityTheme: '', activityTime: '', location: '', participants: '', content: '', recorder: '' });
|
||||
const createBranchEducationEvaluation = () => withKey({ name: '', excellent: '', qualified: '', basicallyQualified: '', unqualified: '', remark: '' });
|
||||
|
||||
const overviewFields = [
|
||||
{ label: '总人数', key: 'totalPeopleCount' },
|
||||
{ label: '团员数', key: 'memberTotal' },
|
||||
{ label: '党员数', key: 'partyMemberCount' },
|
||||
{ label: '青年数', key: 'youthCount' },
|
||||
{ label: '团青比例', key: 'leagueYouthRatio' },
|
||||
{ label: '教职工团员数', key: 'facultyMemberCount' },
|
||||
{ label: '学生团员数', key: 'studentMemberCount' },
|
||||
{ label: '少数民族团员数', key: 'minorityMemberCount' },
|
||||
{ label: '女团员数', key: 'femaleMemberCount' }
|
||||
];
|
||||
|
||||
const branchCommitteeColumns = [
|
||||
{ title: '姓名', key: 'name', width: 140 },
|
||||
{ title: '性别', key: 'gender', width: 100 },
|
||||
{ title: '出生年月', key: 'birthMonth', width: 140 },
|
||||
{ title: '政治面貌', key: 'politics', width: 140 },
|
||||
{ title: '团内职务', key: 'leagueDuty', width: 140 },
|
||||
{ title: '兼任工作', key: 'partTimeWork', width: 220 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const leagueMemberColumns = computed(() => [
|
||||
{ title: '姓名', key: 'name', width: 140 },
|
||||
{ title: '性别', key: 'gender', width: 90 },
|
||||
{ title: '出生年月', key: 'birthMonth', width: 130 },
|
||||
{ title: '民族', key: 'nation', width: 100, options: nationOptions.value },
|
||||
{ title: '入团年月', key: 'joinMonth', width: 130 },
|
||||
{ title: '团内职务', key: 'leagueDuty', width: 160 },
|
||||
{ title: '工作职务', key: 'workDuty', width: 160 },
|
||||
{ title: '备注', key: 'remark', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
]);
|
||||
const youthColumns = computed(() => [
|
||||
{ title: '姓名', key: 'name', width: 140 },
|
||||
{ title: '性别', key: 'gender', width: 90 },
|
||||
{ title: '出生年月', key: 'birthMonth', width: 130 },
|
||||
{ title: '民族', key: 'nation', width: 100, options: nationOptions.value },
|
||||
{ title: '工作职务', key: 'workDuty', width: 180 },
|
||||
{ title: '备注', key: 'remark', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
]);
|
||||
const branchHonorColumns = [
|
||||
{ title: '荣誉名称(校级及以上)', key: 'honorName', width: 280 },
|
||||
{ title: '获奖时间', key: 'awardTime', width: 140 },
|
||||
{ title: '获奖者(集体或个人)', key: 'winner', width: 220 },
|
||||
{ title: '奖励情况', key: 'rewardSituation', width: 160 },
|
||||
{ title: '备注', key: 'remark', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const memberFeeColumns = [
|
||||
{ title: '姓名', key: 'name', width: 140 },
|
||||
{ title: '一月', key: 'januaryAmount', width: 90 },
|
||||
{ title: '二月', key: 'februaryAmount', width: 90 },
|
||||
{ title: '三月', key: 'marchAmount', width: 90 },
|
||||
{ title: '四月', key: 'aprilAmount', width: 90 },
|
||||
{ title: '五月', key: 'mayAmount', width: 90 },
|
||||
{ title: '六月', key: 'juneAmount', width: 90 },
|
||||
{ title: '七月', key: 'julyAmount', width: 90 },
|
||||
{ title: '八月', key: 'augustAmount', width: 90 },
|
||||
{ title: '九月', key: 'septemberAmount', width: 90 },
|
||||
{ title: '十月', key: 'octoberAmount', width: 90 },
|
||||
{ title: '十一月', key: 'novemberAmount', width: 90 },
|
||||
{ title: '十二月', key: 'decemberAmount', width: 90 },
|
||||
{ title: '小计', key: 'totalAmount', width: 100 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const meetingColumns = [
|
||||
{ title: '会议名称', key: 'meetingName', width: 180 },
|
||||
{ title: '会议主题', key: 'meetingTheme', width: 220 },
|
||||
{ title: '时间', key: 'time', width: 150 },
|
||||
{ title: '地点', key: 'location', width: 150 },
|
||||
{ title: '主持人', key: 'host', width: 120 },
|
||||
{ title: '与会人员', key: 'attendees', width: 220, textarea: true },
|
||||
{ title: '缺席人员', key: 'absentees', width: 160 },
|
||||
{ title: '会议记录', key: 'content', width: 320, textarea: true },
|
||||
{ title: '记录人', key: 'recorder', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const activityColumns = [
|
||||
{ title: '活动名称', key: 'activityName', width: 180 },
|
||||
{ title: '活动主题', key: 'activityTheme', width: 220 },
|
||||
{ title: '活动时间', key: 'activityTime', width: 160 },
|
||||
{ title: '活动地点', key: 'location', width: 160 },
|
||||
{ title: '参与人员', key: 'participants', width: 220, textarea: true },
|
||||
{ title: '活动内容', key: 'content', width: 420, textarea: true },
|
||||
{ title: '记录人', key: 'recorder', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const branchEducationColumns = [
|
||||
{ title: '姓名', key: 'name', width: 180 },
|
||||
{ title: '优秀', key: 'excellent', width: 120 },
|
||||
{ title: '合格', key: 'qualified', width: 120 },
|
||||
{ title: '基本合格', key: 'basicallyQualified', width: 140 },
|
||||
{ title: '不合格', key: 'unqualified', width: 120 },
|
||||
{ title: '备注', key: 'remark', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
|
||||
const createDefaultForm = (): WorkLedger => ({
|
||||
id: undefined,
|
||||
unitName: '',
|
||||
branchSecretaryName: '',
|
||||
branchSecretaryPhone: '',
|
||||
totalPeopleCount: '',
|
||||
memberTotal: '',
|
||||
partyMemberCount: '',
|
||||
youthCount: '',
|
||||
leagueYouthRatio: '',
|
||||
facultyMemberCount: '',
|
||||
studentMemberCount: '',
|
||||
minorityMemberCount: '',
|
||||
femaleMemberCount: '',
|
||||
smartLeagueEntryRate: '',
|
||||
schoolSocietyConnectionRate: '',
|
||||
standardLevel: '',
|
||||
branchAnnualSummary: '',
|
||||
branchCommitteeMembers: [createBranchCommitteeMember()],
|
||||
leagueMemberRegisters: [createLeagueMemberRegister()],
|
||||
youthRegisters: [createYouthRegister()],
|
||||
branchHonorRecords: [createBranchHonorRecord()],
|
||||
memberFeeRecords: [createMemberFeeRecord()],
|
||||
branchMeetingRecords: [createBranchMeetingRecord()],
|
||||
branchActivityRecords: [createBranchActivityRecord()],
|
||||
branchEducationEvaluations: [createBranchEducationEvaluation()]
|
||||
});
|
||||
|
||||
const form = reactive<WorkLedger>(createDefaultForm());
|
||||
|
||||
const normalizeRows = (rows: any[] | undefined, createRow: () => Record<string, any>) =>
|
||||
rows?.length ? rows.map((item) => ({ ...createRow(), ...item, __rowKey: rowKey() })) : [createRow()];
|
||||
|
||||
const assignForm = (record?: WorkLedger | null) => {
|
||||
const defaults = createDefaultForm();
|
||||
Object.assign(form, defaults, record || {});
|
||||
form.branchCommitteeMembers = normalizeRows(record?.branchCommitteeMembers, createBranchCommitteeMember) as any;
|
||||
form.leagueMemberRegisters = normalizeRows(record?.leagueMemberRegisters, createLeagueMemberRegister) as any;
|
||||
form.youthRegisters = normalizeRows(record?.youthRegisters, createYouthRegister) as any;
|
||||
form.branchHonorRecords = normalizeRows(record?.branchHonorRecords, createBranchHonorRecord) as any;
|
||||
form.memberFeeRecords = normalizeRows(record?.memberFeeRecords, createMemberFeeRecord) as any;
|
||||
form.branchMeetingRecords = normalizeRows(record?.branchMeetingRecords, createBranchMeetingRecord) as any;
|
||||
form.branchActivityRecords = normalizeRows(record?.branchActivityRecords, createBranchActivityRecord) as any;
|
||||
form.branchEducationEvaluations = normalizeRows(record?.branchEducationEvaluations, createBranchEducationEvaluation) as any;
|
||||
};
|
||||
|
||||
const cleanRows = (rows?: any[]) =>
|
||||
(rows || [])
|
||||
.map(({ __rowKey, ...item }) => item)
|
||||
.filter((item) => Object.values(item).some((value) => String(value || '').trim()));
|
||||
|
||||
const save = () => {
|
||||
loading.value = true;
|
||||
const payload: WorkLedger = {
|
||||
...form,
|
||||
branchCommitteeMembers: cleanRows(form.branchCommitteeMembers) as any,
|
||||
leagueMemberRegisters: cleanRows(form.leagueMemberRegisters) as any,
|
||||
youthRegisters: cleanRows(form.youthRegisters) as any,
|
||||
branchHonorRecords: cleanRows(form.branchHonorRecords) as any,
|
||||
memberFeeRecords: cleanRows(form.memberFeeRecords) as any,
|
||||
branchMeetingRecords: cleanRows(form.branchMeetingRecords) as any,
|
||||
branchActivityRecords: cleanRows(form.branchActivityRecords) as any,
|
||||
branchEducationEvaluations: cleanRows(form.branchEducationEvaluations) as any
|
||||
};
|
||||
const saveOrUpdate = payload.id
|
||||
? leagueBranchLedgerApi.update(payload)
|
||||
: leagueBranchLedgerApi.add(payload);
|
||||
saveOrUpdate
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await loadNationOptions().catch(() => {});
|
||||
isUpdate.value = !!props.data?.id;
|
||||
assignForm(props.data);
|
||||
} else {
|
||||
assignForm();
|
||||
}
|
||||
activeKey.value = 'overview';
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../../tag-tabs.less';
|
||||
|
||||
.gxmu-tag-tabs();
|
||||
|
||||
.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,127 @@
|
||||
<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: 1200 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space wrap>
|
||||
<a-button class="gxmu-toolbar-btn gxmu-add-btn" @click="openEdit()">
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入单位/团支部书记"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<branch-edit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
ColumnItem,
|
||||
DatasourceFunction
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { leagueBranchLedgerApi } from '@/api/gxmu/work-ledger';
|
||||
import type { WorkLedger, WorkLedgerParam } from '@/api/gxmu/work-ledger/model';
|
||||
import BranchEdit from './components/edit.vue';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const current = ref<WorkLedger | null>(null);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{ title: '单位', dataIndex: 'unitName', key: 'unitName', align: 'center', width: 220, ellipsis: true },
|
||||
{ title: '团支部书记', dataIndex: 'branchSecretaryName', key: 'branchSecretaryName', align: 'center', width: 120 },
|
||||
{ title: '手机号码', dataIndex: 'branchSecretaryPhone', key: 'branchSecretaryPhone', align: 'center', width: 140 },
|
||||
{ title: '团员数', dataIndex: 'memberTotal', key: 'memberTotal', align: 'center', width: 100 },
|
||||
{ title: '党员数', dataIndex: 'partyMemberCount', key: 'partyMemberCount', align: 'center', width: 100 },
|
||||
{ title: '青年数', dataIndex: 'youthCount', key: 'youthCount', align: 'center', width: 100 },
|
||||
{ title: '团青比例', dataIndex: 'leagueYouthRatio', key: 'leagueYouthRatio', align: 'center', width: 120 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', align: 'center', width: 180 },
|
||||
{ title: '操作', key: 'action', align: 'center', fixed: 'right', width: 160 }
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return leagueBranchLedgerApi
|
||||
.userPage({
|
||||
page,
|
||||
limit,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as WorkLedgerParam)
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openEdit = (record?: WorkLedger) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as WorkLedger)
|
||||
: null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: WorkLedger) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
leagueBranchLedgerApi
|
||||
.remove(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<work-ledger title="基层团委工作台账" :api="leagueCommitteeLedgerApi" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import WorkLedger from '../workLedger/index.vue';
|
||||
import { leagueCommitteeLedgerApi } from '@/api/gxmu/work-ledger';
|
||||
</script>
|
||||
@@ -0,0 +1,558 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
placement="right"
|
||||
width="1180"
|
||||
:visible="visible"
|
||||
:title="isUpdate ? '编辑' : '新增'"
|
||||
:body-style="{ paddingBottom: '88px' }"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-form ref="formRef" :model="form">
|
||||
<a-form-item
|
||||
label="年份"
|
||||
name="year"
|
||||
:rules="[{ required: true, message: '请选择年份' }]"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:value="form.year"
|
||||
picker="year"
|
||||
value-format="YYYY"
|
||||
style="width: 240px"
|
||||
:disabled="!!props.fixedYear"
|
||||
placeholder="请选择年份"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-tabs v-model:activeKey="activeTab" type="card" class="gxmu-tag-tabs">
|
||||
<a-tab-pane key="base" tab="登记表">
|
||||
<div class="sheet-wrap">
|
||||
<!-- <div class="sheet-main-title"-->
|
||||
<!-- >2025 年广西医科大学青年马克思主义者培养工程培训班学员登记表</div>-->
|
||||
<table class="sheet-table">
|
||||
<colgroup>
|
||||
<col style="width: 14%" />
|
||||
<col style="width: 15%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 14%" />
|
||||
<col style="width: 8%" />
|
||||
<col style="width: 17%" />
|
||||
<col style="width: 22%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sheet-label">姓名</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="form.name"
|
||||
placeholder="请输入姓名"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">性别</td>
|
||||
<td class="sheet-value">
|
||||
<a-select v-model:value="form.gender" placeholder="请选择" style="width: 120px">
|
||||
<a-select-option value="男">男</a-select-option>
|
||||
<a-select-option value="女">女</a-select-option>
|
||||
</a-select>
|
||||
</td>
|
||||
<td class="sheet-label">民族</td>
|
||||
<td class="sheet-value">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:options="nationOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择民族"
|
||||
v-model:value="form.nation"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-photo-cell" rowspan="4">
|
||||
<div class="sheet-photo-title">电子版照片</div>
|
||||
<div class="sheet-photo-box">
|
||||
<a-image
|
||||
v-if="form.photo"
|
||||
:src="form.photo"
|
||||
:width="96"
|
||||
/>
|
||||
<div v-else class="sheet-photo-placeholder">未上传</div>
|
||||
</div>
|
||||
<upload-file
|
||||
v-model:value="form.photo"
|
||||
accept="image/*"
|
||||
button-text="上传照片"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">出生年月</td>
|
||||
<td class="sheet-value">
|
||||
<a-date-picker
|
||||
v-model:value="form.birthMonth"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择出生年月"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">政治面貌</td>
|
||||
<td class="sheet-value">
|
||||
<a-select style="width: 120px"
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.politics"
|
||||
:options="politicsOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择政治面貌"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">籍贯</td>
|
||||
<td class="sheet-value">
|
||||
<a-input style="width: 120px"
|
||||
v-model:value="form.nativePlace"
|
||||
placeholder="请输入籍贯"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">手机号码</td>
|
||||
<td class="sheet-value" colspan="2">
|
||||
<a-input
|
||||
v-model:value="form.phone"
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label" colspan="2">微信号</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="form.wechat"
|
||||
placeholder="请输入微信号"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">电子邮箱</td>
|
||||
<td class="sheet-value" colspan="2">
|
||||
<a-input
|
||||
v-model:value="form.email"
|
||||
placeholder="请输入电子邮箱"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label" colspan="2">QQ号</td>
|
||||
<td class="sheet-value">
|
||||
<a-input v-model:value="form.qq" placeholder="请输入QQ号" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">身份证号</td>
|
||||
<td class="sheet-value" colspan="2">
|
||||
<a-input
|
||||
v-model:value="form.idCardNo"
|
||||
placeholder="请输入身份证号"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label" colspan="2">爱好特长</td>
|
||||
<td class="sheet-value" colspan="2">
|
||||
<a-input
|
||||
v-model:value="form.hobby"
|
||||
placeholder="请输入爱好特长"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label" colspan="4">
|
||||
毕业后是否有志报考广西选调生<br />
|
||||
或志愿到我区基层一线和艰苦边远地区工作
|
||||
</td>
|
||||
<td class="sheet-value" colspan="3">
|
||||
<a-radio-group v-model:value="form.willingToWorkInGuangxi">
|
||||
<a-radio value="是">是</a-radio>
|
||||
<a-radio value="否">否</a-radio>
|
||||
</a-radio-group>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">学校、院系、年级、专业</td>
|
||||
<td class="sheet-value left" colspan="6">
|
||||
<a-input
|
||||
v-model:value="form.schoolInfo"
|
||||
placeholder="请输入学校、院系、年级、专业"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">担任团学职务情况</td>
|
||||
<td class="sheet-value left" colspan="6">
|
||||
<a-input
|
||||
v-model:value="form.leaguePosition"
|
||||
placeholder="请输入担任团学职务情况"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">个人简历</td>
|
||||
<td class="sheet-value left" colspan="6">
|
||||
<a-textarea
|
||||
v-model:value="form.resume"
|
||||
:rows="8"
|
||||
placeholder="请输入个人简历"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">奖惩情况</td>
|
||||
<td class="sheet-value left" colspan="6">
|
||||
<a-textarea
|
||||
v-model:value="form.awards"
|
||||
:rows="7"
|
||||
placeholder="请输入奖惩情况"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane key="review" tab="综合成绩与意见">-->
|
||||
<!-- <div class="sheet-wrap">-->
|
||||
<!-- <table class="sheet-table">-->
|
||||
<!-- <colgroup>-->
|
||||
<!-- <col style="width: 15%" />-->
|
||||
<!-- <col style="width: 85%" />-->
|
||||
<!-- </colgroup>-->
|
||||
<!-- <tbody>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td class="sheet-label">-->
|
||||
<!-- 综合成绩<br />-->
|
||||
<!-- 情况<br />-->
|
||||
<!-- (按最近一<br />-->
|
||||
<!-- 学年填写)-->
|
||||
<!-- </td>-->
|
||||
<!-- <td class="sheet-value left">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.academicPerformance"-->
|
||||
<!-- :rows="12"-->
|
||||
<!-- placeholder="示例:最近学年专业综合排名、补考重修情况、思政课程成绩等"-->
|
||||
<!-- />-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td class="sheet-label">二级团组织意见</td>-->
|
||||
<!-- <td class="sheet-value left opinion-cell">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.secondaryLeagueOpinion"-->
|
||||
<!-- :rows="6"-->
|
||||
<!-- placeholder="请输入二级团组织意见"-->
|
||||
<!-- />-->
|
||||
<!-- <div class="opinion-date-row">-->
|
||||
<!-- <span>(盖章)</span>-->
|
||||
<!-- <a-input-->
|
||||
<!-- v-model:value="form.secondaryLeagueOpinionDate"-->
|
||||
<!-- placeholder="如:2025年5月1日"-->
|
||||
<!-- class="date-input"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td class="sheet-label">二级党组织意见</td>-->
|
||||
<!-- <td class="sheet-value left opinion-cell">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.secondaryPartyOpinion"-->
|
||||
<!-- :rows="6"-->
|
||||
<!-- placeholder="请输入二级党组织意见"-->
|
||||
<!-- />-->
|
||||
<!-- <div class="opinion-date-row">-->
|
||||
<!-- <span>(盖章)</span>-->
|
||||
<!-- <a-input-->
|
||||
<!-- v-model:value="form.secondaryPartyOpinionDate"-->
|
||||
<!-- placeholder="如:2025年5月2日"-->
|
||||
<!-- class="date-input"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td class="sheet-label">学校团委意见</td>-->
|
||||
<!-- <td class="sheet-value left opinion-cell">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.schoolLeagueOpinion"-->
|
||||
<!-- :rows="6"-->
|
||||
<!-- placeholder="请输入学校团委意见"-->
|
||||
<!-- />-->
|
||||
<!-- <div class="opinion-date-row">-->
|
||||
<!-- <span>(盖章)</span>-->
|
||||
<!-- <a-input-->
|
||||
<!-- v-model:value="form.schoolLeagueOpinionDate"-->
|
||||
<!-- placeholder="如:2025年5月3日"-->
|
||||
<!-- class="date-input"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- </tbody>-->
|
||||
<!-- </table>-->
|
||||
<!-- </div>-->
|
||||
<!-- </a-tab-pane>-->
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
<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 { reactive, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import UploadFile from '@/components/UploadFile/index.vue';
|
||||
import { addQmgcForm, updateQmgcForm } from '@/api/gxmu/qmgc-form';
|
||||
import type { QmgcForm } from '@/api/gxmu/qmgc-form/model';
|
||||
import { listDictData } from '@/api/system/dict-data';
|
||||
|
||||
const NATION_DICT_ID = 173;
|
||||
const POLITICS_DICT_ID = 172;
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: QmgcForm | null;
|
||||
fixedYear?: string | number;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const activeTab = ref('base');
|
||||
const nationOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const politicsOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const optionsLoaded = ref(false);
|
||||
|
||||
const createDefaultForm = (): QmgcForm => ({
|
||||
id: undefined,
|
||||
year: '',
|
||||
name: '',
|
||||
gender: '',
|
||||
nation: '',
|
||||
photo: '',
|
||||
birthMonth: '',
|
||||
politics: '',
|
||||
nativePlace: '',
|
||||
phone: '',
|
||||
wechat: '',
|
||||
email: '',
|
||||
qq: '',
|
||||
idCardNo: '',
|
||||
hobby: '',
|
||||
willingToWorkInGuangxi: '',
|
||||
schoolInfo: '',
|
||||
leaguePosition: '',
|
||||
resume: '',
|
||||
awards: '',
|
||||
academicPerformance: '',
|
||||
secondaryLeagueOpinion: '',
|
||||
secondaryLeagueOpinionDate: '',
|
||||
secondaryPartyOpinion: '',
|
||||
secondaryPartyOpinionDate: '',
|
||||
schoolLeagueOpinion: '',
|
||||
schoolLeagueOpinionDate: ''
|
||||
});
|
||||
|
||||
const form = reactive<QmgcForm>(createDefaultForm());
|
||||
|
||||
const filterSelectOption = (input: string, option: any) => {
|
||||
return String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase());
|
||||
};
|
||||
|
||||
const loadNationOptions = async () => {
|
||||
const result = await listDictData({ dictId: NATION_DICT_ID });
|
||||
nationOptions.value = result
|
||||
.filter((item) => !!item.dictDataName)
|
||||
.map((item) => ({
|
||||
label: item.dictDataName as string,
|
||||
value: item.dictDataName as string
|
||||
}));
|
||||
optionsLoaded.value = true;
|
||||
};
|
||||
|
||||
const loadPoliticsOptions = async () => {
|
||||
const result = await listDictData({ dictId: POLITICS_DICT_ID });
|
||||
politicsOptions.value = result
|
||||
.filter((item) => !!item.dictDataName)
|
||||
.map((item) => ({
|
||||
label: item.dictDataName as string,
|
||||
value: item.dictDataName as string
|
||||
}));
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
if (!optionsLoaded.value) {
|
||||
try {
|
||||
await Promise.all([loadNationOptions(), loadPoliticsOptions()]);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '民族字典加载失败');
|
||||
}
|
||||
}
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.assign(form, createDefaultForm(), props.data ?? {});
|
||||
if (props.fixedYear) {
|
||||
form.year = String(props.fixedYear);
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
activeTab.value = 'base';
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateQmgcForm : addQmgcForm;
|
||||
saveOrUpdate({
|
||||
...form,
|
||||
year: props.fixedYear ? String(props.fixedYear) : form.year
|
||||
})
|
||||
.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 '../../form-theme.less';
|
||||
@import '../../tag-tabs.less';
|
||||
|
||||
.gxmu-field-theme();
|
||||
.gxmu-tag-tabs();
|
||||
|
||||
.sheet-wrap {
|
||||
border: 1px solid #d9d9d9;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sheet-main-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sheet-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.sheet-table td {
|
||||
border: 1px solid #3b3b3b;
|
||||
padding: 8px 6px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sheet-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sheet-value.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sheet-photo-cell {
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.sheet-photo-title {
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
margin: 0 auto 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.sheet-photo-box {
|
||||
min-height: 108px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sheet-photo-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.sheet-table .sheet-value :deep(.ant-input),
|
||||
.sheet-table .sheet-value :deep(.ant-select-selector),
|
||||
.sheet-table .sheet-value :deep(.ant-radio-group),
|
||||
.sheet-table .sheet-value :deep(.ant-input-textarea) {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sheet-table .sheet-value :deep(textarea.ant-input) {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.opinion-cell {
|
||||
padding: 12px 12px 18px !important;
|
||||
}
|
||||
|
||||
.opinion-date-row {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sheet-main-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<common-import
|
||||
v-bind="$props"
|
||||
@done="emit('done')"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
module-name="青马工程"
|
||||
file-name="青马工程导入模板.xlsx"
|
||||
:template-fields="templateFields"
|
||||
:sample-row="sampleRow"
|
||||
:create-payload="createPayload"
|
||||
:save-item="saveItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CommonImport from '../../components/common-import.vue';
|
||||
import { addQmgcForm } from '@/api/gxmu/qmgc-form';
|
||||
import type { QmgcForm } from '@/api/gxmu/qmgc-form/model';
|
||||
|
||||
defineProps<{ visible: boolean }>();
|
||||
const emit = defineEmits(['done', 'update:visible']);
|
||||
|
||||
const templateFields = [
|
||||
{ header: '年份', key: 'year', required: true },
|
||||
{ header: '姓名', key: 'name', required: true },
|
||||
{ header: '性别', key: 'gender' },
|
||||
{ header: '民族', key: 'nation' },
|
||||
{ header: '出生年月', key: 'birthMonth' },
|
||||
{ header: '政治面貌', key: 'politics' },
|
||||
{ header: '籍贯', key: 'nativePlace' },
|
||||
{ header: '手机号', key: 'phone' },
|
||||
{ header: '微信号', key: 'wechat' },
|
||||
{ header: '邮箱', key: 'email' },
|
||||
{ header: 'QQ', key: 'qq' },
|
||||
{ header: '身份证号', key: 'idCardNo' },
|
||||
{ header: '兴趣爱好', key: 'hobby' },
|
||||
{ header: '是否愿意留桂工作', key: 'willingToWorkInGuangxi' },
|
||||
{ header: '学校信息', key: 'schoolInfo' },
|
||||
{ header: '团内职务', key: 'leaguePosition' },
|
||||
{ header: '个人简历', key: 'resume' },
|
||||
{ header: '获得奖励', key: 'awards' },
|
||||
{ header: '学习成绩', key: 'academicPerformance' },
|
||||
{ header: '二级团组织意见', key: 'secondaryLeagueOpinion' },
|
||||
{ header: '二级团组织意见日期', key: 'secondaryLeagueOpinionDate' },
|
||||
{ header: '二级党组织意见', key: 'secondaryPartyOpinion' },
|
||||
{ header: '二级党组织意见日期', key: 'secondaryPartyOpinionDate' },
|
||||
{ header: '学校团组织意见', key: 'schoolLeagueOpinion' },
|
||||
{ header: '学校团组织意见日期', key: 'schoolLeagueOpinionDate' }
|
||||
] as const;
|
||||
|
||||
const sampleRow = {
|
||||
年份: '2025',
|
||||
姓名: '孙七',
|
||||
性别: '男',
|
||||
民族: '汉族',
|
||||
出生年月: '2001-01',
|
||||
政治面貌: '中共党员',
|
||||
籍贯: '广西南宁',
|
||||
手机号: '13800138000',
|
||||
微信号: 'sunqi2025',
|
||||
邮箱: 'sunqi@example.com',
|
||||
QQ: '12345678',
|
||||
身份证号: '450101200101011234',
|
||||
兴趣爱好: '篮球',
|
||||
是否愿意留桂工作: '是',
|
||||
学校信息: '广西医科大学 第一临床医学院',
|
||||
团内职务: '学生会骨干',
|
||||
个人简历: '2022年至今担任学生干部',
|
||||
获得奖励: '校级奖学金',
|
||||
学习成绩: '专业前10%',
|
||||
二级团组织意见: '',
|
||||
二级团组织意见日期: '',
|
||||
二级党组织意见: '',
|
||||
二级党组织意见日期: '',
|
||||
学校团组织意见: '',
|
||||
学校团组织意见日期: ''
|
||||
};
|
||||
|
||||
const createPayload = (row: Record<string, any>): QmgcForm => ({
|
||||
year: String(row['年份'] || '').trim(),
|
||||
name: String(row['姓名'] || '').trim(),
|
||||
gender: String(row['性别'] || '').trim(),
|
||||
nation: String(row['民族'] || '').trim(),
|
||||
birthMonth: String(row['出生年月'] || '').trim(),
|
||||
politics: String(row['政治面貌'] || '').trim(),
|
||||
nativePlace: String(row['籍贯'] || '').trim(),
|
||||
phone: String(row['手机号'] || '').trim(),
|
||||
wechat: String(row['微信号'] || '').trim(),
|
||||
email: String(row['邮箱'] || '').trim(),
|
||||
qq: String(row['QQ'] || '').trim(),
|
||||
idCardNo: String(row['身份证号'] || '').trim(),
|
||||
hobby: String(row['兴趣爱好'] || '').trim(),
|
||||
willingToWorkInGuangxi: String(row['是否愿意留桂工作'] || '').trim(),
|
||||
schoolInfo: String(row['学校信息'] || '').trim(),
|
||||
leaguePosition: String(row['团内职务'] || '').trim(),
|
||||
resume: String(row['个人简历'] || '').trim(),
|
||||
awards: String(row['获得奖励'] || '').trim(),
|
||||
academicPerformance: String(row['学习成绩'] || '').trim(),
|
||||
secondaryLeagueOpinion: String(row['二级团组织意见'] || '').trim(),
|
||||
secondaryLeagueOpinionDate: String(row['二级团组织意见日期'] || '').trim(),
|
||||
secondaryPartyOpinion: String(row['二级党组织意见'] || '').trim(),
|
||||
secondaryPartyOpinionDate: String(row['二级党组织意见日期'] || '').trim(),
|
||||
schoolLeagueOpinion: String(row['学校团组织意见'] || '').trim(),
|
||||
schoolLeagueOpinionDate: String(row['学校团组织意见日期'] || '').trim()
|
||||
});
|
||||
|
||||
const saveItem = (item: QmgcForm) => addQmgcForm(item);
|
||||
</script>
|
||||
@@ -0,0 +1,366 @@
|
||||
<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: 2200 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>{{ props.popup ? '立即申报' : '添加' }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
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-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'photo'">
|
||||
<a-image
|
||||
v-if="record.photo"
|
||||
:src="record.photo"
|
||||
:width="44"
|
||||
:height="58"
|
||||
style="object-fit: cover"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'currentReviewStatus'">
|
||||
<ReviewStatusTag :review-list="record.reviewList" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<template v-if="!shouldHideEditByReviewList(record.reviewList)">
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
</template>
|
||||
<a @click="openReviewHistory(record)">审核历史</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:fixed-year="props.fixedYear"
|
||||
@done="reload"
|
||||
/>
|
||||
<import-modal v-model:visible="showImport" @done="reload" />
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="REVIEW_MODULE"
|
||||
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 { 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 ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import { shouldHideEditByReviewList } from '../utils/review';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
exportQmgcForm,
|
||||
userPageQmgcForm,
|
||||
removeQmgcForm
|
||||
} from '@/api/gxmu/qmgc-form';
|
||||
import type { QmgcForm, QmgcFormParam } from '@/api/gxmu/qmgc-form/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
popup?: boolean;
|
||||
fixedYear?: string | number;
|
||||
}>(),
|
||||
{
|
||||
popup: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showImport = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const current = ref<QmgcForm>();
|
||||
const detailCurrent = ref<QmgcForm | null>(null);
|
||||
const REVIEW_MODULE = 'gxmu_qmgc_form';
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<QmgcForm | null>(null);
|
||||
const exporting = ref(false);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '照片',
|
||||
dataIndex: 'photo',
|
||||
key: 'photo',
|
||||
align: 'center',
|
||||
width: 88
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '民族',
|
||||
dataIndex: 'nation',
|
||||
key: 'nation',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '政治面貌',
|
||||
dataIndex: 'politics',
|
||||
key: 'politics',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '手机号码',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '微信号',
|
||||
dataIndex: 'wechat',
|
||||
key: 'wechat',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '学校、院系、年级、专业',
|
||||
dataIndex: 'schoolInfo',
|
||||
key: 'schoolInfo',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '担任团学职务情况',
|
||||
dataIndex: 'leaguePosition',
|
||||
key: 'leaguePosition',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '是否有志到基层一线工作',
|
||||
dataIndex: 'willingToWorkInGuangxi',
|
||||
key: 'willingToWorkInGuangxi',
|
||||
align: 'center',
|
||||
width: 190
|
||||
},
|
||||
{
|
||||
title: '综合成绩情况',
|
||||
dataIndex: 'academicPerformance',
|
||||
key: 'academicPerformance',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '奖惩情况',
|
||||
dataIndex: 'awards',
|
||||
key: 'awards',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 200
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return userPageQmgcForm({
|
||||
page,
|
||||
limit,
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as QmgcFormParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const buildQueryParams = (): QmgcFormParam => {
|
||||
return {
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as QmgcFormParam;
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
const resolveExportUrl = (payload: any) => {
|
||||
return String(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 exportQmgcForm(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 openEdit = (record?: QmgcForm) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as QmgcForm)
|
||||
: undefined;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openReviewHistory = (record: QmgcForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (record: QmgcForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: QmgcForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeQmgcForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'QMGCForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="900"
|
||||
: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="24">
|
||||
<a-form-item
|
||||
label="年份"
|
||||
name="year"
|
||||
:rules="[{ required: true, message: '请选择年份' }]"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:value="form.year"
|
||||
picker="year"
|
||||
value-format="YYYY"
|
||||
style="width: 100%"
|
||||
:disabled="!!props.fixedYear"
|
||||
placeholder="请选择年份"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="申报类别"
|
||||
name="applyType"
|
||||
:rules="[{ required: true, message: '请输入申报类别' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.applyType"
|
||||
placeholder="请输入申报类别"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<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="24">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select
|
||||
v-model:value="form.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择性别"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="出生年月" name="birthMonth">
|
||||
<a-date-picker
|
||||
v-model:value="form.birthMonth"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择出生年月"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="民族" name="nation">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.nation"
|
||||
:options="nationOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择民族"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="政治面貌" name="politics">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.politics"
|
||||
:options="politicsOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择政治面貌"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="学历" name="education">
|
||||
<a-input v-model:value="form.education" placeholder="请输入学历" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="职务" name="position">
|
||||
<a-input v-model:value="form.position" placeholder="请输入职务" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="职称" name="title">
|
||||
<a-input v-model:value="form.title" placeholder="请输入职称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="所在单位" name="unit">
|
||||
<a-input v-model:value="form.unit" placeholder="请输入所在单位" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="近三年曾获校级及以上奖励" name="awards">
|
||||
<a-textarea
|
||||
v-model:value="form.awards"
|
||||
:rows="3"
|
||||
placeholder="请输入近三年曾获校级及以上奖励"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="工作(学习)经历" name="experience">
|
||||
<a-textarea
|
||||
v-model:value="form.experience"
|
||||
:rows="3"
|
||||
placeholder="请输入工作(学习)经历"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="主要事迹" name="mainStory">
|
||||
<a-textarea
|
||||
v-model:value="form.mainStory"
|
||||
:rows="3"
|
||||
placeholder="请输入主要事迹"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="所在二级团组织意见" name="leagueOpinion">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.leagueOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- placeholder="请输入所在二级团组织意见"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="所在单位党组织意见" name="partyOpinion">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.partyOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- placeholder="请输入所在单位党组织意见"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="校团委意见" name="schoolOpinion">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.schoolOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- 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 { addSjqnForm, updateSjqnForm } from '@/api/gxmu/sjqn-form';
|
||||
import type { SjqnForm } from '@/api/gxmu/sjqn-form/model';
|
||||
import {
|
||||
filterSelectOption,
|
||||
useGxmuFormSelectOptions
|
||||
} from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: SjqnForm | null;
|
||||
fixedYear?: string | number;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const { nationOptions, politicsOptions, genderOptions, loadNationOptions, loadPoliticsOptions } =
|
||||
useGxmuFormSelectOptions();
|
||||
|
||||
const createDefaultForm = (): SjqnForm => ({
|
||||
id: undefined,
|
||||
year: '',
|
||||
applyType: '',
|
||||
name: '',
|
||||
gender: '',
|
||||
birthMonth: '',
|
||||
nation: '',
|
||||
politics: '',
|
||||
education: '',
|
||||
position: '',
|
||||
title: '',
|
||||
unit: '',
|
||||
awards: '',
|
||||
experience: '',
|
||||
mainStory: '',
|
||||
leagueOpinion: '',
|
||||
partyOpinion: '',
|
||||
schoolOpinion: ''
|
||||
});
|
||||
|
||||
const form = reactive<SjqnForm>(createDefaultForm());
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
try {
|
||||
await Promise.all([loadNationOptions(), loadPoliticsOptions()]);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '下拉选项加载失败');
|
||||
}
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.assign(form, createDefaultForm(), props.data ?? {});
|
||||
if (props.fixedYear) {
|
||||
form.year = String(props.fixedYear);
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
formRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateSjqnForm : addSjqnForm;
|
||||
saveOrUpdate({
|
||||
...form,
|
||||
year: props.fixedYear ? String(props.fixedYear) : form.year
|
||||
})
|
||||
.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,84 @@
|
||||
<template>
|
||||
<common-import
|
||||
v-bind="$props"
|
||||
@done="emit('done')"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
module-name="十佳青年"
|
||||
file-name="十佳青年导入模板.xlsx"
|
||||
:template-fields="templateFields"
|
||||
:sample-row="sampleRow"
|
||||
:create-payload="createPayload"
|
||||
:save-item="saveItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CommonImport from '../../components/common-import.vue';
|
||||
import { addSjqnForm } from '@/api/gxmu/sjqn-form';
|
||||
import type { SjqnForm } from '@/api/gxmu/sjqn-form/model';
|
||||
|
||||
defineProps<{ visible: boolean }>();
|
||||
const emit = defineEmits(['done', 'update:visible']);
|
||||
|
||||
const templateFields = [
|
||||
{ header: '年份', key: 'year', required: true },
|
||||
{ header: '申报类别', key: 'applyType', required: true },
|
||||
{ header: '姓名', key: 'name', required: true },
|
||||
{ header: '性别', key: 'gender' },
|
||||
{ header: '出生年月', key: 'birthMonth' },
|
||||
{ header: '民族', key: 'nation' },
|
||||
{ header: '政治面貌', key: 'politics' },
|
||||
{ header: '学历', key: 'education' },
|
||||
{ header: '职务', key: 'position' },
|
||||
{ header: '职称', key: 'title' },
|
||||
{ header: '所在单位', key: 'unit' },
|
||||
{ header: '近三年曾获校级及以上奖励', key: 'awards' },
|
||||
{ header: '工作(学习)经历', key: 'experience' },
|
||||
{ header: '主要事迹', key: 'mainStory' },
|
||||
{ header: '所在二级团组织意见', key: 'leagueOpinion' },
|
||||
{ header: '所在二级党组织意见', key: 'partyOpinion' },
|
||||
{ header: '学校团委意见', key: 'schoolOpinion' }
|
||||
] as const;
|
||||
|
||||
const sampleRow = {
|
||||
年份: '2025',
|
||||
申报类别: '教师',
|
||||
姓名: '张三',
|
||||
性别: '男',
|
||||
出生年月: '1998-05',
|
||||
民族: '汉族',
|
||||
政治面貌: '中共党员',
|
||||
学历: '本科',
|
||||
职务: '团支书',
|
||||
职称: '讲师',
|
||||
所在单位: '第一临床医学院',
|
||||
近三年曾获校级及以上奖励: '校级优秀团干',
|
||||
'工作(学习)经历': '2020年至今任职',
|
||||
主要事迹: '长期组织志愿服务活动',
|
||||
所在二级团组织意见: '',
|
||||
所在二级党组织意见: '',
|
||||
学校团委意见: ''
|
||||
};
|
||||
|
||||
const createPayload = (row: Record<string, any>): SjqnForm => ({
|
||||
year: String(row['年份'] || '').trim(),
|
||||
applyType: String(row['申报类别'] || '').trim(),
|
||||
name: String(row['姓名'] || '').trim(),
|
||||
gender: String(row['性别'] || '').trim(),
|
||||
birthMonth: String(row['出生年月'] || '').trim(),
|
||||
nation: String(row['民族'] || '').trim(),
|
||||
politics: String(row['政治面貌'] || '').trim(),
|
||||
education: String(row['学历'] || '').trim(),
|
||||
position: String(row['职务'] || '').trim(),
|
||||
title: String(row['职称'] || '').trim(),
|
||||
unit: String(row['所在单位'] || '').trim(),
|
||||
awards: String(row['近三年曾获校级及以上奖励'] || '').trim(),
|
||||
experience: String(row['工作(学习)经历'] || '').trim(),
|
||||
mainStory: String(row['主要事迹'] || '').trim(),
|
||||
leagueOpinion: String(row['所在二级团组织意见'] || '').trim(),
|
||||
partyOpinion: String(row['所在二级党组织意见'] || '').trim(),
|
||||
schoolOpinion: String(row['学校团委意见'] || '').trim()
|
||||
});
|
||||
|
||||
const saveItem = (item: SjqnForm) => addSjqnForm(item);
|
||||
</script>
|
||||
@@ -0,0 +1,389 @@
|
||||
<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: 2400 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>{{ props.popup ? '立即申报' : '添加' }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
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-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'currentReviewStatus'">
|
||||
<ReviewStatusTag :review-list="record.reviewList" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<template v-if="!shouldHideEditByReviewList(record.reviewList)">
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
</template>
|
||||
<a @click="openReviewHistory(record)">审核历史</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:fixed-year="props.fixedYear"
|
||||
@done="reload"
|
||||
/>
|
||||
<import-modal v-model:visible="showImport" @done="reload" />
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="REVIEW_MODULE"
|
||||
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 { 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 ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import { shouldHideEditByReviewList } from '../utils/review';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
exportSjqnForm,
|
||||
userPageSjqnForm,
|
||||
removeSjqnForm
|
||||
} from '@/api/gxmu/sjqn-form';
|
||||
import type { SjqnForm, SjqnFormParam } from '@/api/gxmu/sjqn-form/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
popup?: boolean;
|
||||
fixedYear?: string | number;
|
||||
}>(),
|
||||
{
|
||||
popup: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showImport = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const current = ref<SjqnForm>();
|
||||
const detailCurrent = ref<SjqnForm | null>(null);
|
||||
const REVIEW_MODULE = 'gxmu_sjqn';
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<SjqnForm | null>(null);
|
||||
const exporting = ref(false);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '申报类别',
|
||||
dataIndex: 'applyType',
|
||||
key: 'applyType',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '出生年月',
|
||||
dataIndex: 'birthMonth',
|
||||
key: 'birthMonth',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '民族',
|
||||
dataIndex: 'nation',
|
||||
key: 'nation',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '政治面貌',
|
||||
dataIndex: 'politics',
|
||||
key: 'politics',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '学历',
|
||||
dataIndex: 'education',
|
||||
key: 'education',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '职务',
|
||||
dataIndex: 'position',
|
||||
key: 'position',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '职称',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '所在单位',
|
||||
dataIndex: 'unit',
|
||||
key: 'unit',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '近三年曾获校级及以上奖励',
|
||||
dataIndex: 'awards',
|
||||
key: 'awards',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '工作(学习)经历',
|
||||
dataIndex: 'experience',
|
||||
key: 'experience',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '主要事迹',
|
||||
dataIndex: 'mainStory',
|
||||
key: 'mainStory',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在二级团组织意见',
|
||||
dataIndex: 'leagueOpinion',
|
||||
key: 'leagueOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在单位党组织意见',
|
||||
dataIndex: 'partyOpinion',
|
||||
key: 'partyOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '校团委意见',
|
||||
dataIndex: 'schoolOpinion',
|
||||
key: 'schoolOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 220
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return userPageSjqnForm({
|
||||
page,
|
||||
limit,
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as SjqnFormParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const buildQueryParams = (): SjqnFormParam => {
|
||||
return {
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as SjqnFormParam;
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
const resolveExportUrl = (payload: any) => {
|
||||
return String(
|
||||
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 exportSjqnForm(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 openEdit = (record?: SjqnForm) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as SjqnForm)
|
||||
: undefined;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openReviewHistory = (record: SjqnForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (record: SjqnForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: SjqnForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeSjqnForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Gxmu'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="900"
|
||||
: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="24">
|
||||
<a-form-item
|
||||
label="年份"
|
||||
name="year"
|
||||
:rules="[{ required: true, message: '请选择年份' }]"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:value="form.year"
|
||||
picker="year"
|
||||
value-format="YYYY"
|
||||
style="width: 100%"
|
||||
:disabled="!!props.fixedYear"
|
||||
placeholder="请选择年份"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<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="24">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select
|
||||
v-model:value="form.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择性别"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="出生年月" name="birthMonth">
|
||||
<a-date-picker
|
||||
v-model:value="form.birthMonth"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择出生年月"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="民族" name="nation">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.nation"
|
||||
:options="nationOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择民族"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="政治面貌" name="politics">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.politics"
|
||||
:options="politicsOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择政治面貌"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="学院班级/单位科室" name="collegeClass">
|
||||
<class-cascader
|
||||
v-model:value="form.collegeClass"
|
||||
mode="collegeClass"
|
||||
placeholder="请选择学院班级/单位科室"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="所在团支部" name="branch">
|
||||
<a-input
|
||||
v-model:value="form.branch"
|
||||
placeholder="请输入所在团支部"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度团员教育评议等次" name="eduEval">
|
||||
<a-input
|
||||
v-model:value="form.eduEval"
|
||||
placeholder="请输入上一年度团员教育评议等次"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="近三年曾获校级及以上奖励" name="awards">
|
||||
<a-textarea
|
||||
v-model:value="form.awards"
|
||||
:rows="3"
|
||||
placeholder="请输入近三年曾获校级及以上奖励"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="工作(学习)经历" name="experience">
|
||||
<a-textarea
|
||||
v-model:value="form.experience"
|
||||
:rows="3"
|
||||
placeholder="请输入工作(学习)经历"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="主要事迹" name="mainStory">
|
||||
<a-textarea
|
||||
v-model:value="form.mainStory"
|
||||
:rows="3"
|
||||
placeholder="请输入主要事迹"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="所在二级团组织意见" name="leagueOpinion">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.leagueOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- placeholder="请输入所在二级团组织意见"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="所在单位党组织意见" name="partyOpinion">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.partyOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- placeholder="请输入所在单位党组织意见"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="校团委意见" name="schoolOpinion">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.schoolOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- 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 {
|
||||
addSjtbzbsjForm,
|
||||
updateSjtbzbsjForm
|
||||
} from '@/api/gxmu/sjtbzbsj-form';
|
||||
import type { SjtbzbsjForm } from '@/api/gxmu/sjtbzbsj-form/model';
|
||||
import ClassCascader from '@/views/gxmu/components/class-cascader.vue';
|
||||
import {
|
||||
filterSelectOption,
|
||||
useGxmuFormSelectOptions
|
||||
} from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: SjtbzbsjForm | null;
|
||||
fixedYear?: string | number;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const { nationOptions, politicsOptions, genderOptions, loadAll } =
|
||||
useGxmuFormSelectOptions();
|
||||
|
||||
const createDefaultForm = (): SjtbzbsjForm => ({
|
||||
id: undefined,
|
||||
year: '',
|
||||
name: '',
|
||||
gender: '',
|
||||
birthMonth: '',
|
||||
nation: '',
|
||||
politics: '',
|
||||
collegeClass: '',
|
||||
branch: '',
|
||||
eduEval: '',
|
||||
awards: '',
|
||||
experience: '',
|
||||
mainStory: '',
|
||||
leagueOpinion: '',
|
||||
partyOpinion: '',
|
||||
schoolOpinion: ''
|
||||
});
|
||||
|
||||
const form = reactive<SjtbzbsjForm>(createDefaultForm());
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
try {
|
||||
await loadAll();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '下拉选项加载失败');
|
||||
}
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.assign(form, createDefaultForm(), props.data ?? {});
|
||||
if (props.fixedYear) {
|
||||
form.year = String(props.fixedYear);
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
formRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateSjtbzbsjForm
|
||||
: addSjtbzbsjForm;
|
||||
saveOrUpdate({
|
||||
...form,
|
||||
year: props.fixedYear ? String(props.fixedYear) : form.year
|
||||
})
|
||||
.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,78 @@
|
||||
<template>
|
||||
<common-import
|
||||
v-bind="$props"
|
||||
@done="emit('done')"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
module-name="十佳团支部书记"
|
||||
file-name="十佳团支部书记导入模板.xlsx"
|
||||
:template-fields="templateFields"
|
||||
:sample-row="sampleRow"
|
||||
:create-payload="createPayload"
|
||||
:save-item="saveItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CommonImport from '../../components/common-import.vue';
|
||||
import { addSjtbzbsjForm } from '@/api/gxmu/sjtbzbsj-form';
|
||||
import type { SjtbzbsjForm } from '@/api/gxmu/sjtbzbsj-form/model';
|
||||
|
||||
defineProps<{ visible: boolean }>();
|
||||
const emit = defineEmits(['done', 'update:visible']);
|
||||
|
||||
const templateFields = [
|
||||
{ header: '年份', key: 'year', required: true },
|
||||
{ header: '姓名', key: 'name', required: true },
|
||||
{ header: '性别', key: 'gender' },
|
||||
{ header: '出生年月', key: 'birthMonth' },
|
||||
{ header: '民族', key: 'nation' },
|
||||
{ header: '政治面貌', key: 'politics' },
|
||||
{ header: '学院班级', key: 'collegeClass' },
|
||||
{ header: '团支部名称', key: 'branch' },
|
||||
{ header: '教育评议情况', key: 'eduEval' },
|
||||
{ header: '近三年曾获校级及以上奖励', key: 'awards' },
|
||||
{ header: '工作(学习)经历', key: 'experience' },
|
||||
{ header: '主要事迹', key: 'mainStory' },
|
||||
{ header: '所在二级团组织意见', key: 'leagueOpinion' },
|
||||
{ header: '所在二级党组织意见', key: 'partyOpinion' },
|
||||
{ header: '学校团委意见', key: 'schoolOpinion' }
|
||||
] as const;
|
||||
|
||||
const sampleRow = {
|
||||
年份: '2025',
|
||||
姓名: '李四',
|
||||
性别: '女',
|
||||
出生年月: '2001-03',
|
||||
民族: '汉族',
|
||||
政治面貌: '共青团员',
|
||||
学院班级: '护理学院2022级1班',
|
||||
团支部名称: '护理学院2022级1班团支部',
|
||||
教育评议情况: '优秀',
|
||||
近三年曾获校级及以上奖励: '校级优秀团支书',
|
||||
'工作(学习)经历': '2022年起任团支书',
|
||||
主要事迹: '组织主题团日活动成效显著',
|
||||
所在二级团组织意见: '',
|
||||
所在二级党组织意见: '',
|
||||
学校团委意见: ''
|
||||
};
|
||||
|
||||
const createPayload = (row: Record<string, any>): SjtbzbsjForm => ({
|
||||
year: String(row['年份'] || '').trim(),
|
||||
name: String(row['姓名'] || '').trim(),
|
||||
gender: String(row['性别'] || '').trim(),
|
||||
birthMonth: String(row['出生年月'] || '').trim(),
|
||||
nation: String(row['民族'] || '').trim(),
|
||||
politics: String(row['政治面貌'] || '').trim(),
|
||||
collegeClass: String(row['学院班级'] || '').trim(),
|
||||
branch: String(row['团支部名称'] || '').trim(),
|
||||
eduEval: String(row['教育评议情况'] || '').trim(),
|
||||
awards: String(row['近三年曾获校级及以上奖励'] || '').trim(),
|
||||
experience: String(row['工作(学习)经历'] || '').trim(),
|
||||
mainStory: String(row['主要事迹'] || '').trim(),
|
||||
leagueOpinion: String(row['所在二级团组织意见'] || '').trim(),
|
||||
partyOpinion: String(row['所在二级党组织意见'] || '').trim(),
|
||||
schoolOpinion: String(row['学校团委意见'] || '').trim()
|
||||
});
|
||||
|
||||
const saveItem = (item: SjtbzbsjForm) => addSjtbzbsjForm(item);
|
||||
</script>
|
||||
@@ -0,0 +1,379 @@
|
||||
<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: 2600 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>{{ props.popup ? '立即申报' : '添加' }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
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-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'currentReviewStatus'">
|
||||
<ReviewStatusTag :review-list="record.reviewList" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<template v-if="!shouldHideEditByReviewList(record.reviewList)">
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
</template>
|
||||
<a @click="openReviewHistory(record)">审核历史</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:fixed-year="props.fixedYear"
|
||||
@done="reload"
|
||||
/>
|
||||
<import-modal v-model:visible="showImport" @done="reload" />
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="REVIEW_MODULE"
|
||||
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 { 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 ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import { shouldHideEditByReviewList } from '../utils/review';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
exportSjtbzbsjForm,
|
||||
userPageSjtbzbsjForm,
|
||||
removeSjtbzbsjForm
|
||||
} from '@/api/gxmu/sjtbzbsj-form';
|
||||
import type {
|
||||
SjtbzbsjForm,
|
||||
SjtbzbsjFormParam
|
||||
} from '@/api/gxmu/sjtbzbsj-form/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
popup?: boolean;
|
||||
fixedYear?: string | number;
|
||||
}>(),
|
||||
{
|
||||
popup: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showImport = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const current = ref<SjtbzbsjForm>();
|
||||
const detailCurrent = ref<SjtbzbsjForm | null>(null);
|
||||
const REVIEW_MODULE = 'gxmu_sjtbzbsj';
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<SjtbzbsjForm | null>(null);
|
||||
const exporting = ref(false);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '出生年月',
|
||||
dataIndex: 'birthMonth',
|
||||
key: 'birthMonth',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '民族',
|
||||
dataIndex: 'nation',
|
||||
key: 'nation',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '政治面貌',
|
||||
dataIndex: 'politics',
|
||||
key: 'politics',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '学院班级/单位科室',
|
||||
dataIndex: 'collegeClass',
|
||||
key: 'collegeClass',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在团支部',
|
||||
dataIndex: 'branch',
|
||||
key: 'branch',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '上一年度团员教育评议等次',
|
||||
dataIndex: 'eduEval',
|
||||
key: 'eduEval',
|
||||
align: 'center',
|
||||
width: 220
|
||||
},
|
||||
{
|
||||
title: '近三年曾获校级及以上奖励',
|
||||
dataIndex: 'awards',
|
||||
key: 'awards',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '工作(学习)经历',
|
||||
dataIndex: 'experience',
|
||||
key: 'experience',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '主要事迹',
|
||||
dataIndex: 'mainStory',
|
||||
key: 'mainStory',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在二级团组织意见',
|
||||
dataIndex: 'leagueOpinion',
|
||||
key: 'leagueOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在单位党组织意见',
|
||||
dataIndex: 'partyOpinion',
|
||||
key: 'partyOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '校团委意见',
|
||||
dataIndex: 'schoolOpinion',
|
||||
key: 'schoolOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 220
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return userPageSjtbzbsjForm({
|
||||
page,
|
||||
limit,
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as SjtbzbsjFormParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const buildQueryParams = (): SjtbzbsjFormParam => {
|
||||
return {
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as SjtbzbsjFormParam;
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
const resolveExportUrl = (payload: any) => {
|
||||
return String(
|
||||
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 exportSjtbzbsjForm(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 openEdit = (record?: SjtbzbsjForm) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as SjtbzbsjForm)
|
||||
: undefined;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openReviewHistory = (record: SjtbzbsjForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (record: SjtbzbsjForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: SjtbzbsjForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeSjtbzbsjForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SJTBZBSJForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,930 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
placement="right"
|
||||
width="80%"
|
||||
:visible="visible"
|
||||
:title="isUpdate ? '编辑' : '新增'"
|
||||
:body-style="{ paddingBottom: '88px' }"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-form ref="formRef" :model="form">
|
||||
<a-form-item
|
||||
label="年份"
|
||||
name="year"
|
||||
:rules="[{ required: true, message: '请选择年份' }]"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:value="form.year"
|
||||
picker="year"
|
||||
value-format="YYYY"
|
||||
style="width: 240px"
|
||||
:disabled="!!props.fixedYear"
|
||||
placeholder="请选择年份"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-tabs v-model:activeKey="activeTab" type="card" class="gxmu-tag-tabs">
|
||||
<a-tab-pane key="base" tab="项目申报表">
|
||||
<div class="sheet-wrap">
|
||||
<div class="sheet-header">
|
||||
<div class="sheet-tag">附件 1</div>
|
||||
<div class="sheet-main-title"
|
||||
>{{ title || '' }}参赛项目申报表</div
|
||||
>
|
||||
</div>
|
||||
<table class="sheet-table">
|
||||
<colgroup>
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 32%" />
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 32%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sheet-label">所在省<br />(区、市)</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="form.provinceCity"
|
||||
placeholder="请输入所在省/市"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">学校名称<br />(全称)</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="form.schoolName"
|
||||
placeholder="请输入学校名称"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">项目名称</td>
|
||||
<td class="sheet-value left" colspan="3">
|
||||
<a-input
|
||||
v-model:value="form.projectName"
|
||||
placeholder="请输入项目名称"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">项目类型</td>
|
||||
<td class="sheet-value left" colspan="3">
|
||||
<a-radio-group
|
||||
v-model:value="form.projectType"
|
||||
class="inline-radio"
|
||||
>
|
||||
<a-radio
|
||||
v-for="item in formProjectTypeOptions"
|
||||
:key="`base-type-${item.value}`"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">
|
||||
项目分组
|
||||
<div class="sheet-note"
|
||||
>(仅可选择1个组别,须在此栏中删除其他组别内容)</div
|
||||
>
|
||||
</td>
|
||||
<td class="sheet-value left" colspan="3">
|
||||
<a-radio-group
|
||||
v-model:value="form.projectGroup"
|
||||
class="group-radio"
|
||||
>
|
||||
<a-radio
|
||||
v-for="item in formProjectGroupOptions"
|
||||
:key="`base-group-${item.value}`"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="sheet-table">
|
||||
<colgroup>
|
||||
<col style="width: 14%" />
|
||||
<col style="width: 12%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 16%" />
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 12%" />
|
||||
<col style="width: 18%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sheet-label" :rowspan="teamMembersCount + 1"
|
||||
>团队成员<br />(最多15人)</td
|
||||
>
|
||||
<td class="sheet-label">姓名</td>
|
||||
<td class="sheet-label">性别</td>
|
||||
<td class="sheet-label">学院</td>
|
||||
<td class="sheet-label">年级、专业</td>
|
||||
<td class="sheet-label">手机</td>
|
||||
<td class="sheet-label"
|
||||
>备注(负责人及各成员学历-本/硕/博)</td
|
||||
>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(item, index) in form.teamMembers"
|
||||
:key="`member-${index}`"
|
||||
>
|
||||
<td class="sheet-value"
|
||||
><a-input v-model:value="item.name"
|
||||
/></td>
|
||||
<td class="sheet-value">
|
||||
<a-select style="width: 120px"
|
||||
v-model:value="item.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-value">
|
||||
<a-select style="width: 120px"
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="item.college"
|
||||
:options="collegeOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-value"
|
||||
><a-input v-model:value="item.gradeMajor"
|
||||
/></td>
|
||||
<td class="sheet-value"
|
||||
><a-input v-model:value="item.phone"
|
||||
/></td>
|
||||
<td class="sheet-value"
|
||||
><a-input v-model:value="item.remark"
|
||||
/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="sheet-table">
|
||||
<colgroup>
|
||||
<col style="width: 14%" />
|
||||
<col style="width: 12%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 16%" />
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 16%" />
|
||||
<col style="width: 14%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sheet-label" :rowspan="advisorCount + 1"
|
||||
>指导教师<br />(最多5人)</td
|
||||
>
|
||||
<td class="sheet-label">姓名</td>
|
||||
<td class="sheet-label">性别</td>
|
||||
<td class="sheet-label">学院</td>
|
||||
<td class="sheet-label">职称</td>
|
||||
<td class="sheet-label">职务</td>
|
||||
<td class="sheet-label">手机</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(item, index) in form.advisors"
|
||||
:key="`advisor-${index}`"
|
||||
>
|
||||
<td class="sheet-value"
|
||||
><a-input v-model:value="item.name"
|
||||
/></td>
|
||||
<td class="sheet-value">
|
||||
<a-select style="width: 120px"
|
||||
v-model:value="item.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-value">
|
||||
<a-select style="width: 120px"
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="item.college"
|
||||
:options="collegeOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-value"
|
||||
><a-input v-model:value="item.title"
|
||||
/></td>
|
||||
<td class="sheet-value"
|
||||
><a-input v-model:value="item.duty"
|
||||
/></td>
|
||||
<td class="sheet-value"
|
||||
><a-input v-model:value="item.phone"
|
||||
/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="sheet-table">
|
||||
<colgroup>
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 82%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sheet-label">项目简介<br />(500字以内)</td>
|
||||
<td class="sheet-value left">
|
||||
<a-textarea
|
||||
v-model:value="form.projectBrief"
|
||||
:rows="4"
|
||||
placeholder="请输入项目简介"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="detail" tab="项目说明">
|
||||
<div class="sheet-wrap">
|
||||
<table class="sheet-table">
|
||||
<colgroup>
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 82%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sheet-label">社会价值<br />(500字以内)</td>
|
||||
<td class="sheet-value left">
|
||||
<a-textarea
|
||||
v-model:value="form.socialValue"
|
||||
:rows="4"
|
||||
placeholder="请输入社会价值"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">实践过程<br />(500字以内)</td>
|
||||
<td class="sheet-value left">
|
||||
<a-textarea
|
||||
v-model:value="form.practiceProcess"
|
||||
:rows="4"
|
||||
placeholder="请输入实践过程"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">创新意义<br />(500字以内)</td>
|
||||
<td class="sheet-value left">
|
||||
<a-textarea
|
||||
v-model:value="form.innovationMeaning"
|
||||
:rows="4"
|
||||
placeholder="请输入创新意义"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">发展前景<br />(500字以内)</td>
|
||||
<td class="sheet-value left">
|
||||
<a-textarea
|
||||
v-model:value="form.developmentProspect"
|
||||
:rows="4"
|
||||
placeholder="请输入发展前景"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">团队协作<br />(500字以内)</td>
|
||||
<td class="sheet-value left">
|
||||
<a-textarea
|
||||
v-model:value="form.teamCooperation"
|
||||
:rows="4"
|
||||
placeholder="请输入团队协作"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">项目介绍材料</td>
|
||||
<td class="sheet-value left">
|
||||
<a-textarea
|
||||
v-model:value="form.projectMaterials"
|
||||
:rows="3"
|
||||
placeholder="请输入项目介绍材料"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">其他相关证明材料</td>
|
||||
<td class="sheet-value left">
|
||||
<a-textarea
|
||||
v-model:value="form.otherProofs"
|
||||
:rows="3"
|
||||
placeholder="选报,所有其他相关证明材料需扫描在同一 PDF 文档中"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="public" tab="公开展示信息表">
|
||||
<div class="sheet-wrap">
|
||||
<div class="public-title">
|
||||
<div class="public-tag">附件 2</div>
|
||||
<div class="public-main-title"
|
||||
>{{ title || '' }}参赛项目公开展示信息表</div
|
||||
>
|
||||
</div>
|
||||
<table class="sheet-table">
|
||||
<colgroup>
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 82%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sheet-label">项目名称</td>
|
||||
<td class="sheet-value left">
|
||||
<a-input
|
||||
v-model:value="form.projectName"
|
||||
placeholder="请输入项目名称"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">参赛学校</td>
|
||||
<td class="sheet-value left">
|
||||
<a-input
|
||||
v-model:value="form.schoolName"
|
||||
placeholder="请输入参赛学校"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">项目类型</td>
|
||||
<td class="sheet-value left">
|
||||
<a-radio-group
|
||||
v-model:value="form.publicProjectType"
|
||||
class="inline-radio"
|
||||
>
|
||||
<a-radio
|
||||
v-for="item in publicProjectTypeOptions"
|
||||
:key="`public-type-${item.value}`"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">项目分组</td>
|
||||
<td class="sheet-value left">
|
||||
<a-radio-group
|
||||
v-model:value="form.publicProjectGroup"
|
||||
class="group-radio"
|
||||
>
|
||||
<a-radio
|
||||
v-for="item in publicProjectGroupOptions"
|
||||
:key="`public-group-${item.value}`"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">我们的项目</td>
|
||||
<td class="sheet-value left">
|
||||
<a-textarea
|
||||
v-model:value="form.projectSummary"
|
||||
:rows="3"
|
||||
placeholder="150字以内,阐述项目的实践来源、社会背景、基本情况等"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">我们的团队</td>
|
||||
<td class="sheet-value left">
|
||||
<div class="media-upload-block">
|
||||
<div v-if="form.teamIntro" class="media-preview-wrap">
|
||||
<img
|
||||
class="media-image"
|
||||
:src="form.teamIntro"
|
||||
alt="团队图片"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="media-empty">请上传团队图片</div>
|
||||
<div class="media-actions">
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
accept="image/*"
|
||||
:customRequest="uploadTeamImage"
|
||||
>
|
||||
<a-button type="primary">上传图片</a-button>
|
||||
</a-upload>
|
||||
<a-button v-if="form.teamIntro" @click="form.teamIntro = ''">
|
||||
移除
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">我们的口号</td>
|
||||
<td class="sheet-value left">
|
||||
<a-input
|
||||
v-model:value="form.teamSlogan"
|
||||
placeholder="20字以内"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">我们的实践日志(选填)</td>
|
||||
<td class="sheet-value left">
|
||||
<div class="media-upload-block">
|
||||
<div v-if="form.practiceLog" class="media-preview-wrap media-preview-wrap--video">
|
||||
<video class="media-video" :src="form.practiceLog" controls></video>
|
||||
</div>
|
||||
<div v-else class="media-empty">请上传实践日志视频</div>
|
||||
<div class="media-actions">
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
accept="video/*"
|
||||
:customRequest="uploadPracticeVideo"
|
||||
>
|
||||
<a-button type="primary">上传视频</a-button>
|
||||
</a-upload>
|
||||
<a-button v-if="form.practiceLog" @click="form.practiceLog = ''">
|
||||
移除
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
<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, reactive, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { addTzbcyForm, updateTzbcyForm } from '@/api/gxmu/tzbcy-form';
|
||||
import { uploadFile } from '@/api/system/file';
|
||||
import type {
|
||||
TzbcyAdvisor,
|
||||
TzbcyForm,
|
||||
TzbcyTeamMember
|
||||
} from '@/api/gxmu/tzbcy-form/model';
|
||||
import {
|
||||
filterSelectOption,
|
||||
useGxmuFormSelectOptions
|
||||
} from '@/views/gxmu/useFormSelectOptions';
|
||||
import {
|
||||
TZBCY_PROJECT_GROUP_OPTIONS,
|
||||
TZBCY_PROJECT_TYPE_OPTIONS
|
||||
} from '../options';
|
||||
|
||||
interface ChoiceOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: TzbcyForm | null;
|
||||
title?: string;
|
||||
fixedYear?: string | number;
|
||||
declareFormProjectTypes?: string[];
|
||||
declareFormProjectGroups?: string[];
|
||||
declarePublicProjectTypes?: string[];
|
||||
declarePublicProjectGroups?: string[];
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const activeTab = ref('base');
|
||||
const { genderOptions, collegeOptions, loadCollegeOptions } =
|
||||
useGxmuFormSelectOptions();
|
||||
|
||||
const createTeamMember = (): TzbcyTeamMember => ({
|
||||
name: '',
|
||||
gender: '',
|
||||
college: '',
|
||||
gradeMajor: '',
|
||||
phone: '',
|
||||
remark: ''
|
||||
});
|
||||
|
||||
const createAdvisor = (): TzbcyAdvisor => ({
|
||||
name: '',
|
||||
gender: '',
|
||||
college: '',
|
||||
title: '',
|
||||
duty: '',
|
||||
phone: ''
|
||||
});
|
||||
|
||||
const createDefaultForm = (): TzbcyForm => ({
|
||||
id: undefined,
|
||||
year: '',
|
||||
provinceCity: '',
|
||||
schoolName: '广西医科大学',
|
||||
projectName: '',
|
||||
projectType: '',
|
||||
projectGroup: '',
|
||||
publicProjectType: '',
|
||||
publicProjectGroup: '',
|
||||
leader: '',
|
||||
phone: '',
|
||||
teamMembers: Array.from({ length: 15 }).map(() => createTeamMember()),
|
||||
advisors: Array.from({ length: 5 }).map(() => createAdvisor()),
|
||||
projectBrief: '',
|
||||
socialValue: '',
|
||||
practiceProcess: '',
|
||||
innovationMeaning: '',
|
||||
developmentProspect: '',
|
||||
teamCooperation: '',
|
||||
projectMaterials: '',
|
||||
otherProofs: '',
|
||||
projectSummary: '',
|
||||
teamIntro: '',
|
||||
teamSlogan: '',
|
||||
practiceLog: ''
|
||||
});
|
||||
|
||||
const fillList = <T extends Record<string, unknown>>(
|
||||
list: T[] | undefined,
|
||||
count: number,
|
||||
createItem: () => T
|
||||
) => {
|
||||
const source = list?.map((item) => ({ ...createItem(), ...item })) ?? [];
|
||||
return Array.from({ length: count }).map(
|
||||
(_, index) => source[index] ?? createItem()
|
||||
);
|
||||
};
|
||||
|
||||
const normalizeForm = (record?: TzbcyForm | null): TzbcyForm => {
|
||||
const defaults = createDefaultForm();
|
||||
if (!record) {
|
||||
return defaults;
|
||||
}
|
||||
const normalizedProjectType = record.projectType || '';
|
||||
const normalizedProjectGroup = record.projectGroup || '';
|
||||
return {
|
||||
...defaults,
|
||||
...record,
|
||||
projectType: normalizedProjectType,
|
||||
projectGroup: normalizedProjectGroup,
|
||||
publicProjectType: record.publicProjectType || normalizedProjectType,
|
||||
publicProjectGroup: record.publicProjectGroup || normalizedProjectGroup,
|
||||
teamMembers: fillList(record.teamMembers, 15, createTeamMember),
|
||||
advisors: fillList(record.advisors, 5, createAdvisor)
|
||||
};
|
||||
};
|
||||
|
||||
const form = reactive<TzbcyForm>(createDefaultForm());
|
||||
|
||||
const teamMembersCount = computed(() => form.teamMembers?.length || 0);
|
||||
const advisorCount = computed(() => form.advisors?.length || 0);
|
||||
|
||||
const buildChoiceOptions = (
|
||||
customValues: string[] | undefined,
|
||||
fallback: readonly ChoiceOption[],
|
||||
currentValue?: string
|
||||
): ChoiceOption[] => {
|
||||
const values = (customValues?.length
|
||||
? customValues
|
||||
: fallback.map((item) => item.value)
|
||||
)
|
||||
.map((item) => item.trim())
|
||||
.filter((item, index, arr) => !!item && arr.indexOf(item) === index);
|
||||
|
||||
if (currentValue?.trim() && !values.includes(currentValue.trim())) {
|
||||
values.push(currentValue.trim());
|
||||
}
|
||||
|
||||
return values.map((value) => {
|
||||
const matched = fallback.find((item) => item.value === value);
|
||||
return {
|
||||
label: matched?.label ?? value,
|
||||
value
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const formProjectTypeOptions = computed(() =>
|
||||
buildChoiceOptions(
|
||||
props.declareFormProjectTypes,
|
||||
TZBCY_PROJECT_TYPE_OPTIONS,
|
||||
form.projectType
|
||||
)
|
||||
);
|
||||
const formProjectGroupOptions = computed(() =>
|
||||
buildChoiceOptions(
|
||||
props.declareFormProjectGroups,
|
||||
TZBCY_PROJECT_GROUP_OPTIONS,
|
||||
form.projectGroup
|
||||
)
|
||||
);
|
||||
const publicProjectTypeOptions = computed(() =>
|
||||
buildChoiceOptions(
|
||||
props.declarePublicProjectTypes,
|
||||
TZBCY_PROJECT_TYPE_OPTIONS,
|
||||
form.publicProjectType
|
||||
)
|
||||
);
|
||||
const publicProjectGroupOptions = computed(() =>
|
||||
buildChoiceOptions(
|
||||
props.declarePublicProjectGroups,
|
||||
TZBCY_PROJECT_GROUP_OPTIONS,
|
||||
form.publicProjectGroup
|
||||
)
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
try {
|
||||
await loadCollegeOptions();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '下拉选项加载失败');
|
||||
}
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.assign(form, normalizeForm(props.data));
|
||||
if (props.fixedYear) {
|
||||
form.year = String(props.fixedYear);
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
activeTab.value = 'base';
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const syncLeaderInfo = () => {
|
||||
const firstMember = form.teamMembers?.find(
|
||||
(item) => item.name?.trim() || item.phone?.trim()
|
||||
);
|
||||
form.leader = firstMember?.name?.trim() || '';
|
||||
form.phone = firstMember?.phone?.trim() || '';
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
syncLeaderInfo();
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateTzbcyForm : addTzbcyForm;
|
||||
saveOrUpdate({
|
||||
...form,
|
||||
year: props.fixedYear ? String(props.fixedYear) : form.year
|
||||
})
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const uploadTeamImage = (item) => {
|
||||
const file = item.file as File;
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error('请上传图片文件');
|
||||
return;
|
||||
}
|
||||
uploadFile(file)
|
||||
.then((data) => {
|
||||
form.teamIntro = String(data.url || '');
|
||||
message.success('图片上传成功');
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const uploadPracticeVideo = (item) => {
|
||||
const file = item.file as File;
|
||||
if (!file.type.startsWith('video/')) {
|
||||
message.error('请上传视频文件');
|
||||
return;
|
||||
}
|
||||
uploadFile(file)
|
||||
.then((data) => {
|
||||
form.practiceLog = String(data.url || '');
|
||||
message.success('视频上传成功');
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../../form-theme.less';
|
||||
@import '../../tag-tabs.less';
|
||||
|
||||
.gxmu-field-theme();
|
||||
.gxmu-tag-tabs();
|
||||
|
||||
.sheet-wrap {
|
||||
border: 1px solid #d9d9d9;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sheet-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sheet-tag {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sheet-main-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sheet-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sheet-table td {
|
||||
border: 1px solid #3b3b3b;
|
||||
padding: 8px 6px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sheet-label {
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sheet-note {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #444;
|
||||
margin-top: 6px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.sheet-value {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sheet-value.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sheet-table .sheet-value :deep(.ant-input),
|
||||
.sheet-table .sheet-value :deep(.ant-select-selector),
|
||||
.sheet-table .sheet-value :deep(.ant-radio-group),
|
||||
.sheet-table .sheet-value :deep(.ant-input-textarea),
|
||||
.sheet-table .sheet-value :deep(.ant-input-affix-wrapper) {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sheet-table .sheet-value :deep(.ant-input) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sheet-table .sheet-value.left :deep(.ant-input),
|
||||
.sheet-table .sheet-value.left :deep(.ant-input-textarea) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sheet-table .sheet-value :deep(textarea.ant-input) {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.inline-radio :deep(.ant-radio) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.group-radio {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.media-upload-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.media-preview-wrap {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.media-preview-wrap--video {
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.media-image {
|
||||
width: 100%;
|
||||
max-height: 220px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.media-video {
|
||||
width: 100%;
|
||||
max-height: 260px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.media-empty {
|
||||
padding: 24px 12px;
|
||||
border-radius: 8px;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.media-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.public-title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.public-tag {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.public-main-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sheet-main-title,
|
||||
.public-main-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="form.id ? '编辑材料' : '新增材料'"
|
||||
:confirm-loading="loading"
|
||||
:mask-closable="false"
|
||||
@ok="save"
|
||||
@cancel="close"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 19 }"
|
||||
>
|
||||
<a-form-item label="材料类型" name="materialType">
|
||||
<a-input
|
||||
v-model:value="form.materialType"
|
||||
allow-clear
|
||||
placeholder="请输入材料类型"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="上传材料" name="fileId">
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
:customRequest="handleUpload"
|
||||
>
|
||||
<a-button :loading="uploading">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
上传文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div v-if="form.fileName" class="file-name">
|
||||
{{ form.fileName }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { uploadFile } from '@/api/system/file';
|
||||
import { addTzbcyMaterial, updateTzbcyMaterial } from '@/api/gxmu/tzbcy-material';
|
||||
import type { TzbcyMaterial } from '@/api/gxmu/tzbcy-material/model';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
formId?: number;
|
||||
data?: TzbcyMaterial | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
const uploading = ref(false);
|
||||
const form = reactive<TzbcyMaterial>({});
|
||||
const rules = {
|
||||
materialType: [{ required: true, message: '请输入材料类型', trigger: 'blur' }],
|
||||
fileId: [{ required: true, message: '请上传材料文件', trigger: 'change' }]
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
Object.assign(form, {
|
||||
id: props.data?.id,
|
||||
formId: props.data?.formId || props.formId,
|
||||
materialType: props.data?.materialType,
|
||||
fileId: props.data?.fileId,
|
||||
fileName: props.data?.fileName
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
const handleUpload = async (options: any) => {
|
||||
const rawFile = options.file as File;
|
||||
uploading.value = true;
|
||||
const hide = message.loading('上传中..', 0);
|
||||
try {
|
||||
const result = await uploadFile(rawFile);
|
||||
form.fileId = result.id;
|
||||
form.fileName = result.name || rawFile.name;
|
||||
options.onSuccess?.(result);
|
||||
formRef.value?.validateFields(['fileId']);
|
||||
message.success('上传成功');
|
||||
} catch (e: any) {
|
||||
options.onError?.(e);
|
||||
message.error(e.message || '上传失败');
|
||||
} finally {
|
||||
hide();
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
await formRef.value?.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
const submitData = { ...form, formId: form.formId || props.formId };
|
||||
const msg = submitData.id
|
||||
? await updateTzbcyMaterial(submitData)
|
||||
: await addTzbcyMaterial(submitData);
|
||||
message.success(msg);
|
||||
emit('done');
|
||||
close();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '提交失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.file-name {
|
||||
margin-top: 8px;
|
||||
color: #4b5563;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="材料上传"
|
||||
:footer="null"
|
||||
:destroy-on-close="true"
|
||||
:width="1000"
|
||||
@cancel="close"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:scroll="{ x: 1100 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-select
|
||||
v-model:value="materialStatus"
|
||||
allow-clear
|
||||
placeholder="请选择审核状态"
|
||||
style="width: 160px"
|
||||
@change="reload"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="openEdit()">
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
新增材料
|
||||
</a-button>
|
||||
<a-button :loading="exporting" @click="exportMaterials">
|
||||
整体导出
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="download(record)">下载</a>
|
||||
<template v-if="record.status !== 1">
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="audit(record, 1)">通过</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openReject(record)">驳回</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm title="确定要删除此材料吗?" @confirm="remove(record)">
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
|
||||
<material-edit
|
||||
v-model:visible="showEdit"
|
||||
:form-id="record?.id"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="showReject"
|
||||
title="驳回材料"
|
||||
:confirm-loading="rejecting"
|
||||
@ok="submitReject"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="rejectReason"
|
||||
:rows="4"
|
||||
placeholder="请输入驳回原因"
|
||||
/>
|
||||
</a-modal>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
ColumnItem,
|
||||
DatasourceFunction
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import MaterialEdit from './materialEdit.vue';
|
||||
import {
|
||||
auditTzbcyMaterial,
|
||||
exportTzbcyMaterial,
|
||||
pageTzbcyMaterial,
|
||||
removeTzbcyMaterial
|
||||
} from '@/api/gxmu/tzbcy-material';
|
||||
import type { TzbcyMaterial } from '@/api/gxmu/tzbcy-material/model';
|
||||
import type { TzbcyForm } from '@/api/gxmu/tzbcy-form/model';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
record?: TzbcyForm | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const showEdit = ref(false);
|
||||
const showReject = ref(false);
|
||||
const rejecting = ref(false);
|
||||
const exporting = ref(false);
|
||||
const current = ref<TzbcyMaterial | null>(null);
|
||||
const rejectCurrent = ref<TzbcyMaterial | null>(null);
|
||||
const rejectReason = ref('');
|
||||
const materialStatus = ref<number>();
|
||||
const statusOptions = [
|
||||
{ label: '待审核', value: 0 },
|
||||
{ label: '通过', value: 1 },
|
||||
{ label: '驳回', value: 2 }
|
||||
];
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '材料类型',
|
||||
dataIndex: 'materialType',
|
||||
key: 'materialType',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '文件名称',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
align: 'center',
|
||||
width: 260,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '驳回原因',
|
||||
dataIndex: 'rejectReason',
|
||||
key: 'rejectReason',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 170
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 260
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
if (!props.record?.id) {
|
||||
return Promise.resolve({ list: [], count: 0 });
|
||||
}
|
||||
return pageTzbcyMaterial({
|
||||
page,
|
||||
limit,
|
||||
formId: props.record.id,
|
||||
status: materialStatus.value
|
||||
}).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => reload());
|
||||
} else {
|
||||
materialStatus.value = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
if (status === 1) {
|
||||
return '通过';
|
||||
}
|
||||
if (status === 2) {
|
||||
return '驳回';
|
||||
}
|
||||
return '待审核';
|
||||
};
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
if (status === 1) {
|
||||
return 'success';
|
||||
}
|
||||
if (status === 2) {
|
||||
return 'error';
|
||||
}
|
||||
return 'processing';
|
||||
};
|
||||
|
||||
const openEdit = (record?: TzbcyMaterial) => {
|
||||
current.value = record ? { ...record } : null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const download = (record: TzbcyMaterial) => {
|
||||
const url = record.downloadUrl || record.fileUrl;
|
||||
if (!url) {
|
||||
message.warning('暂无下载地址');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const audit = async (record: TzbcyMaterial, status: number, reason?: string) => {
|
||||
try {
|
||||
const msg = await auditTzbcyMaterial({
|
||||
id: record.id,
|
||||
status,
|
||||
rejectReason: reason
|
||||
});
|
||||
message.success(msg);
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '审核失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openReject = (record: TzbcyMaterial) => {
|
||||
rejectCurrent.value = record;
|
||||
rejectReason.value = record.rejectReason || '';
|
||||
showReject.value = true;
|
||||
};
|
||||
|
||||
const submitReject = async () => {
|
||||
if (!rejectReason.value.trim()) {
|
||||
message.warning('请输入驳回原因');
|
||||
return;
|
||||
}
|
||||
if (!rejectCurrent.value) {
|
||||
return;
|
||||
}
|
||||
rejecting.value = true;
|
||||
try {
|
||||
await audit(rejectCurrent.value, 2, rejectReason.value.trim());
|
||||
showReject.value = false;
|
||||
} finally {
|
||||
rejecting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (record: TzbcyMaterial) => {
|
||||
try {
|
||||
const msg = await removeTzbcyMaterial(record.id);
|
||||
message.success(msg);
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const exportMaterials = async () => {
|
||||
if (!props.record?.id) {
|
||||
return;
|
||||
}
|
||||
exporting.value = true;
|
||||
try {
|
||||
const result = await exportTzbcyMaterial({
|
||||
formId: props.record.id,
|
||||
status: materialStatus.value
|
||||
});
|
||||
const url = result.url || result.downloadUrl || result.path;
|
||||
if (!url) {
|
||||
message.warning('未获取到导出文件地址');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '导出失败');
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,292 @@
|
||||
<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: 1400 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>{{ props.popup ? '立即申报' : '添加' }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入项目名称/负责人/学校"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
</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'">
|
||||
<a-space>
|
||||
<a @click="openMaterials(record)">材料上传</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<template v-if="!shouldHideEditByReviewList(record.reviewList)">
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
</template>
|
||||
<a @click="openReviewHistory(record)">审核历史</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:title="title"
|
||||
:fixed-year="props.fixedYear"
|
||||
:declare-form-project-types="props.declareFormProjectTypes"
|
||||
:declare-form-project-groups="props.declareFormProjectGroups"
|
||||
:declare-public-project-types="props.declarePublicProjectTypes"
|
||||
:declare-public-project-groups="props.declarePublicProjectGroups"
|
||||
@done="reload"
|
||||
/>
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="挑战杯"
|
||||
:pk="detailCurrent?.id"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
:module="REVIEW_MODULE"
|
||||
:pk="reviewCurrent?.id"
|
||||
:review-list="reviewCurrent?.reviewList"
|
||||
/>
|
||||
<materials-modal
|
||||
v-model:visible="showMaterials"
|
||||
:record="materialCurrent"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UploadOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||||
import EditModal from './components/edit.vue';
|
||||
import MaterialsModal from './components/materials.vue';
|
||||
import ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import { shouldHideEditByReviewList } from '../utils/review';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { userPageTzbcyForm, removeTzbcyForm } from '@/api/gxmu/tzbcy-form';
|
||||
import type { TzbcyForm, TzbcyFormParam } from '@/api/gxmu/tzbcy-form/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
popup?: boolean;
|
||||
fixedYear?: string | number;
|
||||
title?: string;
|
||||
declareFormProjectTypes?: string[];
|
||||
declareFormProjectGroups?: string[];
|
||||
declarePublicProjectTypes?: string[];
|
||||
declarePublicProjectGroups?: string[];
|
||||
}>(),
|
||||
{
|
||||
popup: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const current = ref<TzbcyForm>();
|
||||
const detailCurrent = ref<TzbcyForm | null>(null);
|
||||
const REVIEW_MODULE = 'gxmu_tzbcy_form';
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<TzbcyForm | null>(null);
|
||||
const showMaterials = ref(false);
|
||||
const materialCurrent = ref<TzbcyForm | null>(null);
|
||||
|
||||
const getLeaderInfo = (record: TzbcyForm) => {
|
||||
const firstMember = record.teamMembers?.find(
|
||||
(item) => item.name?.trim() || item.phone?.trim()
|
||||
);
|
||||
return {
|
||||
leader: record.leader || firstMember?.name || '',
|
||||
phone: record.phone || firstMember?.phone || ''
|
||||
};
|
||||
};
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'projectName',
|
||||
key: 'projectName',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '项目类型',
|
||||
dataIndex: 'projectType',
|
||||
key: 'projectType',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '项目分组',
|
||||
dataIndex: 'projectGroup',
|
||||
key: 'projectGroup',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '学校名称',
|
||||
dataIndex: 'schoolName',
|
||||
key: 'schoolName',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'leader',
|
||||
key: 'leader',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
customRender: ({ record }) => getLeaderInfo(record as TzbcyForm).leader
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
align: 'center',
|
||||
width: 140,
|
||||
customRender: ({ record }) => getLeaderInfo(record as TzbcyForm).phone
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 280
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return userPageTzbcyForm({
|
||||
page,
|
||||
limit,
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as TzbcyFormParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
message.info('已触发导入操作(请接入实际导入逻辑)');
|
||||
};
|
||||
|
||||
const openEdit = (record?: TzbcyForm) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as TzbcyForm)
|
||||
: undefined;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openReviewHistory = (record: TzbcyForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (record: TzbcyForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const openMaterials = (record: TzbcyForm) => {
|
||||
materialCurrent.value = record;
|
||||
showMaterials.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: TzbcyForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeTzbcyForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TZBCYForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
export const TZBCY_PROJECT_TYPE_OPTIONS = [
|
||||
{ label: 'I. 普通高校', value: '普通高校' },
|
||||
{ label: 'II. 职业院校', value: '职业院校' },
|
||||
{ label: 'III. 东盟留学生专项', value: '东盟留学生专项' }
|
||||
] as const;
|
||||
|
||||
export const TZBCY_PROJECT_GROUP_OPTIONS = [
|
||||
{ label: 'A. 科技创新和未来产业', value: 'A. 科技创新和未来产业' },
|
||||
{ label: 'B. 乡村振兴和农业农村现代化', value: 'B. 乡村振兴和农业农村现代化' },
|
||||
{ label: 'C. 社会治理和公共服务', value: 'C. 社会治理和公共服务' },
|
||||
{ label: 'D. 生态环保和可持续发展', value: 'D. 生态环保和可持续发展' },
|
||||
{ label: 'E. 文化创意和区域合作', value: 'E. 文化创意和区域合作' }
|
||||
] as const;
|
||||
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="1000"
|
||||
: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="24">
|
||||
<a-form-item
|
||||
label="年份"
|
||||
name="year"
|
||||
:rules="[{ required: true, message: '请选择年份' }]"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:value="form.year"
|
||||
picker="year"
|
||||
value-format="YYYY"
|
||||
style="width: 100%"
|
||||
:disabled="!!props.fixedYear"
|
||||
placeholder="请选择年份"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="二级团组织全称"
|
||||
name="orgName"
|
||||
:rules="[{ required: true, message: '请输入二级团组织全称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.orgName"
|
||||
placeholder="请输入二级团组织全称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="负责人" name="leader">
|
||||
<a-input v-model:value="form.leader" placeholder="请输入负责人" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input v-model:value="form.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="现有团员总数" name="memberTotal">
|
||||
<a-input
|
||||
v-model:value="form.memberTotal"
|
||||
placeholder="请输入现有团员总数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度发展团员人数" name="memberDeveloped2024">
|
||||
<a-input
|
||||
v-model:value="form.memberDeveloped2024"
|
||||
placeholder="请输入上一年度发展团员人数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="是否已登录智慧团建系统" name="smartSystemLogin">
|
||||
<a-select
|
||||
v-model:value="form.smartSystemLogin"
|
||||
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="24">
|
||||
<a-form-item label="团委委员人数" name="committeeCount">
|
||||
<a-input
|
||||
v-model:value="form.committeeCount"
|
||||
placeholder="请输入团委委员人数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="专职团干部数" name="fulltimeCadreCount">
|
||||
<a-input
|
||||
v-model:value="form.fulltimeCadreCount"
|
||||
placeholder="请输入专职团干部数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="兼职团干部数" name="parttimeCadreCount">
|
||||
<a-input
|
||||
v-model:value="form.parttimeCadreCount"
|
||||
placeholder="请输入兼职团干部数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="团委最近一次换届时间" name="lastElectionTime">
|
||||
<a-date-picker
|
||||
v-model:value="form.lastElectionTime"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
placeholder="请选择团委最近一次换届时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度应收团费" name="feeReceivable2024">
|
||||
<a-input
|
||||
v-model:value="form.feeReceivable2024"
|
||||
placeholder="请输入上一年度应收团费"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度实收团费" name="feeReceived2024">
|
||||
<a-input
|
||||
v-model:value="form.feeReceived2024"
|
||||
placeholder="请输入上一年度实收团费"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度应上缴团费" name="feePayable2024">
|
||||
<a-input
|
||||
v-model:value="form.feePayable2024"
|
||||
placeholder="请输入上一年度应上缴团费"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度实际上缴团费" name="feePaid2024">
|
||||
<a-input
|
||||
v-model:value="form.feePaid2024"
|
||||
placeholder="请输入上一年度实际上缴团费"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="团支部数" name="branchCount">
|
||||
<a-input
|
||||
v-model:value="form.branchCount"
|
||||
placeholder="请输入团支部数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="是否开展规范化建设工作"
|
||||
name="standardizedWork2024"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="form.standardizedWork2024"
|
||||
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="24">
|
||||
<a-form-item
|
||||
label="推荐入党积极分子人数"
|
||||
name="recommendActivist2024"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.recommendActivist2024"
|
||||
placeholder="请输入推荐入党积极分子人数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="确定为入党积极分子数" name="activistConfirmed">
|
||||
<a-input
|
||||
v-model:value="form.activistConfirmed"
|
||||
placeholder="请输入确定为入党积极分子数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="推荐党的发展对象人数"
|
||||
name="recommendDevTarget2024"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.recommendDevTarget2024"
|
||||
placeholder="请输入推荐党的发展对象人数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="确定为党的发展对象数" name="devTargetConfirmed">
|
||||
<a-input
|
||||
v-model:value="form.devTargetConfirmed"
|
||||
placeholder="请输入确定为党的发展对象数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="近五年获得校级及以上荣誉情况"
|
||||
name="honorsFiveYears"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="form.honorsFiveYears"
|
||||
:rows="3"
|
||||
placeholder="请输入近五年获得校级及以上荣誉情况"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="近三年开展的主要工作及取得的效果"
|
||||
name="workSummaryThreeYears"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="form.workSummaryThreeYears"
|
||||
:rows="3"
|
||||
placeholder="请输入近三年开展的主要工作及取得的效果"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="所在单位党组织意见" name="partyOpinion">-->
|
||||
<!-- <a-textarea v-model:value="form.partyOpinion" :rows="2" placeholder="请输入所在单位党组织意见" />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="校团委意见" name="schoolOpinion">-->
|
||||
<!-- <a-textarea v-model:value="form.schoolOpinion" :rows="2" 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 { addWshqtwForm, updateWshqtwForm } from '@/api/gxmu/wshqtw-form';
|
||||
import type { WshqtwForm } from '@/api/gxmu/wshqtw-form/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: WshqtwForm | null;
|
||||
fixedYear?: string | number;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
|
||||
const createDefaultForm = (): WshqtwForm => ({
|
||||
id: undefined,
|
||||
year: '',
|
||||
orgName: '',
|
||||
leader: '',
|
||||
phone: '',
|
||||
memberTotal: '',
|
||||
memberDeveloped2024: '',
|
||||
smartSystemLogin: '',
|
||||
committeeCount: '',
|
||||
fulltimeCadreCount: '',
|
||||
parttimeCadreCount: '',
|
||||
lastElectionTime: '',
|
||||
feeReceivable2024: '',
|
||||
feeReceived2024: '',
|
||||
feePayable2024: '',
|
||||
feePaid2024: '',
|
||||
branchCount: '',
|
||||
standardizedWork2024: '',
|
||||
recommendActivist2024: '',
|
||||
activistConfirmed: '',
|
||||
recommendDevTarget2024: '',
|
||||
devTargetConfirmed: '',
|
||||
honorsFiveYears: '',
|
||||
workSummaryThreeYears: '',
|
||||
partyOpinion: '',
|
||||
schoolOpinion: ''
|
||||
});
|
||||
|
||||
const form = reactive<WshqtwForm>(createDefaultForm());
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.assign(form, createDefaultForm(), props.data ?? {});
|
||||
if (props.fixedYear) {
|
||||
form.year = String(props.fixedYear);
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
formRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateWshqtwForm : addWshqtwForm;
|
||||
saveOrUpdate({
|
||||
...form,
|
||||
year: props.fixedYear ? String(props.fixedYear) : form.year
|
||||
})
|
||||
.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,104 @@
|
||||
<template>
|
||||
<common-import
|
||||
v-bind="$props"
|
||||
@done="emit('done')"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
module-name="五四红旗团委"
|
||||
file-name="五四红旗团委导入模板.xlsx"
|
||||
:template-fields="templateFields"
|
||||
:sample-row="sampleRow"
|
||||
:create-payload="createPayload"
|
||||
:save-item="saveItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CommonImport from '../../components/common-import.vue';
|
||||
import { addWshqtwForm } from '@/api/gxmu/wshqtw-form';
|
||||
import type { WshqtwForm } from '@/api/gxmu/wshqtw-form/model';
|
||||
|
||||
defineProps<{ visible: boolean }>();
|
||||
const emit = defineEmits(['done', 'update:visible']);
|
||||
|
||||
const templateFields = [
|
||||
{ header: '年份', key: 'year', required: true },
|
||||
{ header: '二级团组织全称', key: 'orgName', required: true },
|
||||
{ header: '负责人', key: 'leader' },
|
||||
{ header: '联系电话', key: 'phone' },
|
||||
{ header: '现有团员总数', key: 'memberTotal' },
|
||||
{ header: '上一年度发展团员人数', key: 'memberDeveloped2024' },
|
||||
{ header: '是否已登录智慧团建系统', key: 'smartSystemLogin' },
|
||||
{ header: '团委委员人数', key: 'committeeCount' },
|
||||
{ header: '专职团干部数', key: 'fulltimeCadreCount' },
|
||||
{ header: '兼职团干部数', key: 'parttimeCadreCount' },
|
||||
{ header: '团委最近一次换届时间', key: 'lastElectionTime' },
|
||||
{ header: '上一年度应收团费', key: 'feeReceivable2024' },
|
||||
{ header: '上一年度实收团费', key: 'feeReceived2024' },
|
||||
{ header: '上一年度应上缴团费', key: 'feePayable2024' },
|
||||
{ header: '上一年度实际上缴团费', key: 'feePaid2024' },
|
||||
{ header: '团支部数', key: 'branchCount' },
|
||||
{ header: '是否开展规范化建设工作', key: 'standardizedWork2024' },
|
||||
{ header: '推荐入党积极分子人数', key: 'recommendActivist2024' },
|
||||
{ header: '确定为入党积极分子数', key: 'activistConfirmed' },
|
||||
{ header: '推荐党的发展对象人数', key: 'recommendDevTarget2024' },
|
||||
{ header: '确定为党的发展对象数', key: 'devTargetConfirmed' },
|
||||
{ header: '近五年获得校级及以上荣誉情况', key: 'honorsFiveYears' },
|
||||
{ header: '近三年开展的主要工作及取得的效果', key: 'workSummaryThreeYears' }
|
||||
] as const;
|
||||
|
||||
const sampleRow = {
|
||||
年份: '2025',
|
||||
二级团组织全称: '第一临床医学院团委',
|
||||
负责人: '王老师',
|
||||
联系电话: '13800138000',
|
||||
现有团员总数: '560',
|
||||
上一年度发展团员人数: '120',
|
||||
是否已登录智慧团建系统: '是',
|
||||
团委委员人数: '9',
|
||||
专职团干部数: '2',
|
||||
兼职团干部数: '8',
|
||||
团委最近一次换届时间: '2024-06-01',
|
||||
上一年度应收团费: '5000',
|
||||
上一年度实收团费: '4800',
|
||||
上一年度应上缴团费: '3000',
|
||||
上一年度实际上缴团费: '3000',
|
||||
团支部数: '24',
|
||||
是否开展规范化建设工作: '是',
|
||||
推荐入党积极分子人数: '30',
|
||||
确定为入党积极分子数: '28',
|
||||
推荐党的发展对象人数: '12',
|
||||
确定为党的发展对象数: '10',
|
||||
近五年获得校级及以上荣誉情况: '校五四红旗团委',
|
||||
近三年开展的主要工作及取得的效果: '组织青年志愿活动成效显著'
|
||||
};
|
||||
|
||||
const createPayload = (row: Record<string, any>): WshqtwForm => ({
|
||||
year: String(row['年份'] || '').trim(),
|
||||
orgName: String(row['二级团组织全称'] || '').trim(),
|
||||
leader: String(row['负责人'] || '').trim(),
|
||||
phone: String(row['联系电话'] || '').trim(),
|
||||
memberTotal: String(row['现有团员总数'] || '').trim(),
|
||||
memberDeveloped2024: String(row['上一年度发展团员人数'] || '').trim(),
|
||||
smartSystemLogin: String(row['是否已登录智慧团建系统'] || '').trim(),
|
||||
committeeCount: String(row['团委委员人数'] || '').trim(),
|
||||
fulltimeCadreCount: String(row['专职团干部数'] || '').trim(),
|
||||
parttimeCadreCount: String(row['兼职团干部数'] || '').trim(),
|
||||
lastElectionTime: String(row['团委最近一次换届时间'] || '').trim(),
|
||||
feeReceivable2024: String(row['上一年度应收团费'] || '').trim(),
|
||||
feeReceived2024: String(row['上一年度实收团费'] || '').trim(),
|
||||
feePayable2024: String(row['上一年度应上缴团费'] || '').trim(),
|
||||
feePaid2024: String(row['上一年度实际上缴团费'] || '').trim(),
|
||||
branchCount: String(row['团支部数'] || '').trim(),
|
||||
standardizedWork2024: String(row['是否开展规范化建设工作'] || '').trim(),
|
||||
recommendActivist2024: String(row['推荐入党积极分子人数'] || '').trim(),
|
||||
activistConfirmed: String(row['确定为入党积极分子数'] || '').trim(),
|
||||
recommendDevTarget2024: String(row['推荐党的发展对象人数'] || '').trim(),
|
||||
devTargetConfirmed: String(row['确定为党的发展对象数'] || '').trim(),
|
||||
honorsFiveYears: String(row['近五年获得校级及以上荣誉情况'] || '').trim(),
|
||||
workSummaryThreeYears: String(
|
||||
row['近三年开展的主要工作及取得的效果'] || ''
|
||||
).trim()
|
||||
});
|
||||
|
||||
const saveItem = (item: WshqtwForm) => addWshqtwForm(item);
|
||||
</script>
|
||||
@@ -0,0 +1,447 @@
|
||||
<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: 3200 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>{{ props.popup ? '立即申报' : '添加' }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
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-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'currentReviewStatus'">
|
||||
<ReviewStatusTag :review-list="record.reviewList" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<template v-if="!shouldHideEditByReviewList(record.reviewList)">
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
</template>
|
||||
<a @click="openReviewHistory(record)">审核历史</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:fixed-year="props.fixedYear"
|
||||
@done="reload"
|
||||
/>
|
||||
<import-modal v-model:visible="showImport" @done="reload" />
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="REVIEW_MODULE"
|
||||
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 { 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 ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import { shouldHideEditByReviewList } from '../utils/review';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
exportWshqtwForm,
|
||||
userPageWshqtwForm,
|
||||
removeWshqtwForm
|
||||
} from '@/api/gxmu/wshqtw-form';
|
||||
import type {
|
||||
WshqtwForm,
|
||||
WshqtwFormParam
|
||||
} from '@/api/gxmu/wshqtw-form/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
popup?: boolean;
|
||||
fixedYear?: string | number;
|
||||
}>(),
|
||||
{
|
||||
popup: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showImport = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const current = ref<WshqtwForm>();
|
||||
const detailCurrent = ref<WshqtwForm | null>(null);
|
||||
const REVIEW_MODULE = 'gxmu_wshqtw';
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<WshqtwForm | null>(null);
|
||||
const exporting = ref(false);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '二级团组织全称',
|
||||
dataIndex: 'orgName',
|
||||
key: 'orgName',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'leader',
|
||||
key: 'leader',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '现有团员总数',
|
||||
dataIndex: 'memberTotal',
|
||||
key: 'memberTotal',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '上一年度发展团员人数',
|
||||
dataIndex: 'memberDeveloped2024',
|
||||
key: 'memberDeveloped2024',
|
||||
align: 'center',
|
||||
width: 170
|
||||
},
|
||||
{
|
||||
title: '本级是否已登录“智慧团建”系统',
|
||||
dataIndex: 'smartSystemLogin',
|
||||
key: 'smartSystemLogin',
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '团委委员人数',
|
||||
dataIndex: 'committeeCount',
|
||||
key: 'committeeCount',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '专职团干部数(其中教师/学生)',
|
||||
dataIndex: 'fulltimeCadreCount',
|
||||
key: 'fulltimeCadreCount',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '兼职团干部数(其中教师/学生)',
|
||||
dataIndex: 'parttimeCadreCount',
|
||||
key: 'parttimeCadreCount',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '团委最近一次换届时间',
|
||||
dataIndex: 'lastElectionTime',
|
||||
key: 'lastElectionTime',
|
||||
align: 'center',
|
||||
width: 170
|
||||
},
|
||||
{
|
||||
title: '上一年度应收团费',
|
||||
dataIndex: 'feeReceivable2024',
|
||||
key: 'feeReceivable2024',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '上一年度实收团费',
|
||||
dataIndex: 'feeReceived2024',
|
||||
key: 'feeReceived2024',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '上一年度应上缴团费',
|
||||
dataIndex: 'feePayable2024',
|
||||
key: 'feePayable2024',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '上一年度实际上缴团费',
|
||||
dataIndex: 'feePaid2024',
|
||||
key: 'feePaid2024',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '团支部数',
|
||||
dataIndex: 'branchCount',
|
||||
key: 'branchCount',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '上一年度是否开展基层团组织规范化建设工作',
|
||||
dataIndex: 'standardizedWork2024',
|
||||
key: 'standardizedWork2024',
|
||||
align: 'center',
|
||||
width: 240
|
||||
},
|
||||
{
|
||||
title: '上一年度推荐优秀团员作为入党积极分子人数',
|
||||
dataIndex: 'recommendActivist2024',
|
||||
key: 'recommendActivist2024',
|
||||
align: 'center',
|
||||
width: 240
|
||||
},
|
||||
{
|
||||
title: '其中被党组织确定为入党积极分子数',
|
||||
dataIndex: 'activistConfirmed',
|
||||
key: 'activistConfirmed',
|
||||
align: 'center',
|
||||
width: 220
|
||||
},
|
||||
{
|
||||
title: '上一年度推荐优秀团员作为党的发展对象人数',
|
||||
dataIndex: 'recommendDevTarget2024',
|
||||
key: 'recommendDevTarget2024',
|
||||
align: 'center',
|
||||
width: 240
|
||||
},
|
||||
{
|
||||
title: '其中被党组织确定为党的发展对象数',
|
||||
dataIndex: 'devTargetConfirmed',
|
||||
key: 'devTargetConfirmed',
|
||||
align: 'center',
|
||||
width: 220
|
||||
},
|
||||
{
|
||||
title: '近五年获得校级及以上荣誉情况',
|
||||
dataIndex: 'honorsFiveYears',
|
||||
key: 'honorsFiveYears',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '近三年开展的主要工作及取得的效果',
|
||||
dataIndex: 'workSummaryThreeYears',
|
||||
key: 'workSummaryThreeYears',
|
||||
align: 'center',
|
||||
width: 260,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在单位党组织意见',
|
||||
dataIndex: 'partyOpinion',
|
||||
key: 'partyOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '校团委意见',
|
||||
dataIndex: 'schoolOpinion',
|
||||
key: 'schoolOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 220
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return userPageWshqtwForm({
|
||||
page,
|
||||
limit,
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as WshqtwFormParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const buildQueryParams = (): WshqtwFormParam => {
|
||||
return {
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as WshqtwFormParam;
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
const resolveExportUrl = (payload: any) => {
|
||||
return String(
|
||||
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 exportWshqtwForm(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 openEdit = (record?: WshqtwForm) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as WshqtwForm)
|
||||
: undefined;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openReviewHistory = (record: WshqtwForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (record: WshqtwForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: WshqtwForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeWshqtwForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'WSHQTWForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
@@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="1000"
|
||||
: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="24">
|
||||
<a-form-item
|
||||
label="年份"
|
||||
name="year"
|
||||
:rules="[{ required: true, message: '请选择年份' }]"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:value="form.year"
|
||||
picker="year"
|
||||
value-format="YYYY"
|
||||
style="width: 100%"
|
||||
:disabled="!!props.fixedYear"
|
||||
placeholder="请选择年份"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="团支部全称"
|
||||
name="branchName"
|
||||
:rules="[{ required: true, message: '请输入团支部全称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.branchName"
|
||||
placeholder="请输入团支部全称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="所属二级团组织" name="secondOrg">
|
||||
<a-input
|
||||
v-model:value="form.secondOrg"
|
||||
placeholder="请输入所属二级团组织"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="团支部书记" name="secretary">
|
||||
<a-input
|
||||
v-model:value="form.secretary"
|
||||
placeholder="请输入团支部书记"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="政治面貌" name="politics">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.politics"
|
||||
:options="politicsOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择政治面貌"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="联系方式" name="contact">
|
||||
<a-input
|
||||
v-model:value="form.contact"
|
||||
placeholder="请输入联系方式"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="成立时间" name="establishTime">
|
||||
<a-date-picker
|
||||
v-model:value="form.establishTime"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
placeholder="请选择成立时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="最近一次换届时间" name="lastElectionTime">
|
||||
<a-date-picker
|
||||
v-model:value="form.lastElectionTime"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
placeholder="请选择最近一次换届时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="是否已登录智慧团建系统" name="smartSystemLogin">
|
||||
<a-select
|
||||
v-model:value="form.smartSystemLogin"
|
||||
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="24">
|
||||
<a-form-item label="现有团员总数" name="memberTotal">
|
||||
<a-input
|
||||
v-model:value="form.memberTotal"
|
||||
placeholder="请输入现有团员总数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度发展团员数" name="memberDeveloped2024">
|
||||
<a-input
|
||||
v-model:value="form.memberDeveloped2024"
|
||||
placeholder="请输入上一年度发展团员数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度应收团费" name="feeReceivable2024">
|
||||
<a-input
|
||||
v-model:value="form.feeReceivable2024"
|
||||
placeholder="请输入上一年度应收团费"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度实收团费" name="feeReceived2024">
|
||||
<a-input
|
||||
v-model:value="form.feeReceived2024"
|
||||
placeholder="请输入上一年度实收团费"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度应上缴团费" name="feePayable2024">
|
||||
<a-input
|
||||
v-model:value="form.feePayable2024"
|
||||
placeholder="请输入上一年度应上缴团费"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度实际上缴团费" name="feePaid2024">
|
||||
<a-input
|
||||
v-model:value="form.feePaid2024"
|
||||
placeholder="请输入上一年度实际上缴团费"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="推荐入党积极分子人数"
|
||||
name="recommendActivist2024"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.recommendActivist2024"
|
||||
placeholder="请输入推荐入党积极分子人数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="确定为入党积极分子数" name="activistConfirmed">
|
||||
<a-input
|
||||
v-model:value="form.activistConfirmed"
|
||||
placeholder="请输入确定为入党积极分子数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="推荐党的发展对象人数"
|
||||
name="recommendDevTarget2024"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.recommendDevTarget2024"
|
||||
placeholder="请输入推荐党的发展对象人数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="确定为党的发展对象数" name="devTargetConfirmed">
|
||||
<a-input
|
||||
v-model:value="form.devTargetConfirmed"
|
||||
placeholder="请输入确定为党的发展对象数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="团支部委员会会议召开次数"
|
||||
name="branchCommitteeMeetingCount"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.branchCommitteeMeetingCount"
|
||||
placeholder="请输入团支部委员会会议召开次数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="团支部团员大会召开次数"
|
||||
name="branchMemberMeetingCount"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.branchMemberMeetingCount"
|
||||
placeholder="请输入团支部团员大会召开次数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="是否开展团员教育评议" name="eduEvalDone">
|
||||
<a-select v-model:value="form.eduEvalDone" 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="24">
|
||||
<a-form-item label="是否开展团员年度团籍注册" name="annualRegDone">
|
||||
<a-select v-model:value="form.annualRegDone" 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="24">
|
||||
<a-form-item label="开展团课次数" name="classCount">
|
||||
<a-input
|
||||
v-model:value="form.classCount"
|
||||
placeholder="请输入开展团课次数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="是否100%录入智慧团建" name="smartSystem100">
|
||||
<a-select v-model:value="form.smartSystem100" 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="24">
|
||||
<a-form-item
|
||||
label="近五年获得院级及以上荣誉情况"
|
||||
name="honorsFiveYears"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="form.honorsFiveYears"
|
||||
:rows="3"
|
||||
placeholder="请输入近五年获得院级及以上荣誉情况"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="近三年开展的主要工作及取得的效果"
|
||||
name="workSummaryThreeYears"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="form.workSummaryThreeYears"
|
||||
:rows="3"
|
||||
placeholder="请输入近三年开展的主要工作及取得的效果"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="所在二级团组织意见" name="leagueOpinion">-->
|
||||
<!-- <a-textarea v-model:value="form.leagueOpinion" :rows="2" placeholder="请输入所在二级团组织意见" />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="所在单位党组织意见" name="partyOpinion">-->
|
||||
<!-- <a-textarea v-model:value="form.partyOpinion" :rows="2" placeholder="请输入所在单位党组织意见" />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="校团委意见" name="schoolOpinion">-->
|
||||
<!-- <a-textarea v-model:value="form.schoolOpinion" :rows="2" 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 { addWshqtzbForm, updateWshqtzbForm } from '@/api/gxmu/wshqtzb-form';
|
||||
import type { WshqtzbForm } from '@/api/gxmu/wshqtzb-form/model';
|
||||
import {
|
||||
filterSelectOption,
|
||||
useGxmuFormSelectOptions
|
||||
} from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: WshqtzbForm | null;
|
||||
fixedYear?: string | number;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const { politicsOptions, loadPoliticsOptions } = useGxmuFormSelectOptions();
|
||||
|
||||
const createDefaultForm = (): WshqtzbForm => ({
|
||||
id: undefined,
|
||||
year: '',
|
||||
branchName: '',
|
||||
secondOrg: '',
|
||||
secretary: '',
|
||||
politics: '',
|
||||
contact: '',
|
||||
establishTime: '',
|
||||
lastElectionTime: '',
|
||||
smartSystemLogin: '',
|
||||
memberTotal: '',
|
||||
memberDeveloped2024: '',
|
||||
feeReceivable2024: '',
|
||||
feeReceived2024: '',
|
||||
feePayable2024: '',
|
||||
feePaid2024: '',
|
||||
recommendActivist2024: '',
|
||||
activistConfirmed: '',
|
||||
recommendDevTarget2024: '',
|
||||
devTargetConfirmed: '',
|
||||
branchCommitteeMeetingCount: '',
|
||||
branchMemberMeetingCount: '',
|
||||
eduEvalDone: '',
|
||||
annualRegDone: '',
|
||||
classCount: '',
|
||||
smartSystem100: '',
|
||||
honorsFiveYears: '',
|
||||
workSummaryThreeYears: '',
|
||||
leagueOpinion: '',
|
||||
partyOpinion: '',
|
||||
schoolOpinion: ''
|
||||
});
|
||||
|
||||
const form = reactive<WshqtzbForm>(createDefaultForm());
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
try {
|
||||
await loadPoliticsOptions();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '下拉选项加载失败');
|
||||
}
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.assign(form, createDefaultForm(), props.data ?? {});
|
||||
if (props.fixedYear) {
|
||||
form.year = String(props.fixedYear);
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
formRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateWshqtzbForm
|
||||
: addWshqtzbForm;
|
||||
saveOrUpdate({
|
||||
...form,
|
||||
year: props.fixedYear ? String(props.fixedYear) : form.year
|
||||
})
|
||||
.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,116 @@
|
||||
<template>
|
||||
<common-import
|
||||
v-bind="$props"
|
||||
@done="emit('done')"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
module-name="五四红旗团支部"
|
||||
file-name="五四红旗团支部导入模板.xlsx"
|
||||
:template-fields="templateFields"
|
||||
:sample-row="sampleRow"
|
||||
:create-payload="createPayload"
|
||||
:save-item="saveItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CommonImport from '../../components/common-import.vue';
|
||||
import { addWshqtzbForm } from '@/api/gxmu/wshqtzb-form';
|
||||
import type { WshqtzbForm } from '@/api/gxmu/wshqtzb-form/model';
|
||||
|
||||
defineProps<{ visible: boolean }>();
|
||||
const emit = defineEmits(['done', 'update:visible']);
|
||||
|
||||
const templateFields = [
|
||||
{ header: '年份', key: 'year', required: true },
|
||||
{ header: '团支部全称', key: 'branchName', required: true },
|
||||
{ header: '所属二级党组织', key: 'secondOrg' },
|
||||
{ header: '团支部书记', key: 'secretary' },
|
||||
{ header: '政治面貌', key: 'politics' },
|
||||
{ header: '联系方式', key: 'contact' },
|
||||
{ header: '成立时间', key: 'establishTime' },
|
||||
{ header: '最近一次换届时间', key: 'lastElectionTime' },
|
||||
{ header: '智慧团建系统登录情况', key: 'smartSystemLogin' },
|
||||
{ header: '现有团员总数', key: 'memberTotal' },
|
||||
{ header: '上一年度发展团员人数', key: 'memberDeveloped2024' },
|
||||
{ header: '上一年度应收团费', key: 'feeReceivable2024' },
|
||||
{ header: '上一年度实收团费', key: 'feeReceived2024' },
|
||||
{ header: '上一年度应上缴团费', key: 'feePayable2024' },
|
||||
{ header: '上一年度实际上缴团费', key: 'feePaid2024' },
|
||||
{ header: '推荐入党积极分子人数', key: 'recommendActivist2024' },
|
||||
{ header: '确定为入党积极分子数', key: 'activistConfirmed' },
|
||||
{ header: '推荐党的发展对象人数', key: 'recommendDevTarget2024' },
|
||||
{ header: '确定为党的发展对象数', key: 'devTargetConfirmed' },
|
||||
{ header: '支委会次数', key: 'branchCommitteeMeetingCount' },
|
||||
{ header: '支部大会次数', key: 'branchMemberMeetingCount' },
|
||||
{ header: '教育评议是否完成', key: 'eduEvalDone' },
|
||||
{ header: '年度团籍注册是否完成', key: 'annualRegDone' },
|
||||
{ header: '班级数', key: 'classCount' },
|
||||
{ header: '智慧团建系统覆盖率', key: 'smartSystem100' },
|
||||
{ header: '近五年获得校级及以上荣誉情况', key: 'honorsFiveYears' },
|
||||
{ header: '近三年开展的主要工作及取得的效果', key: 'workSummaryThreeYears' }
|
||||
] as const;
|
||||
|
||||
const sampleRow = {
|
||||
年份: '2025',
|
||||
团支部全称: '护理学院2022级1班团支部',
|
||||
所属二级党组织: '护理学院党委',
|
||||
团支部书记: '张同学',
|
||||
政治面貌: '共青团员',
|
||||
联系方式: '13800138000',
|
||||
成立时间: '2022-09-01',
|
||||
最近一次换届时间: '2024-09-01',
|
||||
智慧团建系统登录情况: '是',
|
||||
现有团员总数: '38',
|
||||
上一年度发展团员人数: '12',
|
||||
上一年度应收团费: '380',
|
||||
上一年度实收团费: '380',
|
||||
上一年度应上缴团费: '200',
|
||||
上一年度实际上缴团费: '200',
|
||||
推荐入党积极分子人数: '8',
|
||||
确定为入党积极分子数: '7',
|
||||
推荐党的发展对象人数: '3',
|
||||
确定为党的发展对象数: '2',
|
||||
支委会次数: '12',
|
||||
支部大会次数: '8',
|
||||
教育评议是否完成: '是',
|
||||
年度团籍注册是否完成: '是',
|
||||
班级数: '1',
|
||||
智慧团建系统覆盖率: '100%',
|
||||
近五年获得校级及以上荣誉情况: '校五四红旗团支部',
|
||||
近三年开展的主要工作及取得的效果: '主题团日活动开展扎实'
|
||||
};
|
||||
|
||||
const createPayload = (row: Record<string, any>): WshqtzbForm => ({
|
||||
year: String(row['年份'] || '').trim(),
|
||||
branchName: String(row['团支部全称'] || '').trim(),
|
||||
secondOrg: String(row['所属二级党组织'] || '').trim(),
|
||||
secretary: String(row['团支部书记'] || '').trim(),
|
||||
politics: String(row['政治面貌'] || '').trim(),
|
||||
contact: String(row['联系方式'] || '').trim(),
|
||||
establishTime: String(row['成立时间'] || '').trim(),
|
||||
lastElectionTime: String(row['最近一次换届时间'] || '').trim(),
|
||||
smartSystemLogin: String(row['智慧团建系统登录情况'] || '').trim(),
|
||||
memberTotal: String(row['现有团员总数'] || '').trim(),
|
||||
memberDeveloped2024: String(row['上一年度发展团员人数'] || '').trim(),
|
||||
feeReceivable2024: String(row['上一年度应收团费'] || '').trim(),
|
||||
feeReceived2024: String(row['上一年度实收团费'] || '').trim(),
|
||||
feePayable2024: String(row['上一年度应上缴团费'] || '').trim(),
|
||||
feePaid2024: String(row['上一年度实际上缴团费'] || '').trim(),
|
||||
recommendActivist2024: String(row['推荐入党积极分子人数'] || '').trim(),
|
||||
activistConfirmed: String(row['确定为入党积极分子数'] || '').trim(),
|
||||
recommendDevTarget2024: String(row['推荐党的发展对象人数'] || '').trim(),
|
||||
devTargetConfirmed: String(row['确定为党的发展对象数'] || '').trim(),
|
||||
branchCommitteeMeetingCount: String(row['支委会次数'] || '').trim(),
|
||||
branchMemberMeetingCount: String(row['支部大会次数'] || '').trim(),
|
||||
eduEvalDone: String(row['教育评议是否完成'] || '').trim(),
|
||||
annualRegDone: String(row['年度团籍注册是否完成'] || '').trim(),
|
||||
classCount: String(row['班级数'] || '').trim(),
|
||||
smartSystem100: String(row['智慧团建系统覆盖率'] || '').trim(),
|
||||
honorsFiveYears: String(row['近五年获得校级及以上荣誉情况'] || '').trim(),
|
||||
workSummaryThreeYears: String(
|
||||
row['近三年开展的主要工作及取得的效果'] || ''
|
||||
).trim()
|
||||
});
|
||||
|
||||
const saveItem = (item: WshqtzbForm) => addWshqtzbForm(item);
|
||||
</script>
|
||||
@@ -0,0 +1,481 @@
|
||||
<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: 3600 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>{{ props.popup ? '立即申报' : '添加' }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
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-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'currentReviewStatus'">
|
||||
<ReviewStatusTag :review-list="record.reviewList" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<template v-if="!shouldHideEditByReviewList(record.reviewList)">
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
</template>
|
||||
<a @click="openReviewHistory(record)">审核历史</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:fixed-year="props.fixedYear"
|
||||
@done="reload"
|
||||
/>
|
||||
<import-modal v-model:visible="showImport" @done="reload" />
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="REVIEW_MODULE"
|
||||
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 { 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 ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import { shouldHideEditByReviewList } from '../utils/review';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
exportWshqtzbForm,
|
||||
userPageWshqtzbForm,
|
||||
removeWshqtzbForm
|
||||
} from '@/api/gxmu/wshqtzb-form';
|
||||
import type {
|
||||
WshqtzbForm,
|
||||
WshqtzbFormParam
|
||||
} from '@/api/gxmu/wshqtzb-form/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
popup?: boolean;
|
||||
fixedYear?: string | number;
|
||||
}>(),
|
||||
{
|
||||
popup: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showImport = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const current = ref<WshqtzbForm>();
|
||||
const detailCurrent = ref<WshqtzbForm | null>(null);
|
||||
const REVIEW_MODULE = 'gxmu_wshqtzb';
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<WshqtzbForm | null>(null);
|
||||
const exporting = ref(false);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '团支部全称',
|
||||
dataIndex: 'branchName',
|
||||
key: 'branchName',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '所属二级团组织',
|
||||
dataIndex: 'secondOrg',
|
||||
key: 'secondOrg',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '团支部书记',
|
||||
dataIndex: 'secretary',
|
||||
key: 'secretary',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '政治面貌',
|
||||
dataIndex: 'politics',
|
||||
key: 'politics',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '联系方式',
|
||||
dataIndex: 'contact',
|
||||
key: 'contact',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '成立时间',
|
||||
dataIndex: 'establishTime',
|
||||
key: 'establishTime',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '最近一次换届时间',
|
||||
dataIndex: 'lastElectionTime',
|
||||
key: 'lastElectionTime',
|
||||
align: 'center',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '本级是否已登录“智慧团建”系统',
|
||||
dataIndex: 'smartSystemLogin',
|
||||
key: 'smartSystemLogin',
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '现有团员总数',
|
||||
dataIndex: 'memberTotal',
|
||||
key: 'memberTotal',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '上一年度发展团员数',
|
||||
dataIndex: 'memberDeveloped2024',
|
||||
key: 'memberDeveloped2024',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '上一年度应收团费',
|
||||
dataIndex: 'feeReceivable2024',
|
||||
key: 'feeReceivable2024',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '上一年度实收团费',
|
||||
dataIndex: 'feeReceived2024',
|
||||
key: 'feeReceived2024',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '上一年度应上缴团费',
|
||||
dataIndex: 'feePayable2024',
|
||||
key: 'feePayable2024',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '上一年度实际上缴团费',
|
||||
dataIndex: 'feePaid2024',
|
||||
key: 'feePaid2024',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '推荐优秀团员作入党积极分子人数(上一年度)',
|
||||
dataIndex: 'recommendActivist2024',
|
||||
key: 'recommendActivist2024',
|
||||
align: 'center',
|
||||
width: 240
|
||||
},
|
||||
{
|
||||
title: '其中被党组织确定为入党积极分子数',
|
||||
dataIndex: 'activistConfirmed',
|
||||
key: 'activistConfirmed',
|
||||
align: 'center',
|
||||
width: 220
|
||||
},
|
||||
{
|
||||
title: '推荐优秀团员作党的发展对象人数(上一年度)',
|
||||
dataIndex: 'recommendDevTarget2024',
|
||||
key: 'recommendDevTarget2024',
|
||||
align: 'center',
|
||||
width: 240
|
||||
},
|
||||
{
|
||||
title: '其中被党组织确定为党的发展对象数',
|
||||
dataIndex: 'devTargetConfirmed',
|
||||
key: 'devTargetConfirmed',
|
||||
align: 'center',
|
||||
width: 220
|
||||
},
|
||||
{
|
||||
title: '团支部委员会会议召开次数',
|
||||
dataIndex: 'branchCommitteeMeetingCount',
|
||||
key: 'branchCommitteeMeetingCount',
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '团支部团员大会召开次数',
|
||||
dataIndex: 'branchMemberMeetingCount',
|
||||
key: 'branchMemberMeetingCount',
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '是否开展团员教育评议',
|
||||
dataIndex: 'eduEvalDone',
|
||||
key: 'eduEvalDone',
|
||||
align: 'center',
|
||||
width: 170
|
||||
},
|
||||
{
|
||||
title: '是否开展团员年度团籍注册',
|
||||
dataIndex: 'annualRegDone',
|
||||
key: 'annualRegDone',
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '开展团课次数',
|
||||
dataIndex: 'classCount',
|
||||
key: 'classCount',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '是否100%录入“智慧团建”',
|
||||
dataIndex: 'smartSystem100',
|
||||
key: 'smartSystem100',
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '近五年获得院级及以上荣誉情况',
|
||||
dataIndex: 'honorsFiveYears',
|
||||
key: 'honorsFiveYears',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '近三年开展的主要工作及取得的效果',
|
||||
dataIndex: 'workSummaryThreeYears',
|
||||
key: 'workSummaryThreeYears',
|
||||
align: 'center',
|
||||
width: 260,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在二级团组织意见',
|
||||
dataIndex: 'leagueOpinion',
|
||||
key: 'leagueOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在单位党组织意见',
|
||||
dataIndex: 'partyOpinion',
|
||||
key: 'partyOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '校团委意见',
|
||||
dataIndex: 'schoolOpinion',
|
||||
key: 'schoolOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 220
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return userPageWshqtzbForm({
|
||||
page,
|
||||
limit,
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as WshqtzbFormParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const buildQueryParams = (): WshqtzbFormParam => {
|
||||
return {
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as WshqtzbFormParam;
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
const resolveExportUrl = (payload: any) => {
|
||||
return String(
|
||||
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 exportWshqtzbForm(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 openEdit = (record?: WshqtzbForm) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as WshqtzbForm)
|
||||
: undefined;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openReviewHistory = (record: WshqtzbForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (record: WshqtzbForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: WshqtzbForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeWshqtzbForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'WSHQTZBForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
@@ -0,0 +1,706 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="960"
|
||||
:footer="null"
|
||||
title="项目申报书文件校验"
|
||||
:visible="visible"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="validator-layout">
|
||||
<div class="upload-panel">
|
||||
<div class="validator-intro">
|
||||
上传项目申报书后,系统将调用项目书质量初审模型,从创新性、可行性、应用前景、文本规范性等维度生成初审报告。
|
||||
</div>
|
||||
|
||||
<div class="upload-label">项目申报书</div>
|
||||
<a-upload-dragger
|
||||
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
:file-list="projectFileList"
|
||||
:before-upload="beforeProjectUpload"
|
||||
:max-count="1"
|
||||
@remove="removeProjectFile"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<cloud-upload-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或拖拽上传待审项目申报书</p>
|
||||
<p class="ant-upload-hint">支持 PDF、DOC、DOCX,大小不超过 50MB</p>
|
||||
</a-upload-dragger>
|
||||
|
||||
<div class="upload-label optional-label">自定义评分细则</div>
|
||||
<a-upload
|
||||
accept=".pdf,.doc,.docx,.csv,.xls,.xlsx,.txt,.md"
|
||||
:file-list="rubricFileList"
|
||||
:before-upload="beforeRubricUpload"
|
||||
:max-count="1"
|
||||
@remove="removeRubricFile"
|
||||
>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<upload-outlined />
|
||||
</template>
|
||||
上传评分细则
|
||||
</a-button>
|
||||
</a-upload>
|
||||
|
||||
<div class="action-row">
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!projectFile || loading"
|
||||
@click="startReview"
|
||||
>
|
||||
开始校验
|
||||
</a-button>
|
||||
<a-button :disabled="!loading" @click="stopReview">停止</a-button>
|
||||
<a-button :disabled="loading && !reportMarkdown" @click="clearResult">
|
||||
清空
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-panel">
|
||||
<div class="result-toolbar">
|
||||
<div>
|
||||
<div class="result-title">AI 初审报告</div>
|
||||
<div class="result-subtitle">
|
||||
{{ statusText || '报告生成后将在此展示' }}
|
||||
</div>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-tag :color="loading ? 'processing' : reportMarkdown ? 'success' : 'default'">
|
||||
{{ loading ? '生成中' : reportMarkdown ? '已完成' : '待开始' }}
|
||||
</a-tag>
|
||||
<a-button size="small" :disabled="!reportMarkdown" @click="exportReport">
|
||||
导出
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="!reportMarkdown"
|
||||
@click="fullscreenVisible = true"
|
||||
>
|
||||
全屏查看
|
||||
</a-button>
|
||||
<a-button size="small" :disabled="!reportMarkdown" @click="copyReport">
|
||||
复制
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div ref="reportShellRef" class="report-shell">
|
||||
<div v-if="loading && !reportMarkdown" class="report-streaming">
|
||||
AI 正在分析项目申报书...
|
||||
</div>
|
||||
<ByteMdViewer
|
||||
v-else-if="reportMarkdown"
|
||||
class="report-viewer"
|
||||
:value="reportMarkdown"
|
||||
:plugins="plugins"
|
||||
/>
|
||||
<a-empty v-else description="请先上传项目申报书并开始校验" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</ele-modal>
|
||||
|
||||
<ele-modal
|
||||
:width="'92vw'"
|
||||
:footer="null"
|
||||
title="AI 初审报告"
|
||||
:visible="fullscreenVisible"
|
||||
@update:visible="fullscreenVisible = $event"
|
||||
>
|
||||
<div class="fullscreen-toolbar">
|
||||
<a-space>
|
||||
<a-button :disabled="!reportMarkdown" @click="exportReport">
|
||||
导出
|
||||
</a-button>
|
||||
<a-button :disabled="!reportMarkdown" @click="copyReport">
|
||||
复制
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="fullscreen-report-shell">
|
||||
<ByteMdViewer
|
||||
v-if="reportMarkdown"
|
||||
class="report-viewer"
|
||||
:value="reportMarkdown"
|
||||
:plugins="plugins"
|
||||
/>
|
||||
<a-empty v-else description="暂无初审报告" />
|
||||
</div>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
import gfm from '@bytemd/plugin-gfm';
|
||||
import zhHansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
|
||||
import highlight from '@bytemd/plugin-highlight-ssr';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { computed, ref, watch, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { UploadProps } from 'ant-design-vue';
|
||||
import { CloudUploadOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||
import ByteMdViewer from '@/components/ByteMdViewer/index.vue';
|
||||
import { reviewWxxzxProjectBookQuality } from '@/api/gxmu/wxxzx-form';
|
||||
import { stopMessage } from '@/api/oa/chatgpt';
|
||||
import { API_BASE_URL } from '@/config/setting';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { copyText } from '@/utils/common';
|
||||
import type { WxxzxProjectQualityReviewResult } from '@/api/gxmu/wxxzx-form/model';
|
||||
|
||||
const STREAM_END_TEXT = '__END__';
|
||||
const SOCKET_CONNECTED_TEXT = '连接成功';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const loginUser = computed(() => useUserStore().info ?? {});
|
||||
const plugins = ref([
|
||||
gfm({
|
||||
locale: zhHansGfm
|
||||
}),
|
||||
highlight()
|
||||
]);
|
||||
const loading = ref(false);
|
||||
const statusText = ref('');
|
||||
const projectFile = ref<File>();
|
||||
const rubricFile = ref<File>();
|
||||
const reportMarkdown = ref('');
|
||||
const activeTaskId = ref<string>();
|
||||
const streamStarted = ref(false);
|
||||
const fullscreenVisible = ref(false);
|
||||
const reportShellRef = ref<HTMLElement | null>(null);
|
||||
const webSocketRef = ref<WebSocket | null>(null);
|
||||
const wsConnectPromiseRef = ref<Promise<void> | null>(null);
|
||||
const streamResolverRef = ref<{
|
||||
resolve: () => void;
|
||||
reject: (error: Error) => void;
|
||||
} | null>(null);
|
||||
|
||||
const projectFileList = computed(() =>
|
||||
projectFile.value
|
||||
? [
|
||||
{
|
||||
uid: 'project',
|
||||
name: projectFile.value.name,
|
||||
status: 'done' as const
|
||||
}
|
||||
]
|
||||
: []
|
||||
);
|
||||
const rubricFileList = computed(() =>
|
||||
rubricFile.value
|
||||
? [
|
||||
{
|
||||
uid: 'rubric',
|
||||
name: rubricFile.value.name,
|
||||
status: 'done' as const
|
||||
}
|
||||
]
|
||||
: []
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const beforeProjectUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
if (file.size / 1024 / 1024 > 50) {
|
||||
message.error('项目申报书不能超过 50MB');
|
||||
return false;
|
||||
}
|
||||
projectFile.value = file as File;
|
||||
return false;
|
||||
};
|
||||
|
||||
const beforeRubricUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
if (file.size / 1024 / 1024 > 20) {
|
||||
message.error('评分细则不能超过 20MB');
|
||||
return false;
|
||||
}
|
||||
rubricFile.value = file as File;
|
||||
return false;
|
||||
};
|
||||
|
||||
const removeProjectFile = () => {
|
||||
projectFile.value = undefined;
|
||||
return true;
|
||||
};
|
||||
|
||||
const removeRubricFile = () => {
|
||||
rubricFile.value = undefined;
|
||||
return true;
|
||||
};
|
||||
|
||||
const scrollReportToBottom = () => {
|
||||
nextTick(() => {
|
||||
const el = reportShellRef.value;
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const buildWebSocketUrl = (userId: number) => {
|
||||
if (/^https?:\/\//.test(API_BASE_URL)) {
|
||||
const url = new URL(API_BASE_URL);
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
url.pathname = url.pathname.replace(/\/api\/?$/, '');
|
||||
return `${url.toString().replace(/\/$/, '')}/chat/${userId}`;
|
||||
}
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return `${protocol}//${window.location.host}/chat/${userId}`;
|
||||
};
|
||||
|
||||
const clearStreamResolver = () => {
|
||||
streamResolverRef.value = null;
|
||||
};
|
||||
|
||||
const resolveStream = () => {
|
||||
streamResolverRef.value?.resolve();
|
||||
clearStreamResolver();
|
||||
};
|
||||
|
||||
const rejectStream = (error: Error) => {
|
||||
streamResolverRef.value?.reject(error);
|
||||
clearStreamResolver();
|
||||
};
|
||||
|
||||
const createStreamWaiter = () => {
|
||||
streamStarted.value = false;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
streamResolverRef.value = { resolve, reject };
|
||||
});
|
||||
};
|
||||
|
||||
const disconnectWebSocket = () => {
|
||||
wsConnectPromiseRef.value = null;
|
||||
const socket = webSocketRef.value;
|
||||
webSocketRef.value = null;
|
||||
if (
|
||||
socket &&
|
||||
(socket.readyState === WebSocket.OPEN ||
|
||||
socket.readyState === WebSocket.CONNECTING)
|
||||
) {
|
||||
socket.close();
|
||||
}
|
||||
};
|
||||
|
||||
const pickReportText = (payload: any): string => {
|
||||
if (!payload) {
|
||||
return '';
|
||||
}
|
||||
if (typeof payload === 'string') {
|
||||
return payload;
|
||||
}
|
||||
const outputs = payload.outputs || payload.data?.outputs || payload.raw?.data?.outputs;
|
||||
const direct =
|
||||
payload.answer ||
|
||||
payload.result ||
|
||||
payload.output ||
|
||||
payload.content ||
|
||||
outputs?.answer ||
|
||||
outputs?.result ||
|
||||
outputs?.output ||
|
||||
outputs?.content ||
|
||||
outputs?.report ||
|
||||
outputs?.markdown;
|
||||
if (typeof direct === 'string') {
|
||||
return direct;
|
||||
}
|
||||
if (outputs) {
|
||||
const firstText = Object.values(outputs).find(
|
||||
(item) => typeof item === 'string' && item.trim()
|
||||
);
|
||||
if (typeof firstText === 'string') {
|
||||
return firstText;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const appendReportChunk = (chunk: string, taskId?: string) => {
|
||||
if (!chunk) {
|
||||
return;
|
||||
}
|
||||
if (taskId && taskId !== '0') {
|
||||
activeTaskId.value = taskId;
|
||||
}
|
||||
if (!streamStarted.value) {
|
||||
streamStarted.value = true;
|
||||
reportMarkdown.value = '';
|
||||
}
|
||||
reportMarkdown.value += chunk;
|
||||
statusText.value = '正在通过 WebSocket 接收流式报告';
|
||||
scrollReportToBottom();
|
||||
};
|
||||
|
||||
const finishStream = (taskId?: string) => {
|
||||
if (taskId && taskId !== '0') {
|
||||
activeTaskId.value = taskId;
|
||||
}
|
||||
loading.value = false;
|
||||
statusText.value = '项目书质量初审完成';
|
||||
scrollReportToBottom();
|
||||
resolveStream();
|
||||
disconnectWebSocket();
|
||||
};
|
||||
|
||||
const handleSocketMessage = (raw: string) => {
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
if (data?.stopType && data.stopType !== 'projectQualityReview') {
|
||||
return;
|
||||
}
|
||||
if (data?.answer === STREAM_END_TEXT) {
|
||||
finishStream(data?.taskId);
|
||||
return;
|
||||
}
|
||||
if (data?.taskId && data.taskId !== '0') {
|
||||
activeTaskId.value = data.taskId;
|
||||
}
|
||||
if (data?.loading === 'true' || data?.loading === true) {
|
||||
statusText.value = 'AI 正在检索历史案例并组织评审意见...';
|
||||
return;
|
||||
}
|
||||
if (typeof data?.answer === 'string') {
|
||||
appendReportChunk(data.answer, data?.taskId);
|
||||
}
|
||||
} catch (e) {
|
||||
appendReportChunk(raw);
|
||||
}
|
||||
};
|
||||
|
||||
const connectWebSocket = async () => {
|
||||
const userId = loginUser.value.userId;
|
||||
if (!userId) {
|
||||
return Promise.reject(new Error('未获取到当前登录用户'));
|
||||
}
|
||||
const currentSocket = webSocketRef.value;
|
||||
if (currentSocket?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(currentSocket?.readyState === WebSocket.OPEN ||
|
||||
currentSocket?.readyState === WebSocket.CONNECTING) &&
|
||||
wsConnectPromiseRef.value
|
||||
) {
|
||||
return wsConnectPromiseRef.value;
|
||||
}
|
||||
let cleanupPending = () => undefined;
|
||||
const connectPromise = new Promise<void>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const socket = new WebSocket(buildWebSocketUrl(userId));
|
||||
cleanupPending = () => {
|
||||
if (wsConnectPromiseRef.value === connectPromise) {
|
||||
wsConnectPromiseRef.value = null;
|
||||
}
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
const raw = String(event.data || '');
|
||||
if (raw === SOCKET_CONNECTED_TEXT) {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanupPending();
|
||||
resolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
handleSocketMessage(raw);
|
||||
};
|
||||
socket.onclose = () => {
|
||||
if (webSocketRef.value === socket) {
|
||||
webSocketRef.value = null;
|
||||
}
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanupPending();
|
||||
reject(new Error('项目书质量初审连接已关闭'));
|
||||
return;
|
||||
}
|
||||
if (loading.value) {
|
||||
rejectStream(new Error('项目书质量初审连接已关闭'));
|
||||
loading.value = false;
|
||||
}
|
||||
cleanupPending();
|
||||
};
|
||||
socket.onerror = () => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanupPending();
|
||||
reject(new Error('项目书质量初审连接失败'));
|
||||
return;
|
||||
}
|
||||
if (loading.value) {
|
||||
rejectStream(new Error('项目书质量初审连接失败'));
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
webSocketRef.value = socket;
|
||||
});
|
||||
wsConnectPromiseRef.value = connectPromise;
|
||||
return connectPromise;
|
||||
};
|
||||
|
||||
const clearResult = () => {
|
||||
reportMarkdown.value = '';
|
||||
statusText.value = '';
|
||||
activeTaskId.value = undefined;
|
||||
streamStarted.value = false;
|
||||
};
|
||||
|
||||
const startReview = async () => {
|
||||
if (!projectFile.value) {
|
||||
message.warning('请先上传项目申报书');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
clearResult();
|
||||
statusText.value = '正在建立 WebSocket 连接...';
|
||||
try {
|
||||
await connectWebSocket();
|
||||
statusText.value = '连接成功,已提交项目书质量初审任务...';
|
||||
const waitStream = createStreamWaiter();
|
||||
const result = (await reviewWxxzxProjectBookQuality(
|
||||
projectFile.value,
|
||||
rubricFile.value
|
||||
)) as WxxzxProjectQualityReviewResult;
|
||||
const directReport = pickReportText(result);
|
||||
if (!streamStarted.value && directReport) {
|
||||
reportMarkdown.value = directReport;
|
||||
loading.value = false;
|
||||
statusText.value = '项目书质量初审完成';
|
||||
scrollReportToBottom();
|
||||
resolveStream();
|
||||
disconnectWebSocket();
|
||||
} else {
|
||||
await waitStream;
|
||||
}
|
||||
if (!reportMarkdown.value.trim()) {
|
||||
message.warning('接口已返回,但未解析到初审报告内容');
|
||||
return;
|
||||
}
|
||||
message.success('项目书质量初审完成');
|
||||
} catch (e: any) {
|
||||
disconnectWebSocket();
|
||||
clearStreamResolver();
|
||||
loading.value = false;
|
||||
statusText.value = '';
|
||||
message.error(e.message || '项目书质量初审失败');
|
||||
}
|
||||
};
|
||||
|
||||
const stopReview = async () => {
|
||||
try {
|
||||
await stopMessage({
|
||||
taskId: activeTaskId.value,
|
||||
mode: 'projectQualityReview' as any
|
||||
});
|
||||
loading.value = false;
|
||||
statusText.value = '已停止项目书质量初审';
|
||||
resolveStream();
|
||||
disconnectWebSocket();
|
||||
message.success('已停止');
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '停止失败');
|
||||
}
|
||||
};
|
||||
|
||||
const copyReport = () => {
|
||||
if (!reportMarkdown.value.trim()) {
|
||||
message.warning('暂无可复制内容');
|
||||
return;
|
||||
}
|
||||
copyText(reportMarkdown.value);
|
||||
};
|
||||
|
||||
const exportReport = () => {
|
||||
if (!reportMarkdown.value.trim()) {
|
||||
message.warning('暂无可导出内容');
|
||||
return;
|
||||
}
|
||||
const baseName =
|
||||
projectFile.value?.name?.replace(/\.[^.]+$/, '') || '项目书质量初审报告';
|
||||
const safeName = baseName.replace(/[\\/:*?"<>|]/g, '_');
|
||||
const blob = new Blob([reportMarkdown.value], {
|
||||
type: 'text/markdown;charset=utf-8'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${safeName}-AI初审报告.md`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
message.success('导出成功');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
clearResult();
|
||||
} else {
|
||||
disconnectWebSocket();
|
||||
clearStreamResolver();
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
disconnectWebSocket();
|
||||
clearStreamResolver();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.validator-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 330px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 20px;
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.upload-panel {
|
||||
min-width: 0;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.validator-intro {
|
||||
margin-bottom: 16px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.upload-label {
|
||||
margin: 14px 0 8px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.optional-label {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
min-width: 0;
|
||||
align-self: start;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.result-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.result-subtitle {
|
||||
margin-top: 4px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.report-shell {
|
||||
height: 360px;
|
||||
overflow: auto;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.report-streaming {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.report-viewer :deep(.markdown-body) {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.fullscreen-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fullscreen-report-shell {
|
||||
height: calc(100vh - 210px);
|
||||
overflow: auto;
|
||||
padding: 20px 24px;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.fullscreen-report-shell :deep(.markdown-body) {
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
font-size: 15px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.validator-layout {
|
||||
grid-template-columns: 1fr;
|
||||
max-height: calc(100vh - 180px);
|
||||
}
|
||||
|
||||
.report-shell {
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-drag) {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-drag .ant-upload) {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-drag p.ant-upload-drag-icon) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-drag p.ant-upload-text) {
|
||||
margin-bottom: 6px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload-list) {
|
||||
max-height: 74px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<common-import
|
||||
v-bind="$props"
|
||||
@done="emit('done')"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
module-name="未来学术之星"
|
||||
file-name="未来学术之星导入模板.xlsx"
|
||||
:template-fields="templateFields"
|
||||
:sample-row="sampleRow"
|
||||
:import-description="description"
|
||||
:create-payload="createPayload"
|
||||
:save-item="saveItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CommonImport from '../../components/common-import.vue';
|
||||
import { addWxxzxForm } from '@/api/gxmu/wxxzx-form';
|
||||
import type { WxxzxForm } from '@/api/gxmu/wxxzx-form/model';
|
||||
|
||||
defineProps<{ visible: boolean }>();
|
||||
const emit = defineEmits(['done', 'update:visible']);
|
||||
|
||||
const description =
|
||||
'本模板仅导入未来学术之星主表字段。指导教师、申请人、预算、评审成员、过程材料与归档记录请在详情中继续维护。';
|
||||
|
||||
const templateFields = [
|
||||
{ header: '年份', key: 'year', required: true },
|
||||
{ header: '课题名称', key: 'topicName', required: true },
|
||||
{ header: '课题负责人', key: 'leader' },
|
||||
{ header: '学院、年级、班别', key: 'collegeGradeClass' },
|
||||
{ header: '联系电话', key: 'phone' },
|
||||
{ header: '指导老师', key: 'advisor' },
|
||||
{ header: '课题类别', key: 'topicType' },
|
||||
{ header: '立项依据', key: 'rationale' },
|
||||
{ header: '指导教师意见', key: 'teacherOpinion' },
|
||||
{ header: '学院意见', key: 'collegeOpinion' },
|
||||
{ header: '学校意见', key: 'schoolOpinion' },
|
||||
{ header: '承诺签字人', key: 'promiseSigner' },
|
||||
{ header: '评审名单-学院', key: 'reviewCollege' },
|
||||
{ header: '评审名单-汇报人', key: 'reviewReporter' },
|
||||
{ header: '评审名单-日期', key: 'reviewDate' }
|
||||
] as const;
|
||||
|
||||
const sampleRow = {
|
||||
年份: '2025',
|
||||
课题名称: '医学生科研素养提升路径研究',
|
||||
课题负责人: '周八',
|
||||
'学院、年级、班别': '第一临床医学院 2022级 1班',
|
||||
联系电话: '13800138000',
|
||||
指导老师: '李教授',
|
||||
课题类别: '重点课题',
|
||||
立项依据: '围绕医学生科研训练现状展开研究',
|
||||
指导教师意见: '',
|
||||
学院意见: '',
|
||||
学校意见: '',
|
||||
承诺签字人: '周八',
|
||||
'评审名单-学院': '',
|
||||
'评审名单-汇报人': '',
|
||||
'评审名单-日期': ''
|
||||
};
|
||||
|
||||
const createPayload = (row: Record<string, any>): WxxzxForm => ({
|
||||
year: String(row['年份'] || '').trim(),
|
||||
topicName: String(row['课题名称'] || '').trim(),
|
||||
leader: String(row['课题负责人'] || '').trim(),
|
||||
collegeGradeClass: String(row['学院、年级、班别'] || '').trim(),
|
||||
phone: String(row['联系电话'] || '').trim(),
|
||||
advisor: String(row['指导老师'] || '').trim(),
|
||||
topicType: String(row['课题类别'] || '').trim(),
|
||||
rationale: String(row['立项依据'] || '').trim(),
|
||||
teacherOpinion: String(row['指导教师意见'] || '').trim(),
|
||||
collegeOpinion: String(row['学院意见'] || '').trim(),
|
||||
schoolOpinion: String(row['学校意见'] || '').trim(),
|
||||
promiseSigner: String(row['承诺签字人'] || '').trim(),
|
||||
reviewCollege: String(row['评审名单-学院'] || '').trim(),
|
||||
reviewReporter: String(row['评审名单-汇报人'] || '').trim(),
|
||||
reviewDate: String(row['评审名单-日期'] || '').trim(),
|
||||
guidanceTeachers: [],
|
||||
applicants: [],
|
||||
budgets: [],
|
||||
reviewMembers: [],
|
||||
archiveRecords: []
|
||||
});
|
||||
|
||||
const saveItem = (item: WxxzxForm) => addWxxzxForm(item);
|
||||
</script>
|
||||
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
placement="right"
|
||||
width="760"
|
||||
:visible="visible"
|
||||
title="项目过程材料"
|
||||
:body-style="{ paddingBottom: '88px' }"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<div class="material-header">
|
||||
<div class="material-title">{{ props.data?.topicName || '未命名课题' }}</div>
|
||||
<div class="material-subtitle">
|
||||
负责人:{{ props.data?.leader || '-' }} | 指导老师:{{ props.data?.advisor || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<a-tabs v-model:activeKey="activeKey" type="card" class="gxmu-tag-tabs">
|
||||
<a-tab-pane key="funding" tab="经费管理">
|
||||
<div class="stage-card">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="请分别上传经费预算表和经费划拨明细表,支持覆盖更新。"
|
||||
/>
|
||||
<a-form layout="vertical" class="stage-form">
|
||||
<a-form-item label="经费预算表">
|
||||
<stage-file-upload
|
||||
v-model:value="form.fundingBudgetFile"
|
||||
button-text="上传经费预算表"
|
||||
:multiple="false"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="经费划拨明细表">
|
||||
<stage-file-upload
|
||||
v-model:value="form.fundingAllocationFile"
|
||||
button-text="上传经费划拨明细表"
|
||||
:multiple="false"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="midterm" tab="中期检查">
|
||||
<div class="stage-card">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="学生团队可在此提交中期检查所需的阶段性材料。"
|
||||
/>
|
||||
<a-form layout="vertical" class="stage-form">
|
||||
<a-form-item label="补充说明">
|
||||
<a-textarea
|
||||
v-model:value="form.midtermRemark"
|
||||
:rows="4"
|
||||
placeholder="请输入中期检查说明,如材料清单、阶段进展等"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="中期检查材料">
|
||||
<stage-file-upload
|
||||
v-model:value="form.midtermMaterials"
|
||||
button-text="上传中期检查材料"
|
||||
:multiple="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="final" tab="结题验收">
|
||||
<div class="stage-card">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="学生团队可上传结题验收阶段的最终材料。"
|
||||
/>
|
||||
<a-form layout="vertical" class="stage-form">
|
||||
<a-form-item label="补充说明">
|
||||
<a-textarea
|
||||
v-model:value="form.finalRemark"
|
||||
:rows="4"
|
||||
placeholder="请输入结题说明,如成果概述、材料目录等"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="结题材料">
|
||||
<stage-file-upload
|
||||
v-model:value="form.finalMaterials"
|
||||
button-text="上传结题材料"
|
||||
:multiple="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="archive" tab="成果归档">
|
||||
<div class="stage-card">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="请以表格形式维护成果归档,可添加多条获奖记录并分别上传凭证。"
|
||||
/>
|
||||
<div class="archive-toolbar">
|
||||
<a-button type="dashed" @click="addArchiveRecord">新增一条</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
row-key="__rowKey"
|
||||
:columns="archiveColumns"
|
||||
:data-source="form.archiveRecords"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 980 }"
|
||||
class="archive-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'awardName'">
|
||||
<a-input
|
||||
v-model:value="form.archiveRecords[index].awardName"
|
||||
placeholder="请输入获奖名称"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'awardTime'">
|
||||
<a-date-picker
|
||||
v-model:value="form.archiveRecords[index].awardTime"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
placeholder="请选择获奖时间"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'remark'">
|
||||
<a-textarea
|
||||
v-model:value="form.archiveRecords[index].remark"
|
||||
:rows="2"
|
||||
placeholder="请输入备注说明"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'proofFile'">
|
||||
<stage-file-upload
|
||||
v-model:value="form.archiveRecords[index].proofFile"
|
||||
button-text="上传凭证"
|
||||
:multiple="false"
|
||||
:limit="1"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a class="ele-text-danger" @click="removeArchiveRecord(index)">
|
||||
删除
|
||||
</a>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</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 { reactive, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { updateWxxzxMaterials } from '@/api/gxmu/wxxzx-form';
|
||||
import type {
|
||||
WxxzxArchiveRecord,
|
||||
WxxzxForm
|
||||
} from '@/api/gxmu/wxxzx-form/model';
|
||||
import StageFileUpload from './stageFileUpload.vue';
|
||||
|
||||
type MaterialStageKey = 'funding' | 'midterm' | 'final' | 'archive';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
data?: WxxzxForm | null;
|
||||
initialStage?: MaterialStageKey;
|
||||
}>(),
|
||||
{
|
||||
initialStage: 'funding'
|
||||
}
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
const activeKey = ref<MaterialStageKey>('funding');
|
||||
|
||||
const createArchiveRecord = (): WxxzxArchiveRecord & { __rowKey: string } => ({
|
||||
id: undefined,
|
||||
year: '',
|
||||
awardName: '',
|
||||
awardTime: '',
|
||||
remark: '',
|
||||
proofFile: '',
|
||||
__rowKey: `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
});
|
||||
|
||||
const normalizeArchiveRecord = (record?: WxxzxArchiveRecord | null) => ({
|
||||
...createArchiveRecord(),
|
||||
...record,
|
||||
__rowKey:
|
||||
(record as any)?.__rowKey ||
|
||||
`${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
});
|
||||
|
||||
const archiveColumns = [
|
||||
{
|
||||
title: '获奖名称',
|
||||
dataIndex: 'awardName',
|
||||
key: 'awardName',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '获奖时间',
|
||||
dataIndex: 'awardTime',
|
||||
key: 'awardTime',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '备注说明',
|
||||
dataIndex: 'remark',
|
||||
key: 'remark',
|
||||
width: 280
|
||||
},
|
||||
{
|
||||
title: '上传凭证',
|
||||
dataIndex: 'proofFile',
|
||||
key: 'proofFile',
|
||||
width: 280
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 90,
|
||||
fixed: 'right'
|
||||
}
|
||||
];
|
||||
|
||||
const createDefaultForm = () => ({
|
||||
fundingBudgetFile: '',
|
||||
fundingAllocationFile: '',
|
||||
midtermRemark: '',
|
||||
midtermMaterials: '',
|
||||
finalRemark: '',
|
||||
finalMaterials: '',
|
||||
archiveAwards: '',
|
||||
archiveProofs: '',
|
||||
archiveRecords: [createArchiveRecord()]
|
||||
});
|
||||
|
||||
const form = reactive(createDefaultForm());
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
Object.assign(form, createDefaultForm(), {
|
||||
fundingBudgetFile: props.data?.fundingBudgetFile || '',
|
||||
fundingAllocationFile: props.data?.fundingAllocationFile || '',
|
||||
midtermRemark: props.data?.midtermRemark || '',
|
||||
midtermMaterials: props.data?.midtermMaterials || '',
|
||||
finalRemark: props.data?.finalRemark || '',
|
||||
finalMaterials: props.data?.finalMaterials || '',
|
||||
archiveAwards: props.data?.archiveAwards || '',
|
||||
archiveProofs: props.data?.archiveProofs || '',
|
||||
archiveRecords:
|
||||
props.data?.archiveRecords?.length
|
||||
? props.data.archiveRecords.map((item) =>
|
||||
normalizeArchiveRecord(item)
|
||||
)
|
||||
: [createArchiveRecord()]
|
||||
});
|
||||
activeKey.value = props.initialStage;
|
||||
} else {
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (!props.data?.id) {
|
||||
message.error('请先保存课题申报信息');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
updateWxxzxMaterials(props.data.id, {
|
||||
...form,
|
||||
archiveRecords: form.archiveRecords
|
||||
.map(({ __rowKey, ...item }: any) => item)
|
||||
.filter(
|
||||
(item) =>
|
||||
item.awardName ||
|
||||
item.awardTime ||
|
||||
item.remark ||
|
||||
item.proofFile
|
||||
)
|
||||
})
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const addArchiveRecord = () => {
|
||||
form.archiveRecords.push(createArchiveRecord());
|
||||
};
|
||||
|
||||
const removeArchiveRecord = (index: number) => {
|
||||
form.archiveRecords.splice(index, 1);
|
||||
if (!form.archiveRecords.length) {
|
||||
form.archiveRecords.push(createArchiveRecord());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../../tag-tabs.less';
|
||||
|
||||
.gxmu-tag-tabs();
|
||||
|
||||
.material-header {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #f7fbff 0%, #edf5ff 100%);
|
||||
}
|
||||
|
||||
.material-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.material-subtitle {
|
||||
margin-top: 8px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.stage-card {
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.stage-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.archive-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 16px 0 12px;
|
||||
}
|
||||
|
||||
.archive-table :deep(.ant-table-cell) {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.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,224 @@
|
||||
<template>
|
||||
<div class="stage-file-upload">
|
||||
<a-upload
|
||||
:accept="accept"
|
||||
:multiple="multiple"
|
||||
:showUploadList="false"
|
||||
:customRequest="handleUpload"
|
||||
>
|
||||
<a-button class="ele-btn-icon">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>{{ buttonText }}</span>
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div v-if="fileItems.length" class="file-list">
|
||||
<div
|
||||
v-for="(item, index) in fileItems"
|
||||
:key="`${item.url}-${index}`"
|
||||
class="file-item"
|
||||
>
|
||||
<a :href="item.url" target="_blank" rel="noreferrer">
|
||||
{{ item.name || getFileName(item.url) }}
|
||||
</a>
|
||||
<a class="file-remove" @click="removeFile(index)">删除</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-text">{{ placeholder }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { uploadFile } from '@/api/system/file';
|
||||
|
||||
interface StageUploadFileItem {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
}>();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
multiple?: boolean;
|
||||
accept?: string;
|
||||
buttonText?: string;
|
||||
placeholder?: string;
|
||||
limit?: number;
|
||||
}>(),
|
||||
{
|
||||
value: '',
|
||||
multiple: false,
|
||||
accept:
|
||||
'.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.rar,.jpg,.jpeg,.png',
|
||||
buttonText: '上传文件',
|
||||
placeholder: '暂未上传材料',
|
||||
limit: 20
|
||||
}
|
||||
);
|
||||
|
||||
const fileItems = ref<StageUploadFileItem[]>([]);
|
||||
|
||||
const getFileName = (url?: string) => {
|
||||
if (!url) {
|
||||
return '未命名文件';
|
||||
}
|
||||
const cleanUrl = url.split('?')[0];
|
||||
const fileName = cleanUrl.split('/').pop() || cleanUrl;
|
||||
return decodeURIComponent(fileName);
|
||||
};
|
||||
|
||||
const normalizeItem = (item: unknown) => {
|
||||
if (typeof item === 'string') {
|
||||
const url = item.trim();
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
name: getFileName(url),
|
||||
url
|
||||
};
|
||||
}
|
||||
if (
|
||||
item &&
|
||||
typeof item === 'object' &&
|
||||
typeof (item as StageUploadFileItem).url === 'string'
|
||||
) {
|
||||
const url = (item as StageUploadFileItem).url.trim();
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
name:
|
||||
(item as StageUploadFileItem).name?.trim() || getFileName(url),
|
||||
url
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseValue = (value?: string) => {
|
||||
const text = value?.trim();
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
if (!props.multiple) {
|
||||
return normalizeItem(text) ? [normalizeItem(text)!] : [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return normalizeItem(text) ? [normalizeItem(text)!] : [];
|
||||
}
|
||||
return parsed
|
||||
.map((item) => normalizeItem(item))
|
||||
.filter((item): item is StageUploadFileItem => !!item);
|
||||
} catch (e) {
|
||||
return normalizeItem(text) ? [normalizeItem(text)!] : [];
|
||||
}
|
||||
};
|
||||
|
||||
const emitValue = (list: StageUploadFileItem[]) => {
|
||||
if (!props.multiple) {
|
||||
emit('update:value', list[0]?.url || '');
|
||||
return;
|
||||
}
|
||||
emit('update:value', list.length ? JSON.stringify(list) : '');
|
||||
};
|
||||
|
||||
const syncFileItems = (value?: string) => {
|
||||
fileItems.value = parseValue(value);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
syncFileItems(value);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleUpload = async (options: any) => {
|
||||
const rawFile = options.file as File;
|
||||
if (rawFile.size / 1024 / 1024 > 100) {
|
||||
message.error('大小不能超过 100MB');
|
||||
options.onError?.(new Error('文件过大'));
|
||||
return;
|
||||
}
|
||||
if (props.multiple && fileItems.value.length >= props.limit) {
|
||||
message.warning(`最多上传 ${props.limit} 个文件`);
|
||||
options.onError?.(new Error('超出上传数量限制'));
|
||||
return;
|
||||
}
|
||||
const hide = message.loading('上传中..', 0);
|
||||
try {
|
||||
const result = await uploadFile(rawFile);
|
||||
const nextItem = {
|
||||
name: result.name || rawFile.name,
|
||||
url: String(result.url || result.downloadUrl || result.path || '')
|
||||
};
|
||||
const nextList = props.multiple ? [...fileItems.value, nextItem] : [nextItem];
|
||||
fileItems.value = nextList;
|
||||
emitValue(nextList);
|
||||
message.success('上传成功');
|
||||
options.onSuccess?.(result);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '上传失败');
|
||||
options.onError?.(e);
|
||||
} finally {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
const nextList = fileItems.value.filter((_, currentIndex) => currentIndex !== index);
|
||||
fileItems.value = nextList;
|
||||
emitValue(nextList);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.file-list {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-item + .file-item {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.file-item a:first-child {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-remove {
|
||||
color: #ff4d4f;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin-top: 8px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,378 @@
|
||||
<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: 1200 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>{{ props.popup ? '立即申报' : '添加' }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openDocValidate"
|
||||
>
|
||||
<template #icon>
|
||||
<FileSearchOutlined />
|
||||
</template>
|
||||
<span>文件校验</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
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-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="!shouldHideEditByReviewList(record.reviewList)" @click="openEdit(record)">编辑</a>
|
||||
<a @click="openMaterial(record, 'funding')">经费管理</a>
|
||||
<a @click="openMaterial(record, 'midterm')">中期检查</a>
|
||||
<a @click="openMaterial(record, 'final')">结题验收</a>
|
||||
<a @click="openMaterial(record, 'archive')">成果归档</a>
|
||||
<a @click="openReviewHistory(record)">审核历史</a>
|
||||
<a-popconfirm
|
||||
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"
|
||||
:fixed-year="props.fixedYear"
|
||||
@done="reload"
|
||||
/>
|
||||
<import-modal v-model:visible="showImport" @done="reload" />
|
||||
<materials-modal
|
||||
v-model:visible="showMaterials"
|
||||
:data="materialCurrent"
|
||||
:initial-stage="materialStage"
|
||||
@done="reload"
|
||||
/>
|
||||
<doc-validate-modal v-model:visible="showDocValidate" />
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="REVIEW_MODULE"
|
||||
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 { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
DownloadOutlined,
|
||||
UploadOutlined,
|
||||
PlusOutlined,
|
||||
FileSearchOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import EditModal from './components/edit.vue';
|
||||
import ImportModal from './components/import.vue';
|
||||
import DocValidateModal from './components/docValidate.vue';
|
||||
import MaterialsModal from './components/materials.vue';
|
||||
import ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import { shouldHideEditByReviewList } from '../utils/review';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
exportWxxzxForm,
|
||||
getWxxzxForm,
|
||||
userPageWxxzxForm,
|
||||
removeWxxzxForm
|
||||
} from '@/api/gxmu/wxxzx-form';
|
||||
import type { WxxzxForm, WxxzxFormParam } from '@/api/gxmu/wxxzx-form/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
popup?: boolean;
|
||||
fixedYear?: string | number;
|
||||
}>(),
|
||||
{
|
||||
popup: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showImport = ref(false);
|
||||
const current = ref<WxxzxForm>();
|
||||
const showDetail = ref(false);
|
||||
const detailCurrent = ref<WxxzxForm | null>(null);
|
||||
const showMaterials = ref(false);
|
||||
const materialCurrent = ref<WxxzxForm>();
|
||||
const materialStage = ref<'funding' | 'midterm' | 'final' | 'archive'>(
|
||||
'funding'
|
||||
);
|
||||
const showDocValidate = ref(false);
|
||||
const REVIEW_MODULE = 'gxmu_wxxzx_project';
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<WxxzxForm | null>(null);
|
||||
const exporting = ref(false);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '课题名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '课题负责人',
|
||||
dataIndex: 'leader',
|
||||
key: 'leader',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '学院、年级、班别',
|
||||
dataIndex: 'collegeGradeClass',
|
||||
key: 'collegeGradeClass',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '指导老师',
|
||||
dataIndex: 'advisor',
|
||||
key: 'advisor',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 420
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return userPageWxxzxForm({
|
||||
page,
|
||||
limit,
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as WxxzxFormParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const buildQueryParams = (): WxxzxFormParam => {
|
||||
return {
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as WxxzxFormParam;
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
const openDocValidate = () => {
|
||||
showDocValidate.value = true;
|
||||
};
|
||||
|
||||
const resolveExportUrl = (payload: any) => {
|
||||
return String(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 exportWxxzxForm(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 openEdit = (record?: WxxzxForm) => {
|
||||
if (!record?.id) {
|
||||
current.value = undefined;
|
||||
showEdit.value = true;
|
||||
return;
|
||||
}
|
||||
const hide = message.loading('加载中..', 0);
|
||||
getWxxzxForm(record.id)
|
||||
.then((data) => {
|
||||
hide();
|
||||
current.value = JSON.parse(JSON.stringify(data)) as WxxzxForm;
|
||||
showEdit.value = true;
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const openReviewHistory = (record: WxxzxForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (record: WxxzxForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const openMaterial = (
|
||||
record: WxxzxForm,
|
||||
stage: 'funding' | 'midterm' | 'final' | 'archive'
|
||||
) => {
|
||||
if (!record.id) {
|
||||
message.error('缺少课题记录ID');
|
||||
return;
|
||||
}
|
||||
materialStage.value = stage;
|
||||
const hide = message.loading('加载中..', 0);
|
||||
getWxxzxForm(record.id)
|
||||
.then((data) => {
|
||||
hide();
|
||||
materialCurrent.value = JSON.parse(JSON.stringify(data)) as WxxzxForm;
|
||||
showMaterials.value = true;
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const remove = (record: WxxzxForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeWxxzxForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'WXXZXForm'
|
||||
};
|
||||
</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>
|
||||
@@ -0,0 +1,318 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="900"
|
||||
: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="24">
|
||||
<a-form-item
|
||||
label="年份"
|
||||
name="year"
|
||||
:rules="[{ required: true, message: '请选择年份' }]"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:value="form.year"
|
||||
picker="year"
|
||||
value-format="YYYY"
|
||||
style="width: 100%"
|
||||
:disabled="!!props.fixedYear"
|
||||
placeholder="请选择年份"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<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="24">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select
|
||||
v-model:value="form.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择性别"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="民族" name="nation">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.nation"
|
||||
:options="nationOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择民族"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="出生年月" name="birthMonth">
|
||||
<a-date-picker
|
||||
v-model:value="form.birthMonth"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择出生年月"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="政治面貌" name="politics">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.politics"
|
||||
:options="politicsOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择政治面貌"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="职务" name="position">
|
||||
<a-input v-model:value="form.position" placeholder="请输入职务" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="身份" name="identity">
|
||||
<a-input v-model:value="form.identity" placeholder="请输入身份" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="所在团组织" name="organization">
|
||||
<a-input
|
||||
v-model:value="form.organization"
|
||||
placeholder="请输入所在团组织"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="联系方式" name="contact">
|
||||
<a-input
|
||||
v-model:value="form.contact"
|
||||
placeholder="请输入联系方式"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="发展团员编号" name="memberNo">
|
||||
<a-input
|
||||
v-model:value="form.memberNo"
|
||||
placeholder="请输入发展团员编号"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="任现团内职务时间" name="currentDutyTime">
|
||||
<a-date-picker
|
||||
v-model:value="form.currentDutyTime"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择任现团内职务时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="担任团干部年限" name="cadreYears">
|
||||
<a-input
|
||||
v-model:value="form.cadreYears"
|
||||
placeholder="请输入担任团干部年限"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度工作考核结果" name="assessment2024">
|
||||
<a-input
|
||||
v-model:value="form.assessment2024"
|
||||
placeholder="请输入上一年度工作考核结果"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="成为注册志愿者时间" name="volunteerRegTime">
|
||||
<a-date-picker
|
||||
v-model:value="form.volunteerRegTime"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择成为注册志愿者时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="从事团干部经历" name="cadreExperience">
|
||||
<a-textarea
|
||||
v-model:value="form.cadreExperience"
|
||||
:rows="3"
|
||||
placeholder="请输入从事团干部经历"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="近五年获得校级及以上荣誉情况"
|
||||
name="honorsFiveYears"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="form.honorsFiveYears"
|
||||
:rows="3"
|
||||
placeholder="请输入近五年获得校级及以上荣誉情况"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="主要事迹" name="mainStory">
|
||||
<a-textarea
|
||||
v-model:value="form.mainStory"
|
||||
:rows="3"
|
||||
placeholder="请输入主要事迹"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="所在单位团组织意见" name="leagueOpinion">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.leagueOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- placeholder="请输入所在单位团组织意见"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="所在单位党组织意见" name="partyOpinion">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.partyOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- 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 {
|
||||
addYxgqtdgbForm,
|
||||
updateYxgqtdgbForm
|
||||
} from '@/api/gxmu/yxgqtdgb-form';
|
||||
import type { YxgqtdgbForm } from '@/api/gxmu/yxgqtdgb-form/model';
|
||||
import {
|
||||
filterSelectOption,
|
||||
useGxmuFormSelectOptions
|
||||
} from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: YxgqtdgbForm | null;
|
||||
fixedYear?: string | number;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const { nationOptions, politicsOptions, genderOptions, loadNationOptions, loadPoliticsOptions } =
|
||||
useGxmuFormSelectOptions();
|
||||
|
||||
const createDefaultForm = (): YxgqtdgbForm => ({
|
||||
id: undefined,
|
||||
year: '',
|
||||
name: '',
|
||||
gender: '',
|
||||
nation: '',
|
||||
birthMonth: '',
|
||||
politics: '',
|
||||
position: '',
|
||||
identity: '',
|
||||
organization: '',
|
||||
contact: '',
|
||||
memberNo: '',
|
||||
currentDutyTime: '',
|
||||
cadreYears: '',
|
||||
assessment2024: '',
|
||||
volunteerRegTime: '',
|
||||
cadreExperience: '',
|
||||
honorsFiveYears: '',
|
||||
mainStory: '',
|
||||
leagueOpinion: '',
|
||||
partyOpinion: ''
|
||||
});
|
||||
|
||||
const form = reactive<YxgqtdgbForm>(createDefaultForm());
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
try {
|
||||
await Promise.all([loadNationOptions(), loadPoliticsOptions()]);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '下拉选项加载失败');
|
||||
}
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.assign(form, createDefaultForm(), props.data ?? {});
|
||||
if (props.fixedYear) {
|
||||
form.year = String(props.fixedYear);
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
formRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateYxgqtdgbForm
|
||||
: addYxgqtdgbForm;
|
||||
saveOrUpdate({
|
||||
...form,
|
||||
year: props.fixedYear ? String(props.fixedYear) : form.year
|
||||
})
|
||||
.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,93 @@
|
||||
<template>
|
||||
<common-import
|
||||
v-bind="$props"
|
||||
@done="emit('done')"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
module-name="优秀共青团干部"
|
||||
file-name="优秀共青团干部导入模板.xlsx"
|
||||
:template-fields="templateFields"
|
||||
:sample-row="sampleRow"
|
||||
:create-payload="createPayload"
|
||||
:save-item="saveItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CommonImport from '../../components/common-import.vue';
|
||||
import { addYxgqtdgbForm } from '@/api/gxmu/yxgqtdgb-form';
|
||||
import type { YxgqtdgbForm } from '@/api/gxmu/yxgqtdgb-form/model';
|
||||
|
||||
defineProps<{ visible: boolean }>();
|
||||
const emit = defineEmits(['done', 'update:visible']);
|
||||
|
||||
const templateFields = [
|
||||
{ header: '年份', key: 'year', required: true },
|
||||
{ header: '姓名', key: 'name', required: true },
|
||||
{ header: '性别', key: 'gender' },
|
||||
{ header: '民族', key: 'nation' },
|
||||
{ header: '出生年月', key: 'birthMonth' },
|
||||
{ header: '政治面貌', key: 'politics' },
|
||||
{ header: '职务', key: 'position' },
|
||||
{ header: '身份类别', key: 'identity' },
|
||||
{ header: '所在团组织', key: 'organization' },
|
||||
{ header: '联系方式', key: 'contact' },
|
||||
{ header: '团员编号', key: 'memberNo' },
|
||||
{ header: '现任职务时间', key: 'currentDutyTime' },
|
||||
{ header: '从事团的工作年限', key: 'cadreYears' },
|
||||
{ header: '2024年度考核等次', key: 'assessment2024' },
|
||||
{ header: '志愿汇注册时间', key: 'volunteerRegTime' },
|
||||
{ header: '团干部经历', key: 'cadreExperience' },
|
||||
{ header: '近五年获得校级及以上荣誉情况', key: 'honorsFiveYears' },
|
||||
{ header: '主要事迹', key: 'mainStory' },
|
||||
{ header: '二级团组织意见', key: 'leagueOpinion' },
|
||||
{ header: '二级党组织意见', key: 'partyOpinion' }
|
||||
] as const;
|
||||
|
||||
const sampleRow = {
|
||||
年份: '2025',
|
||||
姓名: '王五',
|
||||
性别: '男',
|
||||
民族: '汉族',
|
||||
出生年月: '1999-04',
|
||||
政治面貌: '中共党员',
|
||||
职务: '团委副书记',
|
||||
身份类别: '学生',
|
||||
所在团组织: '第一临床医学院团委',
|
||||
联系方式: '13800138000',
|
||||
团员编号: 'TW2025001',
|
||||
现任职务时间: '2023-09',
|
||||
从事团的工作年限: '2',
|
||||
'2024年度考核等次': '优秀',
|
||||
志愿汇注册时间: '2021-03',
|
||||
团干部经历: '连续两年担任学院团学骨干',
|
||||
近五年获得校级及以上荣誉情况: '校优秀团干部',
|
||||
主要事迹: '组织多场品牌志愿服务活动',
|
||||
二级团组织意见: '',
|
||||
二级党组织意见: ''
|
||||
};
|
||||
|
||||
const createPayload = (row: Record<string, any>): YxgqtdgbForm => ({
|
||||
year: String(row['年份'] || '').trim(),
|
||||
name: String(row['姓名'] || '').trim(),
|
||||
gender: String(row['性别'] || '').trim(),
|
||||
nation: String(row['民族'] || '').trim(),
|
||||
birthMonth: String(row['出生年月'] || '').trim(),
|
||||
politics: String(row['政治面貌'] || '').trim(),
|
||||
position: String(row['职务'] || '').trim(),
|
||||
identity: String(row['身份类别'] || '').trim(),
|
||||
organization: String(row['所在团组织'] || '').trim(),
|
||||
contact: String(row['联系方式'] || '').trim(),
|
||||
memberNo: String(row['团员编号'] || '').trim(),
|
||||
currentDutyTime: String(row['现任职务时间'] || '').trim(),
|
||||
cadreYears: String(row['从事团的工作年限'] || '').trim(),
|
||||
assessment2024: String(row['2024年度考核等次'] || '').trim(),
|
||||
volunteerRegTime: String(row['志愿汇注册时间'] || '').trim(),
|
||||
cadreExperience: String(row['团干部经历'] || '').trim(),
|
||||
honorsFiveYears: String(row['近五年获得校级及以上荣誉情况'] || '').trim(),
|
||||
mainStory: String(row['主要事迹'] || '').trim(),
|
||||
leagueOpinion: String(row['二级团组织意见'] || '').trim(),
|
||||
partyOpinion: String(row['二级党组织意见'] || '').trim()
|
||||
});
|
||||
|
||||
const saveItem = (item: YxgqtdgbForm) => addYxgqtdgbForm(item);
|
||||
</script>
|
||||
@@ -0,0 +1,412 @@
|
||||
<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: 3000 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>{{ props.popup ? '立即申报' : '添加' }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
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-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'currentReviewStatus'">
|
||||
<ReviewStatusTag :review-list="record.reviewList" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<template v-if="!shouldHideEditByReviewList(record.reviewList)">
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
</template>
|
||||
<a @click="openReviewHistory(record)">审核历史</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:fixed-year="props.fixedYear"
|
||||
@done="reload"
|
||||
/>
|
||||
<import-modal v-model:visible="showImport" @done="reload" />
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="REVIEW_MODULE"
|
||||
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 { 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 ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import { shouldHideEditByReviewList } from '../utils/review';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
exportYxgqtdgbForm,
|
||||
userPageYxgqtdgbForm,
|
||||
removeYxgqtdgbForm
|
||||
} from '@/api/gxmu/yxgqtdgb-form';
|
||||
import type {
|
||||
YxgqtdgbForm,
|
||||
YxgqtdgbFormParam
|
||||
} from '@/api/gxmu/yxgqtdgb-form/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
popup?: boolean;
|
||||
fixedYear?: string | number;
|
||||
}>(),
|
||||
{
|
||||
popup: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showImport = ref(false);
|
||||
const current = ref<YxgqtdgbForm>();
|
||||
const showDetail = ref(false);
|
||||
const detailCurrent = ref<YxgqtdgbForm | null>(null);
|
||||
const REVIEW_MODULE = 'gxmu_yxgqtdgb';
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<YxgqtdgbForm | null>(null);
|
||||
const exporting = ref(false);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '民族',
|
||||
dataIndex: 'nation',
|
||||
key: 'nation',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '出生年月',
|
||||
dataIndex: 'birthMonth',
|
||||
key: 'birthMonth',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '政治面貌',
|
||||
dataIndex: 'politics',
|
||||
key: 'politics',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '职务',
|
||||
dataIndex: 'position',
|
||||
key: 'position',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '身份',
|
||||
dataIndex: 'identity',
|
||||
key: 'identity',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '所在团组织',
|
||||
dataIndex: 'organization',
|
||||
key: 'organization',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '联系方式',
|
||||
dataIndex: 'contact',
|
||||
key: 'contact',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '发展团员编号',
|
||||
dataIndex: 'memberNo',
|
||||
key: 'memberNo',
|
||||
align: 'center',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '任现团内职务时间',
|
||||
dataIndex: 'currentDutyTime',
|
||||
key: 'currentDutyTime',
|
||||
align: 'center',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '担任团干部年限',
|
||||
dataIndex: 'cadreYears',
|
||||
key: 'cadreYears',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '上一年度工作考核结果',
|
||||
dataIndex: 'assessment2024',
|
||||
key: 'assessment2024',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '成为注册志愿者时间',
|
||||
dataIndex: 'volunteerRegTime',
|
||||
key: 'volunteerRegTime',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '从事团干部经历',
|
||||
dataIndex: 'cadreExperience',
|
||||
key: 'cadreExperience',
|
||||
align: 'center',
|
||||
width: 260,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '近五年获得校级及以上荣誉情况',
|
||||
dataIndex: 'honorsFiveYears',
|
||||
key: 'honorsFiveYears',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '主要事迹',
|
||||
dataIndex: 'mainStory',
|
||||
key: 'mainStory',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在单位团组织意见',
|
||||
dataIndex: 'leagueOpinion',
|
||||
key: 'leagueOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在单位党组织意见',
|
||||
dataIndex: 'partyOpinion',
|
||||
key: 'partyOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 220
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return userPageYxgqtdgbForm({
|
||||
page,
|
||||
limit,
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as YxgqtdgbFormParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const buildQueryParams = (): YxgqtdgbFormParam => {
|
||||
return {
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as YxgqtdgbFormParam;
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
const resolveExportUrl = (payload: any) => {
|
||||
return String(
|
||||
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 exportYxgqtdgbForm(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 openEdit = (record?: YxgqtdgbForm) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as YxgqtdgbForm)
|
||||
: undefined;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openReviewHistory = (record: YxgqtdgbForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (record: YxgqtdgbForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: YxgqtdgbForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeYxgqtdgbForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'YXGQTDGBForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="980"
|
||||
: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="24">
|
||||
<a-form-item
|
||||
label="年份"
|
||||
name="year"
|
||||
:rules="[{ required: true, message: '请选择年份' }]"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:value="form.year"
|
||||
picker="year"
|
||||
value-format="YYYY"
|
||||
style="width: 100%"
|
||||
:disabled="!!props.fixedYear"
|
||||
placeholder="请选择年份"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<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="24">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select
|
||||
v-model:value="form.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择性别"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="民族" name="nation">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.nation"
|
||||
:options="nationOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择民族"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="政治面貌" name="politics">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="form.politics"
|
||||
:options="politicsOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择政治面貌"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="出生年月" name="birthMonth">
|
||||
<a-date-picker
|
||||
v-model:value="form.birthMonth"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择出生年月"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="入团时间" name="joinTime">
|
||||
<a-date-picker
|
||||
v-model:value="form.joinTime"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择入团时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="所在学院、专业、班级" name="collegeMajorClass">
|
||||
<class-cascader
|
||||
v-model:value="form.collegeMajorClass"
|
||||
mode="collegeClass"
|
||||
placeholder="请先选择学院,再选择班级"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="职务" name="position">
|
||||
<a-input v-model:value="form.position" placeholder="请输入职务" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="成为注册志愿者时间" name="volunteerRegTime">
|
||||
<a-date-picker
|
||||
v-model:value="form.volunteerRegTime"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择成为注册志愿者时间"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度团员教育评议等次" name="eduEval2024">
|
||||
<a-input
|
||||
v-model:value="form.eduEval2024"
|
||||
placeholder="请输入上一年度团员教育评议等次"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="是否已登录智慧团建系统" name="smartSystem">
|
||||
<a-select v-model:value="form.smartSystem" 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="24">
|
||||
<a-form-item label="联系电话" name="contact">
|
||||
<a-input
|
||||
v-model:value="form.contact"
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="累计志愿服务时长" name="totalVolunteerHours">
|
||||
<a-input
|
||||
v-model:value="form.totalVolunteerHours"
|
||||
placeholder="请输入累计志愿服务时长"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="上一年度志愿服务时长" name="volunteerHours2024">
|
||||
<a-input
|
||||
v-model:value="form.volunteerHours2024"
|
||||
placeholder="请输入上一年度志愿服务时长"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="发展团员编号" name="memberNo">
|
||||
<a-input
|
||||
v-model:value="form.memberNo"
|
||||
placeholder="请输入发展团员编号"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="近五年获得荣誉情况" name="honorsFiveYears">
|
||||
<a-textarea
|
||||
v-model:value="form.honorsFiveYears"
|
||||
:rows="3"
|
||||
placeholder="请输入近五年获得荣誉情况"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="主要事迹" name="mainStory">
|
||||
<a-textarea
|
||||
v-model:value="form.mainStory"
|
||||
:rows="3"
|
||||
placeholder="请输入主要事迹"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item-->
|
||||
<!-- label="学生社团指导教师意见/所在单位团委"-->
|
||||
<!-- name="leagueOpinion"-->
|
||||
<!-- >-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.leagueOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- placeholder="请输入意见"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item-->
|
||||
<!-- label="业务指导单位意见/所在单位党组织"-->
|
||||
<!-- name="partyOpinion"-->
|
||||
<!-- >-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.partyOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- placeholder="请输入意见"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="校团委意见" name="schoolOpinion">-->
|
||||
<!-- <a-textarea-->
|
||||
<!-- v-model:value="form.schoolOpinion"-->
|
||||
<!-- :rows="2"-->
|
||||
<!-- 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 { addYxgqtyForm, updateYxgqtyForm } from '@/api/gxmu/yxgqty-form';
|
||||
import type { YxgqtyForm } from '@/api/gxmu/yxgqty-form/model';
|
||||
import ClassCascader from '@/views/gxmu/components/class-cascader.vue';
|
||||
import {
|
||||
filterSelectOption,
|
||||
useGxmuFormSelectOptions
|
||||
} from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: YxgqtyForm | null;
|
||||
fixedYear?: string | number;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const { nationOptions, politicsOptions, genderOptions, loadAll } =
|
||||
useGxmuFormSelectOptions();
|
||||
|
||||
const createDefaultForm = (): YxgqtyForm => ({
|
||||
id: undefined,
|
||||
year: '',
|
||||
name: '',
|
||||
gender: '',
|
||||
nation: '',
|
||||
politics: '',
|
||||
birthMonth: '',
|
||||
joinTime: '',
|
||||
collegeMajorClass: '',
|
||||
position: '',
|
||||
volunteerRegTime: '',
|
||||
eduEval2024: '',
|
||||
smartSystem: '',
|
||||
contact: '',
|
||||
totalVolunteerHours: '',
|
||||
volunteerHours2024: '',
|
||||
memberNo: '',
|
||||
honorsFiveYears: '',
|
||||
mainStory: '',
|
||||
leagueOpinion: '',
|
||||
partyOpinion: '',
|
||||
schoolOpinion: ''
|
||||
});
|
||||
|
||||
const form = reactive<YxgqtyForm>(createDefaultForm());
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
try {
|
||||
await loadAll();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '下拉选项加载失败');
|
||||
}
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.assign(form, createDefaultForm(), props.data ?? {});
|
||||
if (props.fixedYear) {
|
||||
form.year = String(props.fixedYear);
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
Object.assign(form, createDefaultForm());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
formRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateYxgqtyForm : addYxgqtyForm;
|
||||
saveOrUpdate({
|
||||
...form,
|
||||
year: props.fixedYear ? String(props.fixedYear) : form.year
|
||||
})
|
||||
.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,96 @@
|
||||
<template>
|
||||
<common-import
|
||||
v-bind="$props"
|
||||
@done="emit('done')"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
module-name="优秀共青团员"
|
||||
file-name="优秀共青团员导入模板.xlsx"
|
||||
:template-fields="templateFields"
|
||||
:sample-row="sampleRow"
|
||||
:create-payload="createPayload"
|
||||
:save-item="saveItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CommonImport from '../../components/common-import.vue';
|
||||
import { addYxgqtyForm } from '@/api/gxmu/yxgqty-form';
|
||||
import type { YxgqtyForm } from '@/api/gxmu/yxgqty-form/model';
|
||||
|
||||
defineProps<{ visible: boolean }>();
|
||||
const emit = defineEmits(['done', 'update:visible']);
|
||||
|
||||
const templateFields = [
|
||||
{ header: '年份', key: 'year', required: true },
|
||||
{ header: '姓名', key: 'name', required: true },
|
||||
{ header: '性别', key: 'gender' },
|
||||
{ header: '民族', key: 'nation' },
|
||||
{ header: '政治面貌', key: 'politics' },
|
||||
{ header: '出生年月', key: 'birthMonth' },
|
||||
{ header: '入团时间', key: 'joinTime' },
|
||||
{ header: '所在学院、专业、班级', key: 'collegeMajorClass' },
|
||||
{ header: '职务', key: 'position' },
|
||||
{ header: '志愿汇注册时间', key: 'volunteerRegTime' },
|
||||
{ header: '2024年度教育评议等次', key: 'eduEval2024' },
|
||||
{ header: '智慧团建系统录入情况', key: 'smartSystem' },
|
||||
{ header: '联系方式', key: 'contact' },
|
||||
{ header: '累计志愿服务时长', key: 'totalVolunteerHours' },
|
||||
{ header: '2024年度志愿服务时长', key: 'volunteerHours2024' },
|
||||
{ header: '团员编号', key: 'memberNo' },
|
||||
{ header: '近五年获得校级及以上荣誉情况', key: 'honorsFiveYears' },
|
||||
{ header: '主要事迹', key: 'mainStory' },
|
||||
{ header: '二级团组织意见', key: 'leagueOpinion' },
|
||||
{ header: '二级党组织意见', key: 'partyOpinion' },
|
||||
{ header: '学校团委意见', key: 'schoolOpinion' }
|
||||
] as const;
|
||||
|
||||
const sampleRow = {
|
||||
年份: '2025',
|
||||
姓名: '赵六',
|
||||
性别: '女',
|
||||
民族: '汉族',
|
||||
政治面貌: '共青团员',
|
||||
出生年月: '2002-06',
|
||||
入团时间: '2018-05',
|
||||
'所在学院、专业、班级': '护理学院 护理学 2022级1班',
|
||||
职务: '班长',
|
||||
志愿汇注册时间: '2020-09',
|
||||
'2024年度教育评议等次': '优秀',
|
||||
智慧团建系统录入情况: '是',
|
||||
联系方式: '13800138000',
|
||||
累计志愿服务时长: '120',
|
||||
'2024年度志愿服务时长': '40',
|
||||
团员编号: 'TY2025001',
|
||||
近五年获得校级及以上荣誉情况: '校优秀共青团员',
|
||||
主要事迹: '积极参与志愿服务和学风建设',
|
||||
二级团组织意见: '',
|
||||
二级党组织意见: '',
|
||||
学校团委意见: ''
|
||||
};
|
||||
|
||||
const createPayload = (row: Record<string, any>): YxgqtyForm => ({
|
||||
year: String(row['年份'] || '').trim(),
|
||||
name: String(row['姓名'] || '').trim(),
|
||||
gender: String(row['性别'] || '').trim(),
|
||||
nation: String(row['民族'] || '').trim(),
|
||||
politics: String(row['政治面貌'] || '').trim(),
|
||||
birthMonth: String(row['出生年月'] || '').trim(),
|
||||
joinTime: String(row['入团时间'] || '').trim(),
|
||||
collegeMajorClass: String(row['所在学院、专业、班级'] || '').trim(),
|
||||
position: String(row['职务'] || '').trim(),
|
||||
volunteerRegTime: String(row['志愿汇注册时间'] || '').trim(),
|
||||
eduEval2024: String(row['2024年度教育评议等次'] || '').trim(),
|
||||
smartSystem: String(row['智慧团建系统录入情况'] || '').trim(),
|
||||
contact: String(row['联系方式'] || '').trim(),
|
||||
totalVolunteerHours: String(row['累计志愿服务时长'] || '').trim(),
|
||||
volunteerHours2024: String(row['2024年度志愿服务时长'] || '').trim(),
|
||||
memberNo: String(row['团员编号'] || '').trim(),
|
||||
honorsFiveYears: String(row['近五年获得校级及以上荣誉情况'] || '').trim(),
|
||||
mainStory: String(row['主要事迹'] || '').trim(),
|
||||
leagueOpinion: String(row['二级团组织意见'] || '').trim(),
|
||||
partyOpinion: String(row['二级党组织意见'] || '').trim(),
|
||||
schoolOpinion: String(row['学校团委意见'] || '').trim()
|
||||
});
|
||||
|
||||
const saveItem = (item: YxgqtyForm) => addYxgqtyForm(item);
|
||||
</script>
|
||||
@@ -0,0 +1,870 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="list" tab="名单列表">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:scroll="{ x: 3800 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button
|
||||
class="gxmu-toolbar-btn gxmu-add-btn"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>{{ props.popup ? '立即申报' : '添加' }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
class="gxmu-toolbar-btn gxmu-import-btn"
|
||||
@click="openImport"
|
||||
>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!props.popup"
|
||||
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-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'currentReviewStatus'">
|
||||
<ReviewStatusTag :review-list="record.reviewList" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openSheet(record)">申报表</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openReviewHistory(record)">审核历史</a>
|
||||
<a-divider type="vertical" />
|
||||
<template
|
||||
v-if="!shouldHideEditByReviewList(record.reviewList)"
|
||||
>
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
</template>
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="form" tab="申报表">
|
||||
<div class="sheet-wrap">
|
||||
<div class="sheet-title">
|
||||
广西医科大学2025年“优秀共青团员”申报表
|
||||
</div>
|
||||
<a-form layout="vertical">
|
||||
<div class="sheet-table-wrap">
|
||||
<table class="sheet-table">
|
||||
<colgroup>
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 15%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 15%" />
|
||||
<col style="width: 12%" />
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 10%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sheet-label">姓名</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="sheetForm.name"
|
||||
placeholder="请输入姓名"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">性别</td>
|
||||
<td class="sheet-value">
|
||||
<a-select
|
||||
v-model:value="sheetForm.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">民族</td>
|
||||
<td class="sheet-value">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="sheetForm.nation"
|
||||
:options="nationOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择民族"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">政治面貌</td>
|
||||
<td class="sheet-value">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
v-model:value="sheetForm.politics"
|
||||
:options="politicsOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择政治面貌"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">出生年月</td>
|
||||
<td class="sheet-value">
|
||||
<a-date-picker
|
||||
v-model:value="sheetForm.birthMonth"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择出生年月"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">入团时间</td>
|
||||
<td class="sheet-value">
|
||||
<a-date-picker
|
||||
v-model:value="sheetForm.joinTime"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择入团时间"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">所在学院、专业、班级</td>
|
||||
<td class="sheet-value left" colspan="2">
|
||||
<class-cascader
|
||||
v-model:value="sheetForm.collegeMajorClass"
|
||||
mode="collegeClass"
|
||||
placeholder="请先选择学院,再选择班级"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">职务</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="sheetForm.position"
|
||||
placeholder="请输入职务"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">成为注册志愿者时间</td>
|
||||
<td class="sheet-value">
|
||||
<a-date-picker
|
||||
v-model:value="sheetForm.volunteerRegTime"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择成为注册志愿者时间"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">上一年度团员教育评议等次</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="sheetForm.eduEval2024"
|
||||
placeholder="如:优秀"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">是否已登录“智慧团建”系统</td>
|
||||
<td class="sheet-value">
|
||||
<a-select
|
||||
v-model:value="sheetForm.smartSystem"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<a-select-option value="是">是</a-select-option>
|
||||
<a-select-option value="否">否</a-select-option>
|
||||
</a-select>
|
||||
</td>
|
||||
<td class="sheet-label">联系电话</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="sheetForm.contact"
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">累计志愿服务时长</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="sheetForm.totalVolunteerHours"
|
||||
placeholder="如:200小时"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">上一年度志愿服务时长</td>
|
||||
<td class="sheet-value">
|
||||
<a-input
|
||||
v-model:value="sheetForm.volunteerHours2024"
|
||||
placeholder="如:40小时"
|
||||
/>
|
||||
</td>
|
||||
<td class="sheet-label">发展团员编号</td>
|
||||
<td class="sheet-value left" colspan="3">
|
||||
<a-input
|
||||
v-model:value="sheetForm.memberNo"
|
||||
placeholder="请输入发展团员编号"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">近五年获得荣誉情况</td>
|
||||
<td class="sheet-value sheet-textarea" colspan="7">
|
||||
<div class="sheet-tip"
|
||||
>(2020年1月1日以后,所获荣誉填3-5项)</div
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="sheetForm.honorsFiveYears"
|
||||
:rows="4"
|
||||
placeholder="如:2021年10月 被评为校级优秀团员"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">主要事迹</td>
|
||||
<td class="sheet-value sheet-textarea" colspan="7">
|
||||
<div class="sheet-tip"
|
||||
>(围绕参评条件,突出重点,简明扼要,不超过300字)</div
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="sheetForm.mainStory"
|
||||
:rows="5"
|
||||
placeholder="请输入主要事迹"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">意见栏</td>
|
||||
<td class="sheet-value" colspan="7">
|
||||
<div class="opinion-grid">
|
||||
<div class="opinion-box">
|
||||
<div class="opinion-title"
|
||||
>学生社团指导教师意见 / 所在单位团委</div
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="sheetForm.leagueOpinion"
|
||||
:rows="5"
|
||||
placeholder="请输入意见"
|
||||
/>
|
||||
<div class="opinion-sign">(签章) 年 月 日</div>
|
||||
</div>
|
||||
<div class="opinion-box">
|
||||
<div class="opinion-title"
|
||||
>业务指导单位意见 / 所在单位党组织</div
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="sheetForm.partyOpinion"
|
||||
:rows="5"
|
||||
placeholder="请输入意见"
|
||||
/>
|
||||
<div class="opinion-sign">(签章) 年 月 日</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sheet-label">校团委意见</td>
|
||||
<td class="sheet-value sheet-textarea" colspan="7">
|
||||
<a-textarea
|
||||
v-model:value="sheetForm.schoolOpinion"
|
||||
:rows="4"
|
||||
placeholder="请输入校团委意见"
|
||||
/>
|
||||
<div class="opinion-sign align-right"
|
||||
>(盖章) 年 月 日</div
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="sheet-actions">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="saveSheet"
|
||||
>保存表单</a-button
|
||||
>
|
||||
<a-button @click="resetSheet">重置</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:fixed-year="props.fixedYear"
|
||||
@done="reload"
|
||||
/>
|
||||
<import-modal v-model:visible="showImport" @done="reload" />
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="REVIEW_MODULE"
|
||||
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 { reactive, 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 ReviewHistoryModal from '../components/reviewHistoryModal.vue';
|
||||
import ModuleDataModal from '../reviewList/components/moduleDataModal.vue';
|
||||
import ReviewStatusTag from '../components/reviewStatusTag.vue';
|
||||
import ClassCascader from '@/views/gxmu/components/class-cascader.vue';
|
||||
import { shouldHideEditByReviewList } from '../utils/review';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
addYxgqtyForm,
|
||||
exportYxgqtyForm,
|
||||
userPageYxgqtyForm,
|
||||
removeYxgqtyForm,
|
||||
updateYxgqtyForm
|
||||
} from '@/api/gxmu/yxgqty-form';
|
||||
import type {
|
||||
YxgqtyForm,
|
||||
YxgqtyFormParam
|
||||
} from '@/api/gxmu/yxgqty-form/model';
|
||||
import {
|
||||
filterSelectOption,
|
||||
useGxmuFormSelectOptions
|
||||
} from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
popup?: boolean;
|
||||
fixedYear?: string | number;
|
||||
}>(),
|
||||
{
|
||||
popup: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showImport = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const current = ref<YxgqtyForm>();
|
||||
const detailCurrent = ref<YxgqtyForm | null>(null);
|
||||
const activeKey = ref('list');
|
||||
const REVIEW_MODULE = 'gxmu_yxgqty';
|
||||
const showReviewHistory = ref(false);
|
||||
const reviewCurrent = ref<YxgqtyForm | null>(null);
|
||||
const exporting = ref(false);
|
||||
const { genderOptions, nationOptions, politicsOptions, loadAll } =
|
||||
useGxmuFormSelectOptions();
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '民族',
|
||||
dataIndex: 'nation',
|
||||
key: 'nation',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '政治面貌',
|
||||
dataIndex: 'politics',
|
||||
key: 'politics',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '出生年月',
|
||||
dataIndex: 'birthMonth',
|
||||
key: 'birthMonth',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '入团时间',
|
||||
dataIndex: 'joinTime',
|
||||
key: 'joinTime',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '所在学院、专业、班级',
|
||||
dataIndex: 'collegeMajorClass',
|
||||
key: 'collegeMajorClass',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '职务',
|
||||
dataIndex: 'position',
|
||||
key: 'position',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '成为注册志愿者时间',
|
||||
dataIndex: 'volunteerRegTime',
|
||||
key: 'volunteerRegTime',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '上一年度团员教育评议等次',
|
||||
dataIndex: 'eduEval2024',
|
||||
key: 'eduEval2024',
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '是否已登录智慧团建系统',
|
||||
dataIndex: 'smartSystem',
|
||||
key: 'smartSystem',
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'contact',
|
||||
key: 'contact',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '累计志愿服务时长',
|
||||
dataIndex: 'totalVolunteerHours',
|
||||
key: 'totalVolunteerHours',
|
||||
align: 'center',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '上一年度志愿服务时长',
|
||||
dataIndex: 'volunteerHours2024',
|
||||
key: 'volunteerHours2024',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '发展团员编号',
|
||||
dataIndex: 'memberNo',
|
||||
key: 'memberNo',
|
||||
align: 'center',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '近五年获得荣誉情况',
|
||||
dataIndex: 'honorsFiveYears',
|
||||
key: 'honorsFiveYears',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '主要事迹',
|
||||
dataIndex: 'mainStory',
|
||||
key: 'mainStory',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '学生社团指导教师意见/所在单位团委',
|
||||
dataIndex: 'leagueOpinion',
|
||||
key: 'leagueOpinion',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '业务指导单位意见/所在单位党组织',
|
||||
dataIndex: 'partyOpinion',
|
||||
key: 'partyOpinion',
|
||||
align: 'center',
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '校团委意见',
|
||||
dataIndex: 'schoolOpinion',
|
||||
key: 'schoolOpinion',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 260
|
||||
}
|
||||
]);
|
||||
|
||||
const createSheetDefault = (): YxgqtyForm => ({
|
||||
id: undefined,
|
||||
name: '',
|
||||
gender: '',
|
||||
nation: '',
|
||||
politics: '',
|
||||
birthMonth: '',
|
||||
joinTime: '',
|
||||
collegeMajorClass: '',
|
||||
position: '',
|
||||
volunteerRegTime: '',
|
||||
eduEval2024: '',
|
||||
smartSystem: '',
|
||||
contact: '',
|
||||
totalVolunteerHours: '',
|
||||
volunteerHours2024: '',
|
||||
memberNo: '',
|
||||
honorsFiveYears: '',
|
||||
mainStory: '',
|
||||
leagueOpinion: '',
|
||||
partyOpinion: '',
|
||||
schoolOpinion: ''
|
||||
});
|
||||
|
||||
const sheetForm = reactive<YxgqtyForm>(createSheetDefault());
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return userPageYxgqtyForm({
|
||||
page,
|
||||
limit,
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as YxgqtyFormParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const buildQueryParams = (): YxgqtyFormParam => {
|
||||
return {
|
||||
year: props.fixedYear ? String(props.fixedYear) : undefined,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as YxgqtyFormParam;
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
const resolveExportUrl = (payload: any) => {
|
||||
return String(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 exportYxgqtyForm(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 openReviewHistory = (record: YxgqtyForm) => {
|
||||
reviewCurrent.value = record;
|
||||
showReviewHistory.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (record: YxgqtyForm) => {
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const openEdit = (record?: YxgqtyForm) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as YxgqtyForm)
|
||||
: undefined;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openSheet = (record: YxgqtyForm) => {
|
||||
loadAll().catch((e: any) => {
|
||||
message.error(e.message || '下拉选项加载失败');
|
||||
});
|
||||
current.value = JSON.parse(JSON.stringify(record)) as YxgqtyForm;
|
||||
Object.assign(sheetForm, createSheetDefault(), current.value);
|
||||
activeKey.value = 'form';
|
||||
};
|
||||
|
||||
const remove = (record: YxgqtyForm) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeYxgqtyForm(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
if (sheetForm.id === record.id) {
|
||||
Object.assign(sheetForm, createSheetDefault());
|
||||
activeKey.value = 'list';
|
||||
}
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const saveSheet = () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
const saveOrUpdate = sheetForm.id ? updateYxgqtyForm : addYxgqtyForm;
|
||||
saveOrUpdate(sheetForm)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
activeKey.value = 'list';
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const resetSheet = () => {
|
||||
Object.assign(sheetForm, createSheetDefault(), current.value ?? {});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'YXGQTYForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
|
||||
.sheet-wrap {
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
padding: 16px 18px 20px;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sheet-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.sheet-table {
|
||||
width: 100%;
|
||||
min-width: 980px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.sheet-table td {
|
||||
border: 1px solid #3b3b3b;
|
||||
padding: 8px 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sheet-label {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sheet-value {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sheet-value.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sheet-value :deep(.ant-input),
|
||||
.sheet-value :deep(.ant-select-selector) {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sheet-value :deep(.ant-input) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sheet-value.left :deep(.ant-input) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sheet-value :deep(.ant-select-selector) {
|
||||
height: 24px;
|
||||
min-height: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sheet-value :deep(.ant-select-selection-item),
|
||||
.sheet-value :deep(.ant-select-selection-placeholder) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sheet-textarea {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sheet-textarea :deep(.ant-input) {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 4px 6px;
|
||||
background: transparent;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.sheet-tip {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.opinion-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.opinion-box {
|
||||
border: 1px dashed #6b6b6b;
|
||||
padding: 8px;
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.opinion-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.opinion-sign {
|
||||
text-align: right;
|
||||
margin-top: 8px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.opinion-sign.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sheet-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sheet-wrap {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.opinion-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,564 @@
|
||||
<template>
|
||||
<div class="challenge-stats-page">
|
||||
<a-card :bordered="false" class="hero-card">
|
||||
<div class="hero-wrap">
|
||||
<div>
|
||||
<div class="hero-kicker">CHALLENGE CUP ANALYTICS</div>
|
||||
<div class="hero-title">挑战杯数据统计</div>
|
||||
<div class="hero-desc">
|
||||
基于挑战杯申报表汇总申报作品、参与人数、指导老师和审核通过作品等关键指标,支持按年份查看趋势与结构分布。
|
||||
</div>
|
||||
</div>
|
||||
<a-space wrap>
|
||||
<a-select
|
||||
v-model:value="selectedYear"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
placeholder="全部年份"
|
||||
@change="loadStatistics"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in yearOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-button :loading="loading" @click="loadStatistics"
|
||||
>刷新数据</a-button
|
||||
>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<a-alert
|
||||
v-if="stats.awardCountDescription"
|
||||
class="summary-alert"
|
||||
type="info"
|
||||
show-icon
|
||||
:message="stats.awardCountDescription"
|
||||
/>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :xl="6">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">申报作品数量</div>
|
||||
<div class="metric-value">{{ summary.declarationCount }}</div>
|
||||
<div class="metric-foot">当前筛选范围内的项目总数</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :xl="6">
|
||||
<div class="metric-card accent">
|
||||
<div class="metric-label">参与申报人数</div>
|
||||
<div class="metric-value">{{ summary.participantCount }}</div>
|
||||
<div class="metric-foot">按团队成员去重统计</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :xl="6">
|
||||
<div class="metric-card warm">
|
||||
<div class="metric-label">指导老师数量</div>
|
||||
<div class="metric-value">{{ summary.advisorCount }}</div>
|
||||
<div class="metric-foot">按指导老师去重统计</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :xl="6">
|
||||
<div class="metric-card success">
|
||||
<div class="metric-label">获奖数量</div>
|
||||
<div class="metric-value">{{ summary.awardCount }}</div>
|
||||
<div class="metric-foot">当前按审核通过作品数统计</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="stats-row secondary-row">
|
||||
<a-col :xs="24" :sm="12" :xl="8">
|
||||
<div class="mini-metric-card">
|
||||
<span>申报学校数量</span>
|
||||
<b>{{ summary.schoolCount }}</b>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :xl="8">
|
||||
<div class="mini-metric-card">
|
||||
<span>项目类型数</span>
|
||||
<b>{{ stats.typeDistribution.length }}</b>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :xl="8">
|
||||
<div class="mini-metric-card">
|
||||
<span>项目分组数</span>
|
||||
<b>{{ stats.groupDistribution.length }}</b>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :xl="14">
|
||||
<a-card :bordered="false" class="section-card" title="年度申报趋势">
|
||||
<v-chart :option="yearChartOption" class="chart-box" autoresize />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :xl="10">
|
||||
<a-card :bordered="false" class="section-card" title="项目类型分布">
|
||||
<v-chart :option="typeChartOption" class="chart-box" autoresize />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :xl="12">
|
||||
<a-card :bordered="false" class="section-card" title="项目分组分布">
|
||||
<v-chart :option="groupChartOption" class="chart-box" autoresize />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :xl="12">
|
||||
<a-card :bordered="false" class="section-card" title="学校申报排行">
|
||||
<v-chart :option="schoolChartOption" class="chart-box" autoresize />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-card :bordered="false" class="section-card table-card">
|
||||
<template #title>
|
||||
<div class="table-title">
|
||||
<span>分布明细</span>
|
||||
<a-tag color="processing">
|
||||
{{ selectedYear ? `${selectedYear} 年` : '全部年份' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :xl="12">
|
||||
<a-table
|
||||
row-key="label"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
:columns="distributionColumns"
|
||||
:data-source="stats.typeDistribution"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :xs="24" :xl="12">
|
||||
<a-table
|
||||
row-key="label"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
:columns="distributionColumns"
|
||||
:data-source="stats.groupDistribution"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { use } from 'echarts/core';
|
||||
import type { EChartsCoreOption } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { BarChart, PieChart } from 'echarts/charts';
|
||||
import {
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
TooltipComponent
|
||||
} from 'echarts/components';
|
||||
import VChart from 'vue-echarts';
|
||||
import { getTzbcyStatistics } from '@/api/gxmu/tzbcy-form';
|
||||
import type {
|
||||
TzbcyStatisticItem,
|
||||
TzbcyStatisticsResult
|
||||
} from '@/api/gxmu/tzbcy-form/model';
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
BarChart,
|
||||
PieChart,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
TooltipComponent
|
||||
]);
|
||||
|
||||
const loading = ref(false);
|
||||
const selectedYear = ref<number>();
|
||||
const stats = reactive<TzbcyStatisticsResult>({
|
||||
availableYears: [],
|
||||
summary: {
|
||||
declarationCount: 0,
|
||||
participantCount: 0,
|
||||
advisorCount: 0,
|
||||
awardCount: 0,
|
||||
schoolCount: 0
|
||||
},
|
||||
awardCountDescription: '',
|
||||
yearDistribution: [],
|
||||
typeDistribution: [],
|
||||
groupDistribution: [],
|
||||
schoolDistribution: []
|
||||
});
|
||||
|
||||
const distributionColumns = [
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'label',
|
||||
key: 'label'
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
width: 120
|
||||
}
|
||||
];
|
||||
|
||||
const summary = computed(() => stats.summary);
|
||||
const yearOptions = computed(() =>
|
||||
stats.availableYears.map((item) => ({
|
||||
label: `${item} 年`,
|
||||
value: item
|
||||
}))
|
||||
);
|
||||
|
||||
const createEmptyOption = (title: string): EChartsCoreOption => ({
|
||||
title: {
|
||||
text: title,
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: {
|
||||
color: '#94a3b8',
|
||||
fontSize: 14,
|
||||
fontWeight: 500
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const yearChartOption = computed<EChartsCoreOption>(() => {
|
||||
if (!stats.yearDistribution.length) {
|
||||
return createEmptyOption('暂无年度数据');
|
||||
}
|
||||
return {
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: {
|
||||
left: 40,
|
||||
right: 20,
|
||||
top: 30,
|
||||
bottom: 30
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: stats.yearDistribution.map((item) => item.label),
|
||||
axisLine: {
|
||||
lineStyle: { color: '#cbd5e1' }
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
lineStyle: { color: '#e2e8f0' }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 36,
|
||||
data: stats.yearDistribution.map((item) => item.value),
|
||||
itemStyle: {
|
||||
borderRadius: [10, 10, 0, 0],
|
||||
color: '#2563eb'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const typeChartOption = computed<EChartsCoreOption>(() => {
|
||||
if (!stats.typeDistribution.length) {
|
||||
return createEmptyOption('暂无类型分布');
|
||||
}
|
||||
return {
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: {
|
||||
bottom: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 10
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['42%', '68%'],
|
||||
center: ['50%', '42%'],
|
||||
data: stats.typeDistribution.map((item) => ({
|
||||
name: item.label,
|
||||
value: item.value
|
||||
})),
|
||||
label: {
|
||||
formatter: '{b}\n{c}'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const groupChartOption = computed<EChartsCoreOption>(() => {
|
||||
if (!stats.groupDistribution.length) {
|
||||
return createEmptyOption('暂无分组分布');
|
||||
}
|
||||
const rows = [...stats.groupDistribution].reverse();
|
||||
return {
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
grid: {
|
||||
left: 120,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: 20
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
lineStyle: { color: '#e2e8f0' }
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: rows.map((item) => item.label),
|
||||
axisLine: {
|
||||
lineStyle: { color: '#cbd5e1' }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: rows.map((item) => item.value),
|
||||
barWidth: 20,
|
||||
itemStyle: {
|
||||
borderRadius: [0, 10, 10, 0],
|
||||
color: '#f59e0b'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const schoolChartOption = computed<EChartsCoreOption>(() => {
|
||||
if (!stats.schoolDistribution.length) {
|
||||
return createEmptyOption('暂无学校排行');
|
||||
}
|
||||
const rows = [...stats.schoolDistribution].reverse();
|
||||
return {
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
grid: {
|
||||
left: 110,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: 20
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
lineStyle: { color: '#e2e8f0' }
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: rows.map((item) => item.label),
|
||||
axisLine: {
|
||||
lineStyle: { color: '#cbd5e1' }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: rows.map((item) => item.value),
|
||||
barWidth: 18,
|
||||
itemStyle: {
|
||||
borderRadius: [0, 10, 10, 0],
|
||||
color: '#10b981'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const assignItems = (
|
||||
target: TzbcyStatisticItem[],
|
||||
source?: TzbcyStatisticItem[]
|
||||
) => {
|
||||
target.splice(0, target.length, ...(source ?? []));
|
||||
};
|
||||
|
||||
const loadStatistics = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getTzbcyStatistics({
|
||||
year: selectedYear.value
|
||||
});
|
||||
stats.year = data.year;
|
||||
stats.availableYears = data.availableYears ?? [];
|
||||
stats.summary = data.summary ?? stats.summary;
|
||||
stats.awardCountDescription = data.awardCountDescription || '';
|
||||
assignItems(stats.yearDistribution, data.yearDistribution);
|
||||
assignItems(stats.typeDistribution, data.typeDistribution);
|
||||
assignItems(stats.groupDistribution, data.groupDistribution);
|
||||
assignItems(stats.schoolDistribution, data.schoolDistribution);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '获取挑战杯统计数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadStatistics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'GxmuChallengeCupStats'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.challenge-stats-page {
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
background: radial-gradient(
|
||||
circle at top left,
|
||||
rgba(37, 99, 235, 0.12),
|
||||
transparent 24%
|
||||
),
|
||||
linear-gradient(180deg, #f7fbff 0%, #fffdf8 100%);
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.section-card {
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.hero-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.hero-kicker {
|
||||
margin-bottom: 8px;
|
||||
color: #5b7cc7;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.22em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: #102a43;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
max-width: 760px;
|
||||
margin-top: 12px;
|
||||
color: #52637a;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.summary-alert {
|
||||
margin-top: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.secondary-row {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
height: 100%;
|
||||
padding: 22px 24px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
|
||||
border: 1px solid #dbeafe;
|
||||
}
|
||||
|
||||
.metric-card.accent {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 100%);
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
|
||||
.metric-card.warm {
|
||||
background: linear-gradient(135deg, #fff7ed 0%, #ffffff 100%);
|
||||
border-color: #fed7aa;
|
||||
}
|
||||
|
||||
.metric-card.success {
|
||||
background: linear-gradient(135deg, #ecfeff 0%, #ffffff 100%);
|
||||
border-color: #a5f3fc;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
margin-top: 10px;
|
||||
color: #0f172a;
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.metric-foot {
|
||||
margin-top: 10px;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mini-metric-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 20px;
|
||||
border-radius: 18px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.mini-metric-card b {
|
||||
color: #0f172a;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.challenge-stats-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.hero-wrap {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div class="class-table">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:scroll="{ x: 1450 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space style="flex-wrap: wrap">
|
||||
<a-button type="primary" @click="openEdit()">
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>新增班级</span>
|
||||
</a-button>
|
||||
<a-input-search
|
||||
v-model:value="searchText"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
placeholder="班级名称/编码"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-select
|
||||
v-if="showOrganizationFilter"
|
||||
v-model:value="selectedOrganizationId"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 220px"
|
||||
placeholder="所属机构"
|
||||
:options="organizationOptions"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="status"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
placeholder="状态"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<a-button type="primary" @click="reload">查询</a-button>
|
||||
<a-button @click="resetSearch">重置</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '启用' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此班级吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:default-college-id="currentDefaultOrganizationId"
|
||||
:lock-college-id="lockOrganization"
|
||||
@done="reload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import EditModal from './edit.vue';
|
||||
import { pageClassInfo, removeClassInfo } from '@/api/gxmu/class';
|
||||
import { listOrganizations } from '@/api/system/organization';
|
||||
import type { ClassInfo, ClassInfoParam } from '@/api/gxmu/class/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
organizationId?: number;
|
||||
showOrganizationFilter?: boolean;
|
||||
lockOrganization?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showOrganizationFilter: true,
|
||||
lockOrganization: false
|
||||
}
|
||||
);
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const selectedOrganizationId = ref<number | undefined>(props.organizationId);
|
||||
const status = ref<number>();
|
||||
const showEdit = ref(false);
|
||||
const current = ref<ClassInfo>();
|
||||
const organizationOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 }
|
||||
];
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '班级名称',
|
||||
dataIndex: 'className',
|
||||
key: 'className',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '班级编码',
|
||||
dataIndex: 'classCode',
|
||||
key: 'classCode',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '所属机构',
|
||||
dataIndex: 'collegeName',
|
||||
key: 'collegeName',
|
||||
width: 220
|
||||
},
|
||||
{
|
||||
title: '年级',
|
||||
dataIndex: 'gradeYear',
|
||||
key: 'gradeYear',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '辅导员',
|
||||
dataIndex: 'counselorName',
|
||||
key: 'counselorName',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'counselorPhone',
|
||||
key: 'counselorPhone',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '学生人数',
|
||||
dataIndex: 'studentCount',
|
||||
key: 'studentCount',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sortNumber',
|
||||
key: 'sortNumber',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
sorter: true,
|
||||
customRender: ({ text }) => (text ? toDateString(text) : '-')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 140
|
||||
}
|
||||
]);
|
||||
|
||||
const currentDefaultOrganizationId = computed(
|
||||
() => props.organizationId ?? selectedOrganizationId.value
|
||||
);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
return pageClassInfo({
|
||||
...(where || {}),
|
||||
...orders,
|
||||
page,
|
||||
limit,
|
||||
keywords: searchText.value.trim() || undefined,
|
||||
collegeId: currentDefaultOrganizationId.value,
|
||||
status: status.value
|
||||
} as ClassInfoParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const loadOrganizations = () => {
|
||||
listOrganizations()
|
||||
.then((list) => {
|
||||
organizationOptions.value = list.map((item) => ({
|
||||
label: item.organizationFullName || item.organizationName || '',
|
||||
value: item.organizationId || 0
|
||||
}));
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
searchText.value = '';
|
||||
selectedOrganizationId.value = props.organizationId;
|
||||
status.value = undefined;
|
||||
reload();
|
||||
};
|
||||
|
||||
const openEdit = (record?: ClassInfo) => {
|
||||
current.value = record ? { ...record } : void 0;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: ClassInfo) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeClassInfo(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.organizationId,
|
||||
(value) => {
|
||||
selectedOrganizationId.value = value;
|
||||
reload();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
loadOrganizations();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
reload
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="700"
|
||||
: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="collegeId"
|
||||
:rules="[{ required: true, message: '请选择所属机构' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="form.collegeId"
|
||||
:disabled="lockCollegeId"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="请选择所属机构"
|
||||
:options="organizationOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="班级名称"
|
||||
name="className"
|
||||
:rules="[{ required: true, message: '请输入班级名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.className"
|
||||
placeholder="请输入班级名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="班级编码" name="classCode">
|
||||
<a-input
|
||||
v-model:value="form.classCode"
|
||||
placeholder="请输入班级编码"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="年级" name="gradeYear">
|
||||
<a-input-number
|
||||
v-model:value="form.gradeYear"
|
||||
:min="2000"
|
||||
:max="2100"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="请输入年级"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="辅导员" name="counselorName">
|
||||
<a-input
|
||||
v-model:value="form.counselorName"
|
||||
placeholder="请输入辅导员"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="counselorPhone">
|
||||
<a-input
|
||||
v-model:value="form.counselorPhone"
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="学生人数" name="studentCount">
|
||||
<a-input-number
|
||||
v-model:value="form.studentCount"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="请输入学生人数"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="排序" name="sortNumber">
|
||||
<a-input-number
|
||||
v-model:value="form.sortNumber"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="请输入排序"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="1">启用</a-radio>
|
||||
<a-radio :value="0">停用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="form.remark"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { addClassInfo, updateClassInfo } from '@/api/gxmu/class';
|
||||
import { listOrganizations } from '@/api/system/organization';
|
||||
import type { ClassInfo } from '@/api/gxmu/class/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: ClassInfo | null;
|
||||
defaultCollegeId?: number;
|
||||
lockCollegeId?: boolean;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const organizationOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const form = reactive<ClassInfo>({
|
||||
id: undefined,
|
||||
collegeId: undefined,
|
||||
classCode: '',
|
||||
className: '',
|
||||
gradeYear: undefined,
|
||||
counselorName: '',
|
||||
counselorPhone: '',
|
||||
studentCount: 0,
|
||||
sortNumber: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
form.id = undefined;
|
||||
form.collegeId = props.defaultCollegeId;
|
||||
form.classCode = '';
|
||||
form.className = '';
|
||||
form.gradeYear = undefined;
|
||||
form.counselorName = '';
|
||||
form.counselorPhone = '';
|
||||
form.studentCount = 0;
|
||||
form.sortNumber = 0;
|
||||
form.status = 1;
|
||||
form.remark = '';
|
||||
};
|
||||
|
||||
const loadOrganizationOptions = () => {
|
||||
listOrganizations()
|
||||
.then((list) => {
|
||||
organizationOptions.value = list.map((item) => ({
|
||||
label: item.organizationFullName || item.organizationName || '',
|
||||
value: item.organizationId || 0
|
||||
}));
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const request = isUpdate.value ? updateClassInfo : addClassInfo;
|
||||
request({ ...form })
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
loadOrganizationOptions();
|
||||
if (props.data) {
|
||||
isUpdate.value = true;
|
||||
Object.assign(form, {
|
||||
...props.data
|
||||
});
|
||||
if (props.lockCollegeId && props.defaultCollegeId != null) {
|
||||
form.collegeId = props.defaultCollegeId;
|
||||
}
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
resetForm();
|
||||
}
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,216 @@
|
||||
<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="请先下载模板,按模板填写后上传。导入时会校验所属学院是否存在,不存在则自动创建,上级ID固定为24。"
|
||||
/>
|
||||
<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 { read, utils, writeFile } from 'xlsx';
|
||||
import { importClassInfo } from '@/api/gxmu/class';
|
||||
import type { ClassImportItem } from '@/api/gxmu/class/model';
|
||||
|
||||
const TEMPLATE_HEADERS = [
|
||||
'所属学院',
|
||||
'班级名称',
|
||||
'班级编码',
|
||||
'年级',
|
||||
'辅导员',
|
||||
'联系电话',
|
||||
'学生人数',
|
||||
'排序',
|
||||
'状态',
|
||||
'备注'
|
||||
];
|
||||
|
||||
const TEMPLATE_SAMPLE = [
|
||||
'第一临床医学院',
|
||||
'2022级临床1班',
|
||||
'2022LC01',
|
||||
'2022',
|
||||
'张老师',
|
||||
'13800138000',
|
||||
'45',
|
||||
'1',
|
||||
'启用',
|
||||
'示例数据'
|
||||
];
|
||||
|
||||
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 toNumber = (value: unknown) => {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!text) {
|
||||
return void 0;
|
||||
}
|
||||
const result = Number(text);
|
||||
return Number.isFinite(result) ? result : void 0;
|
||||
};
|
||||
|
||||
const toStatus = (value: unknown) => {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!text) {
|
||||
return 1;
|
||||
}
|
||||
if (['1', '启用', '是', '正常'].includes(text)) {
|
||||
return 1;
|
||||
}
|
||||
if (['0', '停用', '否', '禁用'].includes(text)) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
const downloadTemplate = () => {
|
||||
const sheet = utils.aoa_to_sheet([TEMPLATE_HEADERS, TEMPLATE_SAMPLE]);
|
||||
sheet['!cols'] = TEMPLATE_HEADERS.map((title) => ({
|
||||
wch: Math.max(String(title).length + 4, 16)
|
||||
}));
|
||||
writeFile(
|
||||
{
|
||||
SheetNames: ['班级导入模板'],
|
||||
Sheets: {
|
||||
班级导入模板: sheet
|
||||
}
|
||||
},
|
||||
'班级导入模板.xlsx'
|
||||
);
|
||||
};
|
||||
|
||||
const buildRows = (rows: Record<string, any>[]) => {
|
||||
return rows
|
||||
.map<ClassImportItem>((row) => ({
|
||||
collegeName: String(row['所属学院'] || '').trim(),
|
||||
className: String(row['班级名称'] || '').trim(),
|
||||
classCode: String(row['班级编码'] || '').trim(),
|
||||
gradeYear: toNumber(row['年级']),
|
||||
counselorName: String(row['辅导员'] || '').trim(),
|
||||
counselorPhone: String(row['联系电话'] || '').trim(),
|
||||
studentCount: toNumber(row['学生人数']),
|
||||
sortNumber: toNumber(row['排序']) ?? 0,
|
||||
status: toStatus(row['状态']),
|
||||
remark: String(row['备注'] || '').trim()
|
||||
}))
|
||||
.filter((item) =>
|
||||
[
|
||||
item.collegeName,
|
||||
item.className,
|
||||
item.classCode,
|
||||
item.counselorName,
|
||||
item.remark
|
||||
].some((value) => !!String(value || '').trim())
|
||||
);
|
||||
};
|
||||
|
||||
const validateRows = (rows: ClassImportItem[]) => {
|
||||
const errors: string[] = [];
|
||||
rows.forEach((item, index) => {
|
||||
const rowNo = index + 2;
|
||||
if (!item.collegeName) {
|
||||
errors.push(`第 ${rowNo} 行缺少所属学院`);
|
||||
}
|
||||
if (!item.className) {
|
||||
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 = await readFileBuffer(file);
|
||||
const workbook = read(buffer, { type: 'array' });
|
||||
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
const rows = utils.sheet_to_json<Record<string, any>>(sheet, {
|
||||
defval: ''
|
||||
});
|
||||
if (!rows.length) {
|
||||
throw new Error('导入文件内容为空');
|
||||
}
|
||||
const data = buildRows(rows);
|
||||
if (!data.length) {
|
||||
throw new Error('未识别到有效数据');
|
||||
}
|
||||
const errors = validateRows(data);
|
||||
if (errors.length) {
|
||||
throw new Error(errors.slice(0, 5).join(';'));
|
||||
}
|
||||
const msg = await importClassInfo(data);
|
||||
message.success(msg);
|
||||
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,19 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<class-table />
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ClassTable from './components/class-table.vue';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ClassManagement'
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="640"
|
||||
: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="collegeName"
|
||||
:rules="[{ required: true, message: '请输入学院名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.collegeName"
|
||||
placeholder="请输入学院名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="学院编码"
|
||||
name="collegeCode"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.collegeCode"
|
||||
placeholder="请输入学院编码"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="学院简称" name="shortName">
|
||||
<a-input
|
||||
v-model:value="form.shortName"
|
||||
placeholder="请输入学院简称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="负责人" name="leaderName">
|
||||
<a-input
|
||||
v-model:value="form.leaderName"
|
||||
placeholder="请输入负责人"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="leaderPhone">
|
||||
<a-input
|
||||
v-model:value="form.leaderPhone"
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="排序" name="sortNumber">
|
||||
<a-input-number
|
||||
v-model:value="form.sortNumber"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="请输入排序"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="1">启用</a-radio>
|
||||
<a-radio :value="0">停用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="form.remark"
|
||||
:rows="3"
|
||||
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 { addCollege, updateCollege } from '@/api/gxmu/college';
|
||||
import type { College } from '@/api/gxmu/college/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: College | null;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const form = reactive<College>({
|
||||
id: undefined,
|
||||
collegeCode: '',
|
||||
collegeName: '',
|
||||
shortName: '',
|
||||
leaderName: '',
|
||||
leaderPhone: '',
|
||||
sortNumber: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
form.id = undefined;
|
||||
form.collegeCode = '';
|
||||
form.collegeName = '';
|
||||
form.shortName = '';
|
||||
form.leaderName = '';
|
||||
form.leaderPhone = '';
|
||||
form.sortNumber = 0;
|
||||
form.status = 1;
|
||||
form.remark = '';
|
||||
};
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const request = isUpdate.value ? updateCollege : addCollege;
|
||||
request({ ...form })
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
isUpdate.value = true;
|
||||
Object.assign(form, {
|
||||
...props.data
|
||||
});
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
resetForm();
|
||||
}
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,210 @@
|
||||
<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: 1200 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space style="flex-wrap: wrap">
|
||||
<a-button type="primary" @click="openEdit()">
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>新增学院</span>
|
||||
</a-button>
|
||||
<a-input-search
|
||||
v-model:value="searchText"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
placeholder="学院名称/编码"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="status"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
placeholder="状态"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<a-button type="primary" @click="reload">查询</a-button>
|
||||
<a-button @click="resetSearch">重置</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '启用' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此学院吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import EditModal from './components/edit.vue';
|
||||
import { pageCollege, removeCollege } from '@/api/gxmu/college';
|
||||
import type { College, CollegeParam } from '@/api/gxmu/college/model';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const status = ref<number>();
|
||||
const showEdit = ref(false);
|
||||
const current = ref<College>();
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 }
|
||||
];
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '学院名称',
|
||||
dataIndex: 'collegeName',
|
||||
key: 'collegeName',
|
||||
width: 220
|
||||
},
|
||||
{
|
||||
title: '学院编码',
|
||||
dataIndex: 'collegeCode',
|
||||
key: 'collegeCode',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '学院简称',
|
||||
dataIndex: 'shortName',
|
||||
key: 'shortName',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'leaderName',
|
||||
key: 'leaderName',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'leaderPhone',
|
||||
key: 'leaderPhone',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '班级数',
|
||||
dataIndex: 'classCount',
|
||||
key: 'classCount',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sortNumber',
|
||||
key: 'sortNumber',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
sorter: true,
|
||||
customRender: ({ text }) => (text ? toDateString(text) : '-')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 140
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
return pageCollege({
|
||||
...(where || {}),
|
||||
...orders,
|
||||
page,
|
||||
limit,
|
||||
keywords: searchText.value.trim() || undefined,
|
||||
status: status.value
|
||||
} as CollegeParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
searchText.value = '';
|
||||
status.value = undefined;
|
||||
reload();
|
||||
};
|
||||
|
||||
const openEdit = (record?: College) => {
|
||||
current.value = record;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const remove = (record: College) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeCollege(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'CollegeManagement'
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,460 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :lg="9">
|
||||
<a-card :bordered="false" :body-style="{ padding: '18px' }">
|
||||
<div class="panel-title">历史竞赛文件入库</div>
|
||||
<div class="panel-desc">
|
||||
批量上传历史优秀项目申报书,系统将完成解析、脱敏和向量化入库。
|
||||
</div>
|
||||
|
||||
<a-upload-dragger
|
||||
multiple
|
||||
accept=".pdf,.doc,.docx"
|
||||
:file-list="historyFileList"
|
||||
:before-upload="beforeHistoryUpload"
|
||||
@remove="removeHistoryFile"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<cloud-upload-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">上传历史项目书文件</p>
|
||||
<p class="ant-upload-hint">支持 PDF、DOC、DOCX,可多选</p>
|
||||
</a-upload-dragger>
|
||||
|
||||
<div class="field-label">历史评分/评语 CSV</div>
|
||||
<a-upload
|
||||
accept=".csv,.xls,.xlsx"
|
||||
:file-list="scoreFileList"
|
||||
:before-upload="beforeScoreUpload"
|
||||
:max-count="1"
|
||||
@remove="removeScoreFile"
|
||||
>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<upload-outlined />
|
||||
</template>
|
||||
上传评分文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
|
||||
<div class="field-label">手动评分和评价指导</div>
|
||||
<a-textarea
|
||||
v-model:value="manualText"
|
||||
:rows="5"
|
||||
allow-clear
|
||||
placeholder="可输入评分标准、评语参考或入库补充说明"
|
||||
/>
|
||||
|
||||
<div class="action-row">
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!historyFiles.length"
|
||||
@click="submitPreprocess"
|
||||
>
|
||||
开始入库
|
||||
</a-button>
|
||||
<a-button :disabled="submitting" @click="resetUpload">重置</a-button>
|
||||
</div>
|
||||
|
||||
<div v-if="preprocessText" class="preprocess-result">
|
||||
<div class="result-head">
|
||||
<span>处理结果</span>
|
||||
<a-button size="small" type="link" @click="copyPreprocessResult">
|
||||
复制
|
||||
</a-button>
|
||||
</div>
|
||||
<pre>{{ preprocessText }}</pre>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :lg="15">
|
||||
<a-card :bordered="false" :body-style="{ padding: '18px' }">
|
||||
<div class="table-toolbar">
|
||||
<div>
|
||||
<div class="panel-title">挑战杯历史案例知识库</div>
|
||||
<div class="panel-desc">查看已入库文档,可按文档名称搜索。</div>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
placeholder="搜索文档名称"
|
||||
style="width: 220px"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-button :loading="loading" @click="reload">
|
||||
<template #icon>
|
||||
<reload-outlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data-source="records"
|
||||
:pagination="pagination"
|
||||
:scroll="{ x: 900 }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="doc-name">{{ record.name || '-' }}</div>
|
||||
<div class="doc-id">{{ record.id }}</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record)">
|
||||
{{ getStatusText(record) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'enabled'">
|
||||
<a-tag :color="record.enabled === false ? 'default' : 'success'">
|
||||
{{ record.enabled === false ? '停用' : '启用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'word_count'">
|
||||
{{ formatNumber(record.word_count) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'created_at'">
|
||||
{{ formatDate(record.created_at) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { TablePaginationConfig, UploadProps } from 'ant-design-vue';
|
||||
import {
|
||||
CloudUploadOutlined,
|
||||
UploadOutlined,
|
||||
ReloadOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import {
|
||||
listChallengeCupDocuments,
|
||||
preprocessCompetitionHistoryFiles
|
||||
} from '@/api/ai/competitionHistory';
|
||||
import type {
|
||||
ChallengeCupDocument,
|
||||
CompetitionHistoryPreprocessResult
|
||||
} from '@/api/ai/competitionHistory/model';
|
||||
import { copyText } from '@/utils/common';
|
||||
|
||||
const historyFiles = ref<File[]>([]);
|
||||
const scoreFile = ref<File>();
|
||||
const manualText = ref('');
|
||||
const submitting = ref(false);
|
||||
const loading = ref(false);
|
||||
const keyword = ref('');
|
||||
const records = ref<ChallengeCupDocument[]>([]);
|
||||
const preprocessResult = ref<CompetitionHistoryPreprocessResult | null>(null);
|
||||
const pagination = reactive<TablePaginationConfig>({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '文档',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 320,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '启用状态',
|
||||
key: 'enabled',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '字数',
|
||||
dataIndex: 'word_count',
|
||||
key: 'word_count',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
align: 'center',
|
||||
width: 180
|
||||
}
|
||||
];
|
||||
|
||||
const historyFileList = computed(() =>
|
||||
historyFiles.value.map((file, index) => ({
|
||||
uid: `${index}-${file.name}`,
|
||||
name: file.name,
|
||||
status: 'done' as const
|
||||
}))
|
||||
);
|
||||
|
||||
const scoreFileList = computed(() =>
|
||||
scoreFile.value
|
||||
? [
|
||||
{
|
||||
uid: 'score',
|
||||
name: scoreFile.value.name,
|
||||
status: 'done' as const
|
||||
}
|
||||
]
|
||||
: []
|
||||
);
|
||||
|
||||
const preprocessText = computed(() => {
|
||||
const result = preprocessResult.value;
|
||||
if (!result) {
|
||||
return '';
|
||||
}
|
||||
if (result.answer) {
|
||||
return result.answer;
|
||||
}
|
||||
if (result.outputs) {
|
||||
return JSON.stringify(result.outputs, null, 2);
|
||||
}
|
||||
return JSON.stringify(result, null, 2);
|
||||
});
|
||||
|
||||
const beforeHistoryUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
if (file.size / 1024 / 1024 > 100) {
|
||||
message.error('单个历史项目书文件不能超过 100MB');
|
||||
return false;
|
||||
}
|
||||
historyFiles.value = [...historyFiles.value, file as File];
|
||||
return false;
|
||||
};
|
||||
|
||||
const beforeScoreUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
if (file.size / 1024 / 1024 > 50) {
|
||||
message.error('评分文件不能超过 50MB');
|
||||
return false;
|
||||
}
|
||||
scoreFile.value = file as File;
|
||||
return false;
|
||||
};
|
||||
|
||||
const removeHistoryFile = (file: any) => {
|
||||
historyFiles.value = historyFiles.value.filter(
|
||||
(item, index) => `${index}-${item.name}` !== file.uid
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
const removeScoreFile = () => {
|
||||
scoreFile.value = undefined;
|
||||
return true;
|
||||
};
|
||||
|
||||
const resetUpload = () => {
|
||||
historyFiles.value = [];
|
||||
scoreFile.value = undefined;
|
||||
manualText.value = '';
|
||||
preprocessResult.value = null;
|
||||
};
|
||||
|
||||
const submitPreprocess = async () => {
|
||||
if (!historyFiles.value.length) {
|
||||
message.warning('请先上传历史项目书文件');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
preprocessResult.value = null;
|
||||
try {
|
||||
const result = await preprocessCompetitionHistoryFiles(
|
||||
historyFiles.value,
|
||||
scoreFile.value,
|
||||
manualText.value
|
||||
);
|
||||
preprocessResult.value = result || null;
|
||||
message.success('历史竞赛文件预处理已完成');
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '历史竞赛文件预处理失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const reload = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await listChallengeCupDocuments({
|
||||
page: Number(pagination.current || 1),
|
||||
limit: Number(pagination.pageSize || 20),
|
||||
keyword: keyword.value.trim() || undefined
|
||||
});
|
||||
records.value = result?.data || [];
|
||||
pagination.total = result?.total || records.value.length;
|
||||
pagination.current = result?.page || pagination.current || 1;
|
||||
pagination.pageSize = result?.limit || pagination.pageSize || 20;
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '获取知识库文档失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTableChange = (page: TablePaginationConfig) => {
|
||||
pagination.current = page.current || 1;
|
||||
pagination.pageSize = page.pageSize || 20;
|
||||
reload();
|
||||
};
|
||||
|
||||
const getStatusText = (record: ChallengeCupDocument) => {
|
||||
return (
|
||||
record.display_status ||
|
||||
record.indexing_status ||
|
||||
(record.archived ? '已归档' : '正常')
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusColor = (record: ChallengeCupDocument) => {
|
||||
const text = getStatusText(record);
|
||||
if (/error|fail|失败|错误/.test(text)) {
|
||||
return 'error';
|
||||
}
|
||||
if (/indexing|processing|处理中|解析中/.test(text)) {
|
||||
return 'processing';
|
||||
}
|
||||
if (/completed|available|正常|完成|成功/.test(text)) {
|
||||
return 'success';
|
||||
}
|
||||
return 'default';
|
||||
};
|
||||
|
||||
const formatNumber = (value?: number) => {
|
||||
return typeof value === 'number' ? value.toLocaleString() : '-';
|
||||
};
|
||||
|
||||
const formatDate = (value?: number) => {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
return new Date(value * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
const copyPreprocessResult = () => {
|
||||
if (!preprocessText.value) {
|
||||
message.warning('暂无可复制内容');
|
||||
return;
|
||||
}
|
||||
copyText(preprocessText.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'CompetitionHistoryFiles'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.panel-desc {
|
||||
margin: 6px 0 16px;
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
margin: 18px 0 8px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.82);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.preprocess-result {
|
||||
margin-top: 18px;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.result-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.preprocess-result pre {
|
||||
max-height: 260px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
color: rgba(0, 0, 0, 0.78);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.doc-name {
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.84);
|
||||
}
|
||||
|
||||
.doc-id {
|
||||
margin-top: 4px;
|
||||
color: rgba(0, 0, 0, 0.42);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table-toolbar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.table-toolbar :deep(.ant-space) {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<a-cascader
|
||||
:value="innerValue"
|
||||
:options="classCascaderOptions"
|
||||
:allow-clear="allowClear"
|
||||
:show-search="showSearch"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
style="width: 100%"
|
||||
@update:value="updateValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import type { ValueType } from 'ant-design-vue/es/vc-cascader/Cascader';
|
||||
import {
|
||||
useGxmuFormSelectOptions,
|
||||
type ClassCascadeSelection
|
||||
} from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
mode?: 'class' | 'collegeClass';
|
||||
placeholder?: string;
|
||||
allowClear?: boolean;
|
||||
showSearch?: boolean;
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{
|
||||
mode: 'collegeClass',
|
||||
allowClear: true,
|
||||
showSearch: true
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value?: string): void;
|
||||
(e: 'change', value?: ClassCascadeSelection): void;
|
||||
}>();
|
||||
|
||||
const innerValue = ref<string[]>();
|
||||
const {
|
||||
classCascaderOptions,
|
||||
loadClassCascaderOptions,
|
||||
findClassCascadeSelection,
|
||||
getClassCascadeSelectionByPath
|
||||
} = useGxmuFormSelectOptions();
|
||||
|
||||
const syncInnerValue = () => {
|
||||
const selection = findClassCascadeSelection(props.value, props.mode);
|
||||
innerValue.value = selection?.path;
|
||||
};
|
||||
|
||||
const ensureLoaded = async () => {
|
||||
await loadClassCascaderOptions();
|
||||
syncInnerValue();
|
||||
};
|
||||
|
||||
const updateValue = (value: ValueType) => {
|
||||
const path = (value as string[] | undefined)?.filter(Boolean);
|
||||
if (!path?.length) {
|
||||
innerValue.value = void 0;
|
||||
emit('update:value', void 0);
|
||||
emit('change', void 0);
|
||||
return;
|
||||
}
|
||||
|
||||
innerValue.value = path;
|
||||
const selection = getClassCascadeSelectionByPath(path);
|
||||
if (!selection) {
|
||||
emit('update:value', void 0);
|
||||
emit('change', void 0);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
'update:value',
|
||||
props.mode === 'class' ? selection.className : selection.combinedLabel
|
||||
);
|
||||
emit('change', selection);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
syncInnerValue();
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
ensureLoaded().catch(() => {
|
||||
innerValue.value = void 0;
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="560"
|
||||
:footer="null"
|
||||
:title="`导入${moduleName}`"
|
||||
: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="`${moduleName}导入说明`"
|
||||
:description="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 { computed, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||
import { read, utils, writeFile } from 'xlsx';
|
||||
|
||||
type ImportItem = Record<string, any>;
|
||||
|
||||
export interface ImportFieldConfig {
|
||||
header: string;
|
||||
key: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
moduleName: string;
|
||||
fileName: string;
|
||||
templateFields: ImportFieldConfig[];
|
||||
sampleRow: Record<string, any>;
|
||||
importDescription?: string;
|
||||
createPayload: (row: Record<string, any>, rowIndex: number) => ImportItem;
|
||||
saveItem: (item: ImportItem) => Promise<unknown>;
|
||||
validateItem?: (item: ImportItem, rowIndex: number) => string[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const description = computed(
|
||||
() =>
|
||||
props.importDescription ||
|
||||
`请先下载模板,按模板表头填写后上传。必填列请勿留空。`
|
||||
);
|
||||
|
||||
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 downloadTemplate = () => {
|
||||
const headers = props.templateFields.map((item) => item.header);
|
||||
const sample = headers.map((header) => props.sampleRow[header] ?? '');
|
||||
const sheet = utils.aoa_to_sheet([headers, sample]);
|
||||
sheet['!cols'] = headers.map((title) => ({
|
||||
wch: Math.max(String(title).length + 4, 18)
|
||||
}));
|
||||
writeFile(
|
||||
{
|
||||
SheetNames: ['导入模板'],
|
||||
Sheets: {
|
||||
导入模板: sheet
|
||||
}
|
||||
},
|
||||
props.fileName
|
||||
);
|
||||
};
|
||||
|
||||
const validateRows = (rows: ImportItem[]) => {
|
||||
const errors: string[] = [];
|
||||
rows.forEach((item, index) => {
|
||||
const rowNo = index + 2;
|
||||
props.templateFields.forEach((field) => {
|
||||
if (field.required && !String(item[field.key] ?? '').trim()) {
|
||||
errors.push(`第 ${rowNo} 行缺少${field.header}`);
|
||||
}
|
||||
});
|
||||
if (props.validateItem) {
|
||||
errors.push(...props.validateItem(item, 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 = await readFileBuffer(file);
|
||||
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((row, index) => props.createPayload(row, index + 2))
|
||||
.filter((item) =>
|
||||
props.templateFields.some((field) =>
|
||||
!!String(item[field.key] ?? '').trim()
|
||||
)
|
||||
);
|
||||
if (!data.length) {
|
||||
throw new Error('未识别到有效数据');
|
||||
}
|
||||
const errors = validateRows(data);
|
||||
if (errors.length) {
|
||||
throw new Error(errors.slice(0, 5).join(';'));
|
||||
}
|
||||
for (const item of data) {
|
||||
await props.saveItem(item);
|
||||
}
|
||||
message.success(`成功导入 ${data.length} 条${props.moduleName}数据`);
|
||||
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,133 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="860"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="title || '审核历史'"
|
||||
:body-style="{ paddingBottom: '20px' }"
|
||||
@update:visible="updateVisible"
|
||||
okText="关闭"
|
||||
@ok="updateVisible(false)"
|
||||
>
|
||||
<a-space style="margin-bottom: 12px">
|
||||
<span>当前审核状态:</span>
|
||||
<ReviewStatusTag :review-list="list" />
|
||||
<a-button size="small" :loading="loading" @click="refresh">
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
|
||||
<div style="overflow-y: auto; padding: 10px 0" class="ele-scrollbar-hover">
|
||||
<a-timeline v-if="displayList.length > 0">
|
||||
<a-timeline-item
|
||||
v-for="item in displayList"
|
||||
:key="item.id"
|
||||
:color="item.status === 1 ? 'green' : item.status === 0 ? 'red' : 'gray'"
|
||||
>
|
||||
<div style="margin-bottom: 8px">
|
||||
<span style="color: rgba(0, 0, 0, 0.45)">
|
||||
{{ toDateString(item.createTime, 'yyyy-MM-dd HH:mm:ss') }}
|
||||
</span>
|
||||
<a-tag
|
||||
:color="item.status === 1 ? 'success' : item.status === 2 ? 'error' : 'default'"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ item.status === 1 ? '通过' : item.status === 2 ? '不通过' : '待审核' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div v-if="item.user?.realName" style="margin-bottom: 4px">
|
||||
<span style="font-weight: 500">审核人:</span>
|
||||
<span>{{ item.user.realName }}</span>
|
||||
</div>
|
||||
<div v-if="item.content" style="color: rgba(0, 0, 0, 0.85)">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<a-empty v-else description="暂无审核记录" :image="simpleImage" />
|
||||
</div>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { message, Empty } from 'ant-design-vue';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import { listReviewList } from '@/api/gxmu/reviewList';
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
import ReviewStatusTag from './reviewStatusTag.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
title?: string;
|
||||
module?: string;
|
||||
pk?: number;
|
||||
reviewList?: ReviewList[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const maxable = ref(true);
|
||||
const list = ref<ReviewList[]>([]);
|
||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const displayList = computed(() => {
|
||||
const getTime = (value?: string) => {
|
||||
const t = Date.parse(value || '');
|
||||
return Number.isFinite(t) ? t : 0;
|
||||
};
|
||||
return [...(list.value ?? [])].sort((a, b) => {
|
||||
const diff = getTime(b.createTime) - getTime(a.createTime);
|
||||
if (diff !== 0) return diff;
|
||||
return (b.id || 0) - (a.id || 0);
|
||||
});
|
||||
});
|
||||
|
||||
const refresh = () => {
|
||||
if (!props.module || !props.pk) {
|
||||
list.value = props.reviewList ? [...props.reviewList] : [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
listReviewList({
|
||||
module: props.module,
|
||||
pk: props.pk
|
||||
})
|
||||
.then((data) => {
|
||||
loading.value = false;
|
||||
list.value = data || [];
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
list.value = props.reviewList ? [...props.reviewList] : [];
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
list.value = props.reviewList ? [...props.reviewList] : [];
|
||||
refresh();
|
||||
} else {
|
||||
list.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ele-scrollbar-hover :deep(.ant-timeline-item-last > .ant-timeline-item-content) {
|
||||
min-height: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<template v-if="latest?.status === 1">
|
||||
<a-tag color="green">通过</a-tag>
|
||||
</template>
|
||||
<template v-else-if="latest?.status === 2">
|
||||
<a-tag color="red">不通过</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>待审核</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
|
||||
const props = defineProps<{
|
||||
reviewList?: ReviewList[];
|
||||
}>();
|
||||
|
||||
const latest = computed(() => {
|
||||
const list = props.reviewList ?? [];
|
||||
return [...list].sort((a, b) => Number(b.id || 0) - Number(a.id || 0))[0];
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,999 @@
|
||||
<template>
|
||||
<div class="creative-page">
|
||||
<div class="hero-panel">
|
||||
<div>
|
||||
<div class="hero-kicker">AI CREATIVE ASSISTANT</div>
|
||||
<h1>AI创意助手</h1>
|
||||
<p>
|
||||
支持文本指令与多类型附件联合驱动工作流,自动返回视频、脚本 Markdown 或图像/排版结果。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[24, 24]">
|
||||
<a-col :xs="24" :xl="9">
|
||||
<div class="quick-actions-panel">
|
||||
<div class="quick-actions-title">快捷模板</div>
|
||||
<div class="quick-actions">
|
||||
<a-button block @click="fillExample('video')">短视频创意</a-button>
|
||||
<a-button block @click="fillExample('script')">脚本创作</a-button>
|
||||
<a-button block @click="fillExample('image')">海报排版</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-card :bordered="false" class="panel-card">
|
||||
<template #title>输入配置</template>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="文本指令">
|
||||
<a-textarea
|
||||
v-model:value="content"
|
||||
:auto-size="{ minRows: 10, maxRows: 16 }"
|
||||
placeholder="请输入用户的文本指令或需求描述,例如:根据活动照片和通知文档,生成一条公众号推文封面及 60 秒短视频脚本。"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="上传附件">
|
||||
<a-upload
|
||||
:multiple="true"
|
||||
:show-upload-list="false"
|
||||
:accept="acceptTypes"
|
||||
:customRequest="handleUpload"
|
||||
>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
添加附件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div class="upload-tip">
|
||||
支持图片、视频、文档、音频。
|
||||
</div>
|
||||
<div v-if="uploadedFiles.length" class="file-list">
|
||||
<div
|
||||
v-for="(item, index) in uploadedFiles"
|
||||
:key="`${item.url || item.path || item.name}-${index}`"
|
||||
class="file-item"
|
||||
>
|
||||
<div class="file-main">
|
||||
<div class="file-badge">{{ getFileTypeLabel(item) }}</div>
|
||||
<div class="file-text">
|
||||
<div class="file-name">{{ item.name || getFileName(item) }}</div>
|
||||
<div class="file-url">{{ resolveFileUrl(item) || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="file-remove" @click="removeUploadedFile(index)">移除</a>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div class="submit-bar">
|
||||
<a-button :disabled="loading" @click="resetAll">重置</a-button>
|
||||
<a-button type="primary" :loading="loading" :disabled="!canSubmit" @click="runWorkflow">
|
||||
生成
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :xl="15">
|
||||
<a-card :bordered="false" class="panel-card result-card">
|
||||
<template #title>
|
||||
<div class="result-head">
|
||||
<span>工作流结果</span>
|
||||
<!-- <a-tag :color="resultTagColor">{{ resultTagText }}</a-tag>-->
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button :disabled="!scriptMarkdown" @click="copyScript">
|
||||
复制脚本
|
||||
</a-button>
|
||||
<a-button :disabled="!hasAnyResult" @click="clearResult">清空结果</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-empty
|
||||
v-if="!loading && !hasAnyResult"
|
||||
description="填写文本指令并上传附件后执行工作流"
|
||||
/>
|
||||
|
||||
<div v-else class="result-body">
|
||||
<div v-if="loading" class="loading-panel">
|
||||
<div class="loading-visual">
|
||||
<div class="loading-orbit orbit-one"></div>
|
||||
<div class="loading-orbit orbit-two"></div>
|
||||
<div class="loading-core">
|
||||
<span class="loading-core-dot"></span>
|
||||
<span class="loading-core-dot"></span>
|
||||
<span class="loading-core-dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-copy">
|
||||
<div class="loading-kicker">WORKFLOW RUNNING</div>
|
||||
<div class="loading-title">{{ loadingTitle }}</div>
|
||||
<div class="loading-desc">{{ loadingDescription }}</div>
|
||||
</div>
|
||||
<div class="loading-steps">
|
||||
<div
|
||||
v-for="(item, index) in loadingSteps"
|
||||
:key="item.title"
|
||||
class="loading-step"
|
||||
:class="{ 'is-active': index === 1 }"
|
||||
>
|
||||
<div class="loading-step-index">0{{ index + 1 }}</div>
|
||||
<div class="loading-step-main">
|
||||
<div class="loading-step-title">{{ item.title }}</div>
|
||||
<div class="loading-step-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-alert
|
||||
v-if="statusMessage"
|
||||
class="status-alert"
|
||||
type="info"
|
||||
show-icon
|
||||
:message="statusMessage"
|
||||
/>
|
||||
|
||||
<div v-if="videoFiles.length" class="result-section">
|
||||
<div class="section-title">视频生成结果</div>
|
||||
<div class="video-grid">
|
||||
<div
|
||||
v-for="(item, index) in videoFiles"
|
||||
:key="`${resolveFileUrl(item)}-${index}`"
|
||||
class="video-card"
|
||||
>
|
||||
<video
|
||||
v-if="isVideoFile(item)"
|
||||
class="video-player"
|
||||
controls
|
||||
:src="resolveFileUrl(item)"
|
||||
></video>
|
||||
<div class="result-meta">
|
||||
<div class="result-name">{{ item.name || getFileName(item) }}</div>
|
||||
<a :href="resolveFileUrl(item)" target="_blank" rel="noreferrer">
|
||||
打开文件
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="scriptMarkdown" class="result-section">
|
||||
<div class="section-title">脚本创作结果</div>
|
||||
<ByteMdViewer
|
||||
class="markdown-viewer"
|
||||
:value="scriptMarkdown"
|
||||
:plugins="plugins"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="photoFiles.length" class="result-section">
|
||||
<div class="section-title">图像 / 排版结果</div>
|
||||
<div class="photo-grid">
|
||||
<div
|
||||
v-for="(item, index) in photoFiles"
|
||||
:key="`${resolveFileUrl(item)}-${index}`"
|
||||
class="photo-card"
|
||||
>
|
||||
<a-image
|
||||
v-if="isImageFile(item)"
|
||||
:src="resolveFileUrl(item)"
|
||||
:preview="true"
|
||||
class="photo-image"
|
||||
/>
|
||||
<div v-else class="photo-fallback">
|
||||
<div class="file-badge">{{ getFileTypeLabel(item) }}</div>
|
||||
<a :href="resolveFileUrl(item)" target="_blank" rel="noreferrer">
|
||||
查看结果文件
|
||||
</a>
|
||||
</div>
|
||||
<div class="result-meta">
|
||||
<div class="result-name">{{ item.name || getFileName(item) }}</div>
|
||||
<a :href="resolveFileUrl(item)" target="_blank" rel="noreferrer">
|
||||
打开文件
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div v-if="rawOutputsText" class="result-section">-->
|
||||
<!-- <div class="section-title">原始输出</div>-->
|
||||
<!-- <pre class="raw-output">{{ rawOutputsText }}</pre>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import 'bytemd/dist/index.min.css';
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
import gfm from '@bytemd/plugin-gfm';
|
||||
import zh_HansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
|
||||
import highlight from '@bytemd/plugin-highlight-ssr';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { computed, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { messageLoading } from 'ele-admin-pro';
|
||||
import ByteMdViewer from '@/components/ByteMdViewer/index.vue';
|
||||
import { creativeAssistant, uploadAiFile } from '@/api/ai/creativeAssistant';
|
||||
import type { FileRecord } from '@/api/system/file/model';
|
||||
import type {
|
||||
CreativeAssistantOutputs,
|
||||
CreativeAssistantResponse
|
||||
} from '@/api/ai/creativeAssistant/model';
|
||||
import { copyText } from '@/utils/common';
|
||||
|
||||
type ExampleType = 'video' | 'script' | 'image';
|
||||
|
||||
const acceptTypes =
|
||||
'image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.md';
|
||||
const plugins = ref([
|
||||
gfm({
|
||||
locale: zh_HansGfm
|
||||
}),
|
||||
highlight()
|
||||
]);
|
||||
|
||||
const loading = ref(false);
|
||||
const content = ref('');
|
||||
const uploadedFiles = ref<Partial<FileRecord>[]>([]);
|
||||
const workflowResponse = ref<CreativeAssistantResponse | null>(null);
|
||||
const workflowOutputs = ref<CreativeAssistantOutputs | null>(null);
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return !!content.value.trim() || uploadedFiles.value.length > 0;
|
||||
});
|
||||
|
||||
const videoFiles = computed(() => workflowOutputs.value?.video_result || []);
|
||||
const scriptMarkdown = computed(() => workflowOutputs.value?.Script_result || '');
|
||||
const photoFiles = computed(() => workflowOutputs.value?.photo_files || []);
|
||||
const statusMessage = computed(() => {
|
||||
return (
|
||||
workflowOutputs.value?.video_message ||
|
||||
workflowOutputs.value?.photo_message ||
|
||||
''
|
||||
);
|
||||
});
|
||||
const hasAnyResult = computed(() => {
|
||||
return !!(
|
||||
videoFiles.value.length ||
|
||||
scriptMarkdown.value.trim() ||
|
||||
photoFiles.value.length
|
||||
);
|
||||
});
|
||||
const rawOutputsText = computed(() => {
|
||||
if (!workflowOutputs.value) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(workflowOutputs.value, null, 2);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
const resultTagText = computed(() => {
|
||||
if (loading.value) {
|
||||
return '执行中';
|
||||
}
|
||||
if (videoFiles.value.length) {
|
||||
return '视频生成';
|
||||
}
|
||||
if (scriptMarkdown.value.trim()) {
|
||||
return '脚本创作';
|
||||
}
|
||||
if (photoFiles.value.length) {
|
||||
return '图像/排版';
|
||||
}
|
||||
return '待执行';
|
||||
});
|
||||
const resultTagColor = computed(() => {
|
||||
if (loading.value) {
|
||||
return 'processing';
|
||||
}
|
||||
if (hasAnyResult.value) {
|
||||
return 'success';
|
||||
}
|
||||
return 'default';
|
||||
});
|
||||
const loadingTitle = computed(() => {
|
||||
if (uploadedFiles.value.length && content.value.trim()) {
|
||||
return '正在分析指令并处理附件';
|
||||
}
|
||||
if (uploadedFiles.value.length) {
|
||||
return '正在解析附件并生成结果';
|
||||
}
|
||||
return '正在根据文本指令生成内容';
|
||||
});
|
||||
const loadingDescription = computed(() => {
|
||||
return '该工作流可能持续数分钟,请保持页面开启。系统正在后台完成素材理解、内容生成与结果整理。';
|
||||
});
|
||||
const loadingSteps = [
|
||||
{
|
||||
title: '输入理解',
|
||||
desc: '解析文本指令、识别目标输出和执行路径'
|
||||
},
|
||||
{
|
||||
title: '内容生成',
|
||||
desc: '调用工作流处理视频、脚本或图像排版任务'
|
||||
},
|
||||
{
|
||||
title: '结果整理',
|
||||
desc: '回收文件链接并整理结构化输出'
|
||||
}
|
||||
];
|
||||
|
||||
const normalizeOutputPayload = (payload: CreativeAssistantResponse | null) => {
|
||||
if (!payload) {
|
||||
return {};
|
||||
}
|
||||
if (payload.outputs && typeof payload.outputs === 'object') {
|
||||
return payload.outputs;
|
||||
}
|
||||
if ((payload as any).data?.outputs && typeof (payload as any).data.outputs === 'object') {
|
||||
return (payload as any).data.outputs;
|
||||
}
|
||||
return payload as CreativeAssistantOutputs;
|
||||
};
|
||||
|
||||
const resolveFileUrl = (file?: Partial<FileRecord> | null) => {
|
||||
if (!file) {
|
||||
return '';
|
||||
}
|
||||
return String(file.url || file.downloadUrl || file.thumbnail || file.path || '');
|
||||
};
|
||||
|
||||
const getFileName = (file?: Partial<FileRecord> | null) => {
|
||||
const value = resolveFileUrl(file);
|
||||
if (!value) {
|
||||
return '未命名文件';
|
||||
}
|
||||
return decodeURIComponent(value.split('?')[0].split('/').pop() || value);
|
||||
};
|
||||
|
||||
const getFileExt = (file?: Partial<FileRecord> | null) => {
|
||||
const name = String(file?.name || getFileName(file)).toLowerCase();
|
||||
return name.split('.').pop() || '';
|
||||
};
|
||||
|
||||
const DOCUMENT_FILE_EXTS = new Set([
|
||||
'TXT',
|
||||
'MD',
|
||||
'MARKDOWN',
|
||||
'PDF',
|
||||
'HTML',
|
||||
'XLSX',
|
||||
'XLS',
|
||||
'DOCX',
|
||||
'CSV',
|
||||
'EML',
|
||||
'MSG',
|
||||
'PPTX',
|
||||
'PPT',
|
||||
'XML',
|
||||
'EPUB'
|
||||
]);
|
||||
const AUDIO_FILE_EXTS = new Set(['MP3', 'M4A', 'WAV', 'WEBM', 'AMR']);
|
||||
const VIDEO_FILE_EXTS = new Set(['MP4', 'MOV', 'MPEG', 'MPGA']);
|
||||
|
||||
const isImageFile = (file?: Partial<FileRecord> | null) => {
|
||||
const contentType = String(file?.contentType || '').toLowerCase();
|
||||
if (contentType.startsWith('image/')) {
|
||||
return true;
|
||||
}
|
||||
return ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(getFileExt(file));
|
||||
};
|
||||
|
||||
const isVideoFile = (file?: Partial<FileRecord> | null) => {
|
||||
const contentType = String(file?.contentType || '').toLowerCase();
|
||||
if (contentType.startsWith('video/')) {
|
||||
return true;
|
||||
}
|
||||
return ['mp4', 'mov', 'm4v', 'webm', 'avi'].includes(getFileExt(file));
|
||||
};
|
||||
|
||||
const isAudioFile = (file?: Partial<FileRecord> | null) => {
|
||||
const contentType = String(file?.contentType || '').toLowerCase();
|
||||
if (contentType.startsWith('audio/')) {
|
||||
return true;
|
||||
}
|
||||
return ['mp3', 'wav', 'aac', 'm4a', 'ogg'].includes(getFileExt(file));
|
||||
};
|
||||
|
||||
const getUploadFileType = (
|
||||
file?: Partial<FileRecord> | null
|
||||
): 'image' | 'document' | 'audio' | 'video' => {
|
||||
const contentType = String(file?.contentType || '').toLowerCase();
|
||||
const ext = getFileExt(file).toUpperCase();
|
||||
if (contentType.startsWith('image/')) {
|
||||
return 'image';
|
||||
}
|
||||
if (AUDIO_FILE_EXTS.has(ext)) {
|
||||
return 'audio';
|
||||
}
|
||||
if (VIDEO_FILE_EXTS.has(ext)) {
|
||||
return 'video';
|
||||
}
|
||||
if (DOCUMENT_FILE_EXTS.has(ext)) {
|
||||
return 'document';
|
||||
}
|
||||
return 'document';
|
||||
};
|
||||
|
||||
const getFileTypeLabel = (file?: Partial<FileRecord> | null) => {
|
||||
if (isImageFile(file)) {
|
||||
return '图片';
|
||||
}
|
||||
if (isVideoFile(file)) {
|
||||
return '视频';
|
||||
}
|
||||
if (isAudioFile(file)) {
|
||||
return '音频';
|
||||
}
|
||||
return '文档';
|
||||
};
|
||||
|
||||
const clearResult = () => {
|
||||
workflowResponse.value = null;
|
||||
workflowOutputs.value = null;
|
||||
};
|
||||
|
||||
const resetAll = () => {
|
||||
content.value = '';
|
||||
uploadedFiles.value = [];
|
||||
clearResult();
|
||||
};
|
||||
|
||||
const removeUploadedFile = (index: number) => {
|
||||
uploadedFiles.value = uploadedFiles.value.filter((_, current) => current !== index);
|
||||
};
|
||||
|
||||
const handleUpload = async (options: any) => {
|
||||
const rawFile = options.file as File;
|
||||
if (rawFile.size / 1024 / 1024 > 100) {
|
||||
message.error('单个附件不能超过 100MB');
|
||||
options.onError?.(new Error('文件过大'));
|
||||
return;
|
||||
}
|
||||
const hide = messageLoading({
|
||||
content: '上传中..',
|
||||
duration: 0,
|
||||
mask: true
|
||||
});
|
||||
try {
|
||||
const result = await uploadAiFile(rawFile);
|
||||
uploadedFiles.value = [
|
||||
...uploadedFiles.value,
|
||||
{
|
||||
id: result.id,
|
||||
name: result.name || rawFile.name,
|
||||
path: result.path,
|
||||
url: result.url,
|
||||
downloadUrl: result.downloadUrl,
|
||||
contentType: result.contentType
|
||||
}
|
||||
];
|
||||
options.onSuccess?.(result);
|
||||
message.success('附件上传成功');
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '上传失败');
|
||||
options.onError?.(e);
|
||||
} finally {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
|
||||
const buildRequestPayload = () => {
|
||||
const payload: any = {
|
||||
user_content: content.value.trim(),
|
||||
user_contentString: content.value.trim()
|
||||
};
|
||||
if (uploadedFiles.value.length) {
|
||||
payload.user_files = uploadedFiles.value.map((item) => ({
|
||||
transfer_method: 'local_file',
|
||||
type: getUploadFileType(item),
|
||||
upload_file_id: item.id,
|
||||
url: ''
|
||||
}));
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
const runWorkflow = async () => {
|
||||
if (!canSubmit.value) {
|
||||
message.warning('请先输入文本指令或上传附件');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await creativeAssistant(buildRequestPayload());
|
||||
workflowResponse.value = result || null;
|
||||
workflowOutputs.value = normalizeOutputPayload(result || null);
|
||||
if (
|
||||
!videoFiles.value.length &&
|
||||
!scriptMarkdown.value.trim() &&
|
||||
!photoFiles.value.length
|
||||
) {
|
||||
message.warning('接口已返回,但未识别到文档约定的输出字段');
|
||||
} else {
|
||||
message.success('工作流执行完成');
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '工作流执行失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const copyScript = () => {
|
||||
if (!scriptMarkdown.value.trim()) {
|
||||
message.warning('暂无脚本结果');
|
||||
return;
|
||||
}
|
||||
copyText(scriptMarkdown.value);
|
||||
};
|
||||
|
||||
const fillExample = (type: ExampleType) => {
|
||||
if (type === 'video') {
|
||||
content.value =
|
||||
'请根据我上传的活动照片和通知文档,生成一条 45 秒校园活动宣传短视频,画面节奏明快,配一段适合配音的解说文案,并输出视频结果。';
|
||||
return;
|
||||
}
|
||||
if (type === 'script') {
|
||||
content.value =
|
||||
'围绕“青年志愿者进社区”主题创作一份短视频分镜脚本,要求包含开场钩子、3 个主要场景和结尾号召,输出为 Markdown 脚本。';
|
||||
return;
|
||||
}
|
||||
content.value =
|
||||
'请根据上传的活动照片、LOGO 和文案素材,生成一套公众号头图与活动海报排版,风格青春、明亮,输出图片文件。';
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'OaCreativeAssistant'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.creative-page {
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 154, 93, 0.16), transparent 24%),
|
||||
linear-gradient(180deg, #fff8f3 0%, #f7fbff 100%);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 28px 32px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(135deg, #3b0f14 0%, #8f1f2d 48%, #d6404f 100%);
|
||||
color: #f5fbff;
|
||||
box-shadow: 0 18px 40px rgba(143, 31, 45, 0.2);
|
||||
}
|
||||
|
||||
.hero-kicker {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.22em;
|
||||
color: rgba(255, 235, 237, 0.72);
|
||||
}
|
||||
|
||||
.hero-panel h1 {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
color: #fff8f8;
|
||||
}
|
||||
|
||||
.hero-panel p {
|
||||
max-width: 760px;
|
||||
margin: 14px 0 0;
|
||||
line-height: 1.8;
|
||||
color: rgba(255, 239, 241, 0.88);
|
||||
}
|
||||
|
||||
.panel-card {
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 14px 34px rgba(87, 54, 30, 0.08);
|
||||
}
|
||||
|
||||
.quick-actions-panel {
|
||||
margin-bottom: 14px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 14px 34px rgba(87, 54, 30, 0.08);
|
||||
}
|
||||
|
||||
.quick-actions-title {
|
||||
margin-bottom: 12px;
|
||||
color: #4d3828;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
margin-top: 10px;
|
||||
color: #88715f;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
background: #faf5ef;
|
||||
border: 1px solid #c7e8ff;
|
||||
}
|
||||
|
||||
.file-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-badge {
|
||||
flex-shrink: 0;
|
||||
min-width: 48px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: #f2e1d3;
|
||||
color: #8a4f1d;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name,
|
||||
.file-url,
|
||||
.result-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: #3b2c20;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-url {
|
||||
margin-top: 4px;
|
||||
color: #8c7462;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.file-remove {
|
||||
flex-shrink: 0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.submit-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.result-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.result-body {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.status-alert {
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.loading-panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 22px;
|
||||
border-radius: 20px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 182, 122, 0.24), transparent 30%),
|
||||
linear-gradient(135deg, #fff7ef 0%, #fffdfb 100%);
|
||||
border: 1px solid #c7e8ff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-visual {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loading-orbit {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(214, 117, 44, 0.18);
|
||||
}
|
||||
|
||||
.orbit-one {
|
||||
animation: orbitPulse 2.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.orbit-two {
|
||||
inset: 14px;
|
||||
border-color: rgba(214, 117, 44, 0.28);
|
||||
animation: orbitPulse 2.4s ease-in-out infinite 0.7s;
|
||||
}
|
||||
|
||||
.loading-core {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 18px 24px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, #1590ff 0%, #32bfff 100%);
|
||||
box-shadow: 0 14px 30px rgba(160, 83, 29, 0.2);
|
||||
}
|
||||
|
||||
.loading-core-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #fff8f1;
|
||||
animation: dotBounce 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-core-dot:nth-child(2) {
|
||||
animation-delay: 0.18s;
|
||||
}
|
||||
|
||||
.loading-core-dot:nth-child(3) {
|
||||
animation-delay: 0.36s;
|
||||
}
|
||||
|
||||
.loading-copy {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-kicker {
|
||||
margin-bottom: 8px;
|
||||
color: #b37545;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.24em;
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
color: #4e311c;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.loading-desc {
|
||||
max-width: 680px;
|
||||
margin: 10px auto 0;
|
||||
color: #876650;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.loading-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.loading-step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1px solid #f1e3d7;
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
|
||||
}
|
||||
|
||||
.loading-step.is-active {
|
||||
transform: translateY(-2px);
|
||||
border-color: #edb27f;
|
||||
box-shadow: 0 10px 24px rgba(214, 117, 44, 0.12);
|
||||
}
|
||||
|
||||
.loading-step-index {
|
||||
flex-shrink: 0;
|
||||
color: #ce8348;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.loading-step-title {
|
||||
color: #4f3320;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading-step-desc {
|
||||
margin-top: 4px;
|
||||
color: #8c705d;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
padding: 18px;
|
||||
border-radius: 18px;
|
||||
background: #faf8f5;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 14px;
|
||||
color: #4d3828;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.video-grid,
|
||||
.photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.video-card,
|
||||
.photo-card {
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #efe6dc;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
display: block;
|
||||
//width: 100%;
|
||||
height: 500px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.photo-image :deep(img) {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.photo-fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
height: 220px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #f7f1ea 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.result-meta a {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.markdown-viewer :deep(.markdown-body) {
|
||||
padding: 18px;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.raw-output {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
border-radius: 12px;
|
||||
background: #1f1a17;
|
||||
color: #f7ede2;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@keyframes orbitPulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.25;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.04);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dotBounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.55;
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-4px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.creative-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
flex-direction: column;
|
||||
padding: 22px 20px;
|
||||
}
|
||||
|
||||
.hero-panel h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.video-grid,
|
||||
.photo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.loading-steps {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,887 @@
|
||||
<template>
|
||||
<div class="ele-body cross-board-page">
|
||||
<section class="hero-panel">
|
||||
<div class="hero-copy">
|
||||
<div class="hero-kicker">跨校活动情报看板</div>
|
||||
<h1>把兄弟高校的活动打法,整理成一眼可读的案例板</h1>
|
||||
<p>
|
||||
以信息流和案例卡片形式集中展示同类活动的公开线索,方便快速判断选题趋势、包装方式和传播动作。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a-button type="primary" size="large" @click="selectedHeat = '高热'">
|
||||
本周热点
|
||||
</a-button>
|
||||
<a-button size="large" @click="keyword = '活动'">拟办活动对标</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
<div class="hero-stat stat-primary">
|
||||
<div class="stat-label">案例总量</div>
|
||||
<div class="stat-value">{{ totalCount }}</div>
|
||||
<div class="stat-desc">来自公开新闻站点抓取入库</div>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<div class="stat-label">高热案例</div>
|
||||
<div class="stat-value">{{ highHeatCount }}</div>
|
||||
<div class="stat-desc">适合优先拆解传播路径</div>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<div class="stat-label">覆盖类型</div>
|
||||
<div class="stat-value">{{ categoryCount }}</div>
|
||||
<div class="stat-desc">品牌活动 / 实践 / 科创 / 文体</div>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<div class="stat-label">来源院校</div>
|
||||
<div class="stat-value">{{ schoolCount }}</div>
|
||||
<div class="stat-desc">按院校公开渠道聚合</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="filter-panel">
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">活动类型</span>
|
||||
<button
|
||||
v-for="item in categoryOptions"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="filter-chip"
|
||||
:class="{ active: selectedCategory === item }"
|
||||
@click="selectedCategory = item"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">热度等级</span>
|
||||
<button
|
||||
v-for="item in heatOptions"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="filter-chip"
|
||||
:class="{ active: selectedHeat === item }"
|
||||
@click="selectedHeat = item"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</div>
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
class="keyword-search"
|
||||
placeholder="搜索标题、院校、标签"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<a-card :bordered="false" class="stream-card" :loading="loading">
|
||||
<template #title>情报信息流</template>
|
||||
<div class="stream-list">
|
||||
<article
|
||||
v-for="item in streamItems"
|
||||
:key="item.id"
|
||||
class="stream-item"
|
||||
>
|
||||
<img
|
||||
:src="item.cover"
|
||||
:alt="item.title"
|
||||
class="stream-cover"
|
||||
@error="handleCoverError"
|
||||
/>
|
||||
<div class="stream-content">
|
||||
<div class="stream-top">
|
||||
<div>
|
||||
<div class="stream-title">{{ item.title }}</div>
|
||||
<div class="stream-meta">
|
||||
<span>{{ item.school }}</span>
|
||||
<span>{{ item.source }}</span>
|
||||
<span>{{ item.publishAt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-tag :color="heatColorMap[item.heat]">{{ item.heat }}</a-tag>
|
||||
</div>
|
||||
<div class="stream-summary">{{ item.summary }}</div>
|
||||
<div class="stream-tags">
|
||||
<span v-for="tag in item.tags" :key="tag" class="stream-tag">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<a-empty
|
||||
v-if="!streamItems.length"
|
||||
class="cases-empty"
|
||||
description="暂无情报数据"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false" class="cases-card" :loading="loading">
|
||||
<template #title>
|
||||
<div class="case-title-wrap">
|
||||
<span>案例卡片墙</span>
|
||||
<span class="case-count">共 {{ filteredCases.length }} 条</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="cases-grid">
|
||||
<article
|
||||
v-for="item in filteredCases"
|
||||
:key="item.id"
|
||||
class="case-card"
|
||||
>
|
||||
<div class="case-cover-wrap">
|
||||
<img
|
||||
:src="item.cover"
|
||||
:alt="item.title"
|
||||
class="case-cover"
|
||||
@error="handleCoverError"
|
||||
/>
|
||||
<div class="case-school">{{ item.school }}</div>
|
||||
</div>
|
||||
<div class="case-body">
|
||||
<div class="case-head">
|
||||
<a-tag>{{ item.category }}</a-tag>
|
||||
<a-tag :color="heatColorMap[item.heat]">{{ item.heat }}</a-tag>
|
||||
</div>
|
||||
<h3 class="case-title">{{ item.title }}</h3>
|
||||
<p class="case-summary">{{ item.summary }}</p>
|
||||
<div class="case-info">
|
||||
<span>来源:{{ item.source }}</span>
|
||||
<span>发布时间:{{ item.publishAt }}</span>
|
||||
</div>
|
||||
<div class="case-tags">
|
||||
<span v-for="tag in item.tags" :key="tag" class="case-tag">
|
||||
# {{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="case-footer">
|
||||
<div class="signal-badge">
|
||||
<span class="signal-badge-label">可借鉴动作</span>
|
||||
<span class="signal-badge-value">{{ item.highlight }}</span>
|
||||
</div>
|
||||
<a
|
||||
class="origin-link"
|
||||
:href="item.link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
查看原文
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<a-empty
|
||||
v-if="!filteredCases.length"
|
||||
class="cases-empty"
|
||||
description="暂无匹配的案例"
|
||||
/>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import {
|
||||
listCrossSchoolActivityArticle,
|
||||
syncCrossSchoolActivityArticle
|
||||
} from "@/api/gxmu/cross-school-activity-article";
|
||||
import type { CrossSchoolActivityArticle } from "@/api/gxmu/cross-school-activity-article/model";
|
||||
import defaultCover from "@/assets/暂无图片.png";
|
||||
|
||||
interface ActivityCase {
|
||||
id: number;
|
||||
title: string;
|
||||
school: string;
|
||||
category: string;
|
||||
heat: "高热" | "跟进" | "观察";
|
||||
source: string;
|
||||
publishAt: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
cover: string;
|
||||
link: string;
|
||||
highlight: string;
|
||||
}
|
||||
|
||||
const cases = ref<ActivityCase[]>([]);
|
||||
const loading = ref(false);
|
||||
const syncing = ref(false);
|
||||
const lastSyncTime = ref("");
|
||||
|
||||
const categoryOptions = computed(() => [
|
||||
"全部",
|
||||
...Array.from(new Set(cases.value.map((item) => item.category).filter(Boolean)))
|
||||
]);
|
||||
const heatOptions = ["全部", "高热", "跟进", "观察"];
|
||||
const selectedCategory = ref("全部");
|
||||
const selectedHeat = ref("全部");
|
||||
const keyword = ref("");
|
||||
|
||||
const heatColorMap: Record<ActivityCase["heat"], string> = {
|
||||
高热: "error",
|
||||
跟进: "processing",
|
||||
观察: "default"
|
||||
};
|
||||
|
||||
const parseTags = (value?: string): string[] => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse(value);
|
||||
return Array.isArray(result) ? result.filter(Boolean) : [];
|
||||
} catch (e) {
|
||||
return value
|
||||
.split(/[,,#\s]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (value?: string) => {
|
||||
if (!value) {
|
||||
return "未知时间";
|
||||
}
|
||||
return value.replace("T", " ").slice(0, 16);
|
||||
};
|
||||
|
||||
const handleCoverError = (event: Event) => {
|
||||
const image = event.target as HTMLImageElement | null;
|
||||
if (image && image.src !== defaultCover) {
|
||||
image.src = defaultCover;
|
||||
}
|
||||
};
|
||||
|
||||
const mapArticle = (
|
||||
item: CrossSchoolActivityArticle,
|
||||
index: number
|
||||
): ActivityCase => ({
|
||||
id: item.id || index + 1,
|
||||
title: item.title || "未命名文章",
|
||||
school: item.schoolName || "未知院校",
|
||||
category: item.category || "校园要闻",
|
||||
heat: (item.heatLevel as ActivityCase["heat"]) || "观察",
|
||||
source: item.sourceName || "学校官网",
|
||||
publishAt: formatTime(item.publishTime),
|
||||
summary: item.summary || "来源站点暂未提供摘要,可点击原文查看完整内容。",
|
||||
tags: parseTags(item.tags),
|
||||
cover: item.coverImage?.trim() || defaultCover,
|
||||
link: item.detailUrl || item.sourceUrl || "#",
|
||||
highlight: item.highlight || "选题跟踪 + 案例复盘"
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const list = await listCrossSchoolActivityArticle({
|
||||
limit: 80,
|
||||
sort: "publishTime",
|
||||
order: "desc"
|
||||
});
|
||||
cases.value = list.map(mapArticle);
|
||||
const latest = list.find((item) => item.lastSyncTime)?.lastSyncTime;
|
||||
lastSyncTime.value = latest ? formatTime(latest) : "";
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "跨校活动情报加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
syncing.value = true;
|
||||
try {
|
||||
const count = await syncCrossSchoolActivityArticle();
|
||||
message.success(`同步完成,写入/更新 ${count} 条`);
|
||||
await loadData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "同步失败");
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCases = computed(() => {
|
||||
const text = keyword.value.trim().toLowerCase();
|
||||
return cases.value.filter((item) => {
|
||||
const matchCategory =
|
||||
selectedCategory.value === "全部" || item.category === selectedCategory.value;
|
||||
const matchHeat =
|
||||
selectedHeat.value === "全部" || item.heat === selectedHeat.value;
|
||||
const searchText = [item.title, item.school, item.summary, item.tags.join(" ")]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
const matchKeyword = !text || searchText.includes(text);
|
||||
return matchCategory && matchHeat && matchKeyword;
|
||||
});
|
||||
});
|
||||
|
||||
const totalCount = computed(() => cases.value.length);
|
||||
const highHeatCount = computed(
|
||||
() => cases.value.filter((item) => item.heat === "高热").length
|
||||
);
|
||||
const categoryCount = computed(
|
||||
() => new Set(cases.value.map((item) => item.category)).size
|
||||
);
|
||||
const schoolCount = computed(
|
||||
() => new Set(cases.value.map((item) => item.school)).size
|
||||
);
|
||||
|
||||
const streamItems = computed(() => filteredCases.value.slice(0, 4));
|
||||
|
||||
const insightSignals = computed(() => [
|
||||
{
|
||||
title: "高频选题集中在交流、竞赛与专题学习",
|
||||
desc: `当前抓取到 ${highHeatCount.value} 条高热线索,可优先拆解标题包装、组织协同和后续传播动作。`
|
||||
},
|
||||
{
|
||||
title: "兄弟高校要闻适合做快速对标池",
|
||||
desc: "列表按发布时间汇总,能快速观察近期高校关注的活动主题、承办单位和报道角度。"
|
||||
},
|
||||
{
|
||||
title: "建议保留原文入口做复核",
|
||||
desc: "抓取结果保留来源链接,便于在策划复盘时回到原文确认细节和图片素材。"
|
||||
}
|
||||
]);
|
||||
|
||||
const watchList = [
|
||||
"活动是否配置统一 hashtag 与二创任务",
|
||||
"是否提供报名后自动领取的素材包",
|
||||
"是否能把案例沉淀为往届经验库",
|
||||
"是否区分预热、爆发、回顾三个传播阶段"
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "GxmuCrossSchoolActivityBoard"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.cross-board-page {
|
||||
--board-bg: #f5fbff;
|
||||
--board-card: rgba(255, 255, 255, 0.92);
|
||||
--board-line: rgba(21, 144, 255, 0.12);
|
||||
--board-text: #1f2d3d;
|
||||
--board-subtext: #5f7f9f;
|
||||
--board-primary: @primary-color;
|
||||
--board-primary-soft: #e8f4ff;
|
||||
--board-shadow: 0 16px 48px rgba(21, 144, 255, 0.1);
|
||||
min-height: calc(100vh - 120px);
|
||||
background: radial-gradient(circle at top left, rgba(80, 180, 255, 0.18), transparent 28%),
|
||||
linear-gradient(180deg, #f5fbff 0%, #f5fbff 36%, #fff 100%);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.3fr) minmax(320px, 420px);
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
padding: 28px;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(245, 251, 255, 0.94)),
|
||||
repeating-linear-gradient(90deg,
|
||||
rgba(21, 144, 255, 0.04) 0,
|
||||
rgba(21, 144, 255, 0.04) 1px,
|
||||
transparent 1px,
|
||||
transparent 24px);
|
||||
box-shadow: var(--board-shadow);
|
||||
border: 1px solid rgba(21, 144, 255, 0.14);
|
||||
}
|
||||
|
||||
.hero-panel::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -80px;
|
||||
top: -60px;
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(21, 144, 255, 0.18), transparent 68%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(21, 144, 255, 0.08);
|
||||
color: var(--board-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.hero-copy h1 {
|
||||
max-width: 620px;
|
||||
margin: 18px 0 12px;
|
||||
color: var(--board-text);
|
||||
font-size: 34px;
|
||||
line-height: 1.25;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero-copy p {
|
||||
max-width: 600px;
|
||||
margin: 0;
|
||||
color: var(--board-subtext);
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.hero-stat {
|
||||
padding: 18px;
|
||||
border-radius: 22px;
|
||||
background: var(--board-card);
|
||||
border: 1px solid rgba(21, 144, 255, 0.12);
|
||||
box-shadow: 0 10px 28px rgba(21, 144, 255, 0.07);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stat-primary {
|
||||
background: linear-gradient(160deg, #32bfff 0%, @primary-color 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat-primary .stat-label,
|
||||
.stat-primary .stat-desc {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--board-subtext);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
margin: 10px 0 6px;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.stat-desc {
|
||||
color: var(--board-subtext);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-alert {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(21, 144, 255, 0.18);
|
||||
background: rgba(255, 251, 248, 0.95);
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px;
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(21, 144, 255, 0.12);
|
||||
box-shadow: 0 10px 30px rgba(21, 144, 255, 0.06);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
color: var(--board-subtext);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
padding: 7px 14px;
|
||||
border: 1px solid rgba(21, 144, 255, 0.16);
|
||||
border-radius: 999px;
|
||||
background: #f5fbff;
|
||||
color: var(--board-primary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-chip.active,
|
||||
.filter-chip:hover {
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
background: linear-gradient(135deg, #32bfff, @primary-color);
|
||||
box-shadow: 0 8px 20px rgba(21, 144, 255, 0.22);
|
||||
}
|
||||
|
||||
.keyword-search {
|
||||
width: 280px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.overview-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stream-card,
|
||||
.signal-card,
|
||||
.cases-card {
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 16px 40px rgba(21, 144, 255, 0.08);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.stream-list {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.stream-item {
|
||||
display: grid;
|
||||
grid-template-columns: 160px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(180deg, #f5fbff, #fff);
|
||||
border: 1px solid rgba(21, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.stream-cover {
|
||||
width: 100%;
|
||||
height: 128px;
|
||||
object-fit: cover;
|
||||
border-radius: 16px;
|
||||
background: #e8f4ff;
|
||||
}
|
||||
|
||||
.stream-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stream-title {
|
||||
color: var(--board-text);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.stream-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
margin-top: 6px;
|
||||
color: #5f7f9f;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stream-summary {
|
||||
margin-top: 12px;
|
||||
color: #1f2d3d;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.stream-tags,
|
||||
.case-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.stream-tag,
|
||||
.case-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--board-primary-soft);
|
||||
color: #1590ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.signal-list {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.signal-item {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, #f5fbff, #fff);
|
||||
border: 1px solid rgba(21, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.signal-title {
|
||||
color: var(--board-text);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.signal-title.secondary {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.signal-desc {
|
||||
margin-top: 8px;
|
||||
color: #5f7f9f;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.signal-divider {
|
||||
height: 1px;
|
||||
margin: 18px 0;
|
||||
background: linear-gradient(90deg, rgba(21, 144, 255, 0.18), transparent);
|
||||
}
|
||||
|
||||
.watch-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.watch-item {
|
||||
position: relative;
|
||||
padding-left: 14px;
|
||||
color: #5f7f9f;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.watch-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #1590ff;
|
||||
}
|
||||
|
||||
.case-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.case-count {
|
||||
color: #5f7f9f;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cases-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.case-card {
|
||||
overflow: hidden;
|
||||
border-radius: 22px;
|
||||
border: 1px solid rgba(21, 144, 255, 0.12);
|
||||
background: linear-gradient(180deg, #f5fbff 0%, #fff 45%);
|
||||
box-shadow: 0 12px 30px rgba(21, 144, 255, 0.08);
|
||||
transition: transform 0.24s ease,
|
||||
box-shadow 0.24s ease;
|
||||
}
|
||||
|
||||
.case-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 18px 40px rgba(21, 144, 255, 0.12);
|
||||
}
|
||||
|
||||
.case-cover-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.case-cover {
|
||||
width: 100%;
|
||||
height: 190px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
background: #e8f4ff;
|
||||
}
|
||||
|
||||
.case-school {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
bottom: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(31, 45, 61, 0.72);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.case-body {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.case-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.case-title {
|
||||
margin: 14px 0 10px;
|
||||
color: var(--board-text);
|
||||
font-size: 19px;
|
||||
line-height: 1.5;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.case-summary {
|
||||
min-height: 88px;
|
||||
margin: 0;
|
||||
color: #5f7f9f;
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.case-info {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 14px;
|
||||
color: #5f7f9f;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.case-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.signal-badge {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(245, 251, 255, 0.95);
|
||||
}
|
||||
|
||||
.signal-badge-label {
|
||||
color: #1590ff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.signal-badge-value {
|
||||
color: #1590ff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.origin-link {
|
||||
color: var(--board-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cases-empty {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.hero-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cross-board-page {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
padding: 22px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.hero-copy h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.keyword-search {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.stream-item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stream-cover {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.hero-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.case-footer,
|
||||
.case-title-wrap {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,474 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑申报管理' : '添加申报管理'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="名称" name="title">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.title"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="年份" name="year">
|
||||
<a-date-picker
|
||||
allow-clear
|
||||
picker="year"
|
||||
value-format="YYYY"
|
||||
class="ele-fluid"
|
||||
placeholder="请选择年份"
|
||||
v-model:value="yearValue"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="模块" name="module">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:options="DECLARE_MODULE_OPTIONS"
|
||||
option-filter-prop="label"
|
||||
placeholder="请选择"
|
||||
v-model:value="form.module"
|
||||
/>
|
||||
</a-form-item>
|
||||
<template v-if="isTzbcyModule">
|
||||
<a-form-item label="项目申报表配置">
|
||||
<div class="declare-config-block">
|
||||
<div class="declare-config-title">项目类型</div>
|
||||
<div
|
||||
v-for="(item, index) in form.formProjectType"
|
||||
:key="`form-project-type-${index}`"
|
||||
class="declare-config-row"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.formProjectType[index]"
|
||||
placeholder="请输入项目申报表项目类型"
|
||||
/>
|
||||
<a-button danger @click="removeConfigRow('formProjectType', index)"
|
||||
>删除</a-button
|
||||
>
|
||||
</div>
|
||||
<a-button type="dashed" block @click="addConfigRow('formProjectType')"
|
||||
>新增项目类型</a-button
|
||||
>
|
||||
</div>
|
||||
<div class="declare-config-block">
|
||||
<div class="declare-config-title">项目分组</div>
|
||||
<div
|
||||
v-for="(item, index) in form.formProjectGroup"
|
||||
:key="`form-project-group-${index}`"
|
||||
class="declare-config-row"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.formProjectGroup[index]"
|
||||
placeholder="请输入项目申报表项目分组"
|
||||
/>
|
||||
<a-button
|
||||
danger
|
||||
@click="removeConfigRow('formProjectGroup', index)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button
|
||||
type="dashed"
|
||||
block
|
||||
@click="addConfigRow('formProjectGroup')"
|
||||
>
|
||||
新增项目分组
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="公开展示信息表配置">
|
||||
<div class="declare-config-block">
|
||||
<div class="declare-config-title">项目类型</div>
|
||||
<div
|
||||
v-for="(item, index) in form.publicProjectType"
|
||||
:key="`public-project-type-${index}`"
|
||||
class="declare-config-row"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.publicProjectType[index]"
|
||||
placeholder="请输入公开展示信息表项目类型"
|
||||
/>
|
||||
<a-button
|
||||
danger
|
||||
@click="removeConfigRow('publicProjectType', index)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button
|
||||
type="dashed"
|
||||
block
|
||||
@click="addConfigRow('publicProjectType')"
|
||||
>
|
||||
新增项目类型
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="declare-config-block">
|
||||
<div class="declare-config-title">项目分组</div>
|
||||
<div
|
||||
v-for="(item, index) in form.publicProjectGroup"
|
||||
:key="`public-project-group-${index}`"
|
||||
class="declare-config-row"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.publicProjectGroup[index]"
|
||||
placeholder="请输入公开展示信息表项目分组"
|
||||
/>
|
||||
<a-button
|
||||
danger
|
||||
@click="removeConfigRow('publicProjectGroup', index)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button
|
||||
type="dashed"
|
||||
block
|
||||
@click="addConfigRow('publicProjectGroup')"
|
||||
>
|
||||
新增项目分组
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="开始时间" name="startTime">
|
||||
<a-date-picker
|
||||
allow-clear
|
||||
show-time
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
class="ele-fluid"
|
||||
placeholder="请选择开始时间"
|
||||
v-model:value="form.startTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="结束时间" name="endTime">
|
||||
<a-date-picker
|
||||
allow-clear
|
||||
show-time
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
class="ele-fluid"
|
||||
placeholder="请选择结束时间"
|
||||
v-model:value="form.endTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { addDeclare, updateDeclare } from '@/api/gxmu/declare';
|
||||
import type { Declare } from '@/api/gxmu/declare/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { DECLARE_MODULE_OPTIONS } from '../moduleOptions';
|
||||
import {
|
||||
normalizeDeclareRecord,
|
||||
stringifyDeclareMultiValueField
|
||||
} from '../projectFieldUtils';
|
||||
|
||||
interface DeclareEditForm
|
||||
extends Omit<
|
||||
Declare,
|
||||
| 'projectType'
|
||||
| 'projectGroup'
|
||||
| 'formProjectType'
|
||||
| 'formProjectGroup'
|
||||
| 'publicProjectType'
|
||||
| 'publicProjectGroup'
|
||||
> {
|
||||
projectType: string[];
|
||||
projectGroup: string[];
|
||||
formProjectType: string[];
|
||||
formProjectGroup: string[];
|
||||
publicProjectType: string[];
|
||||
publicProjectGroup: string[];
|
||||
}
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: Declare | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const yearValue = ref<string>();
|
||||
const createDefaultForm = (): DeclareEditForm => ({
|
||||
id: undefined,
|
||||
title: undefined,
|
||||
year: undefined,
|
||||
module: undefined,
|
||||
projectType: [],
|
||||
projectGroup: [],
|
||||
formProjectType: [''],
|
||||
formProjectGroup: [''],
|
||||
publicProjectType: [''],
|
||||
publicProjectGroup: [''],
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
userId: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
deleted: undefined
|
||||
});
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<DeclareEditForm>(createDefaultForm());
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
module: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择模块',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
const isTzbcyModule = computed(() => form.module === 'gxmu_tzbcy_form');
|
||||
|
||||
const normalizeEditableList = (list?: string[]) => {
|
||||
const values = (list || []).map((item) => item?.trim() || '').filter(Boolean);
|
||||
return values.length ? values : [''];
|
||||
};
|
||||
|
||||
const addConfigRow = (
|
||||
key:
|
||||
| 'formProjectType'
|
||||
| 'formProjectGroup'
|
||||
| 'publicProjectType'
|
||||
| 'publicProjectGroup'
|
||||
) => {
|
||||
form[key].push('');
|
||||
};
|
||||
|
||||
const removeConfigRow = (
|
||||
key:
|
||||
| 'formProjectType'
|
||||
| 'formProjectGroup'
|
||||
| 'publicProjectType'
|
||||
| 'publicProjectGroup',
|
||||
index: number
|
||||
) => {
|
||||
form[key].splice(index, 1);
|
||||
if (!form[key].length) {
|
||||
form[key].push('');
|
||||
}
|
||||
};
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
form.year = yearValue.value ? Number(yearValue.value) : undefined;
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const normalizedFormProjectType = normalizeEditableList(
|
||||
form.formProjectType
|
||||
);
|
||||
const normalizedFormProjectGroup = normalizeEditableList(
|
||||
form.formProjectGroup
|
||||
);
|
||||
const normalizedPublicProjectType = normalizeEditableList(
|
||||
form.publicProjectType
|
||||
);
|
||||
const normalizedPublicProjectGroup = normalizeEditableList(
|
||||
form.publicProjectGroup
|
||||
);
|
||||
if (
|
||||
isTzbcyModule.value &&
|
||||
!normalizedFormProjectType.length
|
||||
) {
|
||||
loading.value = false;
|
||||
message.error('请至少配置一个项目申报表项目类型');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isTzbcyModule.value &&
|
||||
!normalizedFormProjectGroup.length
|
||||
) {
|
||||
loading.value = false;
|
||||
message.error('请至少配置一个项目申报表项目分组');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isTzbcyModule.value &&
|
||||
!normalizedPublicProjectType.length
|
||||
) {
|
||||
loading.value = false;
|
||||
message.error('请至少配置一个公开展示信息表项目类型');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isTzbcyModule.value &&
|
||||
!normalizedPublicProjectGroup.length
|
||||
) {
|
||||
loading.value = false;
|
||||
message.error('请至少配置一个公开展示信息表项目分组');
|
||||
return;
|
||||
}
|
||||
const formData = {
|
||||
...form,
|
||||
projectType: stringifyDeclareMultiValueField(normalizedFormProjectType),
|
||||
projectGroup: stringifyDeclareMultiValueField(normalizedFormProjectGroup),
|
||||
formProjectType: stringifyDeclareMultiValueField(
|
||||
normalizedFormProjectType
|
||||
),
|
||||
formProjectGroup: stringifyDeclareMultiValueField(
|
||||
normalizedFormProjectGroup
|
||||
),
|
||||
publicProjectType: stringifyDeclareMultiValueField(
|
||||
normalizedPublicProjectType
|
||||
),
|
||||
publicProjectGroup: stringifyDeclareMultiValueField(
|
||||
normalizedPublicProjectGroup
|
||||
)
|
||||
};
|
||||
if (!isTzbcyModule.value) {
|
||||
formData.projectType = undefined;
|
||||
formData.projectGroup = undefined;
|
||||
formData.formProjectType = undefined;
|
||||
formData.formProjectGroup = undefined;
|
||||
formData.publicProjectType = undefined;
|
||||
formData.publicProjectGroup = undefined;
|
||||
}
|
||||
const saveOrUpdate = isUpdate.value ? updateDeclare : addDeclare;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
const record = normalizeDeclareRecord(props.data);
|
||||
Object.assign(form, createDefaultForm(), record ?? {});
|
||||
form.formProjectType = normalizeEditableList(
|
||||
record?.formProjectType as string[]
|
||||
);
|
||||
form.formProjectGroup = normalizeEditableList(
|
||||
record?.formProjectGroup as string[]
|
||||
);
|
||||
form.publicProjectType = normalizeEditableList(
|
||||
record?.publicProjectType as string[]
|
||||
);
|
||||
form.publicProjectGroup = normalizeEditableList(
|
||||
record?.publicProjectGroup as string[]
|
||||
);
|
||||
yearValue.value = props.data.year ? String(props.data.year) : undefined;
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
Object.assign(form, createDefaultForm());
|
||||
isUpdate.value = false;
|
||||
yearValue.value = undefined;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
Object.assign(form, createDefaultForm());
|
||||
yearValue.value = undefined;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => form.module,
|
||||
(value) => {
|
||||
if (value !== 'gxmu_tzbcy_form') {
|
||||
form.projectType = [];
|
||||
form.projectGroup = [];
|
||||
form.formProjectType = [''];
|
||||
form.formProjectGroup = [''];
|
||||
form.publicProjectType = [''];
|
||||
form.publicProjectGroup = [''];
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.declare-config-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.declare-config-block:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.declare-config-title {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.declare-config-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { GradeParam } from '@/api/user/grade/model';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 选中的角色
|
||||
selection?: [];
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: GradeParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
// 新增
|
||||
const add = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selection,
|
||||
() => {}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,286 @@
|
||||
<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"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'module'">
|
||||
{{ getDeclareModuleLabel(record.module) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'formProjectType'">
|
||||
{{ formatDeclareMultiValueField(record.formProjectType || record.projectType) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'formProjectGroup'">
|
||||
{{ formatDeclareMultiValueField(record.formProjectGroup || record.projectGroup) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'publicProjectType'">
|
||||
{{ formatDeclareMultiValueField(record.publicProjectType || record.projectType) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'publicProjectGroup'">
|
||||
{{ formatDeclareMultiValueField(record.publicProjectGroup || record.projectGroup) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 0" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<DeclareEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import DeclareEdit from './components/declareEdit.vue';
|
||||
import { pageDeclare, removeDeclare, removeBatchDeclare } from '@/api/gxmu/declare';
|
||||
import type { Declare, DeclareParam } from '@/api/gxmu/declare/model';
|
||||
import { getDeclareModuleLabel } from './moduleOptions';
|
||||
import { formatDeclareMultiValueField } from './projectFieldUtils';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<Declare[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<Declare | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageDeclare({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'module',
|
||||
key: 'module',
|
||||
align: 'center',
|
||||
},
|
||||
// {
|
||||
// title: '申报表项目类型',
|
||||
// dataIndex: 'formProjectType',
|
||||
// key: 'formProjectType',
|
||||
// align: 'center',
|
||||
// },
|
||||
// {
|
||||
// title: '申报表项目分组',
|
||||
// dataIndex: 'formProjectGroup',
|
||||
// key: 'formProjectGroup',
|
||||
// align: 'center',
|
||||
// },
|
||||
// {
|
||||
// title: '公开展示项目类型',
|
||||
// dataIndex: 'publicProjectType',
|
||||
// key: 'publicProjectType',
|
||||
// align: 'center',
|
||||
// },
|
||||
// {
|
||||
// title: '公开展示项目分组',
|
||||
// dataIndex: 'publicProjectGroup',
|
||||
// key: 'publicProjectGroup',
|
||||
// align: 'center',
|
||||
// },
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'endTime',
|
||||
key: 'endTime',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: DeclareParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: Declare) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: Declare) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeDeclare(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchDeclare(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: Declare) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Declare'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,94 @@
|
||||
export const DECLARE_MODULE_OPTIONS = [
|
||||
{ label: '十佳青年岗位能手', value: 'gxmu_sjqn' },
|
||||
{ label: '十佳团支部书记', value: 'gxmu_sjtbzbsj' },
|
||||
{ label: '五四红旗团委', value: 'gxmu_wshqtw' },
|
||||
{ label: '五四红旗团支部', value: 'gxmu_wshqtzb' },
|
||||
{ label: '优秀共青团干部', value: 'gxmu_yxgqtdgb' },
|
||||
{ label: '优秀共青团员', value: 'gxmu_yxgqty' },
|
||||
{ label: '挑战杯', value: 'gxmu_tzbcy_form' },
|
||||
{ label: '青马工程', value: 'gxmu_qmgc_form' },
|
||||
{ label: '未来学术之星', value: 'gxmu_wxxzx_project' }
|
||||
] as const;
|
||||
|
||||
export const DECLARE_MODULE_GROUP_OPTIONS = [
|
||||
{
|
||||
label: '五四模块',
|
||||
value: 'wusi',
|
||||
modules: [
|
||||
'gxmu_sjqn',
|
||||
'gxmu_sjtbzbsj',
|
||||
'gxmu_wshqtw',
|
||||
'gxmu_wshqtzb',
|
||||
'gxmu_yxgqtdgb',
|
||||
'gxmu_yxgqty'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '挑战杯模块',
|
||||
value: 'challengeCup',
|
||||
modules: ['gxmu_tzbcy_form']
|
||||
},
|
||||
{
|
||||
label: '未来学术之星',
|
||||
value: 'futureAcademicStar',
|
||||
modules: ['gxmu_wxxzx_project']
|
||||
},
|
||||
{
|
||||
label: '青马工程',
|
||||
value: 'qingma',
|
||||
modules: ['gxmu_qmgc_form']
|
||||
}
|
||||
] as const;
|
||||
|
||||
const DECLARE_MODULE_LABEL_MAP = DECLARE_MODULE_OPTIONS.reduce<
|
||||
Record<string, string>
|
||||
>((result, item) => {
|
||||
result[item.value] = item.label;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const DECLARE_MODULE_GROUP_LABEL_MAP = DECLARE_MODULE_GROUP_OPTIONS.reduce<
|
||||
Record<string, string>
|
||||
>((result, item) => {
|
||||
item.modules.forEach((module) => {
|
||||
result[module] = item.label;
|
||||
});
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const DECLARE_MODULE_ROUTE_MAP: Record<string, string> = {
|
||||
gxmu_sjqn: '/gxmu/SJQNForm',
|
||||
gxmu_sjtbzbsj: '/gxmu/SJTBZBSJForm',
|
||||
gxmu_wshqtw: '/gxmu/WSHQTWForm',
|
||||
gxmu_wshqtzb: '/gxmu/WSHQTZBForm',
|
||||
gxmu_yxgqtdgb: '/gxmu/YXGQTDGBForm',
|
||||
gxmu_yxgqty: '/gxmu/YXGQTYForm',
|
||||
gxmu_tzbcy_form: '/gxmu/TZBCYForm',
|
||||
gxmu_qmgc_form: '/gxmu/QMGCForm',
|
||||
gxmu_wxxzx_project: '/gxmu/WXXZXForm'
|
||||
};
|
||||
|
||||
export const getDeclareModuleLabel = (value?: string) => {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
return DECLARE_MODULE_LABEL_MAP[value] ?? value;
|
||||
};
|
||||
|
||||
export const getDeclareModuleGroupLabel = (value?: string) => {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
return DECLARE_MODULE_GROUP_LABEL_MAP[value] ?? '其他模块';
|
||||
};
|
||||
|
||||
export const getDeclareModuleRoute = (value?: string) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return DECLARE_MODULE_ROUTE_MAP[value];
|
||||
};
|
||||
|
||||
export const isChallengeCupModule = (value?: string) => {
|
||||
return value === 'gxmu_tzbcy_form';
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { Declare } from '@/api/gxmu/declare/model';
|
||||
|
||||
type MultiValueField = string | string[] | null | undefined;
|
||||
|
||||
const normalizeList = (list: unknown[]): string[] => {
|
||||
const result: string[] = [];
|
||||
|
||||
list.forEach((item) => {
|
||||
const value = typeof item === 'string' ? item.trim() : '';
|
||||
if (value && !result.includes(value)) {
|
||||
result.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const parseDeclareMultiValueField = (value: MultiValueField): string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return normalizeList(value);
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (Array.isArray(parsed)) {
|
||||
return normalizeList(parsed);
|
||||
}
|
||||
} catch (e) {
|
||||
// 兼容历史单值字符串
|
||||
}
|
||||
|
||||
return [trimmed];
|
||||
};
|
||||
|
||||
export const stringifyDeclareMultiValueField = (
|
||||
value: MultiValueField
|
||||
): string | undefined => {
|
||||
const list = parseDeclareMultiValueField(value);
|
||||
return list.length ? JSON.stringify(list) : undefined;
|
||||
};
|
||||
|
||||
export const formatDeclareMultiValueField = (value: MultiValueField): string => {
|
||||
const list = parseDeclareMultiValueField(value);
|
||||
return list.length ? list.join('、') : '-';
|
||||
};
|
||||
|
||||
export const normalizeDeclareRecord = (
|
||||
record?: Declare | null
|
||||
): Declare | null => {
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
projectType: parseDeclareMultiValueField(record.projectType),
|
||||
projectGroup: parseDeclareMultiValueField(record.projectGroup),
|
||||
formProjectType: parseDeclareMultiValueField(
|
||||
record.formProjectType ?? record.projectType
|
||||
),
|
||||
formProjectGroup: parseDeclareMultiValueField(
|
||||
record.formProjectGroup ?? record.projectGroup
|
||||
),
|
||||
publicProjectType: parseDeclareMultiValueField(
|
||||
record.publicProjectType ?? record.projectType
|
||||
),
|
||||
publicProjectGroup: parseDeclareMultiValueField(
|
||||
record.publicProjectGroup ?? record.projectGroup
|
||||
)
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,502 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body declare-history-page">
|
||||
<a-card :bordered="false" :body-style="{ padding: '20px' }">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-title">我的申报历史</div>
|
||||
<div class="page-desc">展示当前账号在各申报模块中的历史申报记录</div>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button @click="goDeclareList">返回申报列表</a-button>
|
||||
<a-button :loading="historyLoading" @click="loadHistoryData">刷新</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-left">
|
||||
<div class="filter-label">模块筛选</div>
|
||||
<div class="filter-tag-groups">
|
||||
<div class="filter-tag-row">
|
||||
<a-tag
|
||||
class="filter-tag"
|
||||
:class="{ 'filter-tag-active': !selectedModule }"
|
||||
@click="setSelectedModule()"
|
||||
>
|
||||
全部
|
||||
</a-tag>
|
||||
</div>
|
||||
<div
|
||||
v-for="group in DECLARE_MODULE_GROUP_OPTIONS"
|
||||
:key="group.value"
|
||||
class="filter-tag-group"
|
||||
>
|
||||
<div class="filter-group-name">{{ group.label }}</div>
|
||||
<div class="filter-tag-row">
|
||||
<a-tag
|
||||
v-for="module in group.modules"
|
||||
:key="module"
|
||||
class="filter-tag"
|
||||
:class="{ 'filter-tag-active': selectedModule === module }"
|
||||
@click="setSelectedModule(module)"
|
||||
>
|
||||
{{ getDeclareModuleLabel(module) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
row-key="key"
|
||||
:loading="historyLoading"
|
||||
:columns="historyColumns"
|
||||
:data-source="filteredHistoryList"
|
||||
:pagination="historyPagination"
|
||||
:scroll="{ x: 1180 }"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'module'">
|
||||
{{ getDeclareModuleLabel(record.module) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 1" color="green">通过</a-tag>
|
||||
<a-tag v-else-if="record.status === 2" color="red">不通过</a-tag>
|
||||
<a-tag v-else>待审核</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'submitTime'">
|
||||
{{ formatDateTime(record.submitTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'latestReviewTime'">
|
||||
{{ formatDateTime(record.latestReviewTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">查看详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openReviewFlow(record)">审核流列表</a>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无申报历史" />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDetail"
|
||||
:module="detailCurrent?.module"
|
||||
:module-name="getDeclareModuleLabel(detailCurrent?.module)"
|
||||
:pk="detailCurrent?.id"
|
||||
/>
|
||||
<ReviewFlowModal
|
||||
v-model:visible="showReviewFlow"
|
||||
:module="reviewFlowCurrent?.module"
|
||||
:module-name="getDeclareModuleLabel(reviewFlowCurrent?.module)"
|
||||
:pk="reviewFlowCurrent?.id"
|
||||
@done="loadHistoryData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { message } from "ant-design-vue";
|
||||
import { toDateString } from "ele-admin-pro";
|
||||
import type { TablePaginationConfig } from "ant-design-vue";
|
||||
import type { ColumnType } from "ant-design-vue/es/table";
|
||||
import type { PageResult } from "@/api";
|
||||
import type { ReviewList } from "@/api/gxmu/reviewList/model";
|
||||
import { userPageSjqnForm } from "@/api/gxmu/sjqn-form";
|
||||
import { userPageSjtbzbsjForm } from "@/api/gxmu/sjtbzbsj-form";
|
||||
import { userPageWshqtwForm } from "@/api/gxmu/wshqtw-form";
|
||||
import { userPageWshqtzbForm } from "@/api/gxmu/wshqtzb-form";
|
||||
import { userPageYxgqtdgbForm } from "@/api/gxmu/yxgqtdgb-form";
|
||||
import { userPageYxgqtyForm } from "@/api/gxmu/yxgqty-form";
|
||||
import { userPageTzbcyForm } from "@/api/gxmu/tzbcy-form";
|
||||
import { userPageQmgcForm } from "@/api/gxmu/qmgc-form";
|
||||
import { userPageWxxzxForm } from "@/api/gxmu/wxxzx-form";
|
||||
import ModuleDataModal from "../reviewList/components/moduleDataModal.vue";
|
||||
import ReviewFlowModal from "../reviewList/components/reviewFlowModal.vue";
|
||||
import {
|
||||
DECLARE_MODULE_GROUP_OPTIONS,
|
||||
getDeclareModuleLabel
|
||||
} from "../declare/moduleOptions";
|
||||
|
||||
interface DeclareHistoryRecord {
|
||||
key: string;
|
||||
id?: number;
|
||||
module: string;
|
||||
title: string;
|
||||
year?: string | number;
|
||||
applicant?: string;
|
||||
submitTime?: string;
|
||||
latestReviewTime?: string;
|
||||
status?: number;
|
||||
reviewList?: ReviewList[];
|
||||
}
|
||||
|
||||
const { push } = useRouter();
|
||||
const selectedModule = ref<string>();
|
||||
const historyLoading = ref(false);
|
||||
const historyList = ref<DeclareHistoryRecord[]>([]);
|
||||
const showDetail = ref(false);
|
||||
const detailCurrent = ref<DeclareHistoryRecord | null>(null);
|
||||
const showReviewFlow = ref(false);
|
||||
const reviewFlowCurrent = ref<DeclareHistoryRecord | null>(null);
|
||||
const historyColumns = computed<ColumnType<DeclareHistoryRecord>[]>(() => [
|
||||
{
|
||||
title: "模块",
|
||||
dataIndex: "module",
|
||||
key: "module",
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: "申报名称",
|
||||
dataIndex: "title",
|
||||
key: "title",
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: "年份",
|
||||
dataIndex: "year",
|
||||
key: "year",
|
||||
width: 100,
|
||||
align: "center"
|
||||
},
|
||||
{
|
||||
title: "申报人",
|
||||
dataIndex: "applicant",
|
||||
key: "applicant",
|
||||
width: 140,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: "提交时间",
|
||||
dataIndex: "submitTime",
|
||||
key: "submitTime",
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: "最近审核时间",
|
||||
dataIndex: "latestReviewTime",
|
||||
key: "latestReviewTime",
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: "审核状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 120,
|
||||
align: "center"
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 180,
|
||||
align: "center",
|
||||
fixed: "right"
|
||||
}
|
||||
]);
|
||||
const historyPagination = computed<TablePaginationConfig>(() => ({
|
||||
pageSize: 10,
|
||||
hideOnSinglePage: historyList.value.length <= 10,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}));
|
||||
const filteredHistoryList = computed(() => {
|
||||
if (!selectedModule.value) {
|
||||
return historyList.value;
|
||||
}
|
||||
return historyList.value.filter((item) => item.module === selectedModule.value);
|
||||
});
|
||||
|
||||
const setSelectedModule = (module?: string) => {
|
||||
selectedModule.value = module;
|
||||
};
|
||||
|
||||
const goDeclareList = () => {
|
||||
push("/gxmu/declareList");
|
||||
};
|
||||
|
||||
const openDetail = (record: DeclareHistoryRecord) => {
|
||||
if (!record.id || !record.module) {
|
||||
message.error("当前记录缺少详情参数");
|
||||
return;
|
||||
}
|
||||
detailCurrent.value = record;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const openReviewFlow = (record: DeclareHistoryRecord) => {
|
||||
if (!record.id || !record.module) {
|
||||
message.error("当前记录缺少审核流参数");
|
||||
return;
|
||||
}
|
||||
reviewFlowCurrent.value = record;
|
||||
showReviewFlow.value = true;
|
||||
};
|
||||
|
||||
const formatDateTime = (value?: string) => {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
return toDateString(value, "yyyy-MM-dd HH:mm:ss");
|
||||
};
|
||||
|
||||
const getLatestReview = (reviewList?: ReviewList[]) => {
|
||||
const list = reviewList ?? [];
|
||||
return [...list].sort((a, b) => Number(b.id || 0) - Number(a.id || 0))[0];
|
||||
};
|
||||
|
||||
const mapHistoryRecord = (
|
||||
module: string,
|
||||
record: Record<string, any>,
|
||||
options: {
|
||||
title: (item: Record<string, any>) => string;
|
||||
applicant: (item: Record<string, any>) => string;
|
||||
}
|
||||
): DeclareHistoryRecord => {
|
||||
const latestReview = getLatestReview(record.reviewList as ReviewList[] | undefined);
|
||||
return {
|
||||
key: `${module}_${record.id}`,
|
||||
id: record.id,
|
||||
module,
|
||||
title: options.title(record) || getDeclareModuleLabel(module),
|
||||
year: record.year,
|
||||
applicant: options.applicant(record) || "-",
|
||||
submitTime: record.createTime ?? record.updateTime,
|
||||
latestReviewTime: latestReview?.updateTime ?? latestReview?.createTime,
|
||||
status: latestReview?.status,
|
||||
reviewList: record.reviewList
|
||||
};
|
||||
};
|
||||
|
||||
const loadHistoryPage = async <T extends Record<string, any>>(
|
||||
module: string,
|
||||
requestFn: (params: { page: number; limit: number }) => Promise<PageResult<T>>,
|
||||
options: {
|
||||
title: (item: T) => string;
|
||||
applicant: (item: T) => string;
|
||||
}
|
||||
) => {
|
||||
const firstPage = await requestFn({ page: 1, limit: 100 });
|
||||
const list = [...(firstPage.list ?? [])];
|
||||
const total = Number(firstPage.count ?? list.length);
|
||||
const pageSize = 100;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
|
||||
if (totalPages > 1) {
|
||||
const tasks: Promise<PageResult<T>>[] = [];
|
||||
for (let page = 2; page <= totalPages; page += 1) {
|
||||
tasks.push(requestFn({ page, limit: pageSize }));
|
||||
}
|
||||
const restPages = await Promise.all(tasks);
|
||||
restPages.forEach((pageData) => {
|
||||
list.push(...(pageData.list ?? []));
|
||||
});
|
||||
}
|
||||
|
||||
return list.map((item) => mapHistoryRecord(module, item, options));
|
||||
};
|
||||
|
||||
const loadHistoryData = async () => {
|
||||
historyLoading.value = true;
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
loadHistoryPage("gxmu_sjqn", userPageSjqnForm, {
|
||||
title: (item) => item.name || "十佳青年岗位能手",
|
||||
applicant: (item) => item.name || "-"
|
||||
}),
|
||||
loadHistoryPage("gxmu_sjtbzbsj", userPageSjtbzbsjForm, {
|
||||
title: (item) => item.name || "十佳团支部书记",
|
||||
applicant: (item) => item.name || "-"
|
||||
}),
|
||||
loadHistoryPage("gxmu_wshqtw", userPageWshqtwForm, {
|
||||
title: (item) => item.organizationName || item.schoolName || "五四红旗团委",
|
||||
applicant: (item) => item.secretaryName || item.organizationName || "-"
|
||||
}),
|
||||
loadHistoryPage("gxmu_wshqtzb", userPageWshqtzbForm, {
|
||||
title: (item) => item.organizationName || item.schoolName || "五四红旗团支部",
|
||||
applicant: (item) => item.secretaryName || item.organizationName || "-"
|
||||
}),
|
||||
loadHistoryPage("gxmu_yxgqtdgb", userPageYxgqtdgbForm, {
|
||||
title: (item) => item.name || "优秀共青团干部",
|
||||
applicant: (item) => item.name || "-"
|
||||
}),
|
||||
loadHistoryPage("gxmu_yxgqty", userPageYxgqtyForm, {
|
||||
title: (item) => item.name || "优秀共青团员",
|
||||
applicant: (item) => item.name || "-"
|
||||
}),
|
||||
loadHistoryPage("gxmu_tzbcy_form", userPageTzbcyForm, {
|
||||
title: (item) => item.projectName || "挑战杯",
|
||||
applicant: (item) => item.leader || "-"
|
||||
}),
|
||||
loadHistoryPage("gxmu_qmgc_form", userPageQmgcForm, {
|
||||
title: (item) => item.name || "青马工程",
|
||||
applicant: (item) => item.name || "-"
|
||||
}),
|
||||
loadHistoryPage("gxmu_wxxzx_project", userPageWxxzxForm, {
|
||||
title: (item) => item.topicName || "未来学术之星",
|
||||
applicant: (item) => item.leader || "-"
|
||||
})
|
||||
]);
|
||||
|
||||
historyList.value = results
|
||||
.flat()
|
||||
.sort((a, b) => {
|
||||
const timeA = new Date(a.submitTime || a.latestReviewTime || 0).getTime();
|
||||
const timeB = new Date(b.submitTime || b.latestReviewTime || 0).getTime();
|
||||
return timeB - timeA;
|
||||
});
|
||||
} catch (e) {
|
||||
message.error((e as Error).message);
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadHistoryData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "GxmuDeclareHistory"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.declare-history-page {
|
||||
min-height: calc(100vh - 132px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin-top: 6px;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #f7fbff 0%, #f4f8f6 100%);
|
||||
}
|
||||
|
||||
.filter-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
flex-shrink: 0;
|
||||
color: #1f2329;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.filter-tag-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-tag-group {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-group-name {
|
||||
flex-shrink: 0;
|
||||
min-width: 88px;
|
||||
color: #333333;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.filter-tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
margin: 0;
|
||||
padding: 6px 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #5f6b7a;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-tag:hover {
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.filter-tag-active {
|
||||
border-color: #1677ff;
|
||||
background: linear-gradient(90deg, #4096ff 0%, #0958d9 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header,
|
||||
.filter-bar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-left {
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-tag-group {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-group-name {
|
||||
min-width: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,579 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body declare-list-page">
|
||||
<a-card :bordered="false" :body-style="{ padding: '20px' }">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-title">申报列表</div>
|
||||
<div class="page-desc">当前仅展示可申报项目</div>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button @click="goHistoryPage">我的申报历史</a-button>
|
||||
<a-button :loading="loading" @click="loadData">刷新</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-left">
|
||||
<div class="filter-label">模块筛选</div>
|
||||
<div class="filter-tag-groups">
|
||||
<div class="filter-tag-row">
|
||||
<a-tag
|
||||
class="filter-tag"
|
||||
:class="{ 'filter-tag-active': !selectedModule }"
|
||||
@click="setSelectedModule()"
|
||||
>
|
||||
全部
|
||||
</a-tag>
|
||||
</div>
|
||||
<div
|
||||
v-for="group in DECLARE_MODULE_GROUP_OPTIONS"
|
||||
:key="group.value"
|
||||
class="filter-tag-group"
|
||||
>
|
||||
<div class="filter-group-name">{{ group.label }}</div>
|
||||
<div class="filter-tag-row">
|
||||
<a-tag
|
||||
v-for="module in group.modules"
|
||||
:key="module"
|
||||
class="filter-tag"
|
||||
:class="{ 'filter-tag-active': selectedModule === module }"
|
||||
@click="setSelectedModule(module)"
|
||||
>
|
||||
{{ getDeclareModuleLabel(module) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-button type="link" class="toggle-all-btn" @click="toggleAllGroups">
|
||||
{{ allGroupsCollapsed ? "全部展开" : "全部折叠" }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<a-empty v-if="!loading && !filteredDeclareGroups.length" description="当前暂无可申报项目" />
|
||||
|
||||
<div v-else class="declare-group-list">
|
||||
<section
|
||||
v-for="group in filteredDeclareGroups"
|
||||
:key="group.value"
|
||||
class="declare-group-section"
|
||||
>
|
||||
<div class="group-header">
|
||||
<div>
|
||||
<div class="group-title">{{ group.label }}</div>
|
||||
<div class="group-desc">共 {{ group.items.length }} 个可申报项目</div>
|
||||
</div>
|
||||
<a-button type="link" class="group-toggle-btn" @click="toggleGroup(group.value)">
|
||||
{{ isGroupCollapsed(group.value) ? "展开" : "折叠" }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div v-if="!isGroupCollapsed(group.value)" class="declare-grid">
|
||||
<a-card
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
hoverable
|
||||
class="declare-card"
|
||||
:bordered="false"
|
||||
>
|
||||
<div class="card-top">
|
||||
<div>
|
||||
<div class="card-title">
|
||||
{{ item.title || getDeclareModuleLabel(item.module) }}
|
||||
</div>
|
||||
<div class="card-subtitle">
|
||||
{{ getDeclareModuleLabel(item.module) }}
|
||||
</div>
|
||||
</div>
|
||||
<a-tag color="processing">可申报</a-tag>
|
||||
</div>
|
||||
|
||||
<div class="card-year">
|
||||
{{ item.year ? `${item.year} 年` : "未设置年度" }}
|
||||
</div>
|
||||
|
||||
<div class="card-meta">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">开始时间</span>
|
||||
<span class="meta-value">{{ formatDateTime(item.startTime) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">结束时间</span>
|
||||
<span class="meta-value">{{ formatDateTime(item.endTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
:disabled="!item.module || !DECLARE_MODULE_COMPONENT_MAP[item.module]"
|
||||
@click="goApply(item)"
|
||||
>
|
||||
进入申报
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
:visible="showApplyModal"
|
||||
:title="currentDeclare?.title || getDeclareModuleLabel(currentDeclare?.module)"
|
||||
:footer="null"
|
||||
:mask-closable="false"
|
||||
:destroy-on-close="true"
|
||||
:width="'96vw'"
|
||||
wrap-class-name="declare-apply-modal"
|
||||
:body-style="{ padding: '0', maxHeight: '80vh', overflow: 'auto' }"
|
||||
@update:visible="updateApplyModal"
|
||||
>
|
||||
<component
|
||||
:is="currentComponent"
|
||||
v-if="currentComponent && currentDeclare"
|
||||
:popup="true"
|
||||
:title="currentDeclare?.title"
|
||||
:fixed-year="currentDeclare?.year"
|
||||
:declare-form-project-types="currentDeclareFormProjectTypes"
|
||||
:declare-form-project-groups="currentDeclareFormProjectGroups"
|
||||
:declare-public-project-types="currentDeclarePublicProjectTypes"
|
||||
:declare-public-project-groups="currentDeclarePublicProjectGroups"
|
||||
class="apply-modal-content"
|
||||
/>
|
||||
<a-empty v-else description="当前申报项目未配置展示页面" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { message } from "ant-design-vue";
|
||||
import { toDateString } from "ele-admin-pro";
|
||||
import { listDeclare } from "@/api/gxmu/declare";
|
||||
import type { Declare } from "@/api/gxmu/declare/model";
|
||||
import {
|
||||
DECLARE_MODULE_GROUP_OPTIONS,
|
||||
getDeclareModuleLabel
|
||||
} from "../declare/moduleOptions";
|
||||
import SJQNForm from "../SJQNForm/index.vue";
|
||||
import SJTBZBSJForm from "../SJTBZBSJForm/index.vue";
|
||||
import WSHQTWForm from "../WSHQTWForm/index.vue";
|
||||
import WSHQTZBForm from "../WSHQTZBForm/index.vue";
|
||||
import YXGQTDGBForm from "../YXGQTDGBForm/index.vue";
|
||||
import YXGQTYForm from "../YXGQTYForm/index.vue";
|
||||
import TZBCYForm from "../TZBCYForm/index.vue";
|
||||
import QMGCForm from "../QMGCForm/index.vue";
|
||||
import WXXZXForm from "../WXXZXForm/index.vue";
|
||||
import { parseDeclareMultiValueField } from "../declare/projectFieldUtils";
|
||||
|
||||
const { push } = useRouter();
|
||||
const loading = ref(false);
|
||||
const declareList = ref<Declare[]>([]);
|
||||
const selectedModule = ref<string>();
|
||||
const collapsedGroups = ref<string[]>([]);
|
||||
const showApplyModal = ref(false);
|
||||
const currentDeclare = ref<Declare | null>(null);
|
||||
const DECLARE_MODULE_COMPONENT_MAP: Record<string, any> = {
|
||||
gxmu_sjqn: markRaw(SJQNForm),
|
||||
gxmu_sjtbzbsj: markRaw(SJTBZBSJForm),
|
||||
gxmu_wshqtw: markRaw(WSHQTWForm),
|
||||
gxmu_wshqtzb: markRaw(WSHQTZBForm),
|
||||
gxmu_yxgqtdgb: markRaw(YXGQTDGBForm),
|
||||
gxmu_yxgqty: markRaw(YXGQTYForm),
|
||||
gxmu_tzbcy_form: markRaw(TZBCYForm),
|
||||
gxmu_qmgc_form: markRaw(QMGCForm),
|
||||
gxmu_wxxzx_project: markRaw(WXXZXForm)
|
||||
};
|
||||
const currentComponent = computed(() => {
|
||||
const module = currentDeclare.value?.module;
|
||||
return module ? DECLARE_MODULE_COMPONENT_MAP[module] : undefined;
|
||||
});
|
||||
const currentDeclareFormProjectTypes = computed(() =>
|
||||
parseDeclareMultiValueField(
|
||||
currentDeclare.value?.formProjectType ?? currentDeclare.value?.projectType
|
||||
)
|
||||
);
|
||||
const currentDeclareFormProjectGroups = computed(() =>
|
||||
parseDeclareMultiValueField(
|
||||
currentDeclare.value?.formProjectGroup ?? currentDeclare.value?.projectGroup
|
||||
)
|
||||
);
|
||||
const currentDeclarePublicProjectTypes = computed(() =>
|
||||
parseDeclareMultiValueField(
|
||||
currentDeclare.value?.publicProjectType ?? currentDeclare.value?.projectType
|
||||
)
|
||||
);
|
||||
const currentDeclarePublicProjectGroups = computed(() =>
|
||||
parseDeclareMultiValueField(
|
||||
currentDeclare.value?.publicProjectGroup ?? currentDeclare.value?.projectGroup
|
||||
)
|
||||
);
|
||||
const visibleGroupValues = computed(() =>
|
||||
filteredDeclareGroups.value.map((group) => group.value)
|
||||
);
|
||||
const allGroupsCollapsed = computed(() => {
|
||||
return (
|
||||
visibleGroupValues.value.length > 0 &&
|
||||
visibleGroupValues.value.every((value) => collapsedGroups.value.includes(value))
|
||||
);
|
||||
});
|
||||
const filteredDeclareGroups = computed(() => {
|
||||
return DECLARE_MODULE_GROUP_OPTIONS.map((group) => {
|
||||
const items = declareList.value.filter((item) => {
|
||||
if (!item.module || !group.modules.includes(item.module)) {
|
||||
return false;
|
||||
}
|
||||
if (selectedModule.value && item.module !== selectedModule.value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return {
|
||||
...group,
|
||||
items
|
||||
};
|
||||
}).filter((group) => group.items.length > 0);
|
||||
});
|
||||
const isGroupCollapsed = (groupValue: string) => {
|
||||
return collapsedGroups.value.includes(groupValue);
|
||||
};
|
||||
|
||||
const setSelectedModule = (module?: string) => {
|
||||
selectedModule.value = module;
|
||||
};
|
||||
|
||||
const formatDateTime = (value?: string) => {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
return toDateString(value, "yyyy-MM-dd HH:mm:ss");
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await listDeclare({ usable: true });
|
||||
declareList.value = [...data].sort((a, b) => {
|
||||
const yearA = Number(a.year ?? 0);
|
||||
const yearB = Number(b.year ?? 0);
|
||||
return yearB - yearA;
|
||||
});
|
||||
} catch (e) {
|
||||
message.error((e as Error).message);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const goHistoryPage = () => {
|
||||
push("/gxmu/declareHistory");
|
||||
};
|
||||
|
||||
const updateApplyModal = (value: boolean) => {
|
||||
showApplyModal.value = value;
|
||||
if (!value) {
|
||||
currentDeclare.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleGroup = (groupValue: string) => {
|
||||
if (isGroupCollapsed(groupValue)) {
|
||||
collapsedGroups.value = collapsedGroups.value.filter((item) => item !== groupValue);
|
||||
return;
|
||||
}
|
||||
collapsedGroups.value = [...collapsedGroups.value, groupValue];
|
||||
};
|
||||
|
||||
const toggleAllGroups = () => {
|
||||
if (allGroupsCollapsed.value) {
|
||||
collapsedGroups.value = collapsedGroups.value.filter(
|
||||
(item) => !visibleGroupValues.value.includes(item)
|
||||
);
|
||||
return;
|
||||
}
|
||||
collapsedGroups.value = Array.from(
|
||||
new Set([...collapsedGroups.value, ...visibleGroupValues.value])
|
||||
);
|
||||
};
|
||||
|
||||
const goApply = (item: Declare) => {
|
||||
if (!item.module || !DECLARE_MODULE_COMPONENT_MAP[item.module]) {
|
||||
message.warning("当前申报项目未配置跳转页面");
|
||||
return;
|
||||
}
|
||||
currentDeclare.value = item;
|
||||
showApplyModal.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "GxmuDeclareList"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.declare-list-page {
|
||||
min-height: calc(100vh - 132px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin-top: 6px;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #f7fbff 0%, #f4f8f6 100%);
|
||||
}
|
||||
|
||||
.filter-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
flex-shrink: 0;
|
||||
color: #1f2329;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.filter-tag-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-tag-group {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-group-name {
|
||||
flex-shrink: 0;
|
||||
min-width: 88px;
|
||||
color: #333333;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.filter-tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
margin: 0;
|
||||
padding: 6px 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #5f6b7a;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-tag:hover {
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.filter-tag-active {
|
||||
border-color: #1677ff;
|
||||
background: linear-gradient(90deg, #4096ff 0%, #0958d9 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.toggle-all-btn,
|
||||
.group-toggle-btn {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.declare-group-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.declare-group-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
color: #1f2329;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.group-desc {
|
||||
margin-top: 4px;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.declare-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.declare-card {
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(240, 247, 255, 0.95) 0%, #ffffff 42%),
|
||||
#ffffff;
|
||||
box-shadow: 0 10px 24px rgba(31, 35, 41, 0.06);
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #1f2329;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
color: #5f6b7a;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card-year {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 999px;
|
||||
background: #eef5ff;
|
||||
color: #1554ad;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(246, 248, 250, 0.9);
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #1f2329;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.apply-modal-content {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
:deep(.declare-apply-modal .ant-modal) {
|
||||
top: 24px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.declare-apply-modal .ant-modal-content) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header,
|
||||
.filter-bar,
|
||||
.group-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-left {
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-tag-group {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-group-name {
|
||||
min-width: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
.gxmu-field-theme() {
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-input-affix-wrapper),
|
||||
:deep(.ant-select-selector),
|
||||
:deep(.ant-picker),
|
||||
:deep(.ant-input-number),
|
||||
:deep(.ant-input-search .ant-input),
|
||||
:deep(.ant-input-search .ant-input-affix-wrapper) {
|
||||
border-color: @primary-color !important;
|
||||
border-radius: 10px !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.ant-input:hover),
|
||||
:deep(.ant-input-affix-wrapper:hover),
|
||||
:deep(.ant-select:not(.ant-select-disabled):hover .ant-select-selector),
|
||||
:deep(.ant-picker:hover),
|
||||
:deep(.ant-input-number:hover),
|
||||
:deep(.ant-input-search:hover .ant-input),
|
||||
:deep(.ant-input-search:hover .ant-input-affix-wrapper) {
|
||||
border-color: darken(@primary-color, 5%) !important;
|
||||
}
|
||||
|
||||
:deep(.ant-input:focus),
|
||||
:deep(.ant-input-focused),
|
||||
:deep(.ant-input-affix-wrapper-focused),
|
||||
:deep(.ant-select-focused .ant-select-selector),
|
||||
:deep(.ant-picker-focused),
|
||||
:deep(.ant-input-number-focused),
|
||||
:deep(.ant-input-search .ant-input:focus),
|
||||
:deep(.ant-input-search .ant-input-affix-wrapper-focused) {
|
||||
border-color: darken(@primary-color, 10%) !important;
|
||||
box-shadow: 0 0 0 2px fade(@primary-color, 14%) !important;
|
||||
}
|
||||
|
||||
:deep(.ant-input-group-addon),
|
||||
:deep(.ant-input-search-button) {
|
||||
border-color: @primary-color !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
:deep(textarea.ant-input) {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<div class="topic-search-page">
|
||||
<a-card :bordered="false" class="hero-card">
|
||||
<div class="hero-wrap">
|
||||
<div>
|
||||
<div class="hero-kicker">PROJECT TOPIC RETRIEVAL</div>
|
||||
<div class="hero-title">项目主题语义检索</div>
|
||||
<div class="hero-desc">
|
||||
输入主题关键字后,从未来学术之星项目库中检索课题名称、课题类型和立论依据相关内容,并以卡片方式展示。
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-search">
|
||||
<a-select
|
||||
v-model:value="selectedModule"
|
||||
class="module-select"
|
||||
size="large"
|
||||
:options="moduleOptions"
|
||||
@change="handleModuleChange"
|
||||
/>
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
size="large"
|
||||
allow-clear
|
||||
placeholder="请输入项目主题关键字,例如:乡村振兴、医学科普、心理健康"
|
||||
enter-button="搜索项目"
|
||||
:loading="loading"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false" class="result-card">
|
||||
<template #title>
|
||||
<div class="result-head">
|
||||
<span>检索结果</span>
|
||||
<a-tag color="processing">
|
||||
{{ hasSearched ? `共 ${results.length} 条` : '待搜索' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button :loading="loading" @click="handleSearch">刷新</a-button>
|
||||
</template>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<a-empty
|
||||
v-if="!hasSearched && !loading"
|
||||
description="输入关键字后开始检索项目主题"
|
||||
/>
|
||||
|
||||
<a-empty
|
||||
v-else-if="hasSearched && !results.length && !loading"
|
||||
description="未检索到相关项目"
|
||||
/>
|
||||
|
||||
<div v-else class="card-grid">
|
||||
<div
|
||||
v-for="item in results"
|
||||
:key="item.id"
|
||||
class="topic-card"
|
||||
@click="openDetail(item)"
|
||||
>
|
||||
<div class="topic-card-head">
|
||||
<div class="topic-tags">
|
||||
<a-tag color="geekblue">{{ item.moduleName || getDeclareModuleLabel(item.module) }}</a-tag>
|
||||
<a-tag color="blue">{{ item.topicType || '未分类' }}</a-tag>
|
||||
</div>
|
||||
<span class="score-text">相关度 {{ item.score ?? 0 }}</span>
|
||||
</div>
|
||||
<div class="topic-title">{{ item.topicName || '未命名项目' }}</div>
|
||||
<div class="topic-snippet">{{ item.snippet || '暂无摘要' }}</div>
|
||||
<div class="topic-action">点击查看详情</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
:visible="showDetail"
|
||||
width="760px"
|
||||
title="项目详情"
|
||||
:footer="null"
|
||||
@update:visible="updateDetailVisible"
|
||||
>
|
||||
<template v-if="current">
|
||||
<div class="detail-meta">
|
||||
<a-tag color="geekblue">{{ current.moduleName || getDeclareModuleLabel(current.module) }}</a-tag>
|
||||
<a-tag color="blue">{{ current.topicType || '未分类' }}</a-tag>
|
||||
<a-tag color="geekblue">相关度 {{ current.score ?? 0 }}</a-tag>
|
||||
</div>
|
||||
<div class="detail-title">{{ current.topicName || '未命名项目' }}</div>
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">立论依据</div>
|
||||
<div class="detail-content">{{ current.rationale || '暂无立论依据' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { searchProjectTopics } from '@/api/gxmu/project-topic-semantic-search';
|
||||
import type { WxxzxTopicSemanticSearchItem } from '@/api/gxmu/project-topic-semantic-search/model';
|
||||
import {
|
||||
DECLARE_MODULE_OPTIONS,
|
||||
getDeclareModuleLabel
|
||||
} from '@/views/gxmu/declare/moduleOptions';
|
||||
|
||||
const keyword = ref('');
|
||||
const selectedModule = ref('gxmu_wxxzx_project');
|
||||
const loading = ref(false);
|
||||
const hasSearched = ref(false);
|
||||
const results = ref<WxxzxTopicSemanticSearchItem[]>([]);
|
||||
const showDetail = ref(false);
|
||||
const current = ref<WxxzxTopicSemanticSearchItem>();
|
||||
const moduleOptions = DECLARE_MODULE_OPTIONS.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
}));
|
||||
|
||||
const handleSearch = async () => {
|
||||
const searchValue = keyword.value.trim();
|
||||
hasSearched.value = true;
|
||||
if (!searchValue) {
|
||||
results.value = [];
|
||||
message.warning('请输入检索关键字');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
results.value = await searchProjectTopics(searchValue, selectedModule.value);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '检索失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleModuleChange = () => {
|
||||
if (keyword.value.trim()) {
|
||||
handleSearch();
|
||||
} else if (hasSearched.value) {
|
||||
results.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const openDetail = (item: WxxzxTopicSemanticSearchItem) => {
|
||||
current.value = item;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const updateDetailVisible = (value: boolean) => {
|
||||
showDetail.value = value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ProjectTopicSemanticSearch'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.topic-search-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.result-card {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
margin-bottom: 20px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(22, 119, 255, 0.18), transparent 34%),
|
||||
linear-gradient(135deg, #f3f8ff 0%, #ffffff 52%, #f7fbf7 100%);
|
||||
}
|
||||
|
||||
.hero-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 520px);
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-kicker {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.24em;
|
||||
color: #1677ff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
margin-top: 12px;
|
||||
font-size: 34px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
margin-top: 12px;
|
||||
max-width: 760px;
|
||||
line-height: 1.85;
|
||||
color: rgba(31, 41, 55, 0.78);
|
||||
}
|
||||
|
||||
.hero-search {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(22, 119, 255, 0.12);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
box-shadow: 0 18px 44px rgba(31, 41, 55, 0.08);
|
||||
}
|
||||
|
||||
.module-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.result-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.topic-card {
|
||||
padding: 18px;
|
||||
border: 1px solid #e5edf7;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(243, 248, 255, 0.92), #ffffff 72%),
|
||||
#ffffff;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.topic-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(22, 119, 255, 0.28);
|
||||
box-shadow: 0 16px 32px rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
|
||||
.topic-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.topic-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
margin-top: 14px;
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.topic-snippet {
|
||||
margin-top: 12px;
|
||||
min-height: 72px;
|
||||
line-height: 1.8;
|
||||
color: rgba(17, 24, 39, 0.74);
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.topic-action {
|
||||
margin-top: 16px;
|
||||
color: #1677ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-top: 20px;
|
||||
padding: 18px;
|
||||
border-radius: 16px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
line-height: 1.9;
|
||||
color: rgba(17, 24, 39, 0.82);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.hero-wrap {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,509 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '编辑审核流' : '添加审核流'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="名称" name="title">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入名称"
|
||||
v-model:value="form.title"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="机构" name="organizationIds">
|
||||
<a-tree-select
|
||||
multiple
|
||||
allow-clear
|
||||
tree-default-expand-all
|
||||
:tree-data="organizationTreeData"
|
||||
:value="form.organizationIds"
|
||||
:loading="organizationLoading"
|
||||
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
|
||||
placeholder="请选择审核机构"
|
||||
@update:value="updateOrganizationIds"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核顺序">
|
||||
<div class="flow-step-list">
|
||||
<div
|
||||
v-for="(item, index) in form.flowItems"
|
||||
:key="item.organizationId"
|
||||
class="flow-step-item"
|
||||
>
|
||||
<div class="flow-step-info">
|
||||
<a-tag color="blue">第{{ index + 1 }}级</a-tag>
|
||||
<span>{{ getOrganizationName(item.organizationId) }}</span>
|
||||
</div>
|
||||
<div class="flow-step-reviewer">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:value="item.reviewUserId"
|
||||
:loading="reviewUserLoadingMap[item.organizationId || 0]"
|
||||
:options="getReviewUserOptions(item.organizationId)"
|
||||
:filter-option="filterReviewUserOption"
|
||||
placeholder="请选择审核人"
|
||||
@focus="loadReviewUsers(item.organizationId)"
|
||||
@update:value="updateReviewUser(index, $event)"
|
||||
/>
|
||||
</div>
|
||||
<a-space :size="4">
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="index === 0"
|
||||
@click="moveOrganization(index, 'up')"
|
||||
>
|
||||
上移
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="index === form.flowItems.length - 1"
|
||||
@click="moveOrganization(index, 'down')"
|
||||
>
|
||||
下移
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="removeOrganization(index)">
|
||||
移除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-empty v-if="!form.flowItems.length" description="请选择审核机构" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { toTreeData } from 'ele-admin-pro/es';
|
||||
import { addReviewFlow, removeReviewFlow } from '@/api/gxmu/reviewFlow';
|
||||
import { ReviewFlow } from '@/api/gxmu/reviewFlow/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { listOrganizations } from '@/api/system/organization';
|
||||
import type { Organization } from '@/api/system/organization/model';
|
||||
import { listUsers } from '@/api/system/user';
|
||||
import type { User } from '@/api/system/user/model';
|
||||
|
||||
interface ReviewFlowItem {
|
||||
organizationId?: number;
|
||||
reviewUserId?: number;
|
||||
}
|
||||
|
||||
interface ReviewFlowForm extends Omit<ReviewFlow, 'reviewUserId'> {
|
||||
organizationIds: number[];
|
||||
flowItems: ReviewFlowItem[];
|
||||
}
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ReviewFlow | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
// 机构树数据
|
||||
const organizationTreeData = ref<Organization[]>([]);
|
||||
const organizationLoading = ref(false);
|
||||
const organizationNameMap = ref<Record<number, string>>({});
|
||||
const reviewUserOptionsMap = ref<Record<number, User[]>>({});
|
||||
const reviewUserLoadingMap = ref<Record<number, boolean>>({});
|
||||
const reviewUserNameMap = ref<Record<number, string>>({});
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ReviewFlowForm>({
|
||||
id: undefined,
|
||||
title: undefined,
|
||||
organizationId: undefined,
|
||||
organizationIds: [],
|
||||
flowItems: [],
|
||||
level: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
userId: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
whitespace: true,
|
||||
message: '请输入名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
organizationIds: [
|
||||
{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: '请选择至少一个机构',
|
||||
trigger: 'change'
|
||||
},
|
||||
{
|
||||
validator: async () => {
|
||||
const invalid = form.flowItems.some((item) => !item.reviewUserId);
|
||||
if (invalid) {
|
||||
return Promise.reject(new Error('请为每一级选择审核人'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
const syncOrganizationIds = () => {
|
||||
form.organizationIds = form.flowItems
|
||||
.map((item) => item.organizationId)
|
||||
.filter((item): item is number => typeof item === 'number');
|
||||
};
|
||||
|
||||
const loadOrganizations = async () => {
|
||||
organizationLoading.value = true;
|
||||
try {
|
||||
const result = await listOrganizations();
|
||||
organizationNameMap.value = result.reduce<Record<number, string>>(
|
||||
(map, item) => {
|
||||
if (item.organizationId) {
|
||||
map[item.organizationId] =
|
||||
item.organizationName || `机构${item.organizationId}`;
|
||||
}
|
||||
return map;
|
||||
},
|
||||
{}
|
||||
);
|
||||
organizationTreeData.value = toTreeData({
|
||||
data: result.map((item) => ({
|
||||
...item,
|
||||
key: item.organizationId,
|
||||
value: item.organizationId,
|
||||
title: item.organizationName
|
||||
})),
|
||||
idField: 'organizationId',
|
||||
parentIdField: 'parentId'
|
||||
}) as Organization[];
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '机构列表加载失败');
|
||||
} finally {
|
||||
organizationLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateOrganizationIds = (values?: number[]) => {
|
||||
const selectedIds = values || [];
|
||||
const selectedSet = new Set(selectedIds);
|
||||
const flowItemMap = new Map(
|
||||
form.flowItems.map((item) => [item.organizationId, item] as const)
|
||||
);
|
||||
const keptItems = form.flowItems.filter((item) =>
|
||||
selectedSet.has(item.organizationId || 0)
|
||||
);
|
||||
const appendItems = selectedIds
|
||||
.filter((id) => !flowItemMap.has(id))
|
||||
.map((organizationId) => ({
|
||||
organizationId,
|
||||
reviewUserId: undefined
|
||||
}));
|
||||
form.flowItems = [...keptItems, ...appendItems];
|
||||
syncOrganizationIds();
|
||||
};
|
||||
|
||||
const moveOrganization = (index: number, direction: 'up' | 'down') => {
|
||||
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (targetIndex < 0 || targetIndex >= form.flowItems.length) {
|
||||
return;
|
||||
}
|
||||
const items = [...form.flowItems];
|
||||
[items[index], items[targetIndex]] = [items[targetIndex], items[index]];
|
||||
form.flowItems = items;
|
||||
syncOrganizationIds();
|
||||
};
|
||||
|
||||
const removeOrganization = (index: number) => {
|
||||
form.flowItems = form.flowItems.filter((_, i) => i !== index);
|
||||
syncOrganizationIds();
|
||||
};
|
||||
|
||||
const getOrganizationName = (organizationId?: number) => {
|
||||
if (!organizationId) {
|
||||
return '-';
|
||||
}
|
||||
return organizationNameMap.value[organizationId] || `机构${organizationId}`;
|
||||
};
|
||||
|
||||
const loadReviewUsers = async (organizationId?: number) => {
|
||||
if (!organizationId || reviewUserOptionsMap.value[organizationId]) {
|
||||
return;
|
||||
}
|
||||
reviewUserLoadingMap.value = {
|
||||
...reviewUserLoadingMap.value,
|
||||
[organizationId]: true
|
||||
};
|
||||
try {
|
||||
const result = await listUsers({ organizationId });
|
||||
reviewUserOptionsMap.value = {
|
||||
...reviewUserOptionsMap.value,
|
||||
[organizationId]: result
|
||||
};
|
||||
reviewUserNameMap.value = result.reduce<Record<number, string>>(
|
||||
(map, item) => {
|
||||
if (item.userId) {
|
||||
map[item.userId] =
|
||||
item.realName || item.nickname || `用户${item.userId}`;
|
||||
}
|
||||
return map;
|
||||
},
|
||||
reviewUserNameMap.value
|
||||
);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '审核人列表加载失败');
|
||||
} finally {
|
||||
reviewUserLoadingMap.value = {
|
||||
...reviewUserLoadingMap.value,
|
||||
[organizationId]: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getReviewUserOptions = (organizationId?: number) => {
|
||||
if (!organizationId) {
|
||||
return [];
|
||||
}
|
||||
return (reviewUserOptionsMap.value[organizationId] || []).map((item) => ({
|
||||
value: item.userId,
|
||||
label: item.realName || item.nickname || `用户${item.userId}`
|
||||
}));
|
||||
};
|
||||
|
||||
const filterReviewUserOption = (input: string, option: any) => {
|
||||
return String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase());
|
||||
};
|
||||
|
||||
const updateReviewUser = (index: number, reviewUserId?: number) => {
|
||||
const items = [...form.flowItems];
|
||||
const organizationId = items[index]?.organizationId;
|
||||
const currentUser = organizationId
|
||||
? reviewUserOptionsMap.value[organizationId]?.find(
|
||||
(item) => item.userId === reviewUserId
|
||||
)
|
||||
: undefined;
|
||||
if (reviewUserId && currentUser?.userId) {
|
||||
reviewUserNameMap.value = {
|
||||
...reviewUserNameMap.value,
|
||||
[reviewUserId]:
|
||||
currentUser.realName ||
|
||||
currentUser.nickname ||
|
||||
`用户${currentUser.userId}`
|
||||
};
|
||||
}
|
||||
items[index] = {
|
||||
...items[index],
|
||||
reviewUserId
|
||||
};
|
||||
form.flowItems = items;
|
||||
syncOrganizationIds();
|
||||
};
|
||||
|
||||
const getSortedFlows = () => {
|
||||
if (props.data?.flows?.length) {
|
||||
return [...props.data.flows].sort(
|
||||
(a, b) => (a.level || 0) - (b.level || 0)
|
||||
);
|
||||
}
|
||||
if (props.data?.id) {
|
||||
return [props.data];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const preloadReviewUsers = async () => {
|
||||
for (const item of form.flowItems) {
|
||||
await loadReviewUsers(item.organizationId);
|
||||
}
|
||||
};
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = async () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
let successMessage = '';
|
||||
|
||||
if (isUpdate.value) {
|
||||
const currentFlows = getSortedFlows();
|
||||
for (const currentFlow of currentFlows) {
|
||||
if (currentFlow.id) {
|
||||
await removeReviewFlow(currentFlow.id);
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < form.flowItems.length; index++) {
|
||||
const item = form.flowItems[index];
|
||||
const formData: ReviewFlow = {
|
||||
...form,
|
||||
title: form.title,
|
||||
organizationId: item.organizationId,
|
||||
reviewUserId: item.reviewUserId,
|
||||
level: index + 1
|
||||
};
|
||||
delete (formData as ReviewFlow & { organizationIds?: number[] })
|
||||
.organizationIds;
|
||||
delete (formData as ReviewFlow & { flowItems?: ReviewFlowItem[] })
|
||||
.flowItems;
|
||||
delete formData.id;
|
||||
successMessage = await addReviewFlow(formData);
|
||||
}
|
||||
} else {
|
||||
for (let index = 0; index < form.flowItems.length; index++) {
|
||||
const item = form.flowItems[index];
|
||||
const formData: ReviewFlow = {
|
||||
...form,
|
||||
organizationId: item.organizationId,
|
||||
reviewUserId: item.reviewUserId,
|
||||
level: index + 1
|
||||
};
|
||||
delete (formData as ReviewFlow & { organizationIds?: number[] })
|
||||
.organizationIds;
|
||||
delete (formData as ReviewFlow & { flowItems?: ReviewFlowItem[] })
|
||||
.flowItems;
|
||||
successMessage = await addReviewFlow(formData);
|
||||
}
|
||||
}
|
||||
|
||||
message.success(successMessage || '保存成功');
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
if (e?.errorFields) {
|
||||
return;
|
||||
}
|
||||
message.error(e.message || '保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
if (!organizationTreeData.value.length) {
|
||||
await loadOrganizations();
|
||||
}
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
const currentFlows = getSortedFlows();
|
||||
form.flowItems = currentFlows
|
||||
.filter((item) => typeof item.organizationId === 'number')
|
||||
.map((item) => ({
|
||||
organizationId: item.organizationId,
|
||||
reviewUserId: item.reviewUserId
|
||||
}));
|
||||
syncOrganizationIds();
|
||||
await preloadReviewUsers();
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
resetFields();
|
||||
form.organizationIds = [];
|
||||
form.flowItems = [];
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
form.organizationIds = [];
|
||||
form.flowItems = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.flow-step-list {
|
||||
width: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.flow-step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.flow-step-item + .flow-step-item {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.flow-step-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.flow-step-reviewer {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { GradeParam } from '@/api/user/grade/model';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 选中的角色
|
||||
selection?: [];
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: GradeParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
// 新增
|
||||
const add = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selection,
|
||||
() => {}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,222 @@
|
||||
<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"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 0" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ReviewFlowEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
|
||||
import type { EleProTable } from "ele-admin-pro";
|
||||
import { toDateString } from "ele-admin-pro";
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from "ele-admin-pro/es/ele-pro-table/types";
|
||||
import Search from "./components/search.vue";
|
||||
import ReviewFlowEdit from "./components/reviewFlowEdit.vue";
|
||||
import { pageReviewFlow, removeReviewFlow, removeBatchReviewFlow } from "@/api/gxmu/reviewFlow";
|
||||
import type { ReviewFlow, ReviewFlowParam } from "@/api/gxmu/reviewFlow/model";
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ReviewFlow[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ReviewFlow | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
where.withGroup = true;
|
||||
return pageReviewFlow({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
align: "center",
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "title",
|
||||
key: "title",
|
||||
align: "center"
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createTime",
|
||||
key: "createTime",
|
||||
align: "center",
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, "yyyy-MM-dd HH:mm:ss")
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
align: "center",
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ReviewFlowParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ReviewFlow) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ReviewFlow) => {
|
||||
const hide = message.loading("请求中..", 0);
|
||||
removeReviewFlow(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error("请至少选择一条数据");
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: "提示",
|
||||
content: "确定要删除选中的记录吗?",
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading("请求中..", 0);
|
||||
removeBatchReviewFlow(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ReviewFlow) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "ReviewFlow"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,287 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '编辑' : '添加'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="名称" name="title">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入名称"
|
||||
v-model:value="form.title"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核流" name="flowId">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:options="reviewFlowOptions"
|
||||
:loading="reviewFlowLoading"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择审核流"
|
||||
v-model:value="form.flowId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属部门" name="organizationId">
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
show-search
|
||||
tree-default-expand-all
|
||||
:tree-data="organizationTreeData"
|
||||
:loading="organizationLoading"
|
||||
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
|
||||
placeholder="请选择所属部门"
|
||||
v-model:value="form.organizationId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="模块" name="module">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:options="moduleOptions"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择模块"
|
||||
v-model:value="form.module"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { toTreeData } from 'ele-admin-pro/es';
|
||||
import {
|
||||
addReviewFlowConfig,
|
||||
updateReviewFlowConfig
|
||||
} from '@/api/gxmu/reviewFlowConfig';
|
||||
import { ReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { listReviewFlow } from '@/api/gxmu/reviewFlow';
|
||||
import type { ReviewFlow } from '@/api/gxmu/reviewFlow/model';
|
||||
import { listOrganizations } from '@/api/system/organization';
|
||||
import type { Organization } from '@/api/system/organization/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ReviewFlowConfig | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const reviewFlowLoading = ref(false);
|
||||
const reviewFlowOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const organizationLoading = ref(false);
|
||||
const organizationTreeData = ref<Organization[]>([]);
|
||||
const moduleOptions = [
|
||||
{ label: '稿件', value: 'cms_manuscript' },
|
||||
{ label: '十佳青年岗位能手', value: 'gxmu_sjqn' },
|
||||
{ label: '十佳团支部书记', value: 'gxmu_sjtbzbsj' },
|
||||
{ label: '五四红旗团委', value: 'gxmu_wshqtw' },
|
||||
{ label: '五四红旗团支部', value: 'gxmu_wshqtzb' },
|
||||
{ label: '优秀共青团干部', value: 'gxmu_yxgqtdgb' },
|
||||
{ label: '优秀共青团员', value: 'gxmu_yxgqty' },
|
||||
{ label: '挑战杯', value: 'gxmu_tzbcy_form' },
|
||||
{ label: '青马工程', value: 'gxmu_qmgc_form' },
|
||||
{ label: '未来学术之星', value: 'gxmu_wxxzx_project' }
|
||||
];
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ReviewFlowConfig>({
|
||||
id: undefined,
|
||||
title: undefined,
|
||||
organizationId: undefined,
|
||||
flowId: undefined,
|
||||
module: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
userId: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
whitespace: true,
|
||||
message: '请输入名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
flowId: [
|
||||
{
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请选择审核流',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
organizationId: [
|
||||
{
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请选择所属部门',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
module: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择模块',
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
const loadReviewFlowOptions = async () => {
|
||||
reviewFlowLoading.value = true;
|
||||
try {
|
||||
const result = await listReviewFlow({ withGroup: true });
|
||||
const optionMap = new Map<number, string>();
|
||||
result.forEach((item: ReviewFlow) => {
|
||||
if (item.id && item.title && !optionMap.has(item.id)) {
|
||||
optionMap.set(item.id, item.title);
|
||||
}
|
||||
});
|
||||
reviewFlowOptions.value = Array.from(optionMap.entries()).map(
|
||||
([value, label]) => ({
|
||||
label,
|
||||
value
|
||||
})
|
||||
);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '审核流列表加载失败');
|
||||
} finally {
|
||||
reviewFlowLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadOrganizations = async () => {
|
||||
organizationLoading.value = true;
|
||||
try {
|
||||
const result = await listOrganizations();
|
||||
organizationTreeData.value = toTreeData({
|
||||
data: result.map((item) => ({
|
||||
...item,
|
||||
key: item.organizationId,
|
||||
value: item.organizationId,
|
||||
title: item.organizationName
|
||||
})),
|
||||
idField: 'organizationId',
|
||||
parentIdField: 'parentId'
|
||||
}) as Organization[];
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '部门列表加载失败');
|
||||
} finally {
|
||||
organizationLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const filterSelectOption = (input: string, option: any) => {
|
||||
return String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase());
|
||||
};
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateReviewFlowConfig
|
||||
: addReviewFlowConfig;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
if (!organizationTreeData.value.length) {
|
||||
await loadOrganizations();
|
||||
}
|
||||
if (!reviewFlowOptions.value.length) {
|
||||
await loadReviewFlowOptions();
|
||||
}
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
resetFields();
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { GradeParam } from '@/api/user/grade/model';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 选中的角色
|
||||
selection?: [];
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: GradeParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
// 新增
|
||||
const add = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selection,
|
||||
() => {}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,233 @@
|
||||
<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"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 0" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ReviewFlowConfigEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ReviewFlowConfigEdit from './components/reviewFlowConfigEdit.vue';
|
||||
import { pageReviewFlowConfig, removeReviewFlowConfig, removeBatchReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig';
|
||||
import type { ReviewFlowConfig, ReviewFlowConfigParam } from '@/api/gxmu/reviewFlowConfig/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ReviewFlowConfig[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ReviewFlowConfig | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageReviewFlowConfig({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '审核流',
|
||||
dataIndex: 'flowName',
|
||||
key: 'flowId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'moduleName',
|
||||
key: 'module',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ReviewFlowConfigParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ReviewFlowConfig) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ReviewFlowConfig) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeReviewFlowConfig(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchReviewFlowConfig(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ReviewFlowConfig) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ReviewFlowConfig'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,739 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="960"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="modalTitle"
|
||||
:body-style="{ paddingBottom: '20px' }"
|
||||
okText="关闭"
|
||||
@update:visible="updateVisible"
|
||||
@ok="updateVisible(false)"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<a-empty
|
||||
v-if="!loading && !descriptionItems.length"
|
||||
:description="emptyDescription"
|
||||
/>
|
||||
<a-descriptions v-else bordered :column="2" size="middle">
|
||||
<a-descriptions-item
|
||||
v-for="item in descriptionItems"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
:span="item.span"
|
||||
>
|
||||
<template v-if="isFileField(item.key)">
|
||||
<div
|
||||
v-for="(file, index) in normalizeFileEntries(item.value)"
|
||||
:key="`${item.key}-file-${index}`"
|
||||
class="file-entry"
|
||||
>
|
||||
<a :href="file.url" target="_blank" rel="noreferrer">
|
||||
{{ file.name }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<a-image
|
||||
v-else-if="isImageField(item.key) && typeof item.value === 'string'"
|
||||
:src="item.value"
|
||||
:width="120"
|
||||
/>
|
||||
<template v-else-if="Array.isArray(item.value)">
|
||||
<div
|
||||
v-for="(entry, index) in item.value"
|
||||
:key="`${item.key}-${index}`"
|
||||
class="array-entry"
|
||||
>
|
||||
<div class="array-entry-index">第{{ index + 1 }}项</div>
|
||||
<div
|
||||
v-for="(line, lineIndex) in formatArrayEntry(entry)"
|
||||
:key="`${item.key}-${index}-${lineIndex}`"
|
||||
class="array-entry-line"
|
||||
>
|
||||
{{ line }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="isPlainObject(item.value)">
|
||||
<div
|
||||
v-for="(line, index) in formatObjectLines(item.value)"
|
||||
:key="`${item.key}-obj-${index}`"
|
||||
class="array-entry-line"
|
||||
>
|
||||
{{ line }}
|
||||
</div>
|
||||
</template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
v-else-if="isRichTextField(item.key)"
|
||||
class="rich-text-content markdown-body"
|
||||
v-html="formatRichText(item.value)"
|
||||
></div>
|
||||
<div v-else :class="{ 'multiline-text': isWideField(item.key) }">
|
||||
{{ formatDisplayValue(item.key, item.value) }}
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-spin>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
import { getManuscript } from '@/api/cms/manuscript';
|
||||
import { getQmgcForm } from '@/api/gxmu/qmgc-form';
|
||||
import { getSjqnForm } from '@/api/gxmu/sjqn-form';
|
||||
import { getSjtbzbsjForm } from '@/api/gxmu/sjtbzbsj-form';
|
||||
import { getTyglForm } from '@/api/gxmu/tygl-form';
|
||||
import { getTzbcyForm } from '@/api/gxmu/tzbcy-form';
|
||||
import { getWshqtwForm } from '@/api/gxmu/wshqtw-form';
|
||||
import { getWshqtzbForm } from '@/api/gxmu/wshqtzb-form';
|
||||
import { getWxxzxForm } from '@/api/gxmu/wxxzx-form';
|
||||
import { getYxgqtdgbForm } from '@/api/gxmu/yxgqtdgb-form';
|
||||
import { getYxgqtyForm } from '@/api/gxmu/yxgqty-form';
|
||||
|
||||
interface DescriptionItem {
|
||||
key: string;
|
||||
label: string;
|
||||
value: unknown;
|
||||
span: number;
|
||||
}
|
||||
|
||||
interface ModuleConfig {
|
||||
title: string;
|
||||
fetcher: (id: number) => Promise<Record<string, unknown>>;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
const COMMON_LABELS: Record<string, string> = {
|
||||
year: '年份',
|
||||
id: 'ID',
|
||||
title: '标题',
|
||||
content: '内容',
|
||||
cover: '封面',
|
||||
createTime: '创建时间',
|
||||
updateTime: '更新时间',
|
||||
status: '状态',
|
||||
sortNumber: '排序',
|
||||
provinceCity: '省市',
|
||||
schoolName: '学校名称',
|
||||
projectName: '项目名称',
|
||||
projectType: '项目类型',
|
||||
projectGroup: '项目分组',
|
||||
publicProjectType: '公开展示项目类型',
|
||||
publicProjectGroup: '公开展示项目分组',
|
||||
leader: '负责人',
|
||||
phone: '联系电话',
|
||||
teamMembers: '团队成员',
|
||||
advisors: '指导老师',
|
||||
projectBrief: '项目简介',
|
||||
socialValue: '社会价值',
|
||||
practiceProcess: '实践过程',
|
||||
innovationMeaning: '创新意义',
|
||||
developmentProspect: '发展前景',
|
||||
teamCooperation: '团队协作情况',
|
||||
projectMaterials: '项目佐证材料',
|
||||
otherProofs: '其他证明材料',
|
||||
projectSummary: '项目总结',
|
||||
teamIntro: '团队介绍',
|
||||
teamSlogan: '团队口号',
|
||||
practiceLog: '实践日志',
|
||||
name: '姓名',
|
||||
gender: '性别',
|
||||
nation: '民族',
|
||||
photo: '电子版照片',
|
||||
birthMonth: '出生年月',
|
||||
politics: '政治面貌',
|
||||
nativePlace: '籍贯',
|
||||
wechat: '微信号',
|
||||
email: '邮箱',
|
||||
qq: 'QQ',
|
||||
idCardNo: '身份证号',
|
||||
hobby: '兴趣爱好',
|
||||
willingToWorkInGuangxi: '是否愿意留桂工作',
|
||||
schoolInfo: '学校信息',
|
||||
leaguePosition: '团内职务',
|
||||
resume: '个人简历',
|
||||
awards: '获得奖励',
|
||||
academicPerformance: '学习成绩',
|
||||
secondaryLeagueOpinion: '二级团组织意见',
|
||||
secondaryLeagueOpinionDate: '二级团组织意见日期',
|
||||
secondaryPartyOpinion: '二级党组织意见',
|
||||
secondaryPartyOpinionDate: '二级党组织意见日期',
|
||||
schoolLeagueOpinion: '学校团组织意见',
|
||||
schoolLeagueOpinionDate: '学校团组织意见日期',
|
||||
applyType: '申报类别',
|
||||
education: '学历',
|
||||
position: '职务',
|
||||
unit: '工作单位',
|
||||
experience: '主要经历',
|
||||
mainStory: '主要事迹',
|
||||
leagueOpinion: '团组织意见',
|
||||
partyOpinion: '党组织意见',
|
||||
schoolOpinion: '学校意见',
|
||||
collegeClass: '学院班级',
|
||||
branch: '团支部名称',
|
||||
eduEval: '教育评议情况',
|
||||
serialNo: '序号',
|
||||
archiveInCurrentOrg: '团籍是否在本组织',
|
||||
joinMonth: '入团时间',
|
||||
memberRecords: '团员记录',
|
||||
growthArchives: '成长档案',
|
||||
orgName: '组织名称',
|
||||
memberTotal: '团员总数',
|
||||
memberDeveloped2024: '上一年度发展团员数',
|
||||
smartSystemLogin: '智慧团建系统录入情况',
|
||||
committeeCount: '团委委员数',
|
||||
fulltimeCadreCount: '专职团干部数',
|
||||
parttimeCadreCount: '兼职团干部数',
|
||||
lastElectionTime: '最近一次换届时间',
|
||||
feeReceivable2024: '上一年度应收团费',
|
||||
feeReceived2024: '上一年度实收团费',
|
||||
feePayable2024: '上一年度应缴团费',
|
||||
feePaid2024: '上一年度实缴团费',
|
||||
branchCount: '团支部数量',
|
||||
standardizedWork2024: '上一年度标准化建设开展情况',
|
||||
recommendActivist2024: '上一年度推优入党人数',
|
||||
activistConfirmed: '入党积极分子确定情况',
|
||||
recommendDevTarget2024: '上一年度推荐发展对象人数',
|
||||
devTargetConfirmed: '发展对象确定情况',
|
||||
honorsFiveYears: '近五年获奖情况',
|
||||
workSummaryThreeYears: '近三年工作总结',
|
||||
branchName: '团支部名称',
|
||||
secondOrg: '所属二级党组织',
|
||||
secretary: '团支部书记',
|
||||
contact: '联系方式',
|
||||
establishTime: '成立时间',
|
||||
branchCommitteeMeetingCount: '支委会次数',
|
||||
branchMemberMeetingCount: '支部大会次数',
|
||||
eduEvalDone: '教育评议是否完成',
|
||||
annualRegDone: '年度团籍注册是否完成',
|
||||
classCount: '班级数量',
|
||||
smartSystem100: '智慧团建系统覆盖率',
|
||||
topicName: '课题名称',
|
||||
collegeGradeClass: '学院年级班级',
|
||||
advisor: '指导老师',
|
||||
topicType: '课题类别',
|
||||
guidanceTeachers: '指导教师组',
|
||||
applicants: '申请人信息',
|
||||
budgets: '经费预算',
|
||||
rationale: '立项依据',
|
||||
teacherOpinion: '指导教师意见',
|
||||
teacherSign: '指导教师签名',
|
||||
reviewOpinion: '评审意见',
|
||||
reviewScore: '评审成绩',
|
||||
reviewLeaderSign: '评审负责人签名',
|
||||
collegeOpinion: '学院意见',
|
||||
promiseSigner: '承诺签字人',
|
||||
promiseSignature: '承诺书签名',
|
||||
reviewCollege: '评审学院',
|
||||
reviewReporter: '汇报人',
|
||||
reviewDate: '评审日期',
|
||||
reviewMembers: '评审成员',
|
||||
fundingBudgetFile: '经费预算表',
|
||||
fundingAllocationFile: '经费划拨明细表',
|
||||
midtermRemark: '中期检查说明',
|
||||
midtermMaterials: '中期检查材料',
|
||||
finalRemark: '结题说明',
|
||||
finalMaterials: '结题材料',
|
||||
archiveAwards: '获奖情况',
|
||||
archiveProofs: '佐证材料',
|
||||
archiveRecords: '成果归档',
|
||||
grade: '年级',
|
||||
className: '班级',
|
||||
major: '专业',
|
||||
role: '角色',
|
||||
item: '项目',
|
||||
amount: '金额',
|
||||
reason: '原因',
|
||||
dept: '部门',
|
||||
sign: '签名',
|
||||
department: '部门',
|
||||
remark: '备注',
|
||||
type: '类型',
|
||||
recordTime: '时间',
|
||||
archiveTime: '时间',
|
||||
identity: '身份',
|
||||
organization: '所在组织',
|
||||
memberNo: '团员编号',
|
||||
currentDutyTime: '现任职务时间',
|
||||
cadreYears: '从事团学工作年限',
|
||||
assessment2024: '上一年度考核情况',
|
||||
volunteerRegTime: '志愿汇注册时间',
|
||||
cadreExperience: '团学工作经历',
|
||||
joinTime: '入团时间',
|
||||
collegeMajorClass: '学院专业班级',
|
||||
eduEval2024: '上一年度教育评议情况',
|
||||
smartSystem: '智慧团建系统录入情况',
|
||||
totalVolunteerHours: '累计志愿服务时长',
|
||||
volunteerHours2024: '上一年度志愿服务时长',
|
||||
duty: '职务',
|
||||
college: '学院',
|
||||
gradeMajor: '年级专业',
|
||||
birthDate: '出生日期',
|
||||
branchOrganizationName: '所属团支部'
|
||||
};
|
||||
|
||||
const MODULE_CONFIGS: Record<string, ModuleConfig> = {
|
||||
cms_manuscript: {
|
||||
title: '稿件',
|
||||
fetcher: (id) => getManuscript(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_sjqn: {
|
||||
title: '十佳青年岗位能手',
|
||||
fetcher: (id) => getSjqnForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_sjtbzbsj: {
|
||||
title: '十佳团支部书记',
|
||||
fetcher: (id) => getSjtbzbsjForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_wshqtw: {
|
||||
title: '五四红旗团委',
|
||||
fetcher: (id) => getWshqtwForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_wshqtzb: {
|
||||
title: '五四红旗团支部',
|
||||
fetcher: (id) => getWshqtzbForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_yxgqtdgb: {
|
||||
title: '优秀共青团干部',
|
||||
fetcher: (id) => getYxgqtdgbForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_yxgqty: {
|
||||
title: '优秀共青团员',
|
||||
fetcher: (id) => getYxgqtyForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_tzbcy_form: {
|
||||
title: '挑战杯',
|
||||
fetcher: (id) => getTzbcyForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_qmgc_form: {
|
||||
title: '青马工程',
|
||||
fetcher: (id) => getQmgcForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_wxxzx_project: {
|
||||
title: '未来学术之星',
|
||||
fetcher: (id) => getWxxzxForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS,
|
||||
title: '职称'
|
||||
}
|
||||
},
|
||||
gxmu_tygl_form: {
|
||||
title: '团员管理',
|
||||
fetcher: (id) => getTyglForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
},
|
||||
gxmu_tygl: {
|
||||
title: '团员管理',
|
||||
fetcher: (id) => getTyglForm(id) as Promise<Record<string, unknown>>,
|
||||
labels: {
|
||||
...COMMON_LABELS
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const HIDDEN_FIELDS = new Set([
|
||||
'reviewList',
|
||||
'deleted',
|
||||
'tenantId',
|
||||
'userId',
|
||||
'branchOrganizationId'
|
||||
]);
|
||||
const IMAGE_FIELDS = new Set(['photo', 'cover', 'promiseSignature']);
|
||||
const RICH_TEXT_FIELDS = new Set(['content']);
|
||||
const DATE_TIME_FIELDS = new Set(['createTime', 'updateTime']);
|
||||
const FILE_FIELDS = new Set([
|
||||
'fundingBudgetFile',
|
||||
'fundingAllocationFile',
|
||||
'midtermMaterials',
|
||||
'finalMaterials',
|
||||
'archiveProofs'
|
||||
]);
|
||||
const WIDE_FIELDS = new Set([
|
||||
'projectBrief',
|
||||
'socialValue',
|
||||
'practiceProcess',
|
||||
'innovationMeaning',
|
||||
'developmentProspect',
|
||||
'teamCooperation',
|
||||
'projectMaterials',
|
||||
'otherProofs',
|
||||
'projectSummary',
|
||||
'teamIntro',
|
||||
'practiceLog',
|
||||
'resume',
|
||||
'awards',
|
||||
'academicPerformance',
|
||||
'secondaryLeagueOpinion',
|
||||
'secondaryPartyOpinion',
|
||||
'schoolLeagueOpinion',
|
||||
'experience',
|
||||
'mainStory',
|
||||
'leagueOpinion',
|
||||
'partyOpinion',
|
||||
'schoolOpinion',
|
||||
'workSummaryThreeYears',
|
||||
'honorsFiveYears',
|
||||
'rationale',
|
||||
'teacherOpinion',
|
||||
'reviewOpinion',
|
||||
'collegeOpinion',
|
||||
'cadreExperience',
|
||||
'midtermRemark',
|
||||
'finalRemark',
|
||||
'archiveAwards'
|
||||
]);
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
module?: string;
|
||||
moduleName?: string;
|
||||
pk?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const maxable = ref(true);
|
||||
const detail = ref<Record<string, unknown>>({});
|
||||
const loadFailed = ref(false);
|
||||
|
||||
const currentConfig = computed(() => {
|
||||
return props.module ? MODULE_CONFIGS[props.module] : undefined;
|
||||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return `${props.moduleName || currentConfig.value?.title || '数据'}详情`;
|
||||
});
|
||||
|
||||
const emptyDescription = computed(() => {
|
||||
if (loadFailed.value) {
|
||||
return '数据加载失败';
|
||||
}
|
||||
if (!currentConfig.value) {
|
||||
return '暂不支持查看该模块数据';
|
||||
}
|
||||
return '暂无可展示数据';
|
||||
});
|
||||
|
||||
const getFieldLabel = (key: string) => {
|
||||
const label = currentConfig.value?.labels[key];
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
return key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, (text) => text.toUpperCase())
|
||||
.trim();
|
||||
};
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||
return Object.prototype.toString.call(value) === '[object Object]';
|
||||
};
|
||||
|
||||
const hasVisibleValue = (value: unknown) => {
|
||||
if (value === null || value === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0;
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return Object.values(value).some((item) => hasVisibleValue(item));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isWideField = (key: string) => {
|
||||
return WIDE_FIELDS.has(key);
|
||||
};
|
||||
|
||||
const isImageField = (key: string) => {
|
||||
return IMAGE_FIELDS.has(key);
|
||||
};
|
||||
|
||||
const isFileField = (key: string) => {
|
||||
return FILE_FIELDS.has(key);
|
||||
};
|
||||
|
||||
const isRichTextField = (key: string) => {
|
||||
return props.module === 'cms_manuscript' && RICH_TEXT_FIELDS.has(key);
|
||||
};
|
||||
|
||||
const getFileName = (url?: string) => {
|
||||
if (!url) {
|
||||
return '未命名文件';
|
||||
}
|
||||
const cleanUrl = url.split('?')[0];
|
||||
return decodeURIComponent(cleanUrl.split('/').pop() || cleanUrl);
|
||||
};
|
||||
|
||||
const normalizeFileEntries = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => {
|
||||
if (typeof item === 'string' && item.trim()) {
|
||||
return {
|
||||
name: getFileName(item),
|
||||
url: item
|
||||
};
|
||||
}
|
||||
if (
|
||||
isPlainObject(item) &&
|
||||
typeof item.url === 'string' &&
|
||||
item.url.trim()
|
||||
) {
|
||||
return {
|
||||
name:
|
||||
(typeof item.name === 'string' && item.name.trim()) ||
|
||||
getFileName(item.url),
|
||||
url: item.url
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((item): item is { name: string; url: string } => !!item);
|
||||
}
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return [
|
||||
{
|
||||
name: getFileName(value),
|
||||
url: value
|
||||
}
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const normalizeDetailValue = (key: string, value: unknown) => {
|
||||
if (!isFileField(key) || typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
const text = value.trim();
|
||||
if (!text) {
|
||||
return value;
|
||||
}
|
||||
if (key === 'fundingBudgetFile' || key === 'fundingAllocationFile') {
|
||||
return text;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
return Array.isArray(parsed) ? parsed : text;
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
const formatSimpleValue = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-';
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const formatStatusValue = (value: unknown) => {
|
||||
if (props.module === 'cms_manuscript') {
|
||||
if (Number(value) === 0) {
|
||||
return '待审核';
|
||||
}
|
||||
if (Number(value) === 1) {
|
||||
return '显示';
|
||||
}
|
||||
}
|
||||
return formatSimpleValue(value);
|
||||
};
|
||||
|
||||
const formatDateTimeValue = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-';
|
||||
}
|
||||
return toDateString(String(value), 'yyyy-MM-dd HH:mm:ss');
|
||||
};
|
||||
|
||||
const formatDisplayValue = (key: string, value: unknown) => {
|
||||
if (DATE_TIME_FIELDS.has(key)) {
|
||||
return formatDateTimeValue(value);
|
||||
}
|
||||
if (key === 'status') {
|
||||
return formatStatusValue(value);
|
||||
}
|
||||
return formatSimpleValue(value);
|
||||
};
|
||||
|
||||
const formatRichText = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '<p>-</p>';
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const formatObjectLines = (value: Record<string, unknown>) => {
|
||||
return Object.keys(value)
|
||||
.filter((key) => !HIDDEN_FIELDS.has(key) && hasVisibleValue(value[key]))
|
||||
.map((key) => `${getFieldLabel(key)}:${formatDisplayValue(key, value[key])}`);
|
||||
};
|
||||
|
||||
const formatArrayEntry = (entry: unknown) => {
|
||||
if (isPlainObject(entry)) {
|
||||
const lines = formatObjectLines(entry);
|
||||
return lines.length ? lines : ['-'];
|
||||
}
|
||||
return [formatSimpleValue(entry)];
|
||||
};
|
||||
|
||||
const descriptionItems = computed<DescriptionItem[]>(() => {
|
||||
const source = detail.value || {};
|
||||
const orderedKeys = Object.keys(currentConfig.value?.labels || {}).filter(
|
||||
(key) => !HIDDEN_FIELDS.has(key) && hasVisibleValue(source[key])
|
||||
);
|
||||
const extraKeys = Object.keys(source).filter(
|
||||
(key) =>
|
||||
!HIDDEN_FIELDS.has(key) &&
|
||||
hasVisibleValue(source[key]) &&
|
||||
!orderedKeys.includes(key)
|
||||
);
|
||||
|
||||
return [...orderedKeys, ...extraKeys].map((key) => ({
|
||||
key,
|
||||
label: getFieldLabel(key),
|
||||
value: normalizeDetailValue(key, source[key]),
|
||||
span:
|
||||
isWideField(key) ||
|
||||
isFileField(key) ||
|
||||
isImageField(key) ||
|
||||
Array.isArray(normalizeDetailValue(key, source[key])) ||
|
||||
isPlainObject(normalizeDetailValue(key, source[key]))
|
||||
? 2
|
||||
: 1
|
||||
}));
|
||||
});
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const loadDetail = async () => {
|
||||
if (!props.visible) {
|
||||
return;
|
||||
}
|
||||
detail.value = {};
|
||||
loadFailed.value = false;
|
||||
|
||||
if (!props.pk || !currentConfig.value) {
|
||||
if (props.module && !currentConfig.value) {
|
||||
loadFailed.value = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
detail.value = (await currentConfig.value.fetcher(props.pk)) || {};
|
||||
} catch (e: any) {
|
||||
loadFailed.value = true;
|
||||
message.error(e.message || '数据加载失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.module, props.pk],
|
||||
([visible]) => {
|
||||
if (visible) {
|
||||
loadDetail();
|
||||
} else {
|
||||
detail.value = {};
|
||||
loadFailed.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.markdown-body) {
|
||||
background: #fff;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.multiline-text {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.rich-text-content {
|
||||
min-height: 32px;
|
||||
line-height: 1.8;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.rich-text-content:deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.array-entry {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.array-entry + .array-entry {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.array-entry-index {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.array-entry-line {
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-entry + .file-entry {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="900"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="true"
|
||||
:title="`${moduleName || '项目'}审核流`"
|
||||
:body-style="{ paddingBottom: '20px' }"
|
||||
@update:visible="updateVisible"
|
||||
okText="关闭"
|
||||
@ok="updateVisible(false)"
|
||||
>
|
||||
<a-space style="margin-bottom: 16px">
|
||||
<span>模块:</span>
|
||||
<a-tag>{{ moduleName || module || '-' }}</a-tag>
|
||||
<span>业务ID:</span>
|
||||
<a-tag>{{ pk ?? '-' }}</a-tag>
|
||||
<a-button size="small" :loading="loading" @click="loadData">刷新</a-button>
|
||||
</a-space>
|
||||
|
||||
<div v-if="flowList.length" class="flow-wrap">
|
||||
<a-steps direction="vertical" :current="currentStepIndex">
|
||||
<a-step v-for="item in flowList" :key="item.id">
|
||||
<template #title>
|
||||
<div class="flow-title">
|
||||
<span>{{ item.user?.realName || item.user?.username || '未分配审核人' }}</span>
|
||||
<a-tag :color="getStatusColor(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="flow-desc">
|
||||
<div>创建时间:{{ toDateString(item.createTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
|
||||
<div v-if="item.content">审核意见:{{ item.content }}</div>
|
||||
<a-space v-if="canReview(item)" style="margin-top: 12px">
|
||||
<a-button type="primary" size="small" :loading="submitting" @click="openActionModal('approve', item)">
|
||||
通过
|
||||
</a-button>
|
||||
<a-button danger size="small" :loading="submitting" @click="openActionModal('reject', item)">
|
||||
拒绝
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-step>
|
||||
</a-steps>
|
||||
</div>
|
||||
<a-empty v-else description="暂无审核流数据" :image="simpleImage" />
|
||||
|
||||
<a-modal
|
||||
v-model:visible="actionVisible"
|
||||
:title="actionType === 'approve' ? '审核通过' : '审核拒绝'"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
:confirm-loading="submitting"
|
||||
@ok="submitReview"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="审核意见(非必填)">
|
||||
<a-textarea
|
||||
v-model:value="actionContent"
|
||||
:rows="4"
|
||||
allow-clear
|
||||
placeholder="请输入审核意见"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Empty, message } from 'ant-design-vue';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import { listReviewList, updateReviewList } from '@/api/gxmu/reviewList';
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
module?: string;
|
||||
moduleName?: string;
|
||||
pk?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const loginUserId = computed(() => Number(userStore.info?.userId || 0));
|
||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const flowList = ref<ReviewList[]>([]);
|
||||
const actionVisible = ref(false);
|
||||
const actionType = ref<'approve' | 'reject'>('approve');
|
||||
const actionContent = ref('');
|
||||
const actionTarget = ref<ReviewList | null>(null);
|
||||
|
||||
const currentStepIndex = computed(() => {
|
||||
const pendingIndex = flowList.value.findIndex((item) => item.status === 0);
|
||||
if (pendingIndex >= 0) {
|
||||
return pendingIndex;
|
||||
}
|
||||
return Math.max(flowList.value.length - 1, 0);
|
||||
});
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
if (status === 1) return '通过';
|
||||
if (status === 2) return '拒绝';
|
||||
return '待审核';
|
||||
};
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
if (status === 1) return 'success';
|
||||
if (status === 2) return 'error';
|
||||
return 'default';
|
||||
};
|
||||
|
||||
const canReview = (item: ReviewList) => {
|
||||
return item.status === 0 && Number(item.userId || 0) === loginUserId.value;
|
||||
};
|
||||
|
||||
const loadData = () => {
|
||||
if (!props.module || !props.pk) {
|
||||
flowList.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
listReviewList({
|
||||
module: props.module,
|
||||
pk: props.pk
|
||||
})
|
||||
.then((data) => {
|
||||
flowList.value = [...(data || [])].sort(
|
||||
(a, b) => Number(b.id || 0) - Number(a.id || 0)
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
flowList.value = [];
|
||||
message.error(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const openActionModal = (type: 'approve' | 'reject', item: ReviewList) => {
|
||||
if (!canReview(item)) {
|
||||
message.error('当前记录不是由你审核');
|
||||
return;
|
||||
}
|
||||
actionType.value = type;
|
||||
actionTarget.value = item;
|
||||
actionContent.value = '';
|
||||
actionVisible.value = true;
|
||||
};
|
||||
|
||||
const submitReview = () => {
|
||||
if (!actionTarget.value?.id) {
|
||||
actionVisible.value = false;
|
||||
return;
|
||||
}
|
||||
if (!canReview(actionTarget.value)) {
|
||||
actionVisible.value = false;
|
||||
message.error('当前记录不是由你审核');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
updateReviewList({
|
||||
id: actionTarget.value.id,
|
||||
status: actionType.value === 'approve' ? 1 : 2,
|
||||
module: actionTarget.value.module,
|
||||
pk: actionTarget.value.pk,
|
||||
sortNumber: actionTarget.value.sortNumber,
|
||||
content: actionContent.value || undefined
|
||||
})
|
||||
.then((msg) => {
|
||||
message.success(msg);
|
||||
actionVisible.value = false;
|
||||
emit('done');
|
||||
loadData();
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
submitting.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
loadData();
|
||||
} else {
|
||||
flowList.value = [];
|
||||
actionVisible.value = false;
|
||||
actionTarget.value = null;
|
||||
actionContent.value = '';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.flow-wrap {
|
||||
max-height: 560px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flow-desc {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 1.8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑审核列表' : '添加审核列表'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="模块" name="module">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入模块"
|
||||
v-model:value="form.module"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="主键" name="pk">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入主键"
|
||||
v-model:value="form.pk"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态(0不通过 1通过)" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="1">显示</a-radio>
|
||||
<a-radio :value="0">隐藏</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核意见" name="content">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入审核意见"
|
||||
v-model:value="form.content"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否删除, 0否, 1是" name="deleted">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入是否删除, 0否, 1是"
|
||||
v-model:value="form.deleted"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="修改时间" name="updateTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入修改时间"
|
||||
v-model:value="form.updateTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="userId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="sortNumber">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:max="9999"
|
||||
class="ele-fluid"
|
||||
placeholder="请输入排序号"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import { addReviewList, updateReviewList } from '@/api/gxmu/reviewList';
|
||||
import { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ReviewList | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ReviewList>({
|
||||
id: undefined,
|
||||
module: undefined,
|
||||
pk: undefined,
|
||||
status: undefined,
|
||||
content: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
userId: undefined,
|
||||
sortNumber: undefined,
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
|
||||
});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value ? updateReviewList : addReviewList;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-select
|
||||
v-model:value="where.module"
|
||||
allow-clear
|
||||
placeholder="请选择模块"
|
||||
style="width: 220px"
|
||||
:options="moduleOptions"
|
||||
@change="search"
|
||||
/>
|
||||
<a-input-search
|
||||
v-model:value="where.keywords"
|
||||
allow-clear
|
||||
placeholder="请输入名称/审核意见"
|
||||
style="width: 260px"
|
||||
@search="search"
|
||||
@pressEnter="search"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
import type { ReviewListParam } from '@/api/gxmu/reviewList/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
selection?: [];
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: ReviewListParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
const where = reactive<ReviewListParam>({
|
||||
module: undefined,
|
||||
keywords: undefined
|
||||
});
|
||||
|
||||
const moduleOptions = [
|
||||
{ label: '十佳青年岗位能手', value: 'gxmu_sjqn' },
|
||||
{ label: '十佳团支部书记', value: 'gxmu_sjtbzbsj' },
|
||||
{ label: '五四红旗团委', value: 'gxmu_wshqtw' },
|
||||
{ label: '五四红旗团支部', value: 'gxmu_wshqtzb' },
|
||||
{ label: '优秀共青团干部', value: 'gxmu_yxgqtdgb' },
|
||||
{ label: '优秀共青团员', value: 'gxmu_yxgqty' },
|
||||
{ label: '挑战杯', value: 'gxmu_tzbcy_form' },
|
||||
{ label: '青马工程', value: 'gxmu_qmgc_form' },
|
||||
{ label: '未来学术之星', value: 'gxmu_wxxzx_project' }
|
||||
];
|
||||
|
||||
const search = () => {
|
||||
emit('search', {
|
||||
...where,
|
||||
keywords: where.keywords?.trim() || undefined
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selection,
|
||||
() => {}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,216 @@
|
||||
<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"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0">待审核</a-tag>
|
||||
<a-tag v-else-if="record.status === 1" color="green">通过</a-tag>
|
||||
<a-tag v-else-if="record.status === 2" color="red">拒绝</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDataModal(record)">查看数据</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openFlowModal(record)">查看审核流</a>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<ReviewListEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDataVisible"
|
||||
:module="dataCurrent?.module"
|
||||
:module-name="dataCurrent?.moduleName"
|
||||
:pk="dataCurrent?.pk"
|
||||
/>
|
||||
<ReviewFlowModal
|
||||
v-model:visible="showFlowVisible"
|
||||
:module="flowCurrent?.module"
|
||||
:module-name="flowCurrent?.moduleName"
|
||||
:pk="flowCurrent?.pk"
|
||||
@done="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type { DatasourceFunction, ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ReviewListEdit from './components/reviewListEdit.vue';
|
||||
import ModuleDataModal from './components/moduleDataModal.vue';
|
||||
import ReviewFlowModal from './components/reviewFlowModal.vue';
|
||||
import { pageGroupReviewList, removeBatchReviewList } from '@/api/gxmu/reviewList';
|
||||
import type { ReviewList, ReviewListParam } from '@/api/gxmu/reviewList/model';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const selection = ref<ReviewList[]>([]);
|
||||
const current = ref<ReviewList | null>(null);
|
||||
const dataCurrent = ref<ReviewList | null>(null);
|
||||
const flowCurrent = ref<ReviewList | null>(null);
|
||||
const showEdit = ref(false);
|
||||
const showDataVisible = ref(false);
|
||||
const showFlowVisible = ref(false);
|
||||
const showMove = ref(false);
|
||||
const loading = ref(true);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders, filters }) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageGroupReviewList({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'moduleName',
|
||||
key: 'module',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'businessName',
|
||||
key: 'businessName',
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '最新审核意见',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '当前审核人',
|
||||
dataIndex: ['user', 'realName'],
|
||||
key: 'userId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
const reload = (where?: ReviewListParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where });
|
||||
};
|
||||
|
||||
const openEdit = (row?: ReviewList) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
const openDataModal = (row: ReviewList) => {
|
||||
dataCurrent.value = row;
|
||||
showDataVisible.value = true;
|
||||
};
|
||||
|
||||
const openFlowModal = (row: ReviewList) => {
|
||||
flowCurrent.value = row;
|
||||
showFlowVisible.value = true;
|
||||
};
|
||||
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchReviewList(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ReviewList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,64 @@
|
||||
.gxmu-tag-tabs() {
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav::before) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab) {
|
||||
min-width: 168px;
|
||||
height: 52px;
|
||||
margin-right: 18px !important;
|
||||
padding: 0 28px !important;
|
||||
border: 1px solid #d31212 !important;
|
||||
border-radius: 999px !important;
|
||||
background: linear-gradient(90deg, #ffffff 0%, #fff7f7 100%) !important;
|
||||
color: #c10e0e !important;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab:hover) {
|
||||
color: #d31212 !important;
|
||||
border-color: #d31212 !important;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab.ant-tabs-tab-active) {
|
||||
background: linear-gradient(90deg, #ef1b1b 0%, #c90707 100%) !important;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn) {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab .ant-tabs-tab-btn) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-ink-bar) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav-list) {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab) {
|
||||
min-width: auto;
|
||||
height: 42px;
|
||||
margin-right: 0 !important;
|
||||
padding: 0 18px !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
@import './form-theme.less';
|
||||
|
||||
.gxmu-field-theme();
|
||||
|
||||
.gxmu-toolbar-btn {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border-radius: 18px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
|
||||
background: linear-gradient(82.9deg, #32bfff 14.75%, #1590ff 44.46%);
|
||||
}
|
||||
|
||||
.gxmu-toolbar-btn:hover,
|
||||
.gxmu-toolbar-btn:focus {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.gxmu-toolbar-btn :deep(.ant-btn-loading-icon),
|
||||
.gxmu-toolbar-btn :deep(.anticon) {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.gxmu-toolbar-btn > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.gxmu-add-btn {
|
||||
background: linear-gradient(135deg, #e10000 0%, #c40000 100%);
|
||||
border-color: #f3b3b3;
|
||||
}
|
||||
|
||||
.gxmu-add-btn:hover,
|
||||
.gxmu-add-btn:focus {
|
||||
background: linear-gradient(135deg, #f01515 0%, #cd0a0a 100%);
|
||||
border-color: #f7c3c3;
|
||||
}
|
||||
|
||||
.gxmu-import-btn {
|
||||
background: linear-gradient(135deg, #4ad9cd 0%, #18b68e 100%);
|
||||
border-color: #92efe2;
|
||||
}
|
||||
|
||||
.gxmu-import-btn:hover,
|
||||
.gxmu-import-btn:focus {
|
||||
background: linear-gradient(135deg, #60e3d8 0%, #21c29a 100%);
|
||||
border-color: #b1f6ec;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.gxmu-toolbar-btn {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.gxmu-toolbar-btn :deep(.anticon) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<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"
|
||||
:customRow="customRow"
|
||||
:scroll="{ x: 1450 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-input-search
|
||||
v-model:value="searchText"
|
||||
allow-clear
|
||||
style="width: 260px"
|
||||
placeholder="项目名称/学校/奖项"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="schoolName"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
placeholder="高校名称"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="matchYear"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
placeholder="参赛年份"
|
||||
:options="yearOptions"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="matchLevel"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
placeholder="比赛级别"
|
||||
:options="levelOptions"
|
||||
/>
|
||||
<a-button type="primary" @click="reload">查询</a-button>
|
||||
<a-button @click="resetSearch">重置</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'coverImage'">
|
||||
<a-image
|
||||
v-if="record.coverImage"
|
||||
:src="record.coverImage"
|
||||
:width="48"
|
||||
:height="48"
|
||||
style="object-fit: cover; border-radius: 4px"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'sourceId'">
|
||||
<a v-if="record.detailUrl" @click="copyDetailLink(record)">
|
||||
{{ record.sourceId }}
|
||||
</a>
|
||||
<span v-else>{{ record.sourceId || '-' }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'matchLevel'">
|
||||
<a-tag v-if="record.matchLevel" color="blue">
|
||||
{{ record.matchLevel }}
|
||||
</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="copyDetailLink(record)">复制链接</a>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { pageTzbProjectList } from '@/api/gxmu/tzb-project-list';
|
||||
import type {
|
||||
TzbProjectListRecord,
|
||||
TzbProjectListRecordParam
|
||||
} from '@/api/gxmu/tzb-project-list/model';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const schoolName = ref('');
|
||||
const matchYear = ref<number>();
|
||||
const matchLevel = ref<string>();
|
||||
|
||||
const yearOptions = [2025, 2024, 2023, 2022, 2021, 2020, 2019].map((year) => ({
|
||||
label: `${year}年`,
|
||||
value: year
|
||||
}));
|
||||
const levelOptions = ['国赛', '省赛', '校赛'].map((level) => ({
|
||||
label: level,
|
||||
value: level
|
||||
}));
|
||||
|
||||
const formatDateTime = (value?: string) => {
|
||||
return value ? toDateString(value, 'yyyy-MM-dd HH:mm:ss') : '-';
|
||||
};
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
return pageTzbProjectList({
|
||||
...(where || {}),
|
||||
...orders,
|
||||
page,
|
||||
limit,
|
||||
keywords: searchText.value.trim() || undefined,
|
||||
schoolName: schoolName.value.trim() || undefined,
|
||||
matchYear: matchYear.value,
|
||||
matchLevel: matchLevel.value
|
||||
} as TzbProjectListRecordParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '封面',
|
||||
dataIndex: 'coverImage',
|
||||
key: 'coverImage',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '来源ID',
|
||||
dataIndex: 'sourceId',
|
||||
key: 'sourceId',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'projectName',
|
||||
key: 'projectName',
|
||||
align: 'left',
|
||||
width: 360,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '高校',
|
||||
dataIndex: 'schoolName',
|
||||
key: 'schoolName',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '获奖情况',
|
||||
dataIndex: 'awardName',
|
||||
key: 'awardName',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '参赛年份',
|
||||
dataIndex: 'matchYear',
|
||||
key: 'matchYear',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '参赛届次',
|
||||
dataIndex: 'matchTerm',
|
||||
key: 'matchTerm',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '比赛级别',
|
||||
dataIndex: 'matchLevel',
|
||||
key: 'matchLevel',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '来源页码',
|
||||
dataIndex: 'pageNo',
|
||||
key: 'pageNo',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '同步时间',
|
||||
dataIndex: 'lastSyncTime',
|
||||
key: 'lastSyncTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
sorter: true,
|
||||
customRender: ({ text }) => formatDateTime(text)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 120
|
||||
}
|
||||
]);
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
searchText.value = '';
|
||||
schoolName.value = '';
|
||||
matchYear.value = undefined;
|
||||
matchLevel.value = undefined;
|
||||
reload();
|
||||
};
|
||||
|
||||
const copyText = async (text: string) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
};
|
||||
|
||||
const copyDetailLink = async (record: TzbProjectListRecord) => {
|
||||
if (!record.detailUrl) {
|
||||
message.warning('暂无详情链接');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await copyText(record.detailUrl);
|
||||
message.success('详情链接已复制');
|
||||
} catch (e) {
|
||||
message.error('复制失败,请手动复制链接');
|
||||
}
|
||||
};
|
||||
|
||||
const customRow = (record: TzbProjectListRecord) => {
|
||||
return {
|
||||
onDblclick: () => {
|
||||
copyDetailLink(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TzbProjectList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<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"
|
||||
:customRow="customRow"
|
||||
:scroll="{ x: 1580 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-input-search
|
||||
v-model:value="searchText"
|
||||
allow-clear
|
||||
style="width: 260px"
|
||||
placeholder="姓名/项目/学校/奖项"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="schoolName"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
placeholder="参赛学校"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="talentType"
|
||||
allow-clear
|
||||
style="width: 170px"
|
||||
placeholder="人才类型"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="matchLevel"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
placeholder="比赛级别"
|
||||
:options="levelOptions"
|
||||
/>
|
||||
<a-button type="primary" @click="reload">查询</a-button>
|
||||
<a-button @click="resetSearch">重置</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'coverImage'">
|
||||
<a-image
|
||||
v-if="record.coverImage"
|
||||
:src="record.coverImage"
|
||||
:width="48"
|
||||
:height="48"
|
||||
style="object-fit: cover; border-radius: 4px"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'sourceId'">
|
||||
<a v-if="record.detailUrl" @click="copyDetailLink(record)">
|
||||
{{ record.sourceId }}
|
||||
</a>
|
||||
<span v-else>{{ record.sourceId || '-' }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'matchLevel'">
|
||||
<a-tag v-if="record.matchLevel" color="blue">
|
||||
{{ record.matchLevel }}
|
||||
</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'talentType'">
|
||||
<a-tag v-if="record.talentType" color="green">
|
||||
{{ record.talentType }}
|
||||
</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="copyDetailLink(record)">复制链接</a>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { pageTzbTalentList } from '@/api/gxmu/tzb-talent-list';
|
||||
import type {
|
||||
TzbTalentListRecord,
|
||||
TzbTalentListRecordParam
|
||||
} from '@/api/gxmu/tzb-talent-list/model';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const schoolName = ref('');
|
||||
const talentType = ref('');
|
||||
const matchLevel = ref<string>();
|
||||
|
||||
const levelOptions = ['国赛', '省赛', '校赛'].map((level) => ({
|
||||
label: level,
|
||||
value: level
|
||||
}));
|
||||
|
||||
const formatDateTime = (value?: string) => {
|
||||
return value ? toDateString(value, 'yyyy-MM-dd HH:mm:ss') : '-';
|
||||
};
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
return pageTzbTalentList({
|
||||
...(where || {}),
|
||||
...orders,
|
||||
page,
|
||||
limit,
|
||||
keywords: searchText.value.trim() || undefined,
|
||||
schoolName: schoolName.value.trim() || undefined,
|
||||
talentType: talentType.value.trim() || undefined,
|
||||
matchLevel: matchLevel.value
|
||||
} as TzbTalentListRecordParam).catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '头像',
|
||||
dataIndex: 'coverImage',
|
||||
key: 'coverImage',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '来源ID',
|
||||
dataIndex: 'sourceId',
|
||||
key: 'sourceId',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'personName',
|
||||
key: 'personName',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '参赛学校',
|
||||
dataIndex: 'schoolName',
|
||||
key: 'schoolName',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '人才类型',
|
||||
dataIndex: 'talentType',
|
||||
key: 'talentType',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '参赛项目',
|
||||
dataIndex: 'projectName',
|
||||
key: 'projectName',
|
||||
align: 'left',
|
||||
width: 340,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '获奖情况',
|
||||
dataIndex: 'awardName',
|
||||
key: 'awardName',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '参赛届次',
|
||||
dataIndex: 'matchTerm',
|
||||
key: 'matchTerm',
|
||||
align: 'center',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '比赛级别',
|
||||
dataIndex: 'matchLevel',
|
||||
key: 'matchLevel',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '来源页码',
|
||||
dataIndex: 'pageNo',
|
||||
key: 'pageNo',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '同步时间',
|
||||
dataIndex: 'lastSyncTime',
|
||||
key: 'lastSyncTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
sorter: true,
|
||||
customRender: ({ text }) => formatDateTime(text)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 120
|
||||
}
|
||||
]);
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
searchText.value = '';
|
||||
schoolName.value = '';
|
||||
talentType.value = '';
|
||||
matchLevel.value = undefined;
|
||||
reload();
|
||||
};
|
||||
|
||||
const copyText = async (text: string) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
};
|
||||
|
||||
const copyDetailLink = async (record: TzbTalentListRecord) => {
|
||||
if (!record.detailUrl) {
|
||||
message.warning('暂无详情链接');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await copyText(record.detailUrl);
|
||||
message.success('详情链接已复制');
|
||||
} catch (e) {
|
||||
message.error('复制失败,请手动复制链接');
|
||||
}
|
||||
};
|
||||
|
||||
const customRow = (record: TzbTalentListRecord) => {
|
||||
return {
|
||||
onDblclick: () => {
|
||||
copyDetailLink(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TzbTalentList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,311 @@
|
||||
import { ref } from 'vue';
|
||||
import { listDictData } from '@/api/system/dict-data';
|
||||
import { listCollege } from '@/api/gxmu/college';
|
||||
import { listClassInfo } from '@/api/gxmu/class';
|
||||
import type { ClassInfo } from '@/api/gxmu/class/model';
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ClassCascaderOption {
|
||||
label: string;
|
||||
value: string;
|
||||
children?: ClassCascaderOption[];
|
||||
}
|
||||
|
||||
export interface ClassCascadeSelection {
|
||||
classId?: number;
|
||||
className: string;
|
||||
collegeId?: number;
|
||||
collegeName: string;
|
||||
path: string[];
|
||||
combinedLabel: string;
|
||||
}
|
||||
|
||||
const NATION_DICT_ID = 173;
|
||||
const POLITICS_DICT_ID = 172;
|
||||
|
||||
const genderOptions: SelectOption[] = [
|
||||
{ label: '男', value: '男' },
|
||||
{ label: '女', value: '女' }
|
||||
];
|
||||
|
||||
let nationCache: SelectOption[] | null = null;
|
||||
let politicsCache: SelectOption[] | null = null;
|
||||
let collegeCache: SelectOption[] | null = null;
|
||||
let classCache: SelectOption[] | null = null;
|
||||
let classCascaderCache: ClassCascaderOption[] | null = null;
|
||||
let classCascadeSelectionCache: ClassCascadeSelection[] | null = null;
|
||||
|
||||
const normalizeOptions = (values: string[]) =>
|
||||
values
|
||||
.map((item) => item.trim())
|
||||
.filter((item, index, arr) => !!item && arr.indexOf(item) === index)
|
||||
.map((item) => ({
|
||||
label: item,
|
||||
value: item
|
||||
}));
|
||||
|
||||
const normalizeText = (value?: string) =>
|
||||
String(value || '')
|
||||
.trim()
|
||||
.replace(/[\/|,,、]+/g, ' ')
|
||||
.replace(/\s+/g, ' ');
|
||||
|
||||
const compactText = (value?: string) =>
|
||||
normalizeText(value).replace(/\s+/g, '').toLowerCase();
|
||||
|
||||
const buildCombinedLabel = (collegeName?: string, className?: string) =>
|
||||
[String(collegeName || '').trim(), String(className || '').trim()]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
const buildClassCascadeData = (list: ClassInfo[]) => {
|
||||
const collegeMap = new Map<string, ClassCascaderOption>();
|
||||
const classSelections: ClassCascadeSelection[] = [];
|
||||
|
||||
list.forEach((item, index) => {
|
||||
const className = String(item.className || '').trim();
|
||||
const collegeName = String(item.collegeName || '').trim();
|
||||
|
||||
if (!className || !collegeName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collegeValue = `college:${
|
||||
item.collegeId ?? `${collegeName}:${index}`
|
||||
}`;
|
||||
const classValue = `class:${
|
||||
item.id ?? `${collegeValue}:${className}:${index}`
|
||||
}`;
|
||||
|
||||
let collegeOption = collegeMap.get(collegeValue);
|
||||
if (!collegeOption) {
|
||||
collegeOption = {
|
||||
label: collegeName,
|
||||
value: collegeValue,
|
||||
children: []
|
||||
};
|
||||
collegeMap.set(collegeValue, collegeOption);
|
||||
}
|
||||
|
||||
if (
|
||||
!collegeOption.children?.some(
|
||||
(child) => child.label === className && child.value === classValue
|
||||
)
|
||||
) {
|
||||
collegeOption.children?.push({
|
||||
label: className,
|
||||
value: classValue
|
||||
});
|
||||
}
|
||||
|
||||
classSelections.push({
|
||||
classId: item.id,
|
||||
className,
|
||||
collegeId: item.collegeId,
|
||||
collegeName,
|
||||
path: [collegeValue, classValue],
|
||||
combinedLabel: buildCombinedLabel(collegeName, className)
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
classOptions: normalizeOptions(
|
||||
classSelections.map((item) => item.className)
|
||||
),
|
||||
cascaderOptions: Array.from(collegeMap.values()).filter(
|
||||
(item) => item.children?.length
|
||||
),
|
||||
classSelections
|
||||
};
|
||||
};
|
||||
|
||||
const ensureClassDataLoaded = async () => {
|
||||
if (classCache && classCascaderCache && classCascadeSelectionCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = await listClassInfo({ status: 1, limit: 1000 });
|
||||
const { classOptions, cascaderOptions, classSelections } =
|
||||
buildClassCascadeData(list);
|
||||
|
||||
classCache = classOptions;
|
||||
classCascaderCache = cascaderOptions;
|
||||
classCascadeSelectionCache = classSelections;
|
||||
};
|
||||
|
||||
export const filterSelectOption = (input: string, option: SelectOption) => {
|
||||
return String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase());
|
||||
};
|
||||
|
||||
export const useGxmuFormSelectOptions = () => {
|
||||
const loading = ref(false);
|
||||
const loaded = ref(false);
|
||||
const nationOptions = ref<SelectOption[]>(nationCache ?? []);
|
||||
const politicsOptions = ref<SelectOption[]>(politicsCache ?? []);
|
||||
const collegeOptions = ref<SelectOption[]>(collegeCache ?? []);
|
||||
const classOptions = ref<SelectOption[]>(classCache ?? []);
|
||||
const classCascaderOptions = ref<ClassCascaderOption[]>(
|
||||
classCascaderCache ?? []
|
||||
);
|
||||
|
||||
const loadNationOptions = async () => {
|
||||
if (nationCache) {
|
||||
nationOptions.value = nationCache;
|
||||
return nationCache;
|
||||
}
|
||||
const result = await listDictData({ dictId: NATION_DICT_ID });
|
||||
nationCache = result
|
||||
.filter((item) => !!item.dictDataName)
|
||||
.map((item) => ({
|
||||
label: String(item.dictDataName),
|
||||
value: String(item.dictDataName)
|
||||
}));
|
||||
nationOptions.value = nationCache;
|
||||
return nationCache;
|
||||
};
|
||||
|
||||
const loadCollegeOptions = async () => {
|
||||
if (collegeCache) {
|
||||
collegeOptions.value = collegeCache;
|
||||
return collegeCache;
|
||||
}
|
||||
const list = await listCollege({ status: 1, limit: 500 });
|
||||
collegeCache = normalizeOptions(
|
||||
list.map((item) => item.collegeName || item.shortName || '')
|
||||
);
|
||||
collegeOptions.value = collegeCache;
|
||||
return collegeCache;
|
||||
};
|
||||
|
||||
const loadPoliticsOptions = async () => {
|
||||
if (politicsCache) {
|
||||
politicsOptions.value = politicsCache;
|
||||
return politicsCache;
|
||||
}
|
||||
const result = await listDictData({ dictId: POLITICS_DICT_ID });
|
||||
politicsCache = result
|
||||
.filter((item) => !!item.dictDataName)
|
||||
.map((item) => ({
|
||||
label: String(item.dictDataName),
|
||||
value: String(item.dictDataName)
|
||||
}));
|
||||
politicsOptions.value = politicsCache;
|
||||
return politicsCache;
|
||||
};
|
||||
|
||||
const loadClassOptions = async () => {
|
||||
if (classCache) {
|
||||
classOptions.value = classCache;
|
||||
return classCache;
|
||||
}
|
||||
await ensureClassDataLoaded();
|
||||
classOptions.value = classCache;
|
||||
return classCache;
|
||||
};
|
||||
|
||||
const loadClassCascaderOptions = async () => {
|
||||
if (classCascaderCache) {
|
||||
classCascaderOptions.value = classCascaderCache;
|
||||
return classCascaderCache;
|
||||
}
|
||||
await ensureClassDataLoaded();
|
||||
classCascaderOptions.value = classCascaderCache;
|
||||
return classCascaderCache;
|
||||
};
|
||||
|
||||
const findClassCascadeSelection = (
|
||||
value?: string,
|
||||
mode: 'class' | 'collegeClass' = 'class'
|
||||
) => {
|
||||
const source = classCascadeSelectionCache ?? [];
|
||||
const normalizedValue = normalizeText(value);
|
||||
const compactValue = compactText(value);
|
||||
|
||||
if (!normalizedValue) {
|
||||
return void 0;
|
||||
}
|
||||
|
||||
if (mode === 'collegeClass') {
|
||||
return (
|
||||
source.find(
|
||||
(item) => normalizeText(item.combinedLabel) === normalizedValue
|
||||
) ??
|
||||
source.find(
|
||||
(item) => compactText(item.combinedLabel) === compactValue
|
||||
) ??
|
||||
source.find((item) => {
|
||||
return (
|
||||
compactValue.includes(compactText(item.collegeName)) &&
|
||||
compactValue.includes(compactText(item.className))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
source.find(
|
||||
(item) => normalizeText(item.className) === normalizedValue
|
||||
) ??
|
||||
source.find((item) => compactText(item.className) === compactValue) ??
|
||||
source.find(
|
||||
(item) => normalizeText(item.combinedLabel) === normalizedValue
|
||||
) ??
|
||||
source.find((item) => compactText(item.combinedLabel) === compactValue)
|
||||
);
|
||||
};
|
||||
|
||||
const getClassCascadeSelectionByPath = (path?: string[]) => {
|
||||
if (!path?.length) {
|
||||
return void 0;
|
||||
}
|
||||
const key = path.join('>');
|
||||
return (classCascadeSelectionCache ?? []).find(
|
||||
(item) => item.path.join('>') === key
|
||||
);
|
||||
};
|
||||
|
||||
const loadAll = async () => {
|
||||
if (loaded.value) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all([
|
||||
loadNationOptions(),
|
||||
loadPoliticsOptions(),
|
||||
loadCollegeOptions(),
|
||||
loadClassOptions(),
|
||||
loadClassCascaderOptions()
|
||||
]);
|
||||
loaded.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
loaded,
|
||||
genderOptions,
|
||||
nationOptions,
|
||||
politicsOptions,
|
||||
collegeOptions,
|
||||
classOptions,
|
||||
classCascaderOptions,
|
||||
loadAll,
|
||||
loadNationOptions,
|
||||
loadPoliticsOptions,
|
||||
loadCollegeOptions,
|
||||
loadClassOptions,
|
||||
loadClassCascaderOptions,
|
||||
findClassCascadeSelection,
|
||||
getClassCascadeSelectionByPath,
|
||||
buildCombinedLabel
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
|
||||
export function shouldHideEditByReviewList(reviewList?: ReviewList[]) {
|
||||
const list = reviewList ?? [];
|
||||
if (!list.length) {
|
||||
return false;
|
||||
}
|
||||
const latest = [...list].sort((a, b) => {
|
||||
const timeA = Date.parse(a.createTime || '');
|
||||
const timeB = Date.parse(b.createTime || '');
|
||||
const safeTimeA = Number.isFinite(timeA) ? timeA : 0;
|
||||
const safeTimeB = Number.isFinite(timeB) ? timeB : 0;
|
||||
if (safeTimeA !== safeTimeB) {
|
||||
return safeTimeB - safeTimeA;
|
||||
}
|
||||
return Number(b.id || 0) - Number(a.id || 0);
|
||||
})[0];
|
||||
return latest?.status !== 2;
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
placement="right"
|
||||
width="78%"
|
||||
:visible="visible"
|
||||
:title="isUpdate ? `编辑${title}` : `新增${title}`"
|
||||
:body-style="{ paddingBottom: '88px' }"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-form ref="formRef" :model="form" layout="vertical">
|
||||
<a-tabs v-model:activeKey="activeKey" type="card" class="gxmu-tag-tabs">
|
||||
<a-tab-pane key="cover" tab="封面信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="单位" name="unitName">
|
||||
<a-input v-model:value="form.unitName" placeholder="请输入单位" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="负责人" name="responsiblePerson">
|
||||
<a-input
|
||||
v-model:value="form.responsiblePerson"
|
||||
placeholder="请输入负责人"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="使用开始月份">
|
||||
<a-date-picker
|
||||
v-model:value="form.useStartMonth"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择开始月份"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="使用结束月份">
|
||||
<a-date-picker
|
||||
v-model:value="form.useEndMonth"
|
||||
picker="month"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
placeholder="请选择结束月份"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="overview" tab="团委简况">
|
||||
<a-row :gutter="16">
|
||||
<a-col v-for="item in overviewFields" :key="item.key" :xs="24" :md="8">
|
||||
<a-form-item :label="item.label">
|
||||
<a-input v-model:value="form[item.key]" :placeholder="`请输入${item.label}`" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<editable-table
|
||||
title="团委班子成员名单"
|
||||
:rows="form.committeeMembers"
|
||||
:columns="committeeMemberColumns"
|
||||
:create-row="createCommitteeMember"
|
||||
:scroll-x="1100"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="fees" tab="团费收支">
|
||||
<editable-table
|
||||
title="团费收支记录"
|
||||
:rows="form.feeRecords"
|
||||
:columns="feeRecordColumns"
|
||||
:create-row="createFeeRecord"
|
||||
:scroll-x="980"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="orgs" tab="下属组织">
|
||||
<editable-table
|
||||
title="团委下属团组织情况一览表"
|
||||
:rows="form.subordinateOrgs"
|
||||
:columns="subordinateOrgColumns"
|
||||
:create-row="createSubordinateOrg"
|
||||
:scroll-x="1100"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="honors" tab="荣誉与推优">
|
||||
<editable-table
|
||||
title="荣誉登记表"
|
||||
:rows="form.honorRecords"
|
||||
:columns="honorRecordColumns"
|
||||
:create-row="createHonorRecord"
|
||||
:scroll-x="1120"
|
||||
/>
|
||||
<editable-table
|
||||
title="推荐入党积极分子个人选汇总表"
|
||||
:rows="form.activistRecommendations"
|
||||
:columns="activistColumns"
|
||||
:create-row="createActivistRecommendation"
|
||||
:scroll-x="1120"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="discipline" tab="团纪处理">
|
||||
<editable-table
|
||||
title="团员团纪处理情况记录"
|
||||
:rows="form.disciplineRecords"
|
||||
:columns="disciplineColumns"
|
||||
:create-row="createDisciplineRecord"
|
||||
:scroll-x="920"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="records" tab="会议活动">
|
||||
<editable-table
|
||||
title="团委有关会议记录"
|
||||
:rows="form.meetingRecords"
|
||||
:columns="meetingColumns"
|
||||
:create-row="createMeetingRecord"
|
||||
:scroll-x="1400"
|
||||
/>
|
||||
<editable-table
|
||||
title="团委有关活动记录"
|
||||
:rows="form.activityRecords"
|
||||
:columns="activityColumns"
|
||||
:create-row="createActivityRecord"
|
||||
:scroll-x="1300"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="evaluation" tab="教育评议与总结">
|
||||
<editable-table
|
||||
title="年度团员教育评议结果汇总表"
|
||||
:rows="form.educationEvaluations"
|
||||
:columns="educationColumns"
|
||||
:create-row="createEducationEvaluation"
|
||||
:scroll-x="980"
|
||||
/>
|
||||
<a-form-item label="团委年度工作总结">
|
||||
<a-textarea
|
||||
v-model:value="form.annualSummary"
|
||||
:rows="12"
|
||||
placeholder="请输入团委年度工作总结"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="学社衔接率">
|
||||
<a-input
|
||||
v-model:value="form.schoolSocietyConnectionRate"
|
||||
placeholder="若无请填无"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="编号发展团员智慧团建录入率">
|
||||
<a-input
|
||||
v-model:value="form.smartLeagueEntryRate"
|
||||
placeholder="若无请填无"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
<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, reactive, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { WorkLedger } from '@/api/gxmu/work-ledger/model';
|
||||
import EditableTable from './editable-table.vue';
|
||||
import { useGxmuFormSelectOptions } from '@/views/gxmu/useFormSelectOptions';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: WorkLedger | null;
|
||||
title: string;
|
||||
saveHandler: (data: WorkLedger) => Promise<string>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const activeKey = ref('cover');
|
||||
const formRef = ref();
|
||||
const { nationOptions, loadNationOptions } = useGxmuFormSelectOptions();
|
||||
|
||||
const rowKey = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const withKey = <T extends Record<string, any>>(row: T) => ({ ...row, __rowKey: rowKey() });
|
||||
|
||||
const createCommitteeMember = () => withKey({ name: '', gender: '', birthMonth: '', politics: '', leagueDuty: '', partTimeWork: '' });
|
||||
const createFeeRecord = () => withKey({ date: '', purpose: '', income: '', expense: '', balance: '' });
|
||||
const createSubordinateOrg = () => withKey({ branchName: '', leaderName: '', phone: '', memberCount: '', youthCount: '', feeCount: '', paidCount: '' });
|
||||
const createHonorRecord = () => withKey({ honorName: '', awardTime: '', winner: '', rewardSituation: '', remark: '' });
|
||||
const createActivistRecommendation = () => withKey({ name: '', gender: '', nation: '', birthMonth: '', classMajor: '', currentDuty: '' });
|
||||
const createDisciplineRecord = () => withKey({ name: '', time: '', handlingType: '', reason: '' });
|
||||
const createMeetingRecord = () => withKey({ meetingName: '', meetingTheme: '', time: '', location: '', host: '', attendees: '', absentees: '', content: '', recorder: '' });
|
||||
const createActivityRecord = () => withKey({ activityName: '', activityTheme: '', activityTime: '', location: '', participants: '', content: '', recorder: '' });
|
||||
const createEducationEvaluation = () => withKey({ branchName: '', name: '', result: '', remark: '' });
|
||||
|
||||
const overviewFields = [
|
||||
{ label: '团委书记姓名', key: 'secretaryName' },
|
||||
{ label: '所辖团(总)支部数', key: 'branchCount' },
|
||||
{ label: '团员总数', key: 'memberTotal' },
|
||||
{ label: '教职工团员数', key: 'facultyMemberCount' },
|
||||
{ label: '学生团员数', key: 'studentMemberCount' },
|
||||
{ label: '少数民族团员数', key: 'minorityMemberCount' },
|
||||
{ label: '女团员数', key: 'femaleMemberCount' },
|
||||
{ label: '14-28岁青年数', key: 'youth14To28Count' },
|
||||
{ label: '29-35岁青年数', key: 'youth29To35Count' },
|
||||
{ label: '团干总数', key: 'cadreTotal' },
|
||||
{ label: '职工团干数', key: 'facultyCadreCount' },
|
||||
{ label: '学生团干数', key: 'studentCadreCount' },
|
||||
{ label: '最近一次换届时间', key: 'lastElectionTime' },
|
||||
{ label: '团费余额', key: 'feeBalance' },
|
||||
{ label: '上半年应缴团费', key: 'firstHalfFeeDue' },
|
||||
{ label: '上半年实缴团费', key: 'firstHalfFeePaid' },
|
||||
{ label: '下半年应缴团费', key: 'secondHalfFeeDue' },
|
||||
{ label: '下半年实缴团费', key: 'secondHalfFeePaid' }
|
||||
];
|
||||
|
||||
const committeeMemberColumns = [
|
||||
{ title: '姓名', key: 'name', width: 140 },
|
||||
{ title: '性别', key: 'gender', width: 100 },
|
||||
{ title: '出生年月', key: 'birthMonth', width: 140 },
|
||||
{ title: '政治面貌', key: 'politics', width: 140 },
|
||||
{ title: '团内职务', key: 'leagueDuty', width: 140 },
|
||||
{ title: '兼任工作', key: 'partTimeWork', width: 220 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const feeRecordColumns = [
|
||||
{ title: '日期', key: 'date', width: 140 },
|
||||
{ title: '用途', key: 'purpose', width: 320, textarea: true },
|
||||
{ title: '收入', key: 'income', width: 140 },
|
||||
{ title: '支出', key: 'expense', width: 140 },
|
||||
{ title: '余额', key: 'balance', width: 140 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const subordinateOrgColumns = [
|
||||
{ title: '团支部名称', key: 'branchName', width: 260 },
|
||||
{ title: '负责人姓名', key: 'leaderName', width: 140 },
|
||||
{ title: '联系电话', key: 'phone', width: 160 },
|
||||
{ title: '团员数', key: 'memberCount', width: 120 },
|
||||
{ title: '青年数', key: 'youthCount', width: 120 },
|
||||
{ title: '团费数', key: 'feeCount', width: 120 },
|
||||
{ title: '已缴数', key: 'paidCount', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const honorRecordColumns = [
|
||||
{ title: '荣誉名称(校级及以上)', key: 'honorName', width: 260 },
|
||||
{ title: '获奖时间', key: 'awardTime', width: 140 },
|
||||
{ title: '获奖者(集体或个人)', key: 'winner', width: 240 },
|
||||
{ title: '奖励情况', key: 'rewardSituation', width: 160 },
|
||||
{ title: '备注', key: 'remark', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const activistColumns = computed(() => [
|
||||
{ title: '姓名', key: 'name', width: 140 },
|
||||
{ title: '性别', key: 'gender', width: 100 },
|
||||
{ title: '民族', key: 'nation', width: 120, options: nationOptions.value },
|
||||
{ title: '出生年月', key: 'birthMonth', width: 140 },
|
||||
{ title: '专业班级', key: 'classMajor', width: 220 },
|
||||
{ title: '现任职务', key: 'currentDuty', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
]);
|
||||
const disciplineColumns = [
|
||||
{ title: '姓名', key: 'name', width: 140 },
|
||||
{ title: '时间', key: 'time', width: 160 },
|
||||
{ title: '处理类型', key: 'handlingType', width: 180 },
|
||||
{ title: '处理原因', key: 'reason', width: 420, textarea: true },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const meetingColumns = [
|
||||
{ title: '会议名称', key: 'meetingName', width: 180 },
|
||||
{ title: '会议主题', key: 'meetingTheme', width: 220 },
|
||||
{ title: '时间', key: 'time', width: 150 },
|
||||
{ title: '地点', key: 'location', width: 150 },
|
||||
{ title: '主持人', key: 'host', width: 120 },
|
||||
{ title: '与会人员', key: 'attendees', width: 220, textarea: true },
|
||||
{ title: '缺席人员', key: 'absentees', width: 160 },
|
||||
{ title: '会议记录', key: 'content', width: 320, textarea: true },
|
||||
{ title: '记录人', key: 'recorder', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const activityColumns = [
|
||||
{ title: '活动名称', key: 'activityName', width: 180 },
|
||||
{ title: '活动主题', key: 'activityTheme', width: 220 },
|
||||
{ title: '活动时间', key: 'activityTime', width: 150 },
|
||||
{ title: '活动地点', key: 'location', width: 150 },
|
||||
{ title: '参与人员', key: 'participants', width: 220, textarea: true },
|
||||
{ title: '活动内容', key: 'content', width: 420, textarea: true },
|
||||
{ title: '记录人', key: 'recorder', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
const educationColumns = [
|
||||
{ title: '所在团支部', key: 'branchName', width: 240 },
|
||||
{ title: '姓名', key: 'name', width: 140 },
|
||||
{ title: '评议结果', key: 'result', width: 160 },
|
||||
{ title: '备注', key: 'remark', width: 240 },
|
||||
{ title: '操作', key: 'action', width: 90 }
|
||||
];
|
||||
|
||||
const createDefaultForm = (): WorkLedger => ({
|
||||
id: undefined,
|
||||
unitName: '',
|
||||
responsiblePerson: '',
|
||||
useStartMonth: '',
|
||||
useEndMonth: '',
|
||||
secretaryName: '',
|
||||
branchCount: '',
|
||||
memberTotal: '',
|
||||
facultyMemberCount: '',
|
||||
studentMemberCount: '',
|
||||
minorityMemberCount: '',
|
||||
femaleMemberCount: '',
|
||||
youth14To28Count: '',
|
||||
youth29To35Count: '',
|
||||
cadreTotal: '',
|
||||
facultyCadreCount: '',
|
||||
studentCadreCount: '',
|
||||
lastElectionTime: '',
|
||||
feeBalance: '',
|
||||
firstHalfFeeDue: '',
|
||||
firstHalfFeePaid: '',
|
||||
secondHalfFeeDue: '',
|
||||
secondHalfFeePaid: '',
|
||||
schoolSocietyConnectionRate: '',
|
||||
smartLeagueEntryRate: '',
|
||||
annualSummary: '',
|
||||
committeeMembers: [createCommitteeMember()],
|
||||
feeRecords: [createFeeRecord()],
|
||||
subordinateOrgs: [createSubordinateOrg()],
|
||||
honorRecords: [createHonorRecord()],
|
||||
activistRecommendations: [createActivistRecommendation()],
|
||||
disciplineRecords: [createDisciplineRecord()],
|
||||
meetingRecords: [createMeetingRecord()],
|
||||
activityRecords: [createActivityRecord()],
|
||||
educationEvaluations: [createEducationEvaluation()]
|
||||
});
|
||||
|
||||
const form = reactive<WorkLedger>(createDefaultForm());
|
||||
|
||||
const normalizeRows = (rows: any[] | undefined, createRow: () => Record<string, any>) =>
|
||||
rows?.length ? rows.map((item) => ({ ...createRow(), ...item, __rowKey: rowKey() })) : [createRow()];
|
||||
|
||||
const assignForm = (record?: WorkLedger | null) => {
|
||||
const defaults = createDefaultForm();
|
||||
Object.assign(form, defaults, record || {});
|
||||
form.committeeMembers = normalizeRows(record?.committeeMembers, createCommitteeMember) as any;
|
||||
form.feeRecords = normalizeRows(record?.feeRecords, createFeeRecord) as any;
|
||||
form.subordinateOrgs = normalizeRows(record?.subordinateOrgs, createSubordinateOrg) as any;
|
||||
form.honorRecords = normalizeRows(record?.honorRecords, createHonorRecord) as any;
|
||||
form.activistRecommendations = normalizeRows(record?.activistRecommendations, createActivistRecommendation) as any;
|
||||
form.disciplineRecords = normalizeRows(record?.disciplineRecords, createDisciplineRecord) as any;
|
||||
form.meetingRecords = normalizeRows(record?.meetingRecords, createMeetingRecord) as any;
|
||||
form.activityRecords = normalizeRows(record?.activityRecords, createActivityRecord) as any;
|
||||
form.educationEvaluations = normalizeRows(record?.educationEvaluations, createEducationEvaluation) as any;
|
||||
};
|
||||
|
||||
const cleanRows = (rows?: any[]) =>
|
||||
(rows || [])
|
||||
.map(({ __rowKey, ...item }) => item)
|
||||
.filter((item) => Object.values(item).some((value) => String(value || '').trim()));
|
||||
|
||||
const save = () => {
|
||||
loading.value = true;
|
||||
const payload: WorkLedger = {
|
||||
...form,
|
||||
committeeMembers: cleanRows(form.committeeMembers) as any,
|
||||
feeRecords: cleanRows(form.feeRecords) as any,
|
||||
subordinateOrgs: cleanRows(form.subordinateOrgs) as any,
|
||||
honorRecords: cleanRows(form.honorRecords) as any,
|
||||
activistRecommendations: cleanRows(form.activistRecommendations) as any,
|
||||
disciplineRecords: cleanRows(form.disciplineRecords) as any,
|
||||
meetingRecords: cleanRows(form.meetingRecords) as any,
|
||||
activityRecords: cleanRows(form.activityRecords) as any,
|
||||
educationEvaluations: cleanRows(form.educationEvaluations) as any
|
||||
};
|
||||
props
|
||||
.saveHandler(payload)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await loadNationOptions().catch(() => {});
|
||||
isUpdate.value = !!props.data?.id;
|
||||
assignForm(props.data);
|
||||
} else {
|
||||
formRef.value?.resetFields?.();
|
||||
assignForm();
|
||||
}
|
||||
activeKey.value = 'cover';
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../../tag-tabs.less';
|
||||
|
||||
.gxmu-tag-tabs();
|
||||
|
||||
.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,93 @@
|
||||
<template>
|
||||
<div class="editable-block">
|
||||
<div class="editable-title">
|
||||
<span>{{ title }}</span>
|
||||
<a-button size="small" type="dashed" @click="addRow">新增一行</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
row-key="__rowKey"
|
||||
:columns="columns"
|
||||
:data-source="rows"
|
||||
:pagination="false"
|
||||
:scroll="{ x: scrollX }"
|
||||
size="small"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ column, index }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a class="ele-text-danger" @click="removeRow(index)">删除</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-select
|
||||
v-if="column.options?.length"
|
||||
v-model:value="rows[index][column.key]"
|
||||
:options="column.options"
|
||||
:placeholder="`请选择${column.title}`"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
<a-textarea
|
||||
v-else-if="column.textarea"
|
||||
v-model:value="rows[index][column.key]"
|
||||
:rows="2"
|
||||
:placeholder="`请输入${column.title}`"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="rows[index][column.key]"
|
||||
:placeholder="`请输入${column.title}`"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface EditableColumn {
|
||||
title: string;
|
||||
key: string;
|
||||
width?: number;
|
||||
textarea?: boolean;
|
||||
options?: { label: string; value: string }[];
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
rows: Record<string, any>[];
|
||||
columns: EditableColumn[];
|
||||
createRow: () => Record<string, any>;
|
||||
scrollX?: number;
|
||||
}>(),
|
||||
{
|
||||
scrollX: 980
|
||||
}
|
||||
);
|
||||
|
||||
const addRow = () => {
|
||||
props.rows.push(props.createRow());
|
||||
};
|
||||
|
||||
const removeRow = (index: number) => {
|
||||
props.rows.splice(index, 1);
|
||||
if (!props.rows.length) {
|
||||
props.rows.push(props.createRow());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.editable-block {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.editable-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<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: 1180 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space wrap>
|
||||
<a-button class="gxmu-toolbar-btn gxmu-add-btn" @click="openEdit()">
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入单位/负责人"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<edit-modal
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:title="title"
|
||||
:save-handler="saveHandler"
|
||||
@done="reload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
ColumnItem,
|
||||
DatasourceFunction
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import type { WorkLedger, WorkLedgerParam } from '@/api/gxmu/work-ledger/model';
|
||||
import EditModal from './components/edit.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
api: {
|
||||
userPage: (params: WorkLedgerParam) => Promise<{ list?: WorkLedger[]; count?: number }>;
|
||||
add: (data: WorkLedger) => Promise<string>;
|
||||
update: (data: WorkLedger) => Promise<string>;
|
||||
remove: (id?: number) => Promise<string>;
|
||||
};
|
||||
}>();
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const current = ref<WorkLedger | null>(null);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '单位',
|
||||
dataIndex: 'unitName',
|
||||
key: 'unitName',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'responsiblePerson',
|
||||
key: 'responsiblePerson',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '使用开始月份',
|
||||
dataIndex: 'useStartMonth',
|
||||
key: 'useStartMonth',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '使用结束月份',
|
||||
dataIndex: 'useEndMonth',
|
||||
key: 'useEndMonth',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '团员总数',
|
||||
dataIndex: 'memberTotal',
|
||||
key: 'memberTotal',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '团干总数',
|
||||
dataIndex: 'cadreTotal',
|
||||
key: 'cadreTotal',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
width: 170
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return props.api
|
||||
.userPage({
|
||||
page,
|
||||
limit,
|
||||
keywords: searchText.value.trim() || undefined
|
||||
} as WorkLedgerParam)
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
return { list: [], count: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openEdit = (record?: WorkLedger) => {
|
||||
current.value = record
|
||||
? (JSON.parse(JSON.stringify(record)) as WorkLedger)
|
||||
: null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const saveHandler = (data: WorkLedger) => {
|
||||
return data.id ? props.api.update(data) : props.api.add(data);
|
||||
};
|
||||
|
||||
const remove = (record: WorkLedger) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
props.api
|
||||
.remove(record.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user