feat(user): 支持修改当前用户资料接口并更新个人中心调用
- 新增 updateSelfUser 方法,用于修改当前登录用户的个人资料 - 个人中心调用接口由原 updateUser 切换为 updateSelfUser,避免学生账号权限不足导致的保存失败 - 更新了头像保存、个人信息保存等相关调用代码,添加异常捕获和提示 - 修正环境变量 .env.development 中 API URL 配置为生产地址 - 优化审核记录排序逻辑,新增获取最新审核记录和编辑入口显示判断函数 - 调整审核状态标签使用统一排序规则,防止显示和编辑状态不一致 - 新增社团活动、社团编辑、志愿时长录入、学分记录编辑、社会实践学分申报、青马学员编辑等多个新增组件及表单功能 - 各新增功能支持表单验证、操作反馈和数据加载优化
This commit is contained in:
@@ -13,13 +13,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
import { getLatestReview } from '../utils/review';
|
||||
|
||||
const props = defineProps<{
|
||||
reviewList?: ReviewList[];
|
||||
}>();
|
||||
|
||||
const latest = computed(() => {
|
||||
const list = props.reviewList ?? [];
|
||||
return [...list].sort((a, b) => Number(b.id || 0) - Number(a.id || 0))[0];
|
||||
});
|
||||
// 与「能否编辑」共用同一套排序规则,避免两处结论不一致
|
||||
const latest = computed(() => getLatestReview(props.reviewList));
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" class="placeholder-card">
|
||||
<a-result status="info" title="专家库" sub-title="该功能规划在 P1 阶段实施">
|
||||
<template #extra>
|
||||
<a-space direction="vertical" size="12" style="text-align: left; max-width: 640px">
|
||||
<div class="placeholder-desc">维护评审专家信息,按研究方向与职称筛选。</div>
|
||||
<div class="placeholder-sub">实施依赖:</div>
|
||||
<ul class="placeholder-list">
|
||||
<li>专家信息维护</li>
|
||||
<li>研究方向字典</li>
|
||||
<li>评审回避规则</li>
|
||||
</ul>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-result>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 专家库:P1 阶段交付,当前先占位避免菜单跳转 404
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.placeholder-card {
|
||||
min-height: calc(100vh - 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
color: #595959;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.placeholder-sub {
|
||||
margin-top: 4px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.placeholder-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" class="placeholder-card">
|
||||
<a-result status="info" title="专家评审" sub-title="该功能规划在 P1 阶段实施">
|
||||
<template #extra>
|
||||
<a-space direction="vertical" size="12" style="text-align: left; max-width: 640px">
|
||||
<div class="placeholder-desc">未来学术之星、挑战杯与团委科研课题的线上专家评审、分配与打分汇总。</div>
|
||||
<div class="placeholder-sub">实施依赖:</div>
|
||||
<ul class="placeholder-list">
|
||||
<li>专家任务分配</li>
|
||||
<li>评分维度配置</li>
|
||||
<li>评审结果汇总</li>
|
||||
</ul>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-result>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 专家评审:P1 阶段交付,当前先占位避免菜单跳转 404
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.placeholder-card {
|
||||
min-height: calc(100vh - 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
color: #595959;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.placeholder-sub {
|
||||
margin-top: 4px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.placeholder-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,224 @@
|
||||
<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: 1000 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-input-search
|
||||
v-model:value="searchText"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
placeholder="姓名/学号/学员编号"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<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 === 'totalScore'">
|
||||
<span class="score-text">{{ record.totalScore ?? 0 }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a @click="openScore(record)">评分</a>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<a-drawer
|
||||
v-model:visible="showDetail"
|
||||
:title="`${current?.studentName || ''} · 考核评分`"
|
||||
width="820"
|
||||
>
|
||||
<a-space style="margin-bottom: 16px">
|
||||
<a-button type="primary" @click="openAddScore">新增评分</a-button>
|
||||
<a-tag color="blue">累计 {{ current?.totalScore ?? 0 }} 分</a-tag>
|
||||
</a-space>
|
||||
<a-table
|
||||
row-key="id"
|
||||
size="small"
|
||||
:data-source="scores"
|
||||
:columns="scoreColumns"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a @click="editScore(record)">编辑</a>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-drawer>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="showScoreModal"
|
||||
title="录入考核评分"
|
||||
:confirm-loading="submitting"
|
||||
width="520px"
|
||||
@ok="saveScore"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="评分维度" required>
|
||||
<a-select
|
||||
v-model:value="form.dimension"
|
||||
:options="dimensionOptions"
|
||||
placeholder="请选择维度"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="评分项">
|
||||
<a-input v-model:value="form.item" allow-clear placeholder="如 理论学习" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="得分" required>
|
||||
<a-input-number v-model:value="form.score" :min="0" class="ele-fluid" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="满分">
|
||||
<a-input-number v-model:value="form.maxScore" :min="0" class="ele-fluid" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="评语">
|
||||
<a-textarea v-model:value="form.comment" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
pageQmgcStudents,
|
||||
listQmgcScores,
|
||||
saveQmgcScore
|
||||
} from '@/api/growth/qmgc';
|
||||
import type { QmgcStudent, QmgcScore } from '@/api/growth/qmgc';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const showDetail = ref(false);
|
||||
const showScoreModal = ref(false);
|
||||
const submitting = ref(false);
|
||||
const current = ref<QmgcStudent | null>(null);
|
||||
const scores = ref<QmgcScore[]>([]);
|
||||
const form = ref<Partial<QmgcScore>>({});
|
||||
|
||||
const dimensionOptions = [
|
||||
{ label: '理论学习', value: '理论学习' },
|
||||
{ label: '实践锻炼', value: '实践锻炼' },
|
||||
{ label: '日常表现', value: '日常表现' },
|
||||
{ label: '考核考试', value: '考核考试' }
|
||||
];
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{ title: '姓名', dataIndex: 'studentName', key: 'studentName', width: 110 },
|
||||
{ title: '学号', dataIndex: 'studentNo', key: 'studentNo', width: 140 },
|
||||
{ title: '期次', dataIndex: 'batch', key: 'batch', width: 160 },
|
||||
{ title: '阶段', dataIndex: 'stage', key: 'stage', width: 100 },
|
||||
{ title: '累计得分', key: 'totalScore', width: 110, align: 'center' },
|
||||
{ title: '操作', key: 'action', width: 90, fixed: 'right', align: 'center', hideInSetting: true }
|
||||
]);
|
||||
|
||||
const scoreColumns: ColumnItem[] = [
|
||||
{ title: '维度', dataIndex: 'dimension', key: 'dimension', width: 130 },
|
||||
{ title: '评分项', dataIndex: 'item', key: 'item', width: 160 },
|
||||
{ title: '得分', dataIndex: 'score', key: 'score', width: 90 },
|
||||
{ title: '满分', dataIndex: 'maxScore', key: 'maxScore', width: 90 },
|
||||
{ title: '评语', dataIndex: 'comment', key: 'comment', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 80 }
|
||||
];
|
||||
|
||||
const datasource = ({ page, limit, where, orders }: any) => {
|
||||
return pageQmgcStudents({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit,
|
||||
keywords: searchText.value || undefined
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => tableRef.value?.reload?.({ page: 1 });
|
||||
|
||||
const resetSearch = () => {
|
||||
searchText.value = '';
|
||||
reload();
|
||||
};
|
||||
|
||||
const openScore = async (record: QmgcStudent) => {
|
||||
current.value = record;
|
||||
showDetail.value = true;
|
||||
await loadScores();
|
||||
};
|
||||
|
||||
const loadScores = async () => {
|
||||
if (!current.value?.id) return;
|
||||
try {
|
||||
scores.value = await listQmgcScores(current.value.id);
|
||||
} catch (e: any) {
|
||||
scores.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const openAddScore = () => {
|
||||
form.value = { maxScore: 100 };
|
||||
showScoreModal.value = true;
|
||||
};
|
||||
|
||||
const editScore = (row: QmgcScore) => {
|
||||
form.value = { ...row };
|
||||
showScoreModal.value = true;
|
||||
};
|
||||
|
||||
const saveScore = async () => {
|
||||
if (!current.value?.id) return;
|
||||
if (!form.value.dimension) {
|
||||
message.warning('请选择评分维度');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await saveQmgcScore({
|
||||
...form.value,
|
||||
studentId: current.value.id
|
||||
} as QmgcScore);
|
||||
message.success('保存成功');
|
||||
showScoreModal.value = false;
|
||||
await loadScores();
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '保存失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.score-text {
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<!-- 青马学员编辑 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="680"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '编辑青马学员' : '新增青马学员'"
|
||||
:body-style="{ paddingBottom: '24px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="姓名" name="studentName">
|
||||
<a-input v-model:value="form.studentName" allow-clear placeholder="请输入姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="学号" name="studentNo">
|
||||
<a-input v-model:value="form.studentNo" allow-clear placeholder="请输入学号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="学院" name="collegeName">
|
||||
<a-input v-model:value="form.collegeName" allow-clear placeholder="请输入学院" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="班级" name="className">
|
||||
<a-input v-model:value="form.className" allow-clear placeholder="请输入班级" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="期次" name="batch">
|
||||
<a-input v-model:value="form.batch" allow-clear placeholder="如 2026 年第 1 期" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="年度" name="year">
|
||||
<a-input-number v-model:value="form.year" :min="2000" :max="2100" class="ele-fluid" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="学员编号" name="candidateNo">
|
||||
<a-input v-model:value="form.candidateNo" allow-clear placeholder="请输入学员编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="当前阶段" name="stage">
|
||||
<a-select v-model:value="form.stage" :options="stageOptions" placeholder="请选择阶段" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="培育结果" name="result">
|
||||
<a-select v-model:value="form.result" allow-clear :options="resultOptions" 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="2">结业</a-radio>
|
||||
<a-radio :value="0">退出</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="form.remark" :rows="2" allow-clear placeholder="选填" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { addQmgcStudent, updateQmgcStudent } from '@/api/growth/qmgc';
|
||||
import type { QmgcStudent } from '@/api/growth/qmgc';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: QmgcStudent | null;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const form = reactive<QmgcStudent>({});
|
||||
|
||||
const stageOptions = [
|
||||
{ label: '选拔', value: 'selection' },
|
||||
{ label: '学习', value: 'study' },
|
||||
{ label: '实践', value: 'practice' },
|
||||
{ label: '考核', value: 'exam' },
|
||||
{ label: '结业', value: 'graduated' }
|
||||
];
|
||||
const resultOptions = [
|
||||
{ label: '在培', value: 'ongoing' },
|
||||
{ label: '通过', value: 'passed' },
|
||||
{ label: '未通过', value: 'failed' },
|
||||
{ label: '退出', value: 'quit' }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
studentName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
studentNo: [{ required: true, message: '请输入学号', trigger: 'blur' }]
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(value) => {
|
||||
if (!value) return;
|
||||
isUpdate.value = !!props.data?.id;
|
||||
Object.keys(form).forEach((key) => delete (form as any)[key]);
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
} else {
|
||||
form.status = 1;
|
||||
form.stage = 'selection';
|
||||
form.result = 'ongoing';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => emit('update:visible', value);
|
||||
|
||||
const save = () => {
|
||||
formRef.value
|
||||
?.validate?.()
|
||||
.then(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const msg = isUpdate.value
|
||||
? await updateQmgcStudent(form)
|
||||
: await addQmgcStudent(form);
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,404 @@
|
||||
<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: 1300 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button
|
||||
v-if="hasPermission('gxmu:qmgcStudent:save')"
|
||||
type="primary"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon><plus-outlined /></template>
|
||||
新增学员
|
||||
</a-button>
|
||||
<a-input-search
|
||||
v-model:value="searchText"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
placeholder="姓名/学号/学员编号"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="batch"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
placeholder="期次"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="stage"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
placeholder="阶段"
|
||||
:options="stageOptions"
|
||||
@change="reload"
|
||||
/>
|
||||
<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 === 'stage'">
|
||||
<a-tag color="blue">{{ stageLabel(record.stage) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : record.status === 2 ? 'blue' : 'default'">
|
||||
{{ record.status === 1 ? '在培' : record.status === 2 ? '结业' : '退出' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">档案</a>
|
||||
<a-divider type="vertical" />
|
||||
<a v-if="hasPermission('gxmu:qmgcStudent:update')" @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
v-if="hasPermission('gxmu:qmgcStudent:remove')"
|
||||
title="确定要删除这个学员档案吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<QmgcStudentEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
|
||||
<a-drawer v-model:visible="showDetail" title="青马培养档案" width="880">
|
||||
<template v-if="detail">
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
<a-descriptions-item label="姓名">{{ student?.studentName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="学号">{{ student?.studentNo }}</a-descriptions-item>
|
||||
<a-descriptions-item label="期次">{{ student?.batch || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="学员编号">{{ student?.candidateNo || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前阶段">{{ stageLabel(student?.stage) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="累计得分">{{ student?.totalScore ?? 0 }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-tabs style="margin-top: 16px">
|
||||
<a-tab-pane key="material" tab="培养材料">
|
||||
<a-space style="margin-bottom: 12px">
|
||||
<a-button size="small" type="primary" @click="showMaterial = true">
|
||||
提交材料
|
||||
</a-button>
|
||||
</a-space>
|
||||
<a-empty v-if="!materials.length" description="暂无材料" />
|
||||
<a-list v-else :data-source="materials" item-layout="vertical">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta :title="item.title" :description="item.scoreComment || item.stage" />
|
||||
<template #actions>
|
||||
<a-tag :color="item.status === 1 ? 'green' : 'orange'">
|
||||
{{ item.status === 1 ? `已评分 ${item.score ?? 0}` : '待评分' }}
|
||||
</a-tag>
|
||||
<a @click="openScore(item)">评分</a>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="task" tab="任务进度">
|
||||
<a-empty v-if="!tasks.length" description="暂无任务" />
|
||||
<a-table
|
||||
v-else
|
||||
row-key="taskId"
|
||||
size="small"
|
||||
:data-source="tasks"
|
||||
:columns="taskColumns"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.assignStatus === 1 ? 'green' : 'orange'">
|
||||
{{ record.assignStatus === 1 ? '已完成' : '未完成' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a v-if="record.assignStatus !== 1" @click="completeTask(record)">标记完成</a>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="score" tab="考核评分">
|
||||
<a-table
|
||||
row-key="id"
|
||||
size="small"
|
||||
:data-source="scores"
|
||||
:columns="scoreColumns"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a @click="openScore(record)">编辑</a>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="showMaterial"
|
||||
title="提交培养材料"
|
||||
:confirm-loading="submitting"
|
||||
width="560px"
|
||||
@ok="submitMaterial"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="材料标题" required>
|
||||
<a-input v-model:value="materialForm.title" allow-clear placeholder="请输入标题" />
|
||||
</a-form-item>
|
||||
<a-form-item label="阶段">
|
||||
<a-select
|
||||
v-model:value="materialForm.stage"
|
||||
allow-clear
|
||||
:options="stageOptions"
|
||||
placeholder="请选择阶段"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="内容">
|
||||
<a-textarea v-model:value="materialForm.content" :rows="4" placeholder="请输入材料内容" />
|
||||
</a-form-item>
|
||||
<a-form-item label="附件">
|
||||
<MultiFileUpload v-model="materialFile" :max-count="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="showScoreModal"
|
||||
title="评分"
|
||||
:confirm-loading="submitting"
|
||||
width="480px"
|
||||
@ok="submitScore"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="得分" required>
|
||||
<a-input-number v-model:value="scoreValue" :min="0" :max="100" class="ele-fluid" />
|
||||
</a-form-item>
|
||||
<a-form-item label="评语">
|
||||
<a-textarea v-model:value="scoreComment" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, 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 } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import QmgcStudentEdit from './components/edit.vue';
|
||||
import MultiFileUpload from '@/components/MultiFileUpload/index.vue';
|
||||
import {
|
||||
pageQmgcStudents,
|
||||
removeQmgcStudent,
|
||||
getQmgcStudentDetail,
|
||||
submitQmgcMaterial,
|
||||
scoreQmgcMaterial,
|
||||
completeQmgcTask
|
||||
} from '@/api/growth/qmgc';
|
||||
import type { QmgcStudent, QmgcMaterial, QmgcScore } from '@/api/growth/qmgc';
|
||||
import { hasPermission } from '@/utils/permission';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const searchText = ref('');
|
||||
const batch = ref('');
|
||||
const stage = ref<string>();
|
||||
const showEdit = ref(false);
|
||||
const showDetail = 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 materialForm = ref<Partial<QmgcMaterial>>({});
|
||||
const materialFile = ref('');
|
||||
const submitting = ref(false);
|
||||
const scoreValue = ref(0);
|
||||
const scoreComment = ref('');
|
||||
const scoreTargetId = ref<number>();
|
||||
|
||||
const stageOptions = [
|
||||
{ label: '选拔', value: 'selection' },
|
||||
{ label: '学习', value: 'study' },
|
||||
{ label: '实践', value: 'practice' },
|
||||
{ label: '考核', value: 'exam' },
|
||||
{ label: '结业', value: 'graduated' }
|
||||
];
|
||||
|
||||
const student = computed(() => detail.value?.student || null);
|
||||
const materials = computed<QmgcMaterial[]>(() => detail.value?.materials || []);
|
||||
const tasks = computed(() => detail.value?.tasks || []);
|
||||
const scores = computed<QmgcScore[]>(() => detail.value?.scores || []);
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{ title: '姓名', dataIndex: 'studentName', key: 'studentName', width: 110 },
|
||||
{ title: '学号', dataIndex: 'studentNo', key: 'studentNo', width: 140 },
|
||||
{ title: '学院', dataIndex: 'collegeName', key: 'collegeName', width: 160 },
|
||||
{ title: '期次', dataIndex: 'batch', key: 'batch', width: 160 },
|
||||
{ title: '年度', dataIndex: 'year', key: 'year', width: 90, align: 'center' },
|
||||
{ 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 }
|
||||
]);
|
||||
|
||||
const taskColumns: ColumnItem[] = [
|
||||
{ title: '任务', key: 'title', ellipsis: true },
|
||||
{ title: '类型', key: 'type', width: 90 },
|
||||
{ title: '截止', key: 'deadline', width: 170 },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{ title: '操作', key: 'action', width: 100 }
|
||||
];
|
||||
|
||||
const scoreColumns: ColumnItem[] = [
|
||||
{ title: '维度', dataIndex: 'dimension', key: 'dimension', width: 140 },
|
||||
{ title: '评分项', dataIndex: 'item', key: 'item', width: 180 },
|
||||
{ title: '得分', dataIndex: 'score', key: 'score', width: 90 },
|
||||
{ title: '评语', dataIndex: 'comment', key: 'comment', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 80 }
|
||||
];
|
||||
|
||||
const datasource = ({ page, limit, where, orders }: any) => {
|
||||
return pageQmgcStudents({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit,
|
||||
batch: batch.value || undefined,
|
||||
stage: stage.value,
|
||||
keywords: searchText.value || undefined
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => tableRef.value?.reload?.({ page: 1 });
|
||||
|
||||
const resetSearch = () => {
|
||||
searchText.value = '';
|
||||
batch.value = '';
|
||||
stage.value = undefined;
|
||||
reload();
|
||||
};
|
||||
|
||||
const openEdit = (row?: QmgcStudent) => {
|
||||
current.value = row || null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openDetail = async (row: QmgcStudent) => {
|
||||
current.value = row;
|
||||
if (!row.id) return;
|
||||
try {
|
||||
detail.value = await getQmgcStudentDetail(row.id);
|
||||
showDetail.value = true;
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载档案失败');
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (row: QmgcStudent) => {
|
||||
if (!row.id) return;
|
||||
try {
|
||||
const msg = await removeQmgcStudent(row.id);
|
||||
message.success(msg);
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const reloadDetail = async () => {
|
||||
if (!current.value?.id) return;
|
||||
detail.value = await getQmgcStudentDetail(current.value.id);
|
||||
};
|
||||
|
||||
const submitMaterial = async () => {
|
||||
if (!materialForm.value.title?.trim()) {
|
||||
message.warning('请填写材料标题');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await submitQmgcMaterial({
|
||||
studentId: current.value?.id,
|
||||
title: materialForm.value.title,
|
||||
stage: materialForm.value.stage,
|
||||
content: materialForm.value.content,
|
||||
fileUrl: materialFile.value || undefined
|
||||
});
|
||||
message.success('提交成功');
|
||||
showMaterial.value = false;
|
||||
materialForm.value = {};
|
||||
materialFile.value = '';
|
||||
await reloadDetail();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '提交失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openScore = (item: any) => {
|
||||
scoreTargetId.value = item.id;
|
||||
scoreValue.value = item.score ?? 0;
|
||||
scoreComment.value = item.scoreComment || item.comment || '';
|
||||
showScoreModal.value = true;
|
||||
};
|
||||
|
||||
const submitScore = async () => {
|
||||
if (!scoreTargetId.value) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
await scoreQmgcMaterial(scoreTargetId.value, Number(scoreValue.value), scoreComment.value);
|
||||
message.success('评分成功');
|
||||
showScoreModal.value = false;
|
||||
await reloadDetail();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '评分失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const completeTask = async (record: any) => {
|
||||
if (!current.value?.id) return;
|
||||
try {
|
||||
const msg = await completeQmgcTask(current.value.id, record.task?.id);
|
||||
message.success(msg);
|
||||
await reloadDetail();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const stageLabel = (value?: string) => {
|
||||
const map: Record<string, string> = {
|
||||
selection: '选拔',
|
||||
study: '学习',
|
||||
practice: '实践',
|
||||
exam: '考核',
|
||||
graduated: '结业'
|
||||
};
|
||||
return map[value ?? ''] ?? value ?? '-';
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,256 @@
|
||||
<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: 1100 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" @click="openEdit()">
|
||||
<template #icon><plus-outlined /></template>
|
||||
新增任务
|
||||
</a-button>
|
||||
<a-input
|
||||
v-model:value="batch"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
placeholder="期次"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="type"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
placeholder="任务类型"
|
||||
:options="typeOptions"
|
||||
@change="reload"
|
||||
/>
|
||||
<a-input-search
|
||||
v-model:value="searchText"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
placeholder="任务标题"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<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 === 'type'">
|
||||
<a-tag :color="record.type === 'online' ? 'blue' : 'purple'">
|
||||
{{ record.type === 'online' ? '线上' : '线下' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'mustDo'">
|
||||
<a-tag :color="record.mustDo === 1 ? 'red' : 'default'">
|
||||
{{ record.mustDo === 1 ? '必修' : '选修' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'default'">
|
||||
{{ 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>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="showEdit"
|
||||
:title="current?.id ? '编辑任务' : '新增任务'"
|
||||
:confirm-loading="submitting"
|
||||
width="600px"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="任务标题" required>
|
||||
<a-input v-model:value="form.title" allow-clear placeholder="请输入任务标题" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="适用期次">
|
||||
<a-input v-model:value="form.batch" allow-clear placeholder="空表示全部" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="阶段">
|
||||
<a-select v-model:value="form.stage" allow-clear :options="stageOptions" placeholder="请选择" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="任务类型">
|
||||
<a-radio-group v-model:value="form.type">
|
||||
<a-radio value="online">线上</a-radio>
|
||||
<a-radio value="offline">线下</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="是否必修">
|
||||
<a-switch :checked="form.mustDo === 1" @change="(v) => (form.mustDo = v ? 1 : 0)" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item v-if="form.type === 'online'" label="跳转地址">
|
||||
<a-input v-model:value="form.linkPath" allow-clear placeholder="如 /growth/credit" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="完成可得学分">
|
||||
<a-input-number v-model:value="form.score" :min="0" class="ele-fluid" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="截止时间">
|
||||
<a-date-picker
|
||||
v-model:value="deadline"
|
||||
show-time
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
class="ele-fluid"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="任务说明">
|
||||
<a-textarea v-model:value="form.content" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</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 { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
pageQmgcTasks,
|
||||
addQmgcTask,
|
||||
updateQmgcTask,
|
||||
removeQmgcTask
|
||||
} from '@/api/growth/qmgc';
|
||||
import type { QmgcTask } from '@/api/growth/qmgc';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const batch = ref('');
|
||||
const type = ref<string>();
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const submitting = ref(false);
|
||||
const current = ref<QmgcTask | null>(null);
|
||||
const form = ref<Partial<QmgcTask>>({});
|
||||
const deadline = ref<any>();
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '线上', value: 'online' },
|
||||
{ label: '线下', value: 'offline' }
|
||||
];
|
||||
const stageOptions = [
|
||||
{ label: '选拔', value: 'selection' },
|
||||
{ label: '学习', value: 'study' },
|
||||
{ label: '实践', value: 'practice' },
|
||||
{ label: '考核', value: 'exam' },
|
||||
{ label: '结业', value: 'graduated' }
|
||||
];
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{ title: '任务标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
||||
{ title: '期次', dataIndex: 'batch', key: 'batch', width: 160 },
|
||||
{ title: '阶段', dataIndex: 'stage', key: 'stage', width: 100 },
|
||||
{ title: '类型', key: 'type', width: 90 },
|
||||
{ title: '必修', key: 'mustDo', width: 90 },
|
||||
{ title: '学分', dataIndex: 'score', key: 'score', width: 90 },
|
||||
{
|
||||
title: '截止时间',
|
||||
dataIndex: 'deadline',
|
||||
key: 'deadline',
|
||||
width: 170,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm')
|
||||
},
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{ title: '操作', key: 'action', width: 130, fixed: 'right' }
|
||||
]);
|
||||
|
||||
const datasource = ({ page, limit, where, orders }: any) => {
|
||||
return pageQmgcTasks({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit,
|
||||
batch: batch.value || undefined,
|
||||
type: type.value,
|
||||
keywords: searchText.value || undefined
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => tableRef.value?.reload?.({ page: 1 });
|
||||
|
||||
const resetSearch = () => {
|
||||
batch.value = '';
|
||||
type.value = undefined;
|
||||
searchText.value = '';
|
||||
reload();
|
||||
};
|
||||
|
||||
const openEdit = (row?: QmgcTask) => {
|
||||
current.value = row || null;
|
||||
form.value = row ? { ...row } : { type: 'online', mustDo: 1, status: 1 };
|
||||
deadline.value = row?.deadline;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!form.value.title?.trim()) {
|
||||
message.warning('请输入任务标题');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
form.value.deadline = deadline.value;
|
||||
const msg = current.value?.id
|
||||
? await updateQmgcTask(form.value as QmgcTask)
|
||||
: await addQmgcTask(form.value as QmgcTask);
|
||||
message.success(msg);
|
||||
showEdit.value = false;
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '保存失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (row: QmgcTask) => {
|
||||
if (!row.id) return;
|
||||
try {
|
||||
const msg = await removeQmgcTask(row.id);
|
||||
message.success(msg);
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" class="placeholder-card">
|
||||
<a-result status="info" title="AI辅助教学设计" sub-title="该功能规划在 P1 阶段实施">
|
||||
<template #extra>
|
||||
<a-space direction="vertical" size="12" style="text-align: left; max-width: 640px">
|
||||
<div class="placeholder-desc">输入团课/青马课程主题,生成 PPT 大纲、课堂讨论题与随堂测验。</div>
|
||||
<div class="placeholder-sub">实施依赖:</div>
|
||||
<ul class="placeholder-list">
|
||||
<li>Dify 教学设计工作流</li>
|
||||
<li>AI 生成页外壳组件</li>
|
||||
</ul>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-result>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// AI辅助教学设计:P1 阶段交付,当前先占位避免菜单跳转 404
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.placeholder-card {
|
||||
min-height: calc(100vh - 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
color: #595959;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.placeholder-sub {
|
||||
margin-top: 4px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.placeholder-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,293 @@
|
||||
<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 :size="10" style="flex-wrap: wrap">
|
||||
<a-button
|
||||
v-if="hasPermission('gxmu:thematicCourse:save')"
|
||||
type="primary"
|
||||
@click="openEdit()"
|
||||
>
|
||||
<template #icon><plus-outlined /></template>
|
||||
新增资源
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('gxmu:thematicCourse:sync')"
|
||||
:loading="syncing"
|
||||
@click="doSync"
|
||||
>
|
||||
<template #icon><sync-outlined /></template>
|
||||
同步资源
|
||||
</a-button>
|
||||
<a-select
|
||||
v-model:value="type"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
placeholder="资源类型"
|
||||
:options="THEMATIC_TYPE_OPTIONS"
|
||||
@change="reload"
|
||||
/>
|
||||
<a-input-search
|
||||
v-model:value="searchText"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
placeholder="标题/摘要/正文"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<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 === 'type'">
|
||||
<a-tag color="blue">{{ typeLabel(record.type) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'title'">
|
||||
<a @click="openDetail(record)">{{ record.title }}</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDetail(record)">查看</a>
|
||||
<a-divider type="vertical" />
|
||||
<a v-if="hasPermission('gxmu:thematicCourse:save')" @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>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="showEdit"
|
||||
:title="current?.id ? '编辑团课资源' : '新增团课资源'"
|
||||
:confirm-loading="submitting"
|
||||
width="640px"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="标题" required>
|
||||
<a-input v-model:value="form.title" allow-clear placeholder="请输入标题" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="资源类型">
|
||||
<a-select v-model:value="form.type" :options="THEMATIC_TYPE_OPTIONS" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="标签">
|
||||
<a-input v-model:value="form.tags" allow-clear placeholder="逗号分隔" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="摘要">
|
||||
<a-textarea v-model:value="form.summary" :rows="2" />
|
||||
</a-form-item>
|
||||
<a-form-item label="正文/讲稿">
|
||||
<a-textarea v-model:value="form.content" :rows="6" />
|
||||
</a-form-item>
|
||||
<a-form-item label="附件">
|
||||
<MultiFileUpload v-model="fileUrl" :max-count="3" />
|
||||
</a-form-item>
|
||||
<a-form-item label="来源链接">
|
||||
<a-input v-model:value="form.sourceUrl" allow-clear placeholder="可选" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="showDetail"
|
||||
:title="current?.title || '团课资源详情'"
|
||||
:footer="null"
|
||||
width="820px"
|
||||
>
|
||||
<template v-if="current">
|
||||
<a-space style="margin-bottom: 12px">
|
||||
<a-tag color="blue">{{ typeLabel(current.type) }}</a-tag>
|
||||
<a-tag v-if="current.sourceSite">{{ current.sourceSite }}</a-tag>
|
||||
<a-tag v-if="current.tags">{{ current.tags }}</a-tag>
|
||||
<a-tag>{{ current.heat ?? 0 }} 次浏览</a-tag>
|
||||
</a-space>
|
||||
<div v-if="current.summary" class="detail-summary">{{ current.summary }}</div>
|
||||
<div class="detail-content" v-html="renderedContent"></div>
|
||||
<a-space v-if="current.fileUrl || current.sourceUrl" style="margin-top: 16px">
|
||||
<a-button v-if="current.fileUrl" type="primary" @click="openUrl(current.fileUrl)">
|
||||
下载附件
|
||||
</a-button>
|
||||
<a-button v-if="current.sourceUrl" @click="openUrl(current.sourceUrl)">查看原文</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined, SyncOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import MultiFileUpload from '@/components/MultiFileUpload/index.vue';
|
||||
import {
|
||||
pageThematicCourses,
|
||||
addThematicCourse,
|
||||
updateThematicCourse,
|
||||
removeThematicCourse,
|
||||
syncThematicCourses,
|
||||
THEMATIC_TYPE_OPTIONS
|
||||
} from '@/api/growth/thematic';
|
||||
import type { ThematicCourse } from '@/api/growth/thematic';
|
||||
import { hasPermission } from '@/utils/permission';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const type = ref<string>();
|
||||
const searchText = ref('');
|
||||
const showEdit = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const submitting = ref(false);
|
||||
const syncing = ref(false);
|
||||
const current = ref<ThematicCourse | null>(null);
|
||||
const form = ref<Partial<ThematicCourse>>({});
|
||||
const fileUrl = ref('');
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{ title: '标题', key: 'title', ellipsis: true },
|
||||
{ title: '类型', key: 'type', width: 110, align: 'center' },
|
||||
{ title: '来源', dataIndex: 'sourceSite', key: 'sourceSite', width: 140 },
|
||||
{ title: '标签', dataIndex: 'tags', key: 'tags', width: 180, ellipsis: true },
|
||||
{ title: '热度', dataIndex: 'heat', key: 'heat', width: 90, align: 'center' },
|
||||
{
|
||||
title: '同步时间',
|
||||
dataIndex: 'lastSyncTime',
|
||||
key: 'lastSyncTime',
|
||||
width: 160,
|
||||
customRender: ({ text }) => (text ? toDateString(text, 'yyyy-MM-dd HH:mm') : '-')
|
||||
},
|
||||
{ title: '操作', key: 'action', width: 150, fixed: 'right', align: 'center' }
|
||||
]);
|
||||
|
||||
const datasource = ({ page, limit, where, orders }: any) => {
|
||||
return pageThematicCourses({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit,
|
||||
type: type.value,
|
||||
keywords: searchText.value || undefined
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => tableRef.value?.reload?.({ page: 1 });
|
||||
|
||||
const resetSearch = () => {
|
||||
type.value = undefined;
|
||||
searchText.value = '';
|
||||
reload();
|
||||
};
|
||||
|
||||
const openEdit = (row?: ThematicCourse) => {
|
||||
current.value = row || null;
|
||||
form.value = row ? { ...row } : { type: 'courseware' };
|
||||
fileUrl.value = row?.fileUrl || '';
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openDetail = (row: ThematicCourse) => {
|
||||
current.value = row;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!form.value.title?.trim()) {
|
||||
message.warning('请输入标题');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
form.value.fileUrl = fileUrl.value || undefined;
|
||||
const msg = current.value?.id
|
||||
? await updateThematicCourse(form.value as ThematicCourse)
|
||||
: await addThematicCourse(form.value as ThematicCourse);
|
||||
message.success(msg);
|
||||
showEdit.value = false;
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '保存失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (row: ThematicCourse) => {
|
||||
if (!row.id) return;
|
||||
try {
|
||||
const msg = await removeThematicCourse(row.id);
|
||||
message.success(msg);
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const doSync = async () => {
|
||||
syncing.value = true;
|
||||
try {
|
||||
const msg = await syncThematicCourses();
|
||||
message.success(msg);
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '同步失败');
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openUrl = (url?: string) => {
|
||||
if (url) window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const typeLabel = (value?: string) =>
|
||||
THEMATIC_TYPE_OPTIONS.find((item) => item.value === value)?.label ?? value ?? '-';
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
const content = current.value?.content || '';
|
||||
if (!content) return '<p class="ele-text-secondary">暂无正文</p>';
|
||||
// 简单换行保留,避免引入额外 markdown 依赖
|
||||
return content
|
||||
.split('\n')
|
||||
.map((line) => `<p>${line || ' '}</p>`)
|
||||
.join('');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.detail-summary {
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
line-height: 1.8;
|
||||
color: #262626;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" class="placeholder-card">
|
||||
<a-result status="info" title="AI主题团日设计" sub-title="该功能规划在 P1 阶段实施">
|
||||
<template #extra>
|
||||
<a-space direction="vertical" size="12" style="text-align: left; max-width: 640px">
|
||||
<div class="placeholder-desc">按活动主题与受众特征生成团日活动方案与学习资源。</div>
|
||||
<div class="placeholder-sub">实施依赖:</div>
|
||||
<ul class="placeholder-list">
|
||||
<li>Dify 主题团日工作流</li>
|
||||
<li>复用活动策划页壳</li>
|
||||
</ul>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-result>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// AI主题团日设计:P1 阶段交付,当前先占位避免菜单跳转 404
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.placeholder-card {
|
||||
min-height: calc(100vh - 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
color: #595959;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.placeholder-sub {
|
||||
margin-top: 4px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.placeholder-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.9;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,24 @@
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
|
||||
export function shouldHideEditByReviewList(reviewList?: ReviewList[]) {
|
||||
/**
|
||||
* 取最新一条审核记录。
|
||||
*
|
||||
* <p>统一排序规则:先按 sortNumber(并行组序号)降序,再按 createTime、id 降序。
|
||||
* 原先 reviewStatusTag 只按 id、这里只按 createTime+id,两处可能给出不同结论
|
||||
* (并行审核某组后审通过、前组已驳回时,状态标签与「能否编辑」会互相矛盾),
|
||||
* 现在收敛到这一处。
|
||||
*/
|
||||
export function getLatestReview(reviewList?: ReviewList[]): ReviewList | undefined {
|
||||
const list = reviewList ?? [];
|
||||
if (!list.length) {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
const latest = [...list].sort((a, b) => {
|
||||
return [...list].sort((a, b) => {
|
||||
const sortA = Number(a.sortNumber ?? 0);
|
||||
const sortB = Number(b.sortNumber ?? 0);
|
||||
if (sortA !== sortB) {
|
||||
return sortB - sortA;
|
||||
}
|
||||
const timeA = Date.parse(a.createTime || '');
|
||||
const timeB = Date.parse(b.createTime || '');
|
||||
const safeTimeA = Number.isFinite(timeA) ? timeA : 0;
|
||||
@@ -15,5 +28,15 @@ export function shouldHideEditByReviewList(reviewList?: ReviewList[]) {
|
||||
}
|
||||
return Number(b.id || 0) - Number(a.id || 0);
|
||||
})[0];
|
||||
return latest?.status !== 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最新记录不是驳回时,隐藏编辑入口(待审核或已通过都不允许改)。
|
||||
*/
|
||||
export function shouldHideEditByReviewList(reviewList?: ReviewList[]) {
|
||||
const list = reviewList ?? [];
|
||||
if (!list.length) {
|
||||
return false;
|
||||
}
|
||||
return getLatestReview(list)?.status !== 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user