feat(volunteer): 新增志愿服务证明材料识别及年度证明功能
- 新增志愿证明材料识别功能,支持自动识别材料并回填关键信息 - 在志愿时长编辑页面添加材料识别按钮及识别结果展示 - 新增志愿服务证明模块,支持查询、生成及下载年度志愿服务证明PDF - 志愿时长列表及审核详情中新增“服务地点”字段显示 - 志愿时长导入功能支持“服务地点”字段,完善导入表头与数据映射 - 志愿时长详情、审核流程等界面优化,增加识别材料公章状态及流程节点提示 - 志愿时长待审核项支持用户撤回操作,撤回后记录被删除需重新录入 - 社团年度报告相关接口新增,实现社团年度报告生成及分页查询 - 调整相关样式与界面布局,提升用户体验与信息展示完整性
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
<!-- 青马学员 AI 学情分析 -->
|
||||
<template>
|
||||
<a-drawer
|
||||
:visible="visible"
|
||||
:width="760"
|
||||
:body-style="{ padding: '20px 24px 24px' }"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<template #title>
|
||||
<div class="analysis-title">
|
||||
<span>AI 学情分析</span>
|
||||
<a-tag v-if="studentName" color="blue">{{ studentName }}</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button v-if="loading" size="small" danger @click="stopAnalysis">
|
||||
停止
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
:disabled="!canAnalyze"
|
||||
@click="runAnalysis"
|
||||
>
|
||||
{{ hasResult ? '重新分析' : '开始分析' }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
class="analysis-tip"
|
||||
message="分析依据"
|
||||
:description="dataScopeText"
|
||||
/>
|
||||
|
||||
<div class="analysis-focus">
|
||||
<span class="focus-label">分析侧重</span>
|
||||
<a-radio-group v-model:value="focus" :disabled="loading" size="small">
|
||||
<a-radio-button value="">综合</a-radio-button>
|
||||
<a-radio-button value="thought">思想动态</a-radio-button>
|
||||
<a-radio-button value="difficulty">学习难点</a-radio-button>
|
||||
<a-radio-button value="advice">培养建议</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<a-empty
|
||||
v-if="!loading && !hasResult"
|
||||
description="点击「开始分析」,AI 将结合该学员的培养材料、任务进度与考核评分给出结论"
|
||||
/>
|
||||
|
||||
<div v-else class="analysis-result">
|
||||
<div v-if="loading && !displayContent" class="analysis-loading">
|
||||
<a-spin size="small" />
|
||||
<span>AI 正在分析该学员的学习情况…</span>
|
||||
</div>
|
||||
<ByteMdViewer
|
||||
v-if="displayContent"
|
||||
:value="displayContent"
|
||||
:plugins="plugins"
|
||||
/>
|
||||
<div v-if="loading && displayContent" class="analysis-thinking">
|
||||
<span class="thinking-dot" aria-hidden="true"></span>
|
||||
<span>正在继续生成…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="analysis-footer">
|
||||
<span class="footer-hint">分析结果由 AI 生成,仅供参考</span>
|
||||
<a-space>
|
||||
<a-button :disabled="!displayContent" @click="copyResult"
|
||||
>复制结果</a-button
|
||||
>
|
||||
<a-button :disabled="loading || !displayContent" @click="clearResult"
|
||||
>清空</a-button
|
||||
>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import 'bytemd/dist/index.min.css';
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
import gfm from '@bytemd/plugin-gfm';
|
||||
import zh_HansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
|
||||
import highlight from '@bytemd/plugin-highlight';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import ByteMdViewer from '@/components/ByteMdViewer/index.vue';
|
||||
import { streamQmgcStudentAnalysis } from '@/utils/ai-stream';
|
||||
import { copyText } from '@/utils/common';
|
||||
import { FAILED_TEXT } from '@/utils/ai-result';
|
||||
import type { QmgcMaterial, QmgcScore, QmgcStudent } from '@/api/growth/qmgc';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
/** 当前学员 */
|
||||
student?: QmgcStudent | null;
|
||||
/** 学员档案详情(含 materials / tasks / scores) */
|
||||
detail?: Record<string, any> | null;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const plugins = ref([
|
||||
gfm({
|
||||
locale: zh_HansGfm
|
||||
}),
|
||||
highlight()
|
||||
]);
|
||||
|
||||
const loading = ref(false);
|
||||
const focus = ref('');
|
||||
const answer = ref('');
|
||||
const controller = ref<AbortController | null>(null);
|
||||
|
||||
const STREAM_END_TEXT = '__END__';
|
||||
const THINK_OPEN_TAG = '<think>';
|
||||
const THINK_CLOSE_TAG = '</think>';
|
||||
|
||||
const studentName = computed(() => props.student?.studentName || '');
|
||||
const canAnalyze = computed(() => !!props.student?.id);
|
||||
|
||||
const materials = computed<QmgcMaterial[]>(
|
||||
() => (props.detail?.materials || []) as QmgcMaterial[]
|
||||
);
|
||||
const tasks = computed<any[]>(() => (props.detail?.tasks || []) as any[]);
|
||||
const scores = computed<QmgcScore[]>(
|
||||
() => (props.detail?.scores || []) as QmgcScore[]
|
||||
);
|
||||
|
||||
const doneTaskCount = computed(
|
||||
() => tasks.value.filter((item) => item.assignStatus === 1).length
|
||||
);
|
||||
const scoredMaterialCount = computed(
|
||||
() => materials.value.filter((item) => item.status === 1).length
|
||||
);
|
||||
|
||||
/** 展示给教师的取数范围,避免用户误以为 AI「看过」系统外的数据 */
|
||||
const dataScopeText = computed(() => {
|
||||
if (!props.student?.id) {
|
||||
return '尚未选择学员';
|
||||
}
|
||||
return `培养材料 ${materials.value.length} 条(已评分 ${scoredMaterialCount.value} 条)、任务 ${doneTaskCount.value}/${tasks.value.length} 项已完成、考核评分 ${scores.value.length} 条。系统暂无考勤与课堂互动数据,分析不含这两类记录。`;
|
||||
});
|
||||
|
||||
const hasResult = computed(() => !!answer.value.trim());
|
||||
|
||||
/** 去掉 <think> 思考段,只留正文 */
|
||||
const parseAssistantContent = (value: string) => {
|
||||
if (!value) {
|
||||
return { content: '', thinking: false };
|
||||
}
|
||||
let cursor = 0;
|
||||
let content = '';
|
||||
let thinking = false;
|
||||
while (cursor < value.length) {
|
||||
if (!thinking) {
|
||||
const openIndex = value.indexOf(THINK_OPEN_TAG, cursor);
|
||||
const closeIndex = value.indexOf(THINK_CLOSE_TAG, cursor);
|
||||
if (closeIndex !== -1 && (openIndex === -1 || closeIndex < openIndex)) {
|
||||
cursor = closeIndex + THINK_CLOSE_TAG.length;
|
||||
continue;
|
||||
}
|
||||
if (openIndex === -1) {
|
||||
content += value.slice(cursor);
|
||||
break;
|
||||
}
|
||||
content += value.slice(cursor, openIndex);
|
||||
cursor = openIndex + THINK_OPEN_TAG.length;
|
||||
thinking = true;
|
||||
continue;
|
||||
}
|
||||
const closeIndex = value.indexOf(THINK_CLOSE_TAG, cursor);
|
||||
if (closeIndex === -1) {
|
||||
break;
|
||||
}
|
||||
cursor = closeIndex + THINK_CLOSE_TAG.length;
|
||||
thinking = false;
|
||||
}
|
||||
return { content, thinking };
|
||||
};
|
||||
|
||||
const displayContent = computed(() =>
|
||||
parseAssistantContent(answer.value).content.trim()
|
||||
);
|
||||
|
||||
const truncate = (value: string, max: number) =>
|
||||
value.length > max ? `${value.slice(0, max)}…` : value;
|
||||
|
||||
/**
|
||||
* 组装给 AI 的学员上下文。
|
||||
*
|
||||
* 后端可按 studentId 自行取数;这里同时带上前端已加载的摘要,
|
||||
* 避免后端尚未聚合时 AI「看不到」任何材料内容。
|
||||
*/
|
||||
const buildContext = () => {
|
||||
const student = props.student || {};
|
||||
const materialTexts = materials.value
|
||||
.filter((item) => item.content && item.content.trim())
|
||||
.slice(0, 8)
|
||||
.map(
|
||||
(item) =>
|
||||
`【${item.title || '培养材料'}】${truncate(
|
||||
String(item.content).trim(),
|
||||
800
|
||||
)}`
|
||||
);
|
||||
const taskTexts = tasks.value.slice(0, 20).map((item) => {
|
||||
const title = item.title || item.task?.title || '未命名任务';
|
||||
const status = item.assignStatus === 1 ? '已完成' : '未完成';
|
||||
return `${title}(${status})`;
|
||||
});
|
||||
const scoreTexts = scores.value.slice(0, 20).map((item) => {
|
||||
const parts = [item.dimension, item.item].filter(Boolean).join('/');
|
||||
const comment = item.comment
|
||||
? `,评语:${truncate(item.comment, 200)}`
|
||||
: '';
|
||||
return `${parts || '考核项'}:${item.score ?? 0} 分${comment}`;
|
||||
});
|
||||
|
||||
return {
|
||||
studentId: student.id,
|
||||
studentName: student.studentName,
|
||||
studentNo: student.studentNo,
|
||||
collegeName: student.collegeName,
|
||||
className: student.className,
|
||||
batch: student.batch,
|
||||
stage: student.stage,
|
||||
totalScore: student.totalScore,
|
||||
materialCount: materials.value.length,
|
||||
scoredMaterialCount: scoredMaterialCount.value,
|
||||
taskDoneCount: doneTaskCount.value,
|
||||
taskTotal: tasks.value.length,
|
||||
materialTexts,
|
||||
taskTexts,
|
||||
scoreTexts
|
||||
};
|
||||
};
|
||||
|
||||
const buildPayload = () => ({
|
||||
studentId: props.student?.id,
|
||||
focus: focus.value || undefined,
|
||||
context: buildContext()
|
||||
});
|
||||
|
||||
const clearResult = () => {
|
||||
answer.value = '';
|
||||
};
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
if (!value) {
|
||||
stopAnalysis();
|
||||
}
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const stopAnalysis = () => {
|
||||
controller.value?.abort();
|
||||
controller.value = null;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const runAnalysis = async () => {
|
||||
if (!canAnalyze.value) {
|
||||
message.warning('请先选择学员');
|
||||
return;
|
||||
}
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
clearResult();
|
||||
loading.value = true;
|
||||
const current = new AbortController();
|
||||
controller.value = current;
|
||||
try {
|
||||
await streamQmgcStudentAnalysis(
|
||||
buildPayload() as unknown as Record<string, unknown>,
|
||||
(raw) => {
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
if (data?.answer === STREAM_END_TEXT) {
|
||||
return;
|
||||
}
|
||||
if (data?.loading === 'true' || data?.loading === true) {
|
||||
return;
|
||||
}
|
||||
const chunk =
|
||||
typeof data?.notice === 'string'
|
||||
? data.notice
|
||||
: typeof data?.answer === 'string'
|
||||
? data.answer
|
||||
: '';
|
||||
if (chunk) {
|
||||
answer.value += chunk;
|
||||
}
|
||||
} catch {
|
||||
answer.value += raw;
|
||||
}
|
||||
},
|
||||
current.signal
|
||||
);
|
||||
const text = displayContent.value;
|
||||
if (!text) {
|
||||
message.warning('这次没有生成出内容,请重试');
|
||||
} else if (text === FAILED_TEXT) {
|
||||
message.error('分析失败,请稍后重试');
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
message.error(e?.message || '分析失败');
|
||||
}
|
||||
} finally {
|
||||
if (controller.value === current) {
|
||||
controller.value = null;
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const copyResult = () => {
|
||||
if (!displayContent.value) {
|
||||
message.warning('暂无可复制内容');
|
||||
return;
|
||||
}
|
||||
copyText(displayContent.value);
|
||||
};
|
||||
|
||||
/** 切换学员或关闭时清掉上一次结果,避免张冠李戴 */
|
||||
watch(
|
||||
() => props.student?.id,
|
||||
() => {
|
||||
stopAnalysis();
|
||||
clearResult();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
focus.value = '';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAnalysis();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.analysis-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.analysis-tip {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.analysis-focus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.focus-label {
|
||||
flex: none;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.analysis-result {
|
||||
min-height: 240px;
|
||||
padding: 16px 20px;
|
||||
border: 1px solid #edf2f8;
|
||||
border-radius: 12px;
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.analysis-loading,
|
||||
.analysis-thinking {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.analysis-loading {
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.analysis-thinking {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.thinking-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #1d7bff;
|
||||
animation: thinkingPulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.analysis-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.footer-hint {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@keyframes thinkingPulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.45;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -60,6 +60,8 @@
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">档案</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openAnalysis(record)">AI 分析</a>
|
||||
<a-divider type="vertical" />
|
||||
<a v-if="hasPermission('gxmu:qmgcStudent:update')" @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
@@ -77,6 +79,12 @@
|
||||
|
||||
<QmgcStudentEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
|
||||
<AiAnalysis
|
||||
v-model:visible="showAnalysis"
|
||||
:student="analysisStudent"
|
||||
:detail="analysisDetail"
|
||||
/>
|
||||
|
||||
<a-drawer v-model:visible="showDetail" title="青马培养档案" width="880">
|
||||
<template v-if="detail">
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
@@ -88,6 +96,21 @@
|
||||
<a-descriptions-item label="累计得分">{{ student?.totalScore ?? 0 }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="detail-ai-entry">
|
||||
<span class="detail-ai-hint">
|
||||
结合下面的培养材料、任务进度与考核评分,让 AI
|
||||
分析该学员的思想动态与学习难点
|
||||
</span>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
@click="openAnalysis(current)"
|
||||
>
|
||||
AI 学情分析
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-tabs style="margin-top: 16px">
|
||||
<a-tab-pane key="material" tab="培养材料">
|
||||
<a-space style="margin-bottom: 12px">
|
||||
@@ -209,6 +232,7 @@
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import QmgcStudentEdit from './components/edit.vue';
|
||||
import AiAnalysis from './components/aiAnalysis.vue';
|
||||
import MultiFileUpload from '@/components/MultiFileUpload/index.vue';
|
||||
import {
|
||||
pageQmgcStudents,
|
||||
@@ -227,10 +251,13 @@
|
||||
const stage = ref<string>();
|
||||
const showEdit = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const showAnalysis = ref(false);
|
||||
const showMaterial = ref(false);
|
||||
const showScoreModal = ref(false);
|
||||
const current = ref<QmgcStudent | null>(null);
|
||||
const detail = ref<Record<string, any> | null>(null);
|
||||
const analysisStudent = ref<QmgcStudent | null>(null);
|
||||
const analysisDetail = ref<Record<string, any> | null>(null);
|
||||
const materialForm = ref<Partial<QmgcMaterial>>({});
|
||||
const materialFile = ref('');
|
||||
const submitting = ref(false);
|
||||
@@ -260,7 +287,7 @@
|
||||
{ title: '阶段', key: 'stage', width: 100, align: 'center' },
|
||||
{ title: '累计得分', dataIndex: 'totalScore', key: 'totalScore', width: 100, align: 'center' },
|
||||
{ title: '状态', key: 'status', width: 90, align: 'center' },
|
||||
{ title: '操作', key: 'action', width: 180, fixed: 'right', align: 'center', hideInSetting: true }
|
||||
{ title: '操作', key: 'action', width: 230, fixed: 'right', align: 'center', hideInSetting: true }
|
||||
]);
|
||||
|
||||
const taskColumns: ColumnItem[] = [
|
||||
@@ -316,6 +343,31 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开 AI 学情分析。
|
||||
*
|
||||
* 从列表进入时档案详情还没加载,这里按需取一次,保证 AI 能看到
|
||||
* 培养材料、任务和评分;已加载过(从档案抽屉进入)则直接复用。
|
||||
*/
|
||||
const openAnalysis = async (row?: QmgcStudent | null) => {
|
||||
if (!row?.id) {
|
||||
message.warning('请先选择学员');
|
||||
return;
|
||||
}
|
||||
if (!detail.value || current.value?.id !== row.id) {
|
||||
try {
|
||||
detail.value = await getQmgcStudentDetail(row.id);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载学情数据失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
current.value = row;
|
||||
analysisStudent.value = row;
|
||||
analysisDetail.value = detail.value;
|
||||
showAnalysis.value = true;
|
||||
};
|
||||
|
||||
const remove = async (row: QmgcStudent) => {
|
||||
if (!row.id) return;
|
||||
try {
|
||||
@@ -402,3 +454,23 @@
|
||||
return map[value ?? ''] ?? value ?? '-';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.detail-ai-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
border: 1px dashed #d6e4f5;
|
||||
border-radius: 8px;
|
||||
background: #f7fbff;
|
||||
}
|
||||
|
||||
.detail-ai-hint {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -121,7 +121,9 @@
|
||||
{ label: '优秀共青团员', value: 'gxmu_yxgqty' },
|
||||
{ label: '挑战杯', value: 'gxmu_tzbcy_form' },
|
||||
{ label: '青马工程', value: 'gxmu_qmgc_form' },
|
||||
{ label: '未来学术之星', value: 'gxmu_wxxzx_project' }
|
||||
{ label: '未来学术之星', value: 'gxmu_wxxzx_project' },
|
||||
{ label: '志愿服务时长', value: 'gxmu_volunteer' },
|
||||
{ label: '社会实践学分', value: 'gxmu_practice_credit' }
|
||||
];
|
||||
|
||||
// 用户信息
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="canOperateDeclare" class="declare-actions">
|
||||
<a-space>
|
||||
<a-button size="small" :loading="actionLoading" @click="handleWithdraw">
|
||||
<a-button
|
||||
size="small"
|
||||
:loading="actionLoading"
|
||||
@click="handleWithdraw"
|
||||
>
|
||||
撤回申报
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -225,6 +229,7 @@
|
||||
import { getSjqnForm } from '@/api/gxmu/sjqn-form';
|
||||
import { getSjtbzbsjForm } from '@/api/gxmu/sjtbzbsj-form';
|
||||
import { getTyglForm } from '@/api/gxmu/tygl-form';
|
||||
import { getVolunteerRecord } from '@/api/growth/volunteer';
|
||||
import { getTzbcyForm } from '@/api/gxmu/tzbcy-form';
|
||||
import { getWshqtwForm } from '@/api/gxmu/wshqtw-form';
|
||||
import { getWshqtzbForm } from '@/api/gxmu/wshqtzb-form';
|
||||
@@ -345,6 +350,11 @@
|
||||
gxmu_tygl: {
|
||||
title: '团员管理',
|
||||
fetcher: (id) => getTyglForm(id) as Promise<Record<string, unknown>>
|
||||
},
|
||||
gxmu_volunteer: {
|
||||
title: '志愿服务时长',
|
||||
fetcher: (id) =>
|
||||
getVolunteerRecord(id) as Promise<Record<string, unknown>>
|
||||
}
|
||||
};
|
||||
|
||||
@@ -435,7 +445,10 @@
|
||||
onOk: async () => {
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
await withdrawDeclareRecord(props.module as string, props.pk as number);
|
||||
await withdrawDeclareRecord(
|
||||
props.module as string,
|
||||
props.pk as number
|
||||
);
|
||||
message.success('已撤回,申报人可继续修改');
|
||||
emit('done');
|
||||
updateVisible(false);
|
||||
@@ -1085,5 +1098,4 @@
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -834,6 +834,43 @@ export const MODULE_DETAIL_CONFIGS: Record<string, ModuleDetailConfig> = {
|
||||
],
|
||||
materialFields: []
|
||||
},
|
||||
gxmu_volunteer: {
|
||||
title: '志愿服务时长',
|
||||
sheetName: '志愿服务时长记录',
|
||||
labels: {
|
||||
studentName: '姓名',
|
||||
studentNo: '学号',
|
||||
projectName: '志愿项目',
|
||||
orgName: '组织单位',
|
||||
servicePlace: '服务地点',
|
||||
hours: '服务时长(小时)',
|
||||
serviceDate: '服务开始日期',
|
||||
serviceEndDate: '服务结束日期',
|
||||
content: '服务内容',
|
||||
source: '数据来源',
|
||||
proofFiles: '证明材料'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '学生信息',
|
||||
fields: ['studentName', 'studentNo']
|
||||
},
|
||||
{
|
||||
title: '服务信息',
|
||||
fields: [
|
||||
'projectName',
|
||||
'orgName',
|
||||
'servicePlace',
|
||||
'hours',
|
||||
'serviceDate',
|
||||
'serviceEndDate',
|
||||
'content',
|
||||
'source'
|
||||
]
|
||||
}
|
||||
],
|
||||
materialFields: ['proofFiles']
|
||||
},
|
||||
cms_manuscript: {
|
||||
title: '稿件',
|
||||
sheetName: '稿件',
|
||||
|
||||
@@ -27,11 +27,7 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="主键" name="pk">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入主键"
|
||||
v-model:value="form.pk"
|
||||
/>
|
||||
<a-input allow-clear placeholder="请输入主键" v-model:value="form.pk" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态(0驳回 1通过)" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
@@ -61,11 +57,7 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="userId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
<a-input allow-clear placeholder="请输入" v-model:value="form.userId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="sortNumber">
|
||||
<a-input-number
|
||||
@@ -129,7 +121,7 @@
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
userId: undefined,
|
||||
sortNumber: undefined,
|
||||
sortNumber: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
@@ -138,9 +130,7 @@
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
|
||||
});
|
||||
const rules = reactive({});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@
|
||||
{ label: '优秀共青团员', value: 'gxmu_yxgqty' },
|
||||
{ label: '挑战杯', value: 'gxmu_tzbcy_form' },
|
||||
{ label: '青马工程', value: 'gxmu_qmgc_form' },
|
||||
{ label: '未来学术之星', value: 'gxmu_wxxzx_project' }
|
||||
{ label: '未来学术之星', value: 'gxmu_wxxzx_project' },
|
||||
{ label: '志愿服务时长', value: 'gxmu_volunteer' },
|
||||
{ label: '社会实践学分', value: 'gxmu_practice_credit' }
|
||||
];
|
||||
|
||||
const search = () => {
|
||||
|
||||
Reference in New Issue
Block a user