260720
This commit is contained in:
+35
-52
@@ -5,7 +5,7 @@
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑企业法治检查报告' : '添加企业法治检查报告'"
|
||||
:title="isUpdate ? '编辑' : '添加'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
@@ -19,53 +19,18 @@
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="" name="companyName">
|
||||
<a-form-item label="" name="chatId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.companyName"
|
||||
v-model:value="form.chatId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="检查单位" name="checkCompanyName">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入检查单位"
|
||||
v-model:value="form.checkCompanyName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="checkDate">
|
||||
<a-form-item label="" name="conversationId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.checkDate"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="type">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.type"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="本月检查次数" name="checkNum">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入本月检查次数"
|
||||
v-model:value="form.checkNum"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="comment">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.comment"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="remark">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.remark"
|
||||
v-model:value="form.conversationId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="userId">
|
||||
@@ -75,6 +40,26 @@
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
</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="" name="reply">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.reply"
|
||||
/>
|
||||
</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="是否删除, 0否, 1是" name="deleted">
|
||||
<a-input
|
||||
allow-clear
|
||||
@@ -97,8 +82,8 @@
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import { addLawOrgCheckLog, updateLawOrgCheckLog } from '@/api/law/lawOrgCheckLog';
|
||||
import { LawOrgCheckLog } from '@/api/law/lawOrgCheckLog/model';
|
||||
import { addAiChatHistory, updateAiChatHistory } from '@/api/ai/aiChatHistory';
|
||||
import { AiChatHistory } from '@/api/ai/aiChatHistory/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
@@ -115,7 +100,7 @@
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: LawOrgCheckLog | null;
|
||||
data?: AiChatHistory | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -131,16 +116,14 @@
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<LawOrgCheckLog>({
|
||||
const form = reactive<AiChatHistory>({
|
||||
id: undefined,
|
||||
companyName: undefined,
|
||||
checkCompanyName: undefined,
|
||||
checkDate: undefined,
|
||||
type: undefined,
|
||||
checkNum: undefined,
|
||||
comment: undefined,
|
||||
remark: undefined,
|
||||
chatId: undefined,
|
||||
conversationId: undefined,
|
||||
userId: undefined,
|
||||
content: undefined,
|
||||
reply: undefined,
|
||||
status: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
@@ -171,7 +154,7 @@
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value ? updateLawOrgCheckLog : addLawOrgCheckLog;
|
||||
const saveOrUpdate = isUpdate.value ? updateAiChatHistory : addAiChatHistory;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
@@ -30,7 +30,7 @@
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">详情</a>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@@ -45,7 +45,7 @@
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<LawNotaryListEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
<AiChatHistoryEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,17 +61,17 @@
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import LawNotaryListEdit from './components/lawNotaryListEdit.vue';
|
||||
import { pageLawNotaryList, removeLawNotaryList, removeBatchLawNotaryList } from '@/api/law/lawNotaryList';
|
||||
import type { LawNotaryList, LawNotaryListParam } from '@/api/law/lawNotaryList/model';
|
||||
import AiChatHistoryEdit from './components/aiChatHistoryEdit.vue';
|
||||
import { pageAiChatHistory, removeAiChatHistory, removeBatchAiChatHistory } from '@/api/ai/aiChatHistory';
|
||||
import type { AiChatHistory, AiChatHistoryParam } from '@/api/ai/aiChatHistory/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<LawNotaryList[]>([]);
|
||||
const selection = ref<AiChatHistory[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<LawNotaryList | null>(null);
|
||||
const current = ref<AiChatHistory | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
@@ -90,7 +90,7 @@
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageLawNotaryList({
|
||||
return pageAiChatHistory({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
@@ -101,50 +101,68 @@
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
title: '',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: ['user', 'username'],
|
||||
key: 'uid',
|
||||
title: '',
|
||||
dataIndex: 'chatId',
|
||||
key: 'chatId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '公证事项',
|
||||
title: '',
|
||||
dataIndex: 'conversationId',
|
||||
key: 'conversationId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '用途',
|
||||
dataIndex: 'application',
|
||||
key: 'usage',
|
||||
title: '',
|
||||
dataIndex: 'reply',
|
||||
key: 'reply',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '使用地',
|
||||
dataIndex: 'address',
|
||||
key: 'address',
|
||||
title: '状态, 0正常, 1冻结',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '译文',
|
||||
dataIndex: 'translate',
|
||||
key: 'translate',
|
||||
title: '是否删除, 0否, 1是',
|
||||
dataIndex: 'deleted',
|
||||
key: 'deleted',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
title: '注册时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -157,13 +175,13 @@
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: LawNotaryListParam) => {
|
||||
const reload = (where?: AiChatHistoryParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: LawNotaryList) => {
|
||||
const openEdit = (row?: AiChatHistory) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
@@ -174,9 +192,9 @@
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: LawNotaryList) => {
|
||||
const remove = (row: AiChatHistory) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeLawNotaryList(row.id)
|
||||
removeAiChatHistory(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
@@ -201,7 +219,7 @@
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchLawNotaryList(selection.value.map((d) => d.id))
|
||||
removeBatchAiChatHistory(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
@@ -221,7 +239,7 @@
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: LawNotaryList) => {
|
||||
const customRow = (record: AiChatHistory) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
@@ -238,7 +256,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'LawNotaryList'
|
||||
name: 'AiChatHistory'
|
||||
};
|
||||
</script>
|
||||
|
||||
+36
-45
@@ -19,6 +19,13 @@
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="" name="conversationId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.conversationId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="userId">
|
||||
<a-input
|
||||
allow-clear
|
||||
@@ -26,54 +33,32 @@
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="title">
|
||||
<a-form-item label="" name="roleId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.title"
|
||||
v-model:value="form.roleId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="company">
|
||||
<a-form-item label="" name="roleName">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.company"
|
||||
v-model:value="form.roleName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="address">
|
||||
<a-form-item label="" name="roleDesc">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.address"
|
||||
v-model:value="form.roleDesc"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="date">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.date"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="pics">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.pics"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="video">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.video"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="content">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.content"
|
||||
/>
|
||||
<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="是否删除, 0否, 1是" name="deleted">
|
||||
<a-input
|
||||
@@ -89,6 +74,13 @@
|
||||
v-model:value="form.updateTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="title">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.title"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
@@ -97,8 +89,8 @@
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import { addLawFeedback, updateLawFeedback } from '@/api/law/lawFeedback';
|
||||
import { LawFeedback } from '@/api/law/lawFeedback/model';
|
||||
import { addAiChatList, updateAiChatList } from '@/api/ai/aiChatList';
|
||||
import { AiChatList } from '@/api/ai/aiChatList/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
@@ -115,7 +107,7 @@
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: LawFeedback | null;
|
||||
data?: AiChatList | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -131,20 +123,19 @@
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<LawFeedback>({
|
||||
const form = reactive<AiChatList>({
|
||||
id: undefined,
|
||||
conversationId: undefined,
|
||||
userId: undefined,
|
||||
title: undefined,
|
||||
company: undefined,
|
||||
address: undefined,
|
||||
date: undefined,
|
||||
pics: undefined,
|
||||
video: undefined,
|
||||
content: undefined,
|
||||
roleId: undefined,
|
||||
roleName: undefined,
|
||||
roleDesc: undefined,
|
||||
status: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
title: undefined,
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
@@ -171,7 +162,7 @@
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value ? updateLawFeedback : addLawFeedback;
|
||||
const saveOrUpdate = isUpdate.value ? updateAiChatList : addAiChatList;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
@@ -0,0 +1,269 @@
|
||||
<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>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<AiChatListEdit 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 AiChatListEdit from './components/aiChatListEdit.vue';
|
||||
import { pageAiChatList, removeAiChatList, removeBatchAiChatList } from '@/api/ai/aiChatList';
|
||||
import type { AiChatList, AiChatListParam } from '@/api/ai/aiChatList/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<AiChatList[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<AiChatList | 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 pageAiChatList({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'conversationId',
|
||||
key: 'conversationId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'roleId',
|
||||
key: 'roleId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'roleName',
|
||||
key: 'roleName',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'roleDesc',
|
||||
key: 'roleDesc',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '状态, 0正常, 1冻结',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '是否删除, 0否, 1是',
|
||||
dataIndex: 'deleted',
|
||||
key: 'deleted',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: AiChatListParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: AiChatList) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: AiChatList) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeAiChatList(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);
|
||||
removeBatchAiChatList(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: AiChatList) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AiChatList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<!-- 搜索栏 -->
|
||||
<a-space :size="10" style="flex-wrap: wrap; margin-bottom: 16px">
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
placeholder="按知识库名称搜索"
|
||||
allow-clear
|
||||
style="width: 260px"
|
||||
@search="handleSearch"
|
||||
@change="onKeywordChange"
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-spin :spinning="loading">
|
||||
<a-empty v-if="!loading && list.length === 0" description="暂无知识库" />
|
||||
<a-row v-else :gutter="[16, 16]">
|
||||
<a-col
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:lg="6"
|
||||
>
|
||||
<a-card
|
||||
hoverable
|
||||
class="kb-card"
|
||||
:body-style="{ padding: '16px' }"
|
||||
>
|
||||
<!-- 顶部:图标 + 名称 -->
|
||||
<div class="kb-card-header">
|
||||
<div class="kb-icon">
|
||||
<BookOutlined style="font-size: 22px; color: #1677ff" />
|
||||
</div>
|
||||
<div class="kb-name" :title="item.name">{{ item.name }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div class="kb-desc" v-if="item.description">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<div class="kb-desc empty" v-else>暂无描述</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<a-row class="kb-stats" :gutter="8">
|
||||
<a-col :span="8" class="kb-stat-item">
|
||||
<div class="kb-stat-value">{{ item.total_documents ?? item.document_count ?? 0 }}</div>
|
||||
<div class="kb-stat-label">文档数</div>
|
||||
</a-col>
|
||||
<a-col :span="8" class="kb-stat-item">
|
||||
<div class="kb-stat-value">{{ item.total_available_documents ?? 0 }}</div>
|
||||
<div class="kb-stat-label">可用文档</div>
|
||||
</a-col>
|
||||
<a-col :span="8" class="kb-stat-item">
|
||||
<div class="kb-stat-value">{{ formatWordCount(item.word_count) }}</div>
|
||||
<div class="kb-stat-label">字数</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 底部标签 -->
|
||||
<div class="kb-footer">
|
||||
<a-tag v-if="item.enable_api" color="success">API已开启</a-tag>
|
||||
<a-tag v-else color="default">API未开启</a-tag>
|
||||
<a-tag v-if="item.indexing_technique === 'high_quality'" color="blue">高质量</a-tag>
|
||||
<span class="kb-time">{{ formatTime(item.updated_at) }}</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-spin>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="kb-pagination" v-if="total > 0">
|
||||
<a-pagination
|
||||
v-model:current="page"
|
||||
v-model:pageSize="limit"
|
||||
:total="total"
|
||||
:page-size-options="['10', '20', '50']"
|
||||
show-size-changer
|
||||
show-quick-jumper
|
||||
:show-total="(t: number) => `共 ${t} 条`"
|
||||
@change="fetchList"
|
||||
@showSizeChange="onPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { BookOutlined, SearchOutlined } from '@ant-design/icons-vue';
|
||||
import { listKnowledgeBase } from '@/api/ai/knowledgeBase';
|
||||
import type { KnowledgeBase } from '@/api/ai/knowledgeBase/model';
|
||||
|
||||
const keyword = ref<string>('');
|
||||
const page = ref<number>(1);
|
||||
const limit = ref<number>(20);
|
||||
const total = ref<number>(0);
|
||||
const loading = ref<boolean>(false);
|
||||
const list = ref<KnowledgeBase[]>([]);
|
||||
|
||||
const fetchList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await listKnowledgeBase({
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
keyword: keyword.value || undefined
|
||||
});
|
||||
list.value = res?.data ?? [];
|
||||
total.value = res?.total ?? 0;
|
||||
} catch (e: any) {
|
||||
message.error(e?.message ?? '获取知识库列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
page.value = 1;
|
||||
fetchList();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
keyword.value = '';
|
||||
page.value = 1;
|
||||
fetchList();
|
||||
};
|
||||
|
||||
const onKeywordChange = () => {
|
||||
if (!keyword.value) {
|
||||
page.value = 1;
|
||||
fetchList();
|
||||
}
|
||||
};
|
||||
|
||||
const onPageSizeChange = (_current: number, size: number) => {
|
||||
page.value = 1;
|
||||
limit.value = size;
|
||||
fetchList();
|
||||
};
|
||||
|
||||
const formatWordCount = (count?: number): string => {
|
||||
if (count == null) return '0';
|
||||
if (count >= 10000) return (count / 10000).toFixed(1) + 'w';
|
||||
return String(count);
|
||||
};
|
||||
|
||||
const formatTime = (ts?: number): string => {
|
||||
if (!ts) return '';
|
||||
const d = new Date(ts * 1000);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kb-card {
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.kb-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kb-icon {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #e6f4ff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.kb-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.kb-desc {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
min-height: 36px;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.kb-desc.empty {
|
||||
color: #bbb;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kb-stats {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.kb-stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kb-stat-value {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.kb-stat-label {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.kb-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.kb-time {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.kb-pagination {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -19,17 +19,16 @@
|
||||
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="文章分类" name="categoryId">
|
||||
<a-form-item label="知识库分类" name="categoryId">
|
||||
<category-select
|
||||
:data="categoryList"
|
||||
placeholder="请选择文章分类"
|
||||
placeholder="请选择知识库分类"
|
||||
v-model:value="form.categoryId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="文章标题" name="title">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="请输入文章标题"
|
||||
v-model:value="form.title"
|
||||
/>
|
||||
@@ -56,10 +55,8 @@
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="文章来源" name="source">
|
||||
<source-select
|
||||
v-model:value="form.source"
|
||||
/>
|
||||
<a-form-item label="简介" name="source">
|
||||
<a-input v-model:value="form.source" />
|
||||
</a-form-item>
|
||||
<a-form-item label="展现方式" name="showType">
|
||||
<a-radio-group v-model:value="form.showType">
|
||||
@@ -203,7 +200,7 @@ const rules = reactive<Record<string, Rule[]>>({
|
||||
{
|
||||
required: true,
|
||||
type: "number",
|
||||
message: "请选择文章分类",
|
||||
message: "请选择知识库分类",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
@@ -267,7 +264,7 @@ const onUpload = (item) => {
|
||||
url: data.url,
|
||||
status: "done"
|
||||
});
|
||||
form.image = data.path;
|
||||
form.image = data.url;
|
||||
message.success("上传成功");
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -421,7 +418,7 @@ watch(
|
||||
if (props.data.image) {
|
||||
images.value.push({
|
||||
uid: 1,
|
||||
url: FILE_THUMBNAIL + props.data.image,
|
||||
url: props.data.image,
|
||||
status: "done"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div>
|
||||
<ele-toolbar theme="default">
|
||||
<a-space :size="10">
|
||||
<span>文章分类</span>
|
||||
<span>知识库分类</span>
|
||||
<a-button
|
||||
v-permission="'cms:article:save'"
|
||||
type="primary"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<a-image
|
||||
:width="45"
|
||||
v-if="record.image"
|
||||
:src="FILE_THUMBNAIL + record.image"
|
||||
:src="record.image"
|
||||
:preview="false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
+16
-17
@@ -5,7 +5,7 @@
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑用户填写法律文书内容' : '添加用户填写法律文书内容'"
|
||||
:title="isUpdate ? '编辑' : '添加'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
@@ -19,18 +19,11 @@
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="" name="groupId">
|
||||
<a-form-item label="" name="articleId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.groupId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="content">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.content"
|
||||
v-model:value="form.articleId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="userId">
|
||||
@@ -40,6 +33,12 @@
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
</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="是否删除, 0否, 1是" name="deleted">
|
||||
<a-input
|
||||
allow-clear
|
||||
@@ -62,8 +61,8 @@
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import { addLawLegalDocContent, updateLawLegalDocContent } from '@/api/law/lawLegalDocContent';
|
||||
import { LawLegalDocContent } from '@/api/law/lawLegalDocContent/model';
|
||||
import { addArticleCollect, updateArticleCollect } from '@/api/cms/articleCollect';
|
||||
import { ArticleCollect } from '@/api/cms/articleCollect/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
@@ -80,7 +79,7 @@
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: LawLegalDocContent | null;
|
||||
data?: ArticleCollect | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -96,11 +95,11 @@
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<LawLegalDocContent>({
|
||||
const form = reactive<ArticleCollect>({
|
||||
id: undefined,
|
||||
groupId: undefined,
|
||||
content: undefined,
|
||||
articleId: undefined,
|
||||
userId: undefined,
|
||||
status: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
@@ -131,7 +130,7 @@
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value ? updateLawLegalDocContent : addLawLegalDocContent;
|
||||
const saveOrUpdate = isUpdate.value ? updateArticleCollect : addArticleCollect;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
+23
-23
@@ -45,7 +45,7 @@
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<LawLegalDocContentEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
<ArticleCollectEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,17 +61,17 @@
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import LawLegalDocContentEdit from './components/lawLegalDocContentEdit.vue';
|
||||
import { pageLawLegalDocContent, removeLawLegalDocContent, removeBatchLawLegalDocContent } from '@/api/law/lawLegalDocContent';
|
||||
import type { LawLegalDocContent, LawLegalDocContentParam } from '@/api/law/lawLegalDocContent/model';
|
||||
import ArticleCollectEdit from './components/articleCollectEdit.vue';
|
||||
import { pageArticleCollect, removeArticleCollect, removeBatchArticleCollect } from '@/api/cms/articleCollect';
|
||||
import type { ArticleCollect, ArticleCollectParam } from '@/api/cms/articleCollect/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<LawLegalDocContent[]>([]);
|
||||
const selection = ref<ArticleCollect[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<LawLegalDocContent | null>(null);
|
||||
const current = ref<ArticleCollect | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
@@ -90,7 +90,7 @@
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageLawLegalDocContent({
|
||||
return pageArticleCollect({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
@@ -109,14 +109,8 @@
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'groupId',
|
||||
key: 'groupId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
dataIndex: 'articleId',
|
||||
key: 'articleId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
@@ -125,6 +119,12 @@
|
||||
key: 'userId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '状态, 0正常, 1冻结',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '是否删除, 0否, 1是',
|
||||
dataIndex: 'deleted',
|
||||
@@ -132,7 +132,7 @@
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: '注册时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
@@ -157,13 +157,13 @@
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: LawLegalDocContentParam) => {
|
||||
const reload = (where?: ArticleCollectParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: LawLegalDocContent) => {
|
||||
const openEdit = (row?: ArticleCollect) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
@@ -174,9 +174,9 @@
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: LawLegalDocContent) => {
|
||||
const remove = (row: ArticleCollect) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeLawLegalDocContent(row.id)
|
||||
removeArticleCollect(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
@@ -201,7 +201,7 @@
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchLawLegalDocContent(selection.value.map((d) => d.id))
|
||||
removeBatchArticleCollect(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
@@ -221,7 +221,7 @@
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: LawLegalDocContent) => {
|
||||
const customRow = (record: ArticleCollect) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
@@ -238,7 +238,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'LawLegalDocContent'
|
||||
name: 'ArticleCollect'
|
||||
};
|
||||
</script>
|
||||
|
||||
+23
-16
@@ -5,7 +5,7 @@
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑用户填写法律计算器内容' : '添加用户填写法律计算器内容'"
|
||||
:title="isUpdate ? '编辑' : '添加'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
@@ -19,11 +19,18 @@
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="" name="groupId">
|
||||
<a-form-item label="" name="articleId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.groupId"
|
||||
v-model:value="form.articleId"
|
||||
/>
|
||||
</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="content">
|
||||
@@ -33,12 +40,11 @@
|
||||
v-model:value="form.content"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="userId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
<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="是否删除, 0否, 1是" name="deleted">
|
||||
<a-input
|
||||
@@ -62,8 +68,8 @@
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import { addLawLegalCalContent, updateLawLegalCalContent } from '@/api/law/lawLegalCalContent';
|
||||
import { LawLegalCalContent } from '@/api/law/lawLegalCalContent/model';
|
||||
import { addArticleComment, updateArticleComment } from '@/api/cms/articleComment';
|
||||
import { ArticleComment } from '@/api/cms/articleComment/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
@@ -80,7 +86,7 @@
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: LawLegalCalContent | null;
|
||||
data?: ArticleComment | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -96,11 +102,12 @@
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<LawLegalCalContent>({
|
||||
const form = reactive<ArticleComment>({
|
||||
id: undefined,
|
||||
groupId: undefined,
|
||||
content: undefined,
|
||||
articleId: undefined,
|
||||
userId: undefined,
|
||||
content: undefined,
|
||||
status: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
@@ -131,7 +138,7 @@
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value ? updateLawLegalCalContent : addLawLegalCalContent;
|
||||
const saveOrUpdate = isUpdate.value ? updateArticleComment : addArticleComment;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
+29
-23
@@ -45,7 +45,7 @@
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<LawLegalCalContentEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
<ArticleCommentEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,17 +61,17 @@
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import LawLegalCalContentEdit from './components/lawLegalCalContentEdit.vue';
|
||||
import { pageLawLegalCalContent, removeLawLegalCalContent, removeBatchLawLegalCalContent } from '@/api/law/lawLegalCalContent';
|
||||
import type { LawLegalCalContent, LawLegalCalContentParam } from '@/api/law/lawLegalCalContent/model';
|
||||
import ArticleCommentEdit from './components/articleCommentEdit.vue';
|
||||
import { pageArticleComment, removeArticleComment, removeBatchArticleComment } from '@/api/cms/articleComment';
|
||||
import type { ArticleComment, ArticleCommentParam } from '@/api/cms/articleComment/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<LawLegalCalContent[]>([]);
|
||||
const selection = ref<ArticleComment[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<LawLegalCalContent | null>(null);
|
||||
const current = ref<ArticleComment | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
@@ -90,7 +90,7 @@
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageLawLegalCalContent({
|
||||
return pageArticleComment({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
@@ -109,14 +109,8 @@
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'groupId',
|
||||
key: 'groupId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
dataIndex: 'articleId',
|
||||
key: 'articleId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
@@ -125,6 +119,18 @@
|
||||
key: 'userId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '状态, 0正常, 1冻结',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '是否删除, 0否, 1是',
|
||||
dataIndex: 'deleted',
|
||||
@@ -132,7 +138,7 @@
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: '注册时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
@@ -157,13 +163,13 @@
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: LawLegalCalContentParam) => {
|
||||
const reload = (where?: ArticleCommentParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: LawLegalCalContent) => {
|
||||
const openEdit = (row?: ArticleComment) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
@@ -174,9 +180,9 @@
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: LawLegalCalContent) => {
|
||||
const remove = (row: ArticleComment) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeLawLegalCalContent(row.id)
|
||||
removeArticleComment(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
@@ -201,7 +207,7 @@
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchLawLegalCalContent(selection.value.map((d) => d.id))
|
||||
removeBatchArticleComment(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
@@ -221,7 +227,7 @@
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: LawLegalCalContent) => {
|
||||
const customRow = (record: ArticleComment) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
@@ -238,7 +244,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'LawLegalCalContent'
|
||||
name: 'ArticleComment'
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="1000"
|
||||
: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: '100px' }
|
||||
"
|
||||
: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="content">
|
||||
<div class="editor-wrapper">
|
||||
<Toolbar
|
||||
class="editor-toolbar"
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
mode="default"
|
||||
/>
|
||||
<Editor
|
||||
v-model="contentHtml"
|
||||
class="editor-content"
|
||||
:defaultConfig="editorConfig"
|
||||
mode="default"
|
||||
@onCreated="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
<div class="editor-tip">支持直接粘贴内容,粘贴图片时会自动上传。</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="封面" name="cover">
|
||||
<ele-image-upload
|
||||
v-model:value="coverList"
|
||||
:limit="1"
|
||||
:drag="true"
|
||||
:upload-handler="uploadHandler"
|
||||
@upload="onUpload"
|
||||
/>
|
||||
</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-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-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, ref, reactive, watch, onBeforeUnmount } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { addManuscript, updateManuscript } from '@/api/cms/manuscript';
|
||||
import { Manuscript } from '@/api/cms/manuscript/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { uploadFile } from '@/api/system/file';
|
||||
import type { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||
import type {
|
||||
IDomEditor,
|
||||
IEditorConfig,
|
||||
IToolbarConfig
|
||||
} from '@wangeditor/editor';
|
||||
import '@wangeditor/editor/dist/css/style.css';
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: Manuscript | 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 editorRef = shallowRef<IDomEditor>();
|
||||
const contentHtml = ref('');
|
||||
const coverList = ref<ItemType[]>([]);
|
||||
|
||||
const toolbarConfig: Partial<IToolbarConfig> = {
|
||||
excludeKeys: ['group-video']
|
||||
};
|
||||
|
||||
const editorConfig: Partial<IEditorConfig> = {
|
||||
placeholder: '请输入稿件内容,支持粘贴图片自动上传',
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
allowedFileTypes: ['image/*'],
|
||||
maxFileSize: 2 * 1024 * 1024,
|
||||
base64LimitSize: 0,
|
||||
async customUpload(file: File, insertFn: (url: string) => void) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error('只能上传图片');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await uploadFile(file);
|
||||
insertFn(result.url);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '图片上传失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const form = reactive<Manuscript>({
|
||||
id: undefined,
|
||||
title: undefined,
|
||||
content: undefined,
|
||||
cover: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
userId: undefined,
|
||||
sortNumber: 0,
|
||||
status: 1
|
||||
});
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const hasEditorContent = (html?: string) => {
|
||||
const text = String(html || '')
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /gi, '')
|
||||
.trim();
|
||||
return !!text || /<img\b/i.test(String(html || ''));
|
||||
};
|
||||
|
||||
const rules = reactive({
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
whitespace: true,
|
||||
message: '请输入稿件标题',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
content: [
|
||||
{
|
||||
validator: async () => {
|
||||
if (!hasEditorContent(contentHtml.value)) {
|
||||
return Promise.reject(new Error('请输入稿件内容'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
sortNumber: [
|
||||
{
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入排序号',
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { resetFields, validate } = useForm(form, rules);
|
||||
|
||||
const handleCreated = (editor: IDomEditor) => {
|
||||
editorRef.value = editor;
|
||||
};
|
||||
|
||||
const uploadHandler = (file: File) => {
|
||||
const item: ItemType = {
|
||||
file,
|
||||
uid: (file as any).uid,
|
||||
name: file.name
|
||||
};
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error('只能选择图片');
|
||||
return;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 2) {
|
||||
message.error('大小不能超过 2MB');
|
||||
return;
|
||||
}
|
||||
onUpload(item);
|
||||
};
|
||||
|
||||
const onUpload = (item: ItemType) => {
|
||||
const { file } = item;
|
||||
uploadFile(file as File)
|
||||
.then((data) => {
|
||||
coverList.value = [
|
||||
{
|
||||
uid: data.id,
|
||||
url: data.url,
|
||||
status: 'done'
|
||||
}
|
||||
];
|
||||
form.cover = data.url;
|
||||
message.success('上传成功');
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const resetEditor = () => {
|
||||
contentHtml.value = '';
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form,
|
||||
content: contentHtml.value
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value ? updateManuscript : addManuscript;
|
||||
const msg = await saveOrUpdate(formData);
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
if (!e?.errorFields) {
|
||||
message.error(e.message || '保存失败');
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(contentHtml, () => {
|
||||
form.content = contentHtml.value;
|
||||
validate('content').catch(() => {});
|
||||
});
|
||||
|
||||
watch(
|
||||
coverList,
|
||||
(list) => {
|
||||
if (!list.length) {
|
||||
form.cover = '';
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
contentHtml.value = props.data.content || '';
|
||||
coverList.value = props.data.cover
|
||||
? [
|
||||
{
|
||||
uid: props.data.id || 1,
|
||||
url: props.data.cover,
|
||||
status: 'done'
|
||||
}
|
||||
]
|
||||
: [];
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
resetFields();
|
||||
resetEditor();
|
||||
coverList.value = [];
|
||||
form.status = 1;
|
||||
form.sortNumber = 0;
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
resetEditor();
|
||||
coverList.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value;
|
||||
if (editor != null) {
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.editor-wrapper {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.editor-content :deep(.w-e-text-container) {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.editor-tip {
|
||||
margin-top: 8px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="860"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:confirm-loading="loading"
|
||||
title="审核历史"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-alert
|
||||
v-if="!data?.id"
|
||||
type="warning"
|
||||
show-icon
|
||||
message="未选择稿件,无法新增审核记录"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
|
||||
<a-descriptions
|
||||
v-if="data?.id"
|
||||
size="small"
|
||||
:column="2"
|
||||
style="margin-bottom: 12px"
|
||||
>
|
||||
<a-descriptions-item label="稿件ID">
|
||||
{{ data?.id }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="标题">
|
||||
{{ data?.title || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div style="overflow-y: auto; padding: 10px 0" class="ele-scrollbar-hover">
|
||||
<a-timeline v-if="historyData.length > 0">
|
||||
<a-timeline-item
|
||||
v-for="item in historyData"
|
||||
: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 === 0 ? 'error' : 'default'"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ item.status === 1 ? '通过' : item.status === 0 ? '不通过' : '-' }}
|
||||
</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, reactive, ref, watch } from 'vue';
|
||||
import { Form, message, Empty } from 'ant-design-vue';
|
||||
import type { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { addReviewList } from '@/api/gxmu/reviewList';
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
import type { Manuscript } from '@/api/cms/manuscript/model';
|
||||
|
||||
const REVIEW_MODULE = 'cms_manuscript';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: Manuscript | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
const useForm = Form.useForm;
|
||||
|
||||
const loading = ref(false);
|
||||
const maxable = ref(true);
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const form = reactive<ReviewList>({
|
||||
module: REVIEW_MODULE,
|
||||
pk: undefined,
|
||||
status: undefined,
|
||||
content: undefined
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
status: [
|
||||
{ required: true, type: 'number', message: '请选择审核状态', trigger: 'change' }
|
||||
]
|
||||
});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const historyData = computed(() => {
|
||||
const list = props.data?.reviewList ?? [];
|
||||
return [...list].reverse();
|
||||
});
|
||||
|
||||
const save = () => {
|
||||
if (!props.data?.id) {
|
||||
message.error('未选择稿件');
|
||||
return;
|
||||
}
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const payload: ReviewList = {
|
||||
module: REVIEW_MODULE,
|
||||
pk: props.data?.id,
|
||||
status: form.status,
|
||||
content: form.content
|
||||
};
|
||||
addReviewList(payload)
|
||||
.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) {
|
||||
form.module = REVIEW_MODULE;
|
||||
form.pk = props.data?.id;
|
||||
form.status = undefined;
|
||||
form.content = undefined;
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ 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,276 @@
|
||||
<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 === 'cover'">
|
||||
<a-image :src="record.cover" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'currentReviewStatus'">
|
||||
<template v-if="getLastReview(record)?.status === 1">
|
||||
<a-tag color="green">通过</a-tag>
|
||||
</template>
|
||||
<template v-else-if="getLastReview(record)?.status === 2">
|
||||
<a-tag color="red">不通过</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>未审核</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0">待审核</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openReviewModal(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>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ManuscriptEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
<!-- 新增审核记录弹窗 -->
|
||||
<ReviewRecordModal
|
||||
v-model:visible="showReviewModal"
|
||||
:data="reviewTarget"
|
||||
@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 ManuscriptEdit from './components/manuscriptEdit.vue';
|
||||
import ReviewRecordModal from './components/reviewRecordModal.vue';
|
||||
import { pageManuscript, removeManuscript, removeBatchManuscript } from '@/api/cms/manuscript';
|
||||
import type { Manuscript, ManuscriptParam } from '@/api/cms/manuscript/model';
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<Manuscript[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<Manuscript | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示新增审核记录弹窗
|
||||
const showReviewModal = ref(false);
|
||||
// 当前要新增审核记录的稿件
|
||||
const reviewTarget = ref<Manuscript | null>(null);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageManuscript({
|
||||
...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: 'cover',
|
||||
key: 'cover',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '当前审核状态',
|
||||
dataIndex: 'reviewList',
|
||||
key: 'currentReviewStatus',
|
||||
align: 'center',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 260,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ManuscriptParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: Manuscript) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const getLastReview = (row: Manuscript): ReviewList | undefined => {
|
||||
const list = row.reviewList ?? [];
|
||||
return list.length ? list[list.length - 1] : undefined;
|
||||
};
|
||||
|
||||
/* 打开新增审核记录弹窗 */
|
||||
const openReviewModal = (row: Manuscript) => {
|
||||
reviewTarget.value = row;
|
||||
showReviewModal.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: Manuscript) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeManuscript(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);
|
||||
removeBatchManuscript(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: Manuscript) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Manuscript'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -1,300 +0,0 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="740"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '修改分类' : '新建分类'"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 6, sm: 4, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col
|
||||
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
|
||||
>
|
||||
<a-form-item label="上级分类" name="parentId">
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
:tree-data="categoryList"
|
||||
tree-default-expand-all
|
||||
placeholder="请选择上级分类"
|
||||
:value="form.parentId || undefined"
|
||||
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
|
||||
@update:value="(value?: number) => (form.parentId = value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="分类名称" name="title">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入分类名称"
|
||||
v-model:value="form.title"
|
||||
@pressEnter="save"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col
|
||||
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
|
||||
>
|
||||
<a-form-item label="排序号" name="sortNumber">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:max="99999"
|
||||
class="ele-fluid"
|
||||
placeholder="请输入排序号"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否展示">
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="form.status === 0"
|
||||
@update:checked="updateHideValue"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div style="margin-bottom: 22px">
|
||||
<a-divider />
|
||||
</div>
|
||||
<a-row :gutter="16">
|
||||
<a-col
|
||||
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
|
||||
>
|
||||
<a-form-item label="索引图标" name="image">
|
||||
<ele-image-upload
|
||||
v-model:value="images"
|
||||
:item-style="{ width: '45px', height: '45px' }"
|
||||
:limit="1"
|
||||
@upload="onUpload"
|
||||
@remove="onClose"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col
|
||||
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
|
||||
>
|
||||
<a-form-item
|
||||
label="备注"
|
||||
name="comments"
|
||||
:label-col="
|
||||
styleResponsive ? { md: 3, sm: 4, xs: 24 } : { flex: '90px' }
|
||||
"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 21, sm: 20, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入备注信息"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import useFormData from '@/utils/use-form-data';
|
||||
import type { Category } from '@/api/goods/category/model';
|
||||
import { addCategory, updateCategory } from '@/api/goods/category';
|
||||
import { FILE_SERVER } from "@/config/setting";
|
||||
import { ItemType } from "ele-admin-pro/es/ele-image-upload/types";
|
||||
import { uploadFile } from "@/api/system/file";
|
||||
import { LoginRecordParam } from "@/api/system/login-record/model";
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: Category | null;
|
||||
// 上级分类id
|
||||
parentId?: number;
|
||||
// 全部分类数据
|
||||
categoryList: Category[];
|
||||
}>();
|
||||
|
||||
// 表单数据
|
||||
const { form, resetFields, assignFields } = useFormData<Category>({
|
||||
title: '',
|
||||
categoryId: undefined,
|
||||
parentId: undefined,
|
||||
image: '',
|
||||
status: 0,
|
||||
sortNumber: 100
|
||||
});
|
||||
|
||||
//
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 已上传数据, 可赋初始值用于回显
|
||||
const images = ref([]);
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<Record<string, Rule[]>>({
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入分类名称',
|
||||
type: 'string',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
sortNumber: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入排序号',
|
||||
type: 'number',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
meta: [
|
||||
{
|
||||
type: 'string',
|
||||
validator: async (_rule: Rule, value: string) => {
|
||||
if (value) {
|
||||
const msg = '请输入正确的JSON格式';
|
||||
try {
|
||||
const obj = JSON.parse(value);
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return Promise.reject(msg);
|
||||
}
|
||||
} catch (_e) {
|
||||
return Promise.reject(msg);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 上传文件
|
||||
const onUpload = (d: ItemType) => {
|
||||
uploadFile(<File>d.file)
|
||||
.then((result) => {
|
||||
form.image = result.path;
|
||||
message.success('上传成功');
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
form.image = ''
|
||||
}
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const categoryForm = {
|
||||
...form,
|
||||
parentId: form.parentId || 0
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value ? updateCategory : addCategory;
|
||||
saveOrUpdate(categoryForm)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const updateHideValue = (value: boolean) => {
|
||||
form.status = value ? 0 : 1;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
// 头像赋值
|
||||
images.value = [];
|
||||
if(props.data.image){
|
||||
images.value.push({ uid:1, url: props.data.image, status: '' });
|
||||
}
|
||||
assignFields({
|
||||
...props.data,
|
||||
parentId:
|
||||
props.data.parentId === 0 ? undefined : props.data.parentId
|
||||
});
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
form.parentId = props.parentId;
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import * as icons from '@/layout/menu-icons';
|
||||
|
||||
export default {
|
||||
components: icons,
|
||||
data() {
|
||||
return {
|
||||
iconData: [
|
||||
{
|
||||
title: '已引入的图标',
|
||||
icons: Object.keys(icons)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,281 +0,0 @@
|
||||
<template>
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false">
|
||||
<!-- 表格 -->
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="categoryId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:parse-data="parseData"
|
||||
:need-page="false"
|
||||
:expand-icon-column-index="1"
|
||||
:expanded-row-keys="expandedRowKeys"
|
||||
:scroll="{ x: 1200 }"
|
||||
cache-key="proGoodsCategoryTable"
|
||||
@done="onDone"
|
||||
@expand="onExpand"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>新建</span>
|
||||
</a-button>
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="expandAll">
|
||||
展开全部
|
||||
</a-button>
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="foldAll">
|
||||
折叠全部
|
||||
</a-button>
|
||||
<!-- 搜索表单 -->
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入搜索关键词"
|
||||
@search="search"
|
||||
@pressEnter="search"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'menuType'">
|
||||
<a-tag v-if="isExternalLink(record.path)" color="red">外链</a-tag>
|
||||
<a-tag v-else-if="isExternalLink(record.component)" color="orange">
|
||||
内链
|
||||
</a-tag>
|
||||
<a-tag v-else-if="isDirectory(record)" color="blue">目录</a-tag>
|
||||
<a-tag v-else-if="record.menuType === 0" color="green">分类</a-tag>
|
||||
<a-tag v-else-if="record.menuType === 1">按钮</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'title'">
|
||||
<a-avatar
|
||||
:size="20"
|
||||
v-if="record.image"
|
||||
shape="square"
|
||||
:src="`${record.image}`"
|
||||
/>
|
||||
<a-tooltip :title="`分类ID:${record.categoryId}`">
|
||||
<span>{{ record.title }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="orange">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(null, record.categoryId)">添加</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
placement="topRight"
|
||||
title="确定要删除此分类吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
<!-- 编辑弹窗 -->
|
||||
<category-edit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:parent-id="parentId"
|
||||
:category-list="categoryData"
|
||||
@done="reload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem,
|
||||
EleProTableDone
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
messageLoading,
|
||||
toDateString,
|
||||
isExternalLink,
|
||||
toTreeData,
|
||||
eachTreeData
|
||||
} from 'ele-admin-pro/es';
|
||||
import type { EleProTable } from 'ele-admin-pro/es';
|
||||
import CategoryEdit from './components/category-edit.vue';
|
||||
import type { Category, CategoryParam } from '@/api/goods/category/model';
|
||||
import { listCategory, removeCategory } from "@/api/goods/category";
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
key: 'index',
|
||||
width: 48,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
hideInSetting: true,
|
||||
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
|
||||
},
|
||||
{
|
||||
title: '分类名称',
|
||||
key: 'title',
|
||||
showSorterTooltip: false,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sortNumber',
|
||||
sorter: true,
|
||||
width: 180,
|
||||
showSorterTooltip: false
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
sorter: true,
|
||||
width: 180,
|
||||
showSorterTooltip: false,
|
||||
customRender: ({ text }) => ['显示', '隐藏'][text]
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
sorter: true,
|
||||
showSorterTooltip: false,
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
customRender: ({ text }) => toDateString(text)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
align: 'center'
|
||||
}
|
||||
]);
|
||||
|
||||
// 当前编辑数据
|
||||
const current = ref<Category | null>(null);
|
||||
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
|
||||
// 上级分类id
|
||||
const parentId = ref<number>();
|
||||
|
||||
// 分类数据
|
||||
const categoryData = ref<Category[]>([]);
|
||||
|
||||
// 表格展开的行
|
||||
const expandedRowKeys = ref<number[]>([]);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({ where }) => {
|
||||
return listCategory({ ...where });
|
||||
};
|
||||
|
||||
/* 数据转为树形结构 */
|
||||
const parseData = (data: Category[]) => {
|
||||
return toTreeData({
|
||||
data: data.map((d) => {
|
||||
return { ...d, key: d.categoryId, value: d.categoryId };
|
||||
}),
|
||||
idField: 'categoryId',
|
||||
parentIdField: 'parentId'
|
||||
});
|
||||
};
|
||||
|
||||
/* 表格渲染完成回调 */
|
||||
const onDone: EleProTableDone<Category> = ({ data }) => {
|
||||
categoryData.value = data;
|
||||
};
|
||||
|
||||
/* 刷新表格 */
|
||||
const reload = (where?: CategoryParam) => {
|
||||
tableRef?.value?.reload({ where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: Category | null, id?: number) => {
|
||||
current.value = row ?? null;
|
||||
parentId.value = id;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const search = (searchText) => {
|
||||
reload({ title: searchText });
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: Category) => {
|
||||
if (row.children?.length) {
|
||||
message.error('请先删除子节点');
|
||||
return;
|
||||
}
|
||||
const hide = messageLoading('请求中..', 0);
|
||||
removeCategory(row.categoryId)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 展开全部 */
|
||||
const expandAll = () => {
|
||||
let keys: number[] = [];
|
||||
eachTreeData(categoryData.value, (d) => {
|
||||
if (d.children && d.children.length && d.categoryId) {
|
||||
keys.push(d.categoryId);
|
||||
}
|
||||
});
|
||||
expandedRowKeys.value = keys;
|
||||
};
|
||||
|
||||
/* 折叠全部 */
|
||||
const foldAll = () => {
|
||||
expandedRowKeys.value = [];
|
||||
};
|
||||
|
||||
/* 点击展开图标时触发 */
|
||||
const onExpand = (expanded: boolean, record: Category) => {
|
||||
if (expanded) {
|
||||
expandedRowKeys.value = [
|
||||
...expandedRowKeys.value,
|
||||
record.categoryId as number
|
||||
];
|
||||
} else {
|
||||
expandedRowKeys.value = expandedRowKeys.value.filter(
|
||||
(d) => d !== record.categoryId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/* 判断是否是目录 */
|
||||
const isDirectory = (d: Category) => {
|
||||
return !!d.children?.length;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Category'
|
||||
};
|
||||
</script>
|
||||
@@ -1,158 +0,0 @@
|
||||
<!-- 角色编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="460"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '修改角色' : '添加角色'"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="角色名称" name="roleName">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="请输入角色名称"
|
||||
v-model:value="form.roleName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="角色标识" name="roleCode">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="请输入角色标识"
|
||||
v-model:value="form.roleCode"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入备注"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import useFormData from '@/utils/use-form-data';
|
||||
import { addRole, updateRole } from '@/api/system/role';
|
||||
import type { Role } from '@/api/system/role/model';
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: Role | null;
|
||||
}>();
|
||||
|
||||
//
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const { form, resetFields, assignFields } = useFormData<Role>({
|
||||
roleId: undefined,
|
||||
roleName: '',
|
||||
roleCode: '',
|
||||
comments: ''
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<Record<string, Rule[]>>({
|
||||
roleName: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入角色名称',
|
||||
type: 'string',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
roleCode: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入角色标识',
|
||||
type: 'string',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateRole : addRole;
|
||||
saveOrUpdate(form)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
assignFields(props.data);
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -1,245 +0,0 @@
|
||||
<template>
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false">
|
||||
<!-- 表格 -->
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="commentId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
v-model:selection="selection"
|
||||
:scroll="{ x: 800 }"
|
||||
cache-key="proShopCommentTable"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<!-- <a-button type="primary" class="ele-btn-icon" @click="openEdit()">-->
|
||||
<!-- <template #icon>-->
|
||||
<!-- <plus-outlined />-->
|
||||
<!-- </template>-->
|
||||
<!-- <span>新建</span>-->
|
||||
<!-- </a-button>-->
|
||||
<a-button
|
||||
danger
|
||||
type="primary"
|
||||
class="ele-btn-icon"
|
||||
@click="removeBatch"
|
||||
>
|
||||
<template #icon>
|
||||
<delete-outlined />
|
||||
</template>
|
||||
<span>删除</span>
|
||||
</a-button>
|
||||
<a-radio-group :defaultValue="score" @change="handleTabs">
|
||||
<a-radio-button :value="0">全部</a-radio-button>
|
||||
<a-radio-button :value="10">好评</a-radio-button>
|
||||
<a-radio-button :value="20">中评</a-radio-button>
|
||||
<a-radio-button :value="30">差评</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入关键词"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
>
|
||||
<template #addonBefore>
|
||||
<a-select
|
||||
v-model:value="type"
|
||||
style="width: 100px; margin: -5px -12px"
|
||||
>
|
||||
<a-select-option value="goodsName">商品名称</a-select-option>
|
||||
<a-select-option value="goodsCode">商品编码</a-select-option>
|
||||
<a-select-option value="userId">用户ID</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-search>
|
||||
</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
|
||||
placement="topRight"
|
||||
title="确定要删除此评价吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
<!-- 编辑弹窗 -->
|
||||
<comment-edit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue/es';
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro/es';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { messageLoading, toDateString } from 'ele-admin-pro/es';
|
||||
import CommentEdit from './components/comment-edit.vue';
|
||||
import {
|
||||
pageComments,
|
||||
removeComment,
|
||||
removeComments
|
||||
} from '@/api/goods/comment';
|
||||
import type { Comment, CommentParam } from '@/api/goods/comment/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
key: 'index',
|
||||
width: 48,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
hideInSetting: true,
|
||||
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
|
||||
},
|
||||
{
|
||||
title: '商品信息',
|
||||
dataIndex: 'goods',
|
||||
showSorterTooltip: false
|
||||
},
|
||||
{
|
||||
title: '买家',
|
||||
dataIndex: 'user',
|
||||
showSorterTooltip: false
|
||||
},
|
||||
{
|
||||
title: '评分',
|
||||
dataIndex: 'score',
|
||||
sorter: true,
|
||||
showSorterTooltip: false
|
||||
},
|
||||
{
|
||||
title: '评价内容',
|
||||
dataIndex: 'content'
|
||||
},
|
||||
{
|
||||
title: '图片评价',
|
||||
dataIndex: 'isPicture'
|
||||
},
|
||||
{
|
||||
title: '评价时间',
|
||||
dataIndex: 'createTime',
|
||||
sorter: true,
|
||||
showSorterTooltip: false,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
align: 'center'
|
||||
}
|
||||
]);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<Comment[]>([]);
|
||||
|
||||
// 当前编辑数据
|
||||
const current = ref<Comment | null>(null);
|
||||
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
const type = ref('goodsName');
|
||||
const searchText = ref('');
|
||||
const score = ref(0);
|
||||
|
||||
const handleTabs = (e) => {
|
||||
score.value = e.target.value;
|
||||
};
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
if (type.value == 'goodsName') {
|
||||
where.goodsName = searchText.value;
|
||||
}
|
||||
if (type.value == 'goodsCode') {
|
||||
where.goodsCode = searchText.value;
|
||||
}
|
||||
if (type.value == 'userId') {
|
||||
where.userId = searchText.value;
|
||||
}
|
||||
return pageComments({ ...where, ...orders, page, limit });
|
||||
};
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: CommentParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ page: 1, where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: Comment) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: Comment) => {
|
||||
const hide = messageLoading('请求中..', 0);
|
||||
removeComment(row.commentId)
|
||||
.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 = messageLoading('请求中..', 0);
|
||||
removeComments(selection.value.map((d) => d.commentId))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopComment'
|
||||
};
|
||||
</script>
|
||||
@@ -1,433 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-form-item label="商品规格" :labelCol="labelCol" :wrapperCol="wrapperCol">
|
||||
<div v-if="true" class="form-item-help" style="line-height: 36px">
|
||||
<small>最多添加3个商品规格组,生成的SKU数量不能超出50个</small>
|
||||
</div>
|
||||
<!-- 规格组 -->
|
||||
<div
|
||||
class="spec-group"
|
||||
>
|
||||
<div class="spec-group-item clearfix">
|
||||
<a-input
|
||||
class="group-item-input"
|
||||
:readOnly="isSpecLocked"
|
||||
placeholder="请输入规格名称"
|
||||
@change="onChangeSpecGroupIpt"
|
||||
/>
|
||||
<a
|
||||
v-if="!isSpecLocked"
|
||||
class="group-item-delete"
|
||||
href="javascript:;"
|
||||
@click="handleDeleteSpecGroup(index)"
|
||||
>删除规格组</a
|
||||
>
|
||||
</div>
|
||||
<div class="spec-value clearfix">
|
||||
<div
|
||||
class="spec-value-item"
|
||||
>
|
||||
<a-input
|
||||
class="value-item-input"
|
||||
:readOnly="isSpecLocked"
|
||||
placeholder="请输入规格值"
|
||||
@change="onChangeSpecValueIpt"
|
||||
/>
|
||||
<a-icon
|
||||
v-if="!isSpecLocked"
|
||||
class="icon-close"
|
||||
theme="filled"
|
||||
type="close-circle"
|
||||
@click="handleDeleteSpecValue(index, idx)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isSpecLocked" class="spec-value-add">
|
||||
<a
|
||||
class="group-item-delete"
|
||||
href="javascript:;"
|
||||
@click="handleAddSpecValue(index)"
|
||||
>新增规格值</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 添加规格按钮 -->
|
||||
<a-button
|
||||
v-if="!isSpecLocked && multiSpecData.specList.length < 3"
|
||||
class="spec-group-add-btn"
|
||||
icon="plus"
|
||||
@click="handleAddSpecGroup"
|
||||
>添加规格组</a-button
|
||||
>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-show="multiSpecData.skuList.length"
|
||||
label="SKU列表"
|
||||
:labelCol="labelCol"
|
||||
:wrapperCol="wrapperCol"
|
||||
>
|
||||
<!-- 批量设置 -->
|
||||
<div v-if="multiSpecData.skuList.length > 1" class="sku-batch">
|
||||
<span class="title">批量设置:</span>
|
||||
<a-input-number
|
||||
placeholder="商品价格"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
/>
|
||||
<a-input-number
|
||||
placeholder="成本价格"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
<a-input-number
|
||||
placeholder="库存数量"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
<a-input-number
|
||||
placeholder="商品重量"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
<a-input
|
||||
placeholder="sku编码"
|
||||
/>
|
||||
<a-button @click="handleSkuBatch">立即设置</a-button>
|
||||
</div>
|
||||
<!-- sku列表table -->
|
||||
<a-table
|
||||
class="sku-list"
|
||||
:columns="multiSpecData.skuColumns"
|
||||
:dataSource="multiSpecData.skuList"
|
||||
:scroll="{ x: true }"
|
||||
:pagination="false"
|
||||
bordered
|
||||
>
|
||||
<!-- 预览图 -->
|
||||
<template #image="text, item">
|
||||
<SelectImage
|
||||
v-model="item.image_id"
|
||||
:defaultList="item.image_id > 0 && item.image ? [item.image] : []"
|
||||
:width="50"
|
||||
/>
|
||||
</template>
|
||||
<!-- 商品价格 -->
|
||||
<template #goods_price="text, item">
|
||||
<a-input-number
|
||||
v-model="item.goods_price"
|
||||
size="small"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
/>
|
||||
</template>
|
||||
<!-- 成本价格 -->
|
||||
<template #line_price="text, item">
|
||||
<a-input-number
|
||||
v-model="item.line_price"
|
||||
size="small"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</template>
|
||||
<!-- 库存数量 -->
|
||||
<template #stock_num="text, item">
|
||||
<a-input-number
|
||||
v-model="item.stock_num"
|
||||
size="small"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
</template>
|
||||
<!-- 商品重量 -->
|
||||
<template #goods_weight="text, item">
|
||||
<a-input-number
|
||||
v-model="item.goods_weight"
|
||||
size="small"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</template>
|
||||
<!-- sku编码 -->
|
||||
<template #goods_sku_no="text, item">
|
||||
<a-input v-model="item.goods_sku_no" size="small" />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PropTypes from 'ant-design-vue/es/_util/vue-types';
|
||||
import MultiSpecModel from '@/common/model/goods/MultiSpec';
|
||||
// import { SelectImage } from '@/components';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// SelectImage
|
||||
},
|
||||
props: {
|
||||
// 默认的规格列表
|
||||
defaultSpecList: PropTypes.array.def([]),
|
||||
// 默认的SKU列表
|
||||
defaultSkuList: PropTypes.array.def([]),
|
||||
// 商品规格是否锁定(锁定状态下不允许编辑规格)
|
||||
isSpecLocked: PropTypes.bool.def(false)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 标签布局属性
|
||||
labelCol: { span: 3 },
|
||||
// 输入框布局属性
|
||||
wrapperCol: { span: 21 },
|
||||
// 商品多规格模型
|
||||
MultiSpecModel: new MultiSpecModel(),
|
||||
// MultiSpecModel: Object,
|
||||
multiSpecData: {
|
||||
// 规格列表
|
||||
specList: [],
|
||||
// SKU列表
|
||||
skuList: []
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
defaultSpecList(val) {
|
||||
if (val.length && this.MultiSpecModel.isEmpty()) {
|
||||
this.getData();
|
||||
}
|
||||
}
|
||||
},
|
||||
// 初始化数据
|
||||
created() {
|
||||
// 获取规格及SKU信息
|
||||
this.getData();
|
||||
},
|
||||
methods: {
|
||||
// 获取规格及SKU信息(展示)
|
||||
getData() {
|
||||
const { defaultSpecList, defaultSkuList } = this;
|
||||
this.multiSpecData = this.MultiSpecModel.getData(
|
||||
defaultSpecList,
|
||||
defaultSkuList
|
||||
);
|
||||
},
|
||||
|
||||
// 获取规格及SKU信息(表单提交)
|
||||
getFromSpecData() {
|
||||
return this.MultiSpecModel.getFromSpecData();
|
||||
},
|
||||
|
||||
// 添加规格组
|
||||
handleAddSpecGroup() {
|
||||
if (this.checkSkuMaxNum()) {
|
||||
this.MultiSpecModel.handleAddSpecGroup();
|
||||
}
|
||||
},
|
||||
|
||||
// 删除规格组
|
||||
handleDeleteSpecGroup(groupIndex) {
|
||||
const app = this;
|
||||
const modal = this.$confirm({
|
||||
title: '您确定要删除该规格组吗?',
|
||||
content: '删除后不可恢复',
|
||||
onOk() {
|
||||
// 删除元素
|
||||
app.MultiSpecModel.handleDeleteSpecGroup(groupIndex);
|
||||
// 关闭对话框
|
||||
modal.destroy();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 新增规格值
|
||||
handleAddSpecValue(groupIndex) {
|
||||
if (this.checkSkuMaxNum()) {
|
||||
this.MultiSpecModel.handleAddSpecValue(groupIndex);
|
||||
}
|
||||
},
|
||||
|
||||
// 删除规格值
|
||||
handleDeleteSpecValue(groupIndex, valueIndex) {
|
||||
const app = this;
|
||||
const modal = this.$confirm({
|
||||
title: '您确定要删除该规格值吗?',
|
||||
content: '删除后不可恢复',
|
||||
onOk() {
|
||||
// 删除元素
|
||||
app.MultiSpecModel.handleDeleteSpecValue(groupIndex, valueIndex);
|
||||
// 关闭对话框
|
||||
modal.destroy();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 规格组输入框change事件
|
||||
onChangeSpecGroupIpt() {
|
||||
// 更新skuList
|
||||
this.MultiSpecModel.onUpdate(true);
|
||||
},
|
||||
|
||||
// 规格值输入框change事件
|
||||
onChangeSpecValueIpt(event, itm) {
|
||||
// 更新skuList
|
||||
this.MultiSpecModel.onUpdate(true);
|
||||
},
|
||||
|
||||
// 验证最大sku数量
|
||||
checkSkuMaxNum() {
|
||||
const skuList = this.multiSpecData.skuList;
|
||||
if (skuList.length >= 50) {
|
||||
this.$message.error(
|
||||
`生成的sku列表数量不能大于50个,当前数量:${skuList.length}个`,
|
||||
2.5
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// 批量设置sku事件
|
||||
handleSkuBatch() {
|
||||
this.MultiSpecModel.handleSkuBatch();
|
||||
},
|
||||
|
||||
// 验证多规格表单
|
||||
verifyForm() {
|
||||
if (!this.MultiSpecModel.verifyForm()) {
|
||||
this.$message.error(this.MultiSpecModel.getError(), 2);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// 商品多规格
|
||||
.spec-group {
|
||||
width: 895px;
|
||||
margin-bottom: 15px;
|
||||
line-height: normal;
|
||||
|
||||
.ant-input {
|
||||
height: 28px;
|
||||
padding: 4px 11px;
|
||||
font-size: @font-size-base;
|
||||
line-height: 1.5;
|
||||
|
||||
&::placeholder {
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-help {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.spec-group-item {
|
||||
background: #f4f5f9;
|
||||
padding: 7px 12px;
|
||||
line-height: 28px;
|
||||
|
||||
.group-item-input {
|
||||
float: left;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.group-item-delete {
|
||||
font-size: 12px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.spec-value {
|
||||
background: #fbfbfb;
|
||||
padding: 8px 35px;
|
||||
|
||||
.spec-value-add,
|
||||
.spec-value-item {
|
||||
float: left;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.spec-value-add {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
|
||||
a {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.spec-value-item {
|
||||
position: relative;
|
||||
margin-right: 20px;
|
||||
|
||||
.value-item-input {
|
||||
width: 186px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.icon-close {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-close {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #7d7d7d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加规格按钮
|
||||
.spec-group-add-btn {
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
// 批量设置sku
|
||||
.sku-batch {
|
||||
line-height: 40px;
|
||||
margin-bottom: 12px;
|
||||
.title {
|
||||
line-height: 28px;
|
||||
margin-right: 15px;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
/deep/.ant-input,
|
||||
/deep/.ant-input-number {
|
||||
width: 120px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
/deep/.ant-input {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
// sku列表
|
||||
.sku-list {
|
||||
width: 895px;
|
||||
/deep/.ant-table-thead > tr > th,
|
||||
/deep/.ant-table-tbody > tr > td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
/deep/.ant-table-tbody > tr > td {
|
||||
padding: 12px 18px;
|
||||
}
|
||||
|
||||
/deep/.ant-input-sm,
|
||||
/deep/.ant-input-number-sm {
|
||||
height: 28px;
|
||||
}
|
||||
/deep/.ant-input-number-sm input {
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,39 +0,0 @@
|
||||
<!-- 目录选择下拉框 -->
|
||||
<template>
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
tree-default-expand-all
|
||||
:placeholder="placeholder"
|
||||
:value="value || undefined"
|
||||
:tree-data="data"
|
||||
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
|
||||
@update:value="updateValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Goods } from '@/api/goods/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value?: number): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
// 选中的数据(v-modal)
|
||||
value?: number;
|
||||
// 提示信息
|
||||
placeholder?: string;
|
||||
// 文章分类
|
||||
data: Goods[];
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择上级分类'
|
||||
}
|
||||
);
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value?: number) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
</script>
|
||||
@@ -1,45 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择服务器配置'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('serverConfiguration');
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
</script>
|
||||
@@ -1,51 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
@change="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
(e: 'change'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择客户跟进状态'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('customerFollowStatus');
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
|
||||
/* 选择事件 */
|
||||
const onChange = (e) => {
|
||||
emit('change', e);
|
||||
};
|
||||
</script>
|
||||
@@ -1,70 +0,0 @@
|
||||
<!-- 角色选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
@change="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { listDictionaryData } from '@/api/system/dictionary-data';
|
||||
import type { SelectProps } from 'ant-design-vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
(e: 'change'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择客户来源'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = ref<SelectProps['options']>([]);
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 获取字典数据 */
|
||||
listDictionaryData({
|
||||
dictCode: 'customerSource'
|
||||
})
|
||||
.then((list) => {
|
||||
data.value = list.map((d) => {
|
||||
return {
|
||||
value: d.dictDataCode,
|
||||
label: d.dictDataName
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
|
||||
/* 选择事件 */
|
||||
const onChange = (e) => {
|
||||
emit('change', e);
|
||||
};
|
||||
</script>
|
||||
@@ -1,45 +0,0 @@
|
||||
<!-- 角色选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择状态'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('status');
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
</script>
|
||||
@@ -1,63 +0,0 @@
|
||||
<!-- 角色选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { listDictionaryData } from '@/api/system/dictionary-data';
|
||||
import type { SelectProps } from 'ant-design-vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择客户类型'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = ref<SelectProps['options']>([]);
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 获取字典数据 */
|
||||
listDictionaryData({
|
||||
dictCode: 'customerType'
|
||||
})
|
||||
.then((list) => {
|
||||
data.value = list.map((d) => {
|
||||
return {
|
||||
value: d.dictDataCode,
|
||||
label: d.dictDataName
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
</script>
|
||||
@@ -1,76 +0,0 @@
|
||||
<!-- 角色选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@search="onSearch"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { listUsers } from '@/api/system/user';
|
||||
import type { SelectProps } from 'ant-design-vue';
|
||||
import { UserParam } from '@/api/system/user/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择订单类型'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = ref<SelectProps['options']>([]);
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
// 默认搜索条件
|
||||
const where = ref<UserParam>({});
|
||||
|
||||
const search = () => {
|
||||
/* 获取用户列 */
|
||||
listUsers({ ...where?.value })
|
||||
.then((result) => {
|
||||
data.value = result?.map((d) => {
|
||||
return {
|
||||
value: d.userId,
|
||||
label: d.nickname
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const onSearch = (e) => {
|
||||
where.value.nickname = e;
|
||||
search();
|
||||
};
|
||||
|
||||
search();
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
</script>
|
||||
@@ -1,70 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
@search="onSearch"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { pageCustomer } from '@/api/oa/customer';
|
||||
import type { SelectProps } from 'ant-design-vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: number;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择所属客户'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = ref<SelectProps['options']>([]);
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 获取客户列表数据 */
|
||||
pageCustomer({ progress: '4' })
|
||||
.then((result) => {
|
||||
data.value = result?.list.map((d) => {
|
||||
return {
|
||||
value: d.customerId,
|
||||
label: d.customerName
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
|
||||
// 联动搜索
|
||||
const onSearch = (customerName) => {
|
||||
pageCustomer({ customerName }).then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -1,53 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
import { ref } from "vue";
|
||||
import { SelectProps } from "ant-design-vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择运费模板'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = ref<SelectProps['options']>([
|
||||
{
|
||||
label: '全国包邮(除偏远地区)',
|
||||
text: '全国包邮(除偏远地区)',
|
||||
value: 0
|
||||
}
|
||||
]);
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
</script>
|
||||
@@ -1,629 +0,0 @@
|
||||
<!-- 用户编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="880"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:maxable="maxAble"
|
||||
:title="isUpdate ? '编辑商品' : '添加商品'"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
@update:visible="updateVisible"
|
||||
:maskClosable="false"
|
||||
@ok="save"
|
||||
>
|
||||
<a-tabs v-model:activeKey="activeKey" type="card">
|
||||
<a-tab-pane key="1" tab="基本信息">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ md: { span: 8 }, sm: { span: 24 } }"
|
||||
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col
|
||||
v-bind="
|
||||
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }
|
||||
"
|
||||
>
|
||||
<a-form-item label="商品名称" name="goodsName">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入商品名称"
|
||||
v-model:value="form.goodsName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="参与机型">
|
||||
<a-select
|
||||
mode="multiple"
|
||||
v-model:value="goodsPhoneModelList"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option :value="item.id" v-for="(item, index) in phoneModelList" :key="index">
|
||||
{{ item.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col
|
||||
v-bind="
|
||||
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }
|
||||
"
|
||||
>
|
||||
<a-form-item label="商品编码" name="goodsCode">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入商品编码"
|
||||
:disabled="isUpdate"
|
||||
v-model:value="form.goodsCode"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品状态" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="0">上架</a-radio>
|
||||
<a-radio :value="1">下架</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<!-- <a-form-item-->
|
||||
<!-- label="运费模板"-->
|
||||
<!-- name="deliveryId"-->
|
||||
<!-- v-if="form.goodsType === 1"-->
|
||||
<!-- >-->
|
||||
<!-- <deliver-select v-model:value="form.deliveryId" />-->
|
||||
<!-- </a-form-item>-->
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- <a-form-item label="商品图片" name="images">-->
|
||||
<!-- <ele-image-upload-->
|
||||
<!-- v-model:value="images"-->
|
||||
<!-- :limit="14"-->
|
||||
<!-- :drag="true"-->
|
||||
<!-- :multiple="true"-->
|
||||
<!-- :upload-handler="uploadHandler"-->
|
||||
<!-- @upload="onUpload"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="价格">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:label-col="{ md: { span: 8 }, sm: { span: 24 } }"
|
||||
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
|
||||
layout="vertical"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
>
|
||||
<!-- <a-form-item label="规格类型" name="specType">-->
|
||||
<!-- <a-radio-group v-model:value="form.specType">-->
|
||||
<!-- <a-radio :value="0">单规格</a-radio>-->
|
||||
<!-- <!– <a-radio :value="1">多规格</a-radio>–>-->
|
||||
<!-- </a-radio-group>-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- <div style="margin-bottom: 22px">-->
|
||||
<!-- <a-divider />-->
|
||||
<!-- </div>-->
|
||||
<a-row :gutter="16">
|
||||
<a-col
|
||||
v-bind="
|
||||
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }
|
||||
"
|
||||
>
|
||||
<a-form-item
|
||||
label="商品默认价格"
|
||||
name="goodsPriceMin"
|
||||
>
|
||||
<a-input-number
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
style="width: 200px"
|
||||
v-model:value="form.goodsPriceMin"
|
||||
/>
|
||||
<span class="ml-10">元</span>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="成本价"
|
||||
name="cost"
|
||||
>
|
||||
<a-input-number
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
style="width: 200px"
|
||||
v-model:value="form.cost"
|
||||
/>
|
||||
<span class="ml-10">元</span>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="划线价(原价)"
|
||||
name="linePrice"
|
||||
>
|
||||
<a-input-number
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
style="width: 200px"
|
||||
@change="calCost"
|
||||
v-model:value="form.linePriceMin"
|
||||
/>
|
||||
<span class="ml-10">元</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col-->
|
||||
<!-- v-bind="-->
|
||||
<!-- styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }-->
|
||||
<!-- "-->
|
||||
<!-- >-->
|
||||
<!-- <a-form-item-->
|
||||
<!-- label="当前库存数量"-->
|
||||
<!-- name="stockTotal"-->
|
||||
<!-- v-if="form.specType === 0"-->
|
||||
<!-- >-->
|
||||
<!-- <a-input-number-->
|
||||
<!-- :min="0"-->
|
||||
<!-- :precision="0"-->
|
||||
<!-- style="width: 200px"-->
|
||||
<!-- v-model:value="form.stockTotal"-->
|
||||
<!-- />-->
|
||||
<!-- <span class="ml-10">件</span>-->
|
||||
<!-- </a-form-item>-->
|
||||
<!--<!– <a-form-item–>-->
|
||||
<!--<!– label="商品重量"–>-->
|
||||
<!--<!– name="goodsWeight"–>-->
|
||||
<!--<!– v-if="form.specType === 0"–>-->
|
||||
<!--<!– >–>-->
|
||||
<!--<!– <a-input-number–>-->
|
||||
<!--<!– :min="0"–>-->
|
||||
<!--<!– :precision="0"–>-->
|
||||
<!--<!– style="width: 200px"–>-->
|
||||
<!--<!– v-model:value="form.goodsWeight"–>-->
|
||||
<!--<!– />–>-->
|
||||
<!--<!– <span class="ml-10">千克 (Kg)</span>–>-->
|
||||
<!--<!– </a-form-item>–>-->
|
||||
<!-- </a-col>-->
|
||||
</a-row>
|
||||
<a-form-item
|
||||
label="商品规格"
|
||||
name="deductStockType"
|
||||
v-if="form.specType === 1"
|
||||
>
|
||||
<!-- <MultiSpec ref="MultiSpec" />-->
|
||||
</a-form-item>
|
||||
<div style="margin-bottom: 22px">
|
||||
<a-divider />
|
||||
</div>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane key="3" tab="商品详情">-->
|
||||
<!-- <a-form-->
|
||||
<!-- ref="formRef"-->
|
||||
<!-- :label-col="{ md: { span: 8 }, sm: { span: 24 } }"-->
|
||||
<!-- :wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"-->
|
||||
<!-- layout="vertical"-->
|
||||
<!-- :model="form"-->
|
||||
<!-- :rules="rules"-->
|
||||
<!-- >-->
|
||||
<!-- <a-form-item label="" name="content">-->
|
||||
<!-- <!– 编辑器 –>-->
|
||||
<!-- <tinymce-editor-->
|
||||
<!-- v-model:value="content"-->
|
||||
<!-- :disabled="disabled"-->
|
||||
<!-- :init="config"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-form>-->
|
||||
<!-- </a-tab-pane>-->
|
||||
<a-tab-pane key="4" tab="更多设置">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:label-col="{ md: { span: 8 }, sm: { span: 24 } }"
|
||||
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
|
||||
layout="vertical"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
>
|
||||
<!-- <a-form-item label="商品卖点" name="sellingPoint">-->
|
||||
<!-- <a-input-->
|
||||
<!-- allow-clear-->
|
||||
<!-- placeholder="【入手皆因热爱】限时限量领券至高立减400元,限时优惠,限量抢购!!赶快下单吧!"-->
|
||||
<!-- v-model:value="form.sellingPoint"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<a-form-item label="排序号" name="sortNumber">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:max="99999"
|
||||
class="ele-fluid"
|
||||
placeholder="请输入排序号(数字越小越靠前)"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import type { Goods } from "@/api/goods/model";
|
||||
import { addGoods, updateGoods } from "@/api/goods";
|
||||
import { FormInstance, Rule, RuleObject } from "ant-design-vue/es/form";
|
||||
import { ItemType } from "ele-admin-pro/es/ele-image-upload/types";
|
||||
import { uploadFile } from "@/api/system/file";
|
||||
import { FILE_THUMBNAIL } from "@/config/setting";
|
||||
import useFormData from "@/utils/use-form-data";
|
||||
import CategorySelect from "./category-select.vue";
|
||||
import TinymceEditor from "@/components/TinymceEditor/index.vue";
|
||||
import { Category } from "@/api/goods/category/model";
|
||||
import { useThemeStore } from "@/store/modules/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
import DeliverSelect from "./deliver-select.vue";
|
||||
import { createCode } from "@/utils/common";
|
||||
import { GoodsPhoneModel } from "@/api/goods/goodsPhoneModel";
|
||||
import { listPhoneModel } from "@/api/vivo/phoneModel";
|
||||
import { PhoneModel } from "@/api/vivo/phoneModel/model";
|
||||
// import MultiSpec from './MultiSpec.vue';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxAble = ref(true);
|
||||
const disabled = ref(false);
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: Goods | null;
|
||||
categoryList?: Category[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "done"): void;
|
||||
(e: "update:visible", visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 已上传数据
|
||||
const images = ref<ItemType[]>([]);
|
||||
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
// 选项卡位置
|
||||
const activeKey = ref("1");
|
||||
// 编辑器内容,双向绑定
|
||||
const content = ref<any>("");
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit("update:visible", value);
|
||||
};
|
||||
|
||||
// 表单数据
|
||||
const { form, resetFields, assignFields } = useFormData<Goods>({
|
||||
goodsType: 0,
|
||||
cost: 0,
|
||||
goodsName: "",
|
||||
goodsCode: `G${createCode()}`,
|
||||
categoryId: 0,
|
||||
image: "",
|
||||
files: "",
|
||||
goodsPriceMin: 0.01,
|
||||
linePriceMin: 0.01,
|
||||
createTime: "",
|
||||
comments: "",
|
||||
sortNumber: 100,
|
||||
status: 0,
|
||||
goodsId: 0,
|
||||
specType: 0,
|
||||
deliveryType: 0,
|
||||
content: "",
|
||||
deliveryId: 0,
|
||||
sellingPoint: "",
|
||||
purchaseLimit: 0,
|
||||
deductStockType: 10,
|
||||
salesActual: 180,
|
||||
goodsWeight: 0,
|
||||
stockTotal: 2000,
|
||||
goodsPhoneModelList: []
|
||||
});
|
||||
|
||||
/* 上传事件 */
|
||||
const uploadHandler = (file: File) => {
|
||||
const item: ItemType = {
|
||||
file,
|
||||
uid: (file as any).uid,
|
||||
name: file.name
|
||||
};
|
||||
if (!file.type.startsWith("image")) {
|
||||
message.error("只能选择图片");
|
||||
return;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 2) {
|
||||
message.error("大小不能超过 2MB");
|
||||
return;
|
||||
}
|
||||
onUpload(item);
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
const onUpload = (item) => {
|
||||
const { file } = item;
|
||||
uploadFile(file)
|
||||
.then((data) => {
|
||||
images.value.push({
|
||||
uid: data.id,
|
||||
url: FILE_THUMBNAIL + data.path,
|
||||
status: "done"
|
||||
});
|
||||
message.success("上传成功");
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const calCost = (value: number) => {
|
||||
form.cost = value * 0.58
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<Record<string, Rule[]>>({
|
||||
goodsName: [
|
||||
{
|
||||
required: true,
|
||||
message: "请输入商品名称",
|
||||
type: "string",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
goodsCode: [
|
||||
{
|
||||
required: true,
|
||||
type: "string",
|
||||
message: "请填写商品编号",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
goodsPriceMin: [
|
||||
{
|
||||
required: true,
|
||||
type: "number",
|
||||
message: "请输入商品价格",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
// stockTotal: [
|
||||
// {
|
||||
// required: true,
|
||||
// type: "number",
|
||||
// message: "请输入商品库存",
|
||||
// trigger: "blur"
|
||||
// }
|
||||
// ],
|
||||
// specType: [
|
||||
// {
|
||||
// required: true,
|
||||
// type: "number",
|
||||
// message: "请选择规格类型",
|
||||
// trigger: "blur"
|
||||
// }
|
||||
// ],
|
||||
status: [
|
||||
{
|
||||
required: true,
|
||||
type: "number",
|
||||
message: "请选择商品状态",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
sortNumber: [
|
||||
{
|
||||
required: true,
|
||||
type: "number",
|
||||
message: "请输入排序号",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
// images: [
|
||||
// {
|
||||
// required: true,
|
||||
// type: "string",
|
||||
// message: "请选择商品图片",
|
||||
// trigger: "blur",
|
||||
// validator: async (_rule: RuleObject, value: string) => {
|
||||
// if (images.value.length == 0) {
|
||||
// return Promise.reject("请上传商品图片");
|
||||
// }
|
||||
// return Promise.resolve();
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
// customerId: [
|
||||
// {
|
||||
// required: true,
|
||||
// type: "number",
|
||||
// message: "请选择客户"
|
||||
// }
|
||||
// ],
|
||||
linePriceMin: [
|
||||
{
|
||||
required: true,
|
||||
type: "number",
|
||||
message: "请输入商品划线价",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
cost: [
|
||||
{
|
||||
required: true,
|
||||
type: "number",
|
||||
message: "请输入成本价",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
|
||||
// content: [
|
||||
// {
|
||||
// required: true,
|
||||
// type: "string",
|
||||
// message: "请输入文章内容",
|
||||
// trigger: "blur",
|
||||
// validator: async (_rule: RuleObject, value: string) => {
|
||||
// if (content.value == "") {
|
||||
// return Promise.reject("请输入文字内容");
|
||||
// }
|
||||
// return Promise.resolve();
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
// brand: [
|
||||
// {
|
||||
// required: true,
|
||||
// type: "string",
|
||||
// message: "请选项商品厂商",
|
||||
// trigger: "blur"
|
||||
// }
|
||||
// ]
|
||||
});
|
||||
|
||||
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
|
||||
const config = ref({
|
||||
height: 500,
|
||||
// 自定义文件上传(这里使用把选择的文件转成 blob 演示)
|
||||
file_picker_callback: (callback: any, _value: any, meta: any) => {
|
||||
const input = document.createElement("input");
|
||||
input.setAttribute("type", "file");
|
||||
// 设定文件可选类型
|
||||
if (meta.filetype === "image") {
|
||||
input.setAttribute("accept", "image/*");
|
||||
} else if (meta.filetype === "media") {
|
||||
input.setAttribute("accept", "video/*");
|
||||
}
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (meta.filetype === "media") {
|
||||
if (!file.type.startsWith("video/")) {
|
||||
editorRef.value?.alert({ content: "只能选择视频文件" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 20) {
|
||||
editorRef.value?.alert({ content: "大小不能超过 20MB" });
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (e.target?.result != null) {
|
||||
const blob = new Blob([e.target.result], { type: file.type });
|
||||
callback(URL.createObjectURL(blob));
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
});
|
||||
|
||||
// const onSelect = (item) => {
|
||||
// form.goodsId = item.goodsId;
|
||||
// };
|
||||
|
||||
const goodsPhoneModelList = ref<number[]>([])
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
// 获取第一张图片作为封面图
|
||||
// images.value.map((d, i) => {
|
||||
// if (i === 0) {
|
||||
// form.image = d.url;
|
||||
// }
|
||||
// });
|
||||
console.log(form)
|
||||
const goodsForm = {
|
||||
...form,
|
||||
goodsPhoneModelList: goodsPhoneModelList.value.length > 0 ? goodsPhoneModelList.value.map(item => {
|
||||
return {
|
||||
modelId: item
|
||||
}
|
||||
}) : [],
|
||||
files: '[]'
|
||||
// files: JSON.stringify(images.value),
|
||||
// content: content.value
|
||||
};
|
||||
console.log(isUpdate.value)
|
||||
const saveOrUpdate = isUpdate.value ? updateGoods : addGoods;
|
||||
saveOrUpdate(goodsForm)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit("done");
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const phoneModelList = ref< PhoneModel[]>([]);
|
||||
const getPhoneModelList = async () => {
|
||||
phoneModelList.value = await listPhoneModel();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
getPhoneModelList()
|
||||
if (props.data) {
|
||||
loading.value = false;
|
||||
assignFields({
|
||||
...props.data,
|
||||
files: JSON.parse(String(props.data.files))
|
||||
});
|
||||
images.value = JSON.parse(String(props.data.files));
|
||||
content.value = props.data.content;
|
||||
isUpdate.value = true;
|
||||
if (props.data.goodsPhoneModelList && props.data.goodsPhoneModelList.length > 0) {
|
||||
goodsPhoneModelList.value = props.data.goodsPhoneModelList.map(item => {
|
||||
return item.modelId
|
||||
})
|
||||
}
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
content.value = "";
|
||||
}
|
||||
} else {
|
||||
goodsPhoneModelList.value = []
|
||||
resetFields();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="less">
|
||||
.tab-pane {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.ml-10 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,243 +0,0 @@
|
||||
<!-- 用户编辑弹窗 -->
|
||||
<template>
|
||||
<a-drawer
|
||||
width="75%"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:maxable="maxAble"
|
||||
:title="'商品详情'"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
@update:visible="updateVisible"
|
||||
:footer="null"
|
||||
>
|
||||
<a-form
|
||||
:label-col="{ md: { span: 4 }, sm: { span: 24 } }"
|
||||
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
|
||||
>
|
||||
<div class="base-form" style="margin-bottom: 20px">
|
||||
<a-descriptions :title="assets.name" bordered>
|
||||
<a-descriptions-item label="商品名称">
|
||||
{{ assets.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="商品IP">
|
||||
{{ assets.code }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag v-if="assets.status === '0'" color="green">正常</a-tag>
|
||||
<a-tag v-if="assets.status === '1'" color="red">过期</a-tag>
|
||||
<a-tag v-if="assets.status === '2'" color="purple">不再续</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="宝塔面板">
|
||||
<a href="#" @click="openUrl(assets)">{{ assets.panel }}</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="宝塔账号">
|
||||
{{ assets.panelAccount }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="宝塔密码">
|
||||
<span v-role="['admin']">{{ assets.panelPassword }}</span>
|
||||
<span v-any-role="['user', 'guest', 'superAdmin', 'developer']"
|
||||
>****</span
|
||||
>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="品牌厂商">
|
||||
{{ assets.brand }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="商品配置">
|
||||
{{ assets.configuration }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开发端口">
|
||||
{{ assets.openPort }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="有效期">
|
||||
<div v-if="assets.startTime && assets.endTime">
|
||||
<div class="ele-text-info">{{ assets.startTime }}</div>
|
||||
<div v-html="getEndTime(assets.endTime)"></div>
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="商品账号">
|
||||
<span class="ele-text-danger">root</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="商品密码">
|
||||
<span class="ele-text-danger" v-role="['admin']">{{
|
||||
assets.password
|
||||
}}</span>
|
||||
<span v-any-role="['user', 'guest', 'superAdmin', 'developer']"
|
||||
>****</span
|
||||
>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ timeAgo(assets.createTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="排序">
|
||||
{{ assets.sortNumber }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布人">
|
||||
{{ assets.userId }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item span="1" label="备注">
|
||||
{{ assets.comments }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<a-space style="display: none">
|
||||
<a-tabs v-model:activeKey="activeKey" v-role="'admin'">
|
||||
<a-tab-pane key="1" tab="宝塔面板" force-render>
|
||||
<a-descriptions title="宝塔面板" class="tab-pane">
|
||||
<a-descriptions-item label="宝塔面板">
|
||||
<a href="#" @click="openUrl(assets)">{{ assets.panel }}</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="宝塔面板账号">
|
||||
{{ assets.panelAccount }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="宝塔面板密码">
|
||||
{{ assets.panelPassword }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开发端口">
|
||||
{{ assets.openPort }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="账号密码" force-render>
|
||||
<a-descriptions title="商品账号密码" class="tab-pane">
|
||||
<a-descriptions-item label="商品账号">
|
||||
root
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="商品密码">
|
||||
{{ assets.password }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-descriptions title="账号密码" class="tab-pane">
|
||||
<a-descriptions-item label="登录账号">
|
||||
{{ assets.brandAccount }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="登录密码">
|
||||
{{ assets.brandPassword }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="财务信息">
|
||||
<a-descriptions title="财务信息" class="tab-pane">
|
||||
<a-descriptions-item label="合同金额">
|
||||
<span class="ele-text-warning">
|
||||
¥{{ assets.financeAmount }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="购买年限">
|
||||
{{ assets.financeYears }}年
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="每年续费金额">
|
||||
<span class="ele-text-warning">
|
||||
¥{{ assets.financeRenew }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="客户名称">
|
||||
{{ assets.financeCustomerName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="客户联系人">
|
||||
{{ assets.financeCustomerContact }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系方式">
|
||||
{{ assets.financeCustomerPhone }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getEndTime } from '@/utils/common';
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { assignObject, timeAgo } from 'ele-admin-pro';
|
||||
import type { Assets } from '@/api/oa/assets/model';
|
||||
|
||||
const useForm = Form.useForm;
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: Assets | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 用户信息
|
||||
const assets = reactive<Assets>({
|
||||
code: '',
|
||||
createTime: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
name: '',
|
||||
brand: undefined,
|
||||
account: '',
|
||||
password: '',
|
||||
configuration: '',
|
||||
panelAccount: '',
|
||||
panelPassword: '',
|
||||
panel: '',
|
||||
financeAmount: 0,
|
||||
financeYears: 0,
|
||||
financeRenew: 0,
|
||||
financeCustomerName: '',
|
||||
financeCustomerContact: '',
|
||||
financeCustomerPhone: '',
|
||||
brandAccount: '',
|
||||
brandPassword: '',
|
||||
openPort: '',
|
||||
comments: '',
|
||||
root: 'root',
|
||||
userId: 0,
|
||||
sortNumber: undefined,
|
||||
status: undefined,
|
||||
type: '',
|
||||
assetsId: 0
|
||||
});
|
||||
|
||||
// 请求状态
|
||||
const loading = ref(true);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxAble = ref(true);
|
||||
// 选项卡位置
|
||||
const activeKey = ref('1');
|
||||
const { resetFields } = useForm(assets);
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
/* 打开外部链接 */
|
||||
const openUrl = (record) => {
|
||||
window.open(record.panel);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
loading.value = false;
|
||||
assignObject(assets, props.data);
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="less">
|
||||
.tab-pane {
|
||||
min-height: 100px;
|
||||
}
|
||||
.important {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,311 +0,0 @@
|
||||
<!-- 用户编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="880"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:maxable="maxAble"
|
||||
:title="'设置商品区域价格'"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
@update:visible="updateVisible"
|
||||
:maskClosable="false"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ md: { span: 8 }, sm: { span: 24 } }"
|
||||
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row style="margin-bottom: 1rem">
|
||||
<a-col :span="12">
|
||||
<span v-if="props.data" class="ml-10">成本价: {{ props.data.cost }}元</span>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<span v-if="props.data && form.amount"
|
||||
class="ml-10">利润:{{ (form.amount - props.data.cost).toFixed(2)
|
||||
}}元</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="价格区域">
|
||||
<a-cascader v-model:value="selectedArea" :options="areaList" placeholder="请选择区域"
|
||||
:disabled="loginUser.cityId"
|
||||
@change="selectArea" :fieldNames="{label: 'name', value: 'id'}" />
|
||||
</a-form-item>
|
||||
<template v-if="form.city">
|
||||
<a-form-item label="商品价格" name="amount">
|
||||
<a-input-number :min="0.01" :precision="2" style="width: 200px" v-model:value="form.amount">
|
||||
<template #addonAfter>
|
||||
<span>元</span>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<!-- <a-form-item :label="form.level3commissionMode === 1 ? '零售店长返利比例' : '零售店长返利金额'" name="level3commissionRate">-->
|
||||
<!-- <a-input-number :min="0.01" :max="100" :precision="2" style="width: 200px"-->
|
||||
<!-- v-model:value="form.level3commissionRate" >-->
|
||||
<!-- <template #addonAfter>-->
|
||||
<!-- <a-select v-model:value="form.level3commissionMode" style="width: 60px;">-->
|
||||
<!-- <a-select-option :value="0">元</a-select-option>-->
|
||||
<!-- <a-select-option :value="1">%</a-select-option>-->
|
||||
<!-- </a-select>-->
|
||||
<!-- </template>-->
|
||||
<!-- </a-input-number>-->
|
||||
<!-- </a-form-item>-->
|
||||
<a-form-item :label="form.level4commissionMode === 1 ? '导购返利比例' : '导购返利金额'"
|
||||
name="level4commissionRate">
|
||||
<a-input-number :min="0.01" :max="100" :precision="2" style="width: 200px"
|
||||
v-model:value="form.level4commissionRate">
|
||||
<template #addonAfter>
|
||||
<a-select v-model:value="form.level4commissionMode" style="width: 60px;">
|
||||
<a-select-option :value="0">元</a-select-option>
|
||||
<a-select-option :value="1">%</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item :label="form.level6commissionMode === 1 ? '散户返利比例' : '散户返利金额'"
|
||||
name="level6commissionRate">
|
||||
<a-input-number :min="0.01" :max="100" :precision="2" style="width: 200px"
|
||||
v-model:value="form.level6commissionRate">
|
||||
<template #addonAfter>
|
||||
<a-select v-model:value="form.level6commissionMode" style="width: 60px;">
|
||||
<a-select-option :value="0">元</a-select-option>
|
||||
<a-select-option :value="1">%</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item :label="form.level7commissionMode === 1 ? '编外合伙人返利比例' : '编外合伙人返利金额'"
|
||||
name="level7commissionRate">
|
||||
<a-input-number :min="0.01" :max="100" :precision="2" style="width: 200px"
|
||||
v-model:value="form.level7commissionRate">
|
||||
<template #addonAfter>
|
||||
<a-select v-model:value="form.level7commissionMode" style="width: 60px;">
|
||||
<a-select-option :value="0">元</a-select-option>
|
||||
<a-select-option :value="1">%</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="售后返利比例" name="level5commissionRate">
|
||||
<a-input-number :min="0.01" :max="100" :precision="2" style="width: 200px"
|
||||
v-model:value="form.level5commissionRate">
|
||||
<template #addonAfter>
|
||||
<span>%</span>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<!-- <a-form-item label="售后店长返利比例" name="level8commissionRate">-->
|
||||
<!-- <a-input-number :min="0.01" :max="100" :precision="2" style="width: 200px"-->
|
||||
<!-- v-model:value="form.level8commissionRate" >-->
|
||||
<!-- <template #addonAfter>-->
|
||||
<!-- <span>%</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- </a-input-number>-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- <a-form-item label="售后店长门店返利比例(基于门店售后返利比例)" name="fixStoreLeaderCommissionRate">-->
|
||||
<!-- <a-input-number :min="0.01" :max="100" :precision="2" style="width: 200px"-->
|
||||
<!-- v-model:value="form.fixStoreLeaderCommissionRate" >-->
|
||||
<!-- <template #addonAfter>-->
|
||||
<!-- <span>%</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- </a-input-number>-->
|
||||
<!-- </a-form-item>-->
|
||||
|
||||
</template>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch, computed } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { FormInstance, Rule, RuleObject } from "ant-design-vue/es/form";
|
||||
import { ItemType } from "ele-admin-pro/es/ele-image-upload/types";
|
||||
import useFormData from "@/utils/use-form-data";
|
||||
import { Category } from "@/api/goods/category/model";
|
||||
import { useThemeStore } from "@/store/modules/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { GoodsPrice } from "@/api/order/goodsPrice/model";
|
||||
import { addGoodsPrice, goodsPriceByGoodsNCity, updateGoodsPrice } from "@/api/order/goodsPrice";
|
||||
import { Goods } from "@/api/goods/model";
|
||||
import { Area } from "@/api/system/area/model";
|
||||
import { listArea, listAreaByParent } from "@/api/system/area";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { User } from "@/api/system/user/model";
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxAble = ref(true);
|
||||
const disabled = ref(false);
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: Goods | null;
|
||||
categoryList?: Category[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "done"): void;
|
||||
(e: "update:visible", visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 已上传数据
|
||||
const images = ref<ItemType[]>([]);
|
||||
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
// 选项卡位置
|
||||
const activeKey = ref("1");
|
||||
// 编辑器内容,双向绑定
|
||||
const content = ref<any>("");
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit("update:visible", value);
|
||||
};
|
||||
|
||||
const userStore = useUserStore();
|
||||
const loginUser = computed(() => userStore.info ?? {});
|
||||
|
||||
// 表单数据
|
||||
const { form, resetFields, assignFields } = useFormData<GoodsPrice>({
|
||||
id: 0,
|
||||
goodsId: 0,
|
||||
amount: 0,
|
||||
city: 0,
|
||||
level3commissionRate: 0,
|
||||
level4commissionRate: 0,
|
||||
level5commissionRate: 0,
|
||||
level6commissionRate: 0,
|
||||
level7commissionRate: 0,
|
||||
level8commissionRate: 0,
|
||||
fixStoreLeaderCommissionRate: 0
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<Record<string, Rule[]>>({});
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateGoodsPrice : addGoodsPrice;
|
||||
saveOrUpdate(form)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit("done");
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const selectedArea = ref<String[]>([]);
|
||||
|
||||
const areaList = ref<Area[]>([]);
|
||||
const getAreaList = async () => {
|
||||
areaList.value = await listAreaByParent();
|
||||
};
|
||||
|
||||
const selectArea = async value => {
|
||||
form.city = value[0];
|
||||
const res = await goodsPriceByGoodsNCity({
|
||||
goodsId: props.data?.goodsId,
|
||||
cityId: value[0]
|
||||
});
|
||||
if (res) {
|
||||
form.id = res.id;
|
||||
form.amount = res.amount;
|
||||
form.level3commissionRate = res.level3commissionRate;
|
||||
form.level3commissionMode = res.level3commissionMode;
|
||||
form.level4commissionRate = res.level4commissionRate;
|
||||
form.level4commissionMode = res.level4commissionMode;
|
||||
form.level5commissionRate = res.level5commissionRate;
|
||||
form.level6commissionRate = res.level6commissionRate;
|
||||
form.level6commissionMode = res.level6commissionMode;
|
||||
form.level7commissionRate = res.level7commissionRate;
|
||||
form.level7commissionMode = res.level7commissionMode;
|
||||
form.level8commissionRate = res.level8commissionRate;
|
||||
form.fixStoreLeaderCommissionRate = res.fixStoreLeaderCommissionRate;
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
form.id = 0;
|
||||
if (props?.data && props?.data.goodsPriceMin) form.amount = props.data.goodsPriceMin;
|
||||
form.level3commissionRate = 0;
|
||||
form.level3commissionMode = 0;
|
||||
form.level4commissionRate = 0;
|
||||
form.level4commissionMode = 0;
|
||||
form.level5commissionRate = 0;
|
||||
form.level6commissionRate = 0;
|
||||
form.level6commissionMode = 0;
|
||||
form.level7commissionRate = 0;
|
||||
form.level7commissionMode = 0;
|
||||
form.level8commissionRate = 0;
|
||||
form.fixStoreLeaderCommissionRate = 0;
|
||||
isUpdate.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
await getAreaList();
|
||||
if (visible) {
|
||||
isUpdate.value = false;
|
||||
if (props.data) {
|
||||
loading.value = false;
|
||||
if (props.data.goodsId) form.goodsId = props.data.goodsId;
|
||||
if (props.data.goodsPrice) {
|
||||
form.id = props.data.goodsPrice.id;
|
||||
form.city = props.data.goodsPrice.city;
|
||||
form.amount = props.data.goodsPrice.amount;
|
||||
form.level3commissionRate = props.data.goodsPrice.level3commissionRate;
|
||||
form.level3commissionMode = props.data.goodsPrice.level3commissionMode;
|
||||
form.level4commissionRate = props.data.goodsPrice.level4commissionRate;
|
||||
form.level4commissionMode = props.data.goodsPrice.level4commissionMode;
|
||||
form.level5commissionRate = props.data.goodsPrice.level5commissionRate;
|
||||
form.level6commissionRate = props.data.goodsPrice.level6commissionRate;
|
||||
form.level6commissionMode = props.data.goodsPrice.level6commissionMode;
|
||||
form.level7commissionRate = props.data.goodsPrice.level7commissionRate;
|
||||
form.level7commissionMode = props.data.goodsPrice.level7commissionMode;
|
||||
form.level8commissionRate = props.data.goodsPrice.level8commissionRate;
|
||||
form.fixStoreLeaderCommissionRate = props.data.goodsPrice.fixStoreLeaderCommissionRate;
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
if (props?.data && props?.data.goodsPriceMin) form.amount = props.data.goodsPriceMin;
|
||||
}
|
||||
if (loginUser.value.cityId) {
|
||||
const cityItem = areaList.value.find((item) => item.id === loginUser.value.cityId);
|
||||
if (cityItem) selectedArea.value = [cityItem.fid, cityItem.id];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedArea.value = [];
|
||||
resetFields();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="less">
|
||||
.tab-pane {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.ml-10 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-form
|
||||
:label-col="{ md: { span: 6 }, sm: { span: 24 } }"
|
||||
:wrapper-col="{ md: { span: 18 }, sm: { span: 24 } }"
|
||||
>
|
||||
<a-row>
|
||||
<a-col :lg="6" :md="12" :sm="24" :xs="24">
|
||||
<a-form-item label="服务器名称">
|
||||
<a-input
|
||||
v-model:value.trim="where.name"
|
||||
placeholder="请输入服务器名称"
|
||||
allow-clear
|
||||
@pressEnter="search"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :sm="24" :xs="24">
|
||||
<a-form-item label="IP地址">
|
||||
<a-input
|
||||
v-model:value.trim="where.code"
|
||||
placeholder="请输入服务器IP"
|
||||
allow-clear
|
||||
@pressEnter="search"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :sm="24" :xs="24">
|
||||
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="search">查询</a-button>
|
||||
<a-button @click="reset">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSearch from '@/utils/use-search';
|
||||
import type { AssetsParam } from '@/api/oa/assets/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: AssetsParam): void;
|
||||
}>();
|
||||
|
||||
// 表单数据
|
||||
const { where, resetFields } = useSearch<AssetsParam>({
|
||||
name: '',
|
||||
code: '',
|
||||
isExpire: ''
|
||||
});
|
||||
|
||||
/* 搜索 */
|
||||
const search = () => {
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
/* 重置 */
|
||||
const reset = () => {
|
||||
resetFields();
|
||||
search();
|
||||
};
|
||||
</script>
|
||||
@@ -1,289 +0,0 @@
|
||||
<!-- 用户编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="680"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑订单' : '添加订单'"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-space>
|
||||
<a-form
|
||||
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
|
||||
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :md="12" :sm="24" :xs="24">
|
||||
<a-form-item label="订单名称" v-bind="validateInfos.customerName">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="30"
|
||||
placeholder="请输入订单名称"
|
||||
v-model:value="form.customerName"
|
||||
@blur="
|
||||
validate('customerName', { trigger: 'blur' }).catch(() => {})
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="订单标识" v-bind="validateInfos.customerCode">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="请输入用户账号"
|
||||
v-model:value="form.customerCode"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="跟进状态" v-bind="validateInfos.progress">
|
||||
<progress-select
|
||||
v-model:value="form.progress"
|
||||
@blur="
|
||||
validate('progress', { trigger: 'blur' }).catch(() => {})
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="订单类型" v-bind="validateInfos.customerType">
|
||||
<type-select
|
||||
v-model:value="form.customerType"
|
||||
@blur="
|
||||
validate('customerType', { trigger: 'blur' }).catch(() => {})
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="订单来源" v-bind="validateInfos.customerSource">
|
||||
<source-select
|
||||
v-model:value="form.customerSource"
|
||||
@blur="
|
||||
validate('customerSource', { trigger: 'blur' }).catch(() => {})
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="头像" v-bind="validateInfos.customerAvatar">
|
||||
<ele-image-upload
|
||||
v-model:value="images"
|
||||
:item-style="{ width: '90px', height: '90px' }"
|
||||
:limit="1"
|
||||
@upload="onUpload"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="12" :sm="24" :xs="24">
|
||||
<a-form-item label="联系人" v-bind="validateInfos.customerContacts">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="请填写联系人"
|
||||
v-model:value="form.customerContacts"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号码" v-bind="validateInfos.customerMobile">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="请填写联系人手机号码"
|
||||
v-model:value="form.customerMobile"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="联系地址"
|
||||
v-bind="validateInfos.customerAddress"
|
||||
>
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请填写联系地址"
|
||||
v-model:value="form.customerAddress"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="公司座机" v-bind="validateInfos.customerPhone">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="请填写公司座机电话"
|
||||
v-model:value="form.customerPhone"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" v-bind="validateInfos.sortNumber">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="排序"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" v-bind="validateInfos.comments">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入备注"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-space>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, reactive, watch, computed} from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import TypeSelect from './customer-edit/type-select.vue';
|
||||
import ProgressSelect from './customer-edit/progress-select.vue';
|
||||
import SourceSelect from './customer-edit/source-select.vue';
|
||||
import { addCustomer, updateCustomer } from '@/api/oa/customer';
|
||||
import type { Customer } from '@/api/oa/customer/model';
|
||||
import { createCode } from '@/utils/common';
|
||||
import { uploadFile } from '@/api/system/file';
|
||||
import type { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FILE_SERVER } from '@/config/setting';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
// 当前用户信息
|
||||
const loginUser = computed(() => userStore.info ?? {});
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: Customer | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<Customer>({
|
||||
customerCode: '',
|
||||
customerName: '',
|
||||
customerType: undefined,
|
||||
progress: undefined,
|
||||
customerMobile: '',
|
||||
customerAvatar: '',
|
||||
customerPhone: '',
|
||||
customerContacts: '',
|
||||
customerAddress: '',
|
||||
comments: '',
|
||||
status: '0',
|
||||
sortNumber: 100,
|
||||
customerId: 0,
|
||||
userId: '',
|
||||
});
|
||||
|
||||
// 已上传数据, 可赋初始值用于回显
|
||||
const images = ref(<any>[]);
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
customerName: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入订单名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
customerCode: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入合法的IP地址',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
progress: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请选择跟进状态',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { resetFields, validate, validateInfos } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
// 去除空格
|
||||
form.customerName = form.customerName?.replace(/\s*/g, '');
|
||||
if(isUpdate.value == false) {
|
||||
form.userId = loginUser.value.userId;
|
||||
}
|
||||
const data = {
|
||||
...form
|
||||
};
|
||||
// 转字符串
|
||||
const saveOrUpdate = isUpdate.value ? updateCustomer : addCustomer;
|
||||
saveOrUpdate(data)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const onUpload = (d: ItemType) => {
|
||||
uploadFile(<File>d.file)
|
||||
.then((result) => {
|
||||
form.customerAvatar = result.path;
|
||||
message.success('上传成功');
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
loading.value = false;
|
||||
// 头像赋值
|
||||
images.value = [];
|
||||
if(props.data.customerAvatar){
|
||||
images.value.push({ uid:1, url: FILE_SERVER + props.data.customerAvatar, status: '' });
|
||||
}
|
||||
assignObject(form, props.data);
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
form.customerCode = createCode();
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="less"></style>
|
||||
@@ -1,155 +0,0 @@
|
||||
<!-- 用户编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
width="75%"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:title="'订单详情'"
|
||||
:maxable="true"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
@update:visible="updateVisible"
|
||||
:footer="null"
|
||||
>
|
||||
<a-form
|
||||
:label-col="{ md: { span: 4 }, sm: { span: 24 } }"
|
||||
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
|
||||
>
|
||||
<div class="base-form" style="margin-bottom: 20px">
|
||||
<div class="card-head">
|
||||
<a-avatar
|
||||
:size="50"
|
||||
style="margin-right: 10px"
|
||||
:src="
|
||||
customer.customerAvatar
|
||||
? FILE_SERVER + customer.customerAvatar
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<h2>{{ customer.customerName }}</h2>
|
||||
</div>
|
||||
<a-descriptions bordered>
|
||||
<a-descriptions-item label="订单名称">
|
||||
{{ customer.customerName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="订单标识">
|
||||
{{ customer.customerCode }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="跟进状态">
|
||||
<div color="blue" v-for="(d, index) in progress" :key="index">
|
||||
<span v-if="d.key == customer.progress">{{ d.name }}</span>
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="座机电话">
|
||||
{{ customer.customerPhone }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系人">
|
||||
{{ customer.customerContacts }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="订单类型">
|
||||
<div color="blue" v-for="(d, index) in customerType" :key="index">
|
||||
<span v-if="d.key == customer.customerType">{{ d.name }}</span>
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系地址">
|
||||
{{ customer.customerAddress }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">
|
||||
{{ customer.customerMobile }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注">
|
||||
{{ customer.comments }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<!-- <a-descriptions-->
|
||||
<!-- title="其他信息"-->
|
||||
<!-- :column="1"-->
|
||||
<!-- bordered-->
|
||||
<!-- style="margin-top: 30px"-->
|
||||
<!-- >-->
|
||||
<!-- <a-descriptions-item label="相关项目">-->
|
||||
<!-- {{ customer.comments }}-->
|
||||
<!-- </a-descriptions-item>-->
|
||||
<!-- </a-descriptions>-->
|
||||
</div>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import type { Customer } from '@/api/oa/customer/model';
|
||||
import { FILE_SERVER } from '@/config/setting';
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const useForm = Form.useForm;
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: Customer | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 用户信息
|
||||
const customer = reactive<Customer>({
|
||||
customerCode: '',
|
||||
customerName: '',
|
||||
customerType: undefined,
|
||||
customerMobile: '',
|
||||
customerAvatar: '',
|
||||
customerPhone: '',
|
||||
customerContacts: '',
|
||||
customerAddress: '',
|
||||
comments: '',
|
||||
progress: '',
|
||||
status: '0',
|
||||
sortNumber: 100,
|
||||
customerId: 0
|
||||
});
|
||||
|
||||
// 请求状态
|
||||
const loading = ref(true);
|
||||
|
||||
const { resetFields } = useForm(customer);
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
/* 获取字典数据 */
|
||||
const customerType = getDictionaryOptions('customerType');
|
||||
const progress = getDictionaryOptions('customerFollowStatus');
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
loading.value = false;
|
||||
assignObject(customer, props.data);
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="less">
|
||||
.tab-pane {
|
||||
min-height: 100px;
|
||||
}
|
||||
.card-head {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,46 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
mode="multiple"
|
||||
show-search
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择已开发的端口'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('serverOpenPort');
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
</script>
|
||||
@@ -1,50 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-space>
|
||||
<a-button type="dashed" @click="reset">全部</a-button>
|
||||
<a-button
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
type="dashed"
|
||||
v-model:value.trim="where.progress"
|
||||
@click="search(item.value)"
|
||||
>{{ item.label }}</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CustomerParam } from '@/api/oa/customer/model';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: CustomerParam): void;
|
||||
}>();
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('customerFollowStatus');
|
||||
|
||||
// 表单数据
|
||||
const { where, resetFields } = useSearch<CustomerParam>({
|
||||
progress: '',
|
||||
customerType: '',
|
||||
customerSource: '',
|
||||
betweenTime: '',
|
||||
userId: ''
|
||||
});
|
||||
|
||||
/* 搜索 */
|
||||
const search = (val) => {
|
||||
where.progress = val == 'all' ? null : val;
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
/* 重置 */
|
||||
const reset = () => {
|
||||
resetFields();
|
||||
search('all');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-space>
|
||||
<a-button type="dashed" @click="reset">全部</a-button>
|
||||
<a-button
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
type="dashed"
|
||||
v-model:value.trim="where.customerSource"
|
||||
@click="search(item.value)"
|
||||
>{{ item.label }}</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CustomerParam } from '@/api/oa/customer/model';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: CustomerParam): void;
|
||||
}>();
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('customerSource');
|
||||
|
||||
// 表单数据
|
||||
const { where, resetFields } = useSearch<CustomerParam>({
|
||||
progress: '',
|
||||
customerType: '',
|
||||
customerSource: '',
|
||||
betweenTime: '',
|
||||
userId: ''
|
||||
});
|
||||
|
||||
/* 搜索 */
|
||||
const search = (val) => {
|
||||
where.customerSource = val == 'all' ? null : val;
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
/* 重置 */
|
||||
const reset = () => {
|
||||
resetFields();
|
||||
search('all');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
@@ -1,45 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
// value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择状态'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('status');
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
</script>
|
||||
@@ -1,48 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-space>
|
||||
<a-button type="dashed" @click="reset">全部</a-button>
|
||||
<a-button
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
type="dashed"
|
||||
v-model:value.trim="where.betweenTime"
|
||||
@click="search(item.value)"
|
||||
>{{ item.label }}</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CustomerParam } from '@/api/oa/customer/model';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: CustomerParam): void;
|
||||
}>();
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('timeFilter');
|
||||
|
||||
// 表单数据
|
||||
const { where, resetFields } = useSearch<CustomerParam>({
|
||||
progress: '',
|
||||
customerType: '',
|
||||
customerSource: '',
|
||||
betweenTime: '',
|
||||
userId: ''
|
||||
});
|
||||
|
||||
/* 搜索 */
|
||||
const search = (val) => {
|
||||
where.betweenTime = val == 'all' ? null : val;
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
/* 重置 */
|
||||
const reset = () => {
|
||||
resetFields();
|
||||
search('all');
|
||||
};
|
||||
</script>
|
||||
@@ -1,53 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-space>
|
||||
<a-button type="dashed" @click="reset">全部</a-button>
|
||||
<a-button
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
type="dashed"
|
||||
v-model:value.trim="where.customerType"
|
||||
@click="search(item.value)"
|
||||
>{{ item.label }}</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CustomerParam } from '@/api/oa/customer/model';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: CustomerParam): void;
|
||||
}>();
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('customerType');
|
||||
|
||||
// 表单数据
|
||||
const { where, resetFields } = useSearch<CustomerParam>({
|
||||
progress: '',
|
||||
customerType: '',
|
||||
customerSource: '',
|
||||
betweenTime: '',
|
||||
userId: ''
|
||||
});
|
||||
|
||||
/* 搜索 */
|
||||
const search = (val) => {
|
||||
where.customerType = val == 'all' ? null : val;
|
||||
emit('search', where);
|
||||
console.log(where);
|
||||
};
|
||||
|
||||
/* 重置 */
|
||||
const reset = () => {
|
||||
resetFields();
|
||||
search('all');
|
||||
};
|
||||
|
||||
// reset();
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
@@ -1,78 +0,0 @@
|
||||
<!-- 选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:placeholder="placeholder"
|
||||
@change="onChange"
|
||||
style="width: 300px"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { listUsers } from '@/api/system/user';
|
||||
import type { SelectProps } from 'ant-design-vue';
|
||||
import { UserParam } from '@/api/system/user/model';
|
||||
import { CustomerParam } from '@/api/oa/customer/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: CustomerParam): void;
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
// value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择客户负责人'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = ref<SelectProps['options']>([]);
|
||||
|
||||
/* 更新选中数据 */
|
||||
// const updateValue = (value: string) => {
|
||||
// emit('update:value', value);
|
||||
// };
|
||||
|
||||
// 默认搜索条件
|
||||
const where = ref<UserParam>({});
|
||||
|
||||
const search = () => {
|
||||
/* 获取用户列 */
|
||||
listUsers({ ...where?.value })
|
||||
.then((result) => {
|
||||
data.value = result?.map((d) => {
|
||||
return {
|
||||
value: d.userId,
|
||||
label: d.nickname
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const onSearch = (userId) => {
|
||||
};
|
||||
|
||||
search();
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
};
|
||||
|
||||
// 搜素
|
||||
const onChange = (userId) => {
|
||||
emit('search', { userId });
|
||||
};
|
||||
</script>
|
||||
@@ -1,140 +0,0 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="ele-btn-icon"
|
||||
v-permission="'shop:goods:save'"
|
||||
@click="add"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>新建</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
type="primary"
|
||||
class="ele-btn-icon"
|
||||
:v-role="`dev`"
|
||||
@click="removeBatch"
|
||||
v-if="props.selection.length > 0"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
<span>批量删除</span>
|
||||
</a-button>
|
||||
<a-radio-group v-model:value="listType" @change="handleTabs">
|
||||
<a-radio-button value="all">全部</a-radio-button>
|
||||
<a-radio-button value="onSale">出售中</a-radio-button>
|
||||
<a-radio-button value="offSale">已下架</a-radio-button>
|
||||
<a-radio-button value="soldOut">已售罄</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入搜索关键词"
|
||||
@search="search"
|
||||
@pressEnter="search"
|
||||
>
|
||||
<template #addonBefore>
|
||||
<a-select v-model:value="type" style="width: 120px; margin: -5px -12px">
|
||||
<a-select-option value="goodsName">商品名称</a-select-option>
|
||||
<a-select-option value="goodsCode">商品编号</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-search>
|
||||
<!-- <CustomerSelect-->
|
||||
<!-- v-model:value="where.goodsId"-->
|
||||
<!-- :placeholder="`按商品筛选`"-->
|
||||
<!-- @select="onSelect"-->
|
||||
<!-- @clear="onClear"-->
|
||||
<!-- />-->
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import type { GoodsParam } from '@/api/goods/model';
|
||||
// import CustomerSelect from '@/components/CustomerSelect/index.vue';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: GoodsParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
// 勾选的项目
|
||||
selection?: [];
|
||||
}>();
|
||||
|
||||
// 表单数据
|
||||
const { where, resetFields } = useSearch<GoodsParam>({
|
||||
goodsId: undefined,
|
||||
goodsName: '',
|
||||
goodsCode: ''
|
||||
});
|
||||
|
||||
// 下来选项
|
||||
const type = ref('goodsName');
|
||||
// tabType
|
||||
const listType = ref('all');
|
||||
// 搜索内容
|
||||
const searchText = ref('');
|
||||
|
||||
/* 搜索 */
|
||||
const search = () => {
|
||||
if (type.value == 'goodsName') {
|
||||
where.goodsName = searchText.value;
|
||||
where.goodsCode = undefined;
|
||||
}
|
||||
if (type.value == 'goodsCode') {
|
||||
where.goodsCode = searchText.value;
|
||||
where.goodsName = undefined;
|
||||
}
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
const handleTabs = (e) => {
|
||||
listType.value = e.target.value;
|
||||
if (listType.value == 'all') {
|
||||
resetFields();
|
||||
}
|
||||
if (listType.value == 'onSale') {
|
||||
where.status = 0;
|
||||
}
|
||||
if (listType.value == 'offSale') {
|
||||
where.status = 1;
|
||||
}
|
||||
if (listType.value == 'soldOut') {
|
||||
where.stockTotal = 0;
|
||||
}
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
// 新增
|
||||
const add = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const removeBatch = () => {
|
||||
emit('remove');
|
||||
};
|
||||
|
||||
/* 重置 */
|
||||
// const reset = () => {
|
||||
// resetFields();
|
||||
// search();
|
||||
// };
|
||||
|
||||
// 监听字典id变化
|
||||
watch(
|
||||
() => props.selection,
|
||||
() => {}
|
||||
);
|
||||
</script>
|
||||
@@ -1,45 +0,0 @@
|
||||
<!-- 角色选择下拉框 -->
|
||||
<template>
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="label"
|
||||
:options="data"
|
||||
allow-clear
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDictionaryOptions } from '@/utils/common';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
(e: 'blur'): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择年限'
|
||||
}
|
||||
);
|
||||
|
||||
// 字典数据
|
||||
const data = getDictionaryOptions('serverYears');
|
||||
|
||||
/* 更新选中数据 */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 失去焦点 */
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
</script>
|
||||
@@ -1,367 +0,0 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<!-- <a-page-header :ghost="false" title="商品管理">-->
|
||||
<!-- <div class="ele-text-secondary">-->
|
||||
<!-- 商品管理模块,支持视频上传、多图上传、多规格。-->
|
||||
<!-- </div>-->
|
||||
<!-- </a-page-header>-->
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false">
|
||||
<!-- 表格 -->
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="goodsId"
|
||||
:columns="columns"
|
||||
:height="tableHeight"
|
||||
:datasource="datasource"
|
||||
v-model:selection="selection"
|
||||
:scroll="{ x: 800 }"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
:selection="selection"
|
||||
@search="reload"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :preview="false" :width="80" />
|
||||
</template>
|
||||
<template v-if="column.key === 'goodsPhoneModelList'">
|
||||
<p v-if="!record.hide">{{ record.goodsPhoneModelList[0].phoneModelName
|
||||
}}</p>
|
||||
<template v-else>
|
||||
<p v-for="(item, index) in record.goodsPhoneModelList" :key="index">
|
||||
{{ item.phoneModelName }}
|
||||
</p>
|
||||
</template>
|
||||
<a-button @click.native="record.hide = !record.hide"
|
||||
v-if="record.goodsPhoneModelList.length > 0"
|
||||
type="link"
|
||||
size="small"
|
||||
>
|
||||
<template v-if="!record.hide">
|
||||
<span>展开</span>
|
||||
<DownOutlined />
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>折叠</span>
|
||||
<UpOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">上架</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="orange">下架</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'comments'">
|
||||
<a-tooltip :title="record.comments" placement="topLeft">
|
||||
{{ record.comments }}
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-if="column.key === 'createTime'">
|
||||
<a-tooltip :title="`${toDateString(record.createTime)}`">
|
||||
{{ timeAgo(record.createTime) }}
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openPriceEdit(record)">价格设置</a>
|
||||
<a-divider type="vertical" />
|
||||
<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>
|
||||
<!-- 编辑弹窗 -->
|
||||
<goods-edit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:category-list="data"
|
||||
@done="reload"
|
||||
/>
|
||||
<goods-price-edit
|
||||
v-model:visible="showPriceEdit"
|
||||
:data="current"
|
||||
:category-list="data"
|
||||
@done="reload"
|
||||
/>
|
||||
<!-- 用户详情 -->
|
||||
<goods-info v-model:visible="showInfo" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--suppress TypeScriptValidateTypes -->
|
||||
<script lang="ts" setup>
|
||||
import { timeAgo, toTreeData } from "ele-admin-pro";
|
||||
import { createVNode, computed, ref } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { ExclamationCircleOutlined, DownOutlined, UpOutlined } from "@ant-design/icons-vue";
|
||||
import type { EleProTable } from "ele-admin-pro";
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from "ele-admin-pro/es/ele-pro-table/types";
|
||||
import { toDateString } from "ele-admin-pro";
|
||||
import Search from "./components/search.vue";
|
||||
import GoodsEdit from "./components/goods-edit.vue";
|
||||
import GoodsInfo from "./components/goods-info.vue";
|
||||
import { pageGoods, removeGoods, removeBatchGoods } from "@/api/goods";
|
||||
import type { Goods, GoodsParam } from "@/api/goods/model";
|
||||
import { listCategory } from "@/api/goods/category";
|
||||
import { Category } from "@/api/goods/category/model";
|
||||
import { FILE_THUMBNAIL } from "@/config/setting";
|
||||
import GoodsPriceEdit from "@/views/goods/index/components/goods-price-edit.vue";
|
||||
// import { getDictionaryOptions } from '@/utils/common';
|
||||
// import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 是否显示资产详情
|
||||
const showInfo = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
// 树展开的key
|
||||
const expandedRowKeys = ref<number[]>([]);
|
||||
// 树选中的key
|
||||
const selectedRowKeys = ref<number[]>([]);
|
||||
// 当前用户信息
|
||||
// const brand = getDictionaryOptions('serverBrand');
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
key: "index",
|
||||
width: 48,
|
||||
align: "center",
|
||||
fixed: "left",
|
||||
hideInSetting: true,
|
||||
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
|
||||
},
|
||||
{
|
||||
title: "商品名称",
|
||||
dataIndex: "goodsName"
|
||||
},
|
||||
// {
|
||||
// title: "商品分类",
|
||||
// dataIndex: "categoryName"
|
||||
// },
|
||||
{
|
||||
title: "参与机型",
|
||||
key: "goodsPhoneModelList"
|
||||
},
|
||||
{
|
||||
title: "成本价",
|
||||
dataIndex: "cost",
|
||||
customRender: ({ text }) => `${text}元`
|
||||
},
|
||||
{
|
||||
title: "划线价",
|
||||
dataIndex: "linePriceMin",
|
||||
customRender: ({ text }) => `${text}元`
|
||||
},
|
||||
{
|
||||
title: "备注",
|
||||
dataIndex: "comments",
|
||||
key: "comments",
|
||||
hideInTable: true,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
sorter: true,
|
||||
key: "status",
|
||||
showSorterTooltip: false,
|
||||
customRender: ({ text }) => ["上架", "下架"][text]
|
||||
},
|
||||
{
|
||||
title: "排序",
|
||||
dataIndex: "sortNumber",
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createTime",
|
||||
key: "createTime",
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
hideInTable: true,
|
||||
customRender: ({ text }) => toDateString(text)
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
align: "center",
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<Goods[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<Goods | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
const showPriceEdit = ref(false);
|
||||
// 树形数据
|
||||
const data = ref<Category[]>([]);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.brand = filters.brand;
|
||||
}
|
||||
return pageGoods({ ...where, ...orders, page, limit });
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
listCategory()
|
||||
.then((list) => {
|
||||
loading.value = false;
|
||||
const eks: number[] = [];
|
||||
list.forEach((d) => {
|
||||
d.key = d.categoryId;
|
||||
d.value = d.categoryId;
|
||||
if (typeof d.categoryId === "number") {
|
||||
eks.push(d.categoryId);
|
||||
}
|
||||
});
|
||||
expandedRowKeys.value = eks;
|
||||
data.value = toTreeData({
|
||||
data: list,
|
||||
idField: "categoryId",
|
||||
parentIdField: "parentId"
|
||||
});
|
||||
if (list.length) {
|
||||
if (typeof list[0].categoryId === "number") {
|
||||
selectedRowKeys.value = [list[0].categoryId];
|
||||
}
|
||||
current.value = list[0];
|
||||
} else {
|
||||
selectedRowKeys.value = [];
|
||||
current.value = null;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: GoodsParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
// 搜索是否展开
|
||||
const searchExpand = ref(false);
|
||||
|
||||
// 表格固定高度
|
||||
const fixedHeight = ref(false);
|
||||
|
||||
// 表格高度
|
||||
const tableHeight = computed(() => {
|
||||
return fixedHeight.value
|
||||
? searchExpand.value
|
||||
? "calc(100vh - 618px)"
|
||||
: "calc(100vh - 562px)"
|
||||
: void 0;
|
||||
});
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: Goods) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openPriceEdit = (row?: Goods) => {
|
||||
current.value = row ?? null;
|
||||
showPriceEdit.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: Goods) => {
|
||||
const hide = message.loading("请求中..", 0);
|
||||
removeGoods(row.goodsId)
|
||||
.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);
|
||||
removeBatchGoods(selection.value.map((d) => d.goodsId))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "GoodsIndex"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 文字超出隐藏(两行)
|
||||
// 需要设置文字容器的宽度
|
||||
.twoline-hide {
|
||||
width: 320px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
@@ -1,181 +0,0 @@
|
||||
<!-- 服务编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="500"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '修改记录' : '添加记录'"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="名称" name="name">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="请输入服务承诺名称"
|
||||
v-model:value="form.name"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="描述" name="summary">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入描述内容"
|
||||
v-model:value="form.summary"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sortNumber">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="排序"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="默认勾选" name="isDefault">
|
||||
<a-radio-group v-model:value="form.isDefault">
|
||||
<a-radio :value="1">是</a-radio>
|
||||
<a-radio :value="0">否</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import useFormData from '@/utils/use-form-data';
|
||||
import { addService, updateService } from '@/api/goods/service/index';
|
||||
import type { Service } from '@/api/goods/service/model';
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: Service | null;
|
||||
}>();
|
||||
|
||||
//
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const { form, resetFields, assignFields } = useFormData<Service>({
|
||||
serviceId: undefined,
|
||||
name: '',
|
||||
summary: '',
|
||||
isDefault: 1,
|
||||
sortNumber: 100
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<Record<string, Rule[]>>({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入服务承诺名称',
|
||||
type: 'string',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
summary: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入服务承诺描述',
|
||||
type: 'string',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
isDefault: [
|
||||
{
|
||||
required: true,
|
||||
message: '请设置是否勾选',
|
||||
type: 'number',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
sortNumber: [
|
||||
{
|
||||
required: true,
|
||||
message: '请填写排序',
|
||||
type: 'number',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateService : addService;
|
||||
saveOrUpdate(form)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
assignFields(props.data);
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false">
|
||||
<!-- 表格 -->
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="serviceId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:scroll="{ x: 800 }"
|
||||
cache-key="proShopServiceTable"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button type="primary" class="ele-btn-icon" @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 === 'isDefault'">
|
||||
<!-- 是否默认 -->
|
||||
<a-tag :color="record.isDefault ? 'green' : ''">
|
||||
{{ record.isDefault ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
placement="topRight"
|
||||
title="确定要删除此服务承诺吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
<!-- 编辑弹窗 -->
|
||||
<service-edit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue/es';
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro/es';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { messageLoading, toDateString } from 'ele-admin-pro/es';
|
||||
import ServiceEdit from './components/service-edit.vue';
|
||||
import {
|
||||
pageServices,
|
||||
removeService,
|
||||
removeServices
|
||||
} from '@/api/goods/service';
|
||||
import type { Service, ServiceParam } from '@/api/goods/service/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
key: 'index',
|
||||
width: 48,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
hideInSetting: true,
|
||||
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
|
||||
},
|
||||
{
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
showSorterTooltip: false
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'summary',
|
||||
showSorterTooltip: false
|
||||
},
|
||||
{
|
||||
title: '是否默认',
|
||||
dataIndex: 'isDefault',
|
||||
key: 'isDefault',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180,
|
||||
customRender: ({ text }) => toDateString(text)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
align: 'center'
|
||||
}
|
||||
]);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<Service[]>([]);
|
||||
|
||||
// 当前编辑数据
|
||||
const current = ref<Service | null>(null);
|
||||
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
const type = ref('goodsName');
|
||||
const searchText = ref('');
|
||||
const score = ref(0);
|
||||
|
||||
const handleTabs = (e) => {
|
||||
score.value = e.target.value;
|
||||
};
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
if (type.value == 'goodsName') {
|
||||
where.goodsName = searchText.value;
|
||||
}
|
||||
if (type.value == 'goodsCode') {
|
||||
where.goodsCode = searchText.value;
|
||||
}
|
||||
if (type.value == 'userId') {
|
||||
where.userId = searchText.value;
|
||||
}
|
||||
return pageServices({ ...where, ...orders, page, limit });
|
||||
};
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ServiceParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ page: 1, where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: Service) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: Service) => {
|
||||
const hide = messageLoading('请求中..', 0);
|
||||
removeService(row.serviceId)
|
||||
.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 = messageLoading('请求中..', 0);
|
||||
removeServices(selection.value.map((d) => d.serviceId))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopService'
|
||||
};
|
||||
</script>
|
||||
@@ -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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user