forked from b2894lxlx/tuanwei-admin
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0444846422 | |||
| 42fddc5c85 | |||
| 6d19cb5a05 | |||
| 4b810a731c | |||
| 11e16ca311 | |||
| 3888438694 | |||
| c81e24c977 | |||
| 92d6bdf811 | |||
| fd443aced3 | |||
| 35e527ce53 |
@@ -0,0 +1,34 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult } from '@/api';
|
||||
|
||||
/**
|
||||
* 撤回申报:待审核 -> 草稿
|
||||
*/
|
||||
export async function withdrawDeclareRecord(module: string, id: number) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/gxmu/declare-record/withdraw',
|
||||
{ module, id }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 作废申报(等同强制放行,作废后申报人可重新申报)
|
||||
*/
|
||||
export async function voidDeclareRecord(
|
||||
module: string,
|
||||
id: number,
|
||||
reason?: string
|
||||
) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/gxmu/declare-record/void',
|
||||
{ module, id, reason }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
@@ -36,6 +36,18 @@ export interface Declare {
|
||||
createTime?: string;
|
||||
// 更新时间
|
||||
updateTime?: string;
|
||||
// 是否允许重复提交:0否 1是
|
||||
allowRepeat?: number;
|
||||
// 最多同时进行中的申报条数
|
||||
maxPending?: number;
|
||||
// 当前用户在该项目下的最新申报状态:0草稿 1待审核 2通过 3驳回 4作废
|
||||
myStatus?: number;
|
||||
// 当前用户在该项目下的最新申报记录id
|
||||
myRecordId?: number;
|
||||
// 当前用户在该项目下的最新申报记录提交时间
|
||||
mySubmitTime?: string;
|
||||
// 当前用户在该项目下的最新申报记录状态备注
|
||||
myStatusRemark?: string;
|
||||
// 删除标记:0未删 1已删
|
||||
deleted?: number;
|
||||
// 是否可申报
|
||||
@@ -55,4 +67,4 @@ export interface DeclareParam extends PageParam {
|
||||
formProjectGroup?: string;
|
||||
publicProjectType?: string;
|
||||
publicProjectGroup?: string;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export interface ReviewList {
|
||||
businessName?: string;
|
||||
// 主键
|
||||
pk?: number;
|
||||
// 状态(0不通过 1通过)
|
||||
// 状态(0驳回 1通过)
|
||||
status?: number;
|
||||
// 审核意见
|
||||
content?: string;
|
||||
|
||||
@@ -39,6 +39,8 @@ export interface FileRecordParam extends PageParam {
|
||||
name?: string;
|
||||
path?: string;
|
||||
contentType?: string;
|
||||
// 媒体大类: image/video/audio/doc,空表示不限
|
||||
mediaCategory?: string;
|
||||
createNickname?: string;
|
||||
count?: number;
|
||||
page?: number;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* 所以打开编辑弹窗时先把非 HTML 的正文转成 HTML,保存时统一写 `contentFormat: 'html'`。
|
||||
*/
|
||||
import { getProcessor } from 'bytemd';
|
||||
import type { BytemdPlugin } from 'bytemd';
|
||||
|
||||
const HTML_TAG_RE = /<\/?([a-z][a-z0-9]*)\b[^>]*>/i;
|
||||
const MARKDOWN_LINE_RE =
|
||||
@@ -57,11 +58,20 @@ export function detectFormat(content?: string | null): ContentFormat {
|
||||
return looksLikeMarkdown(text) ? 'markdown' : 'text';
|
||||
}
|
||||
|
||||
/** markdown → HTML,用的是后台已有的 bytemd(和 ByteMdViewer 同一套解析)。 */
|
||||
export function markdownToHtml(markdown: string): string {
|
||||
/**
|
||||
* markdown → HTML,用的是后台已有的 bytemd(和 ByteMdViewer 同一套解析)。
|
||||
*
|
||||
* `plugins` 决定润色到什么程度:默认不挂 GFM,表格会原样漏成一堆 `|` 管道符、
|
||||
* 删除线也会丢。要让「表格/删除线」和预览一致,调用方传 gfm 插件进来
|
||||
* (generate 页的一键排版就是这么做的)。转换口径只留这一处,别再各写一套。
|
||||
*/
|
||||
export function markdownToHtml(
|
||||
markdown: string,
|
||||
plugins: BytemdPlugin[] = []
|
||||
): string {
|
||||
try {
|
||||
// processSync 返回的是 vfile(`toString()` 即 HTML),不是裸字符串
|
||||
const result = getProcessor({}).processSync(String(markdown ?? ''));
|
||||
const result = getProcessor({ plugins }).processSync(String(markdown ?? ''));
|
||||
return String(result);
|
||||
} catch (e) {
|
||||
console.error('markdown 转换失败', e);
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { Component } from 'vue';
|
||||
import {
|
||||
CustomerServiceOutlined,
|
||||
FileExcelOutlined,
|
||||
FileMarkdownOutlined,
|
||||
FilePdfOutlined,
|
||||
FilePptOutlined,
|
||||
FileTextOutlined,
|
||||
FileUnknownOutlined,
|
||||
FileWordOutlined,
|
||||
VideoCameraOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
/**
|
||||
* 媒体大类。
|
||||
*
|
||||
* 注意它和 `contentType`(MIME 字符串)不是一回事:MIME 里「文档」没有统一前缀
|
||||
* (pdf/docx/zip 都是 application/…),所以大类由 MIME 前缀 + 后缀一起判定。
|
||||
*/
|
||||
export type MediaKind = 'image' | 'video' | 'audio' | 'doc';
|
||||
|
||||
/** 与后端 UploadRules 的白名单保持一致,改这里要同步改后端 */
|
||||
export const MEDIA_ACCEPT = [
|
||||
'image/*',
|
||||
'.pdf',
|
||||
'.doc',
|
||||
'.docx',
|
||||
'.xls',
|
||||
'.xlsx',
|
||||
'.ppt',
|
||||
'.pptx',
|
||||
'.txt',
|
||||
'.md',
|
||||
'.csv',
|
||||
'.mp3',
|
||||
'.wav',
|
||||
'.m4a',
|
||||
'.aac',
|
||||
'.mp4',
|
||||
'.mov',
|
||||
'.webm'
|
||||
].join(',');
|
||||
|
||||
const DOC_SUFFIXES = [
|
||||
'pdf',
|
||||
'doc',
|
||||
'docx',
|
||||
'xls',
|
||||
'xlsx',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'txt',
|
||||
'md',
|
||||
'csv'
|
||||
];
|
||||
const VIDEO_SUFFIXES = ['mp4', 'mov', 'webm'];
|
||||
const AUDIO_SUFFIXES = ['mp3', 'wav', 'm4a', 'aac'];
|
||||
|
||||
/** 图片大小上限(MB),其余类型上限(MB),受后端 multipart 100MB 硬顶约束 */
|
||||
export const MEDIA_MAX_MB_IMAGE = 10;
|
||||
export const MEDIA_MAX_MB_OTHER = 100;
|
||||
|
||||
/** 类型筛选项,value 为空表示不限 */
|
||||
export const MEDIA_CATEGORY_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: '', label: '全部类型' },
|
||||
{ value: 'image', label: '图片' },
|
||||
{ value: 'doc', label: '文档' },
|
||||
{ value: 'audio', label: '音频' },
|
||||
{ value: 'video', label: '视频' }
|
||||
];
|
||||
|
||||
export const MEDIA_KIND_LABEL: Record<MediaKind, string> = {
|
||||
image: '图片',
|
||||
video: '视频',
|
||||
audio: '音频',
|
||||
doc: '文档'
|
||||
};
|
||||
|
||||
/** 取小写后缀,不带点 */
|
||||
export function mediaSuffix(name?: string | null): string {
|
||||
if (!name || name.lastIndexOf('.') < 0) {
|
||||
return '';
|
||||
}
|
||||
return name.substring(name.lastIndexOf('.') + 1).toLowerCase();
|
||||
}
|
||||
|
||||
/** 由 MIME + 文件名判定大类 */
|
||||
export function mediaKindOf(
|
||||
contentType?: string | null,
|
||||
name?: string | null
|
||||
): MediaKind {
|
||||
const mime = (contentType || '').toLowerCase();
|
||||
if (mime.startsWith('image/')) {
|
||||
return 'image';
|
||||
}
|
||||
if (mime.startsWith('video/')) {
|
||||
return 'video';
|
||||
}
|
||||
if (mime.startsWith('audio/')) {
|
||||
return 'audio';
|
||||
}
|
||||
const suffix = mediaSuffix(name);
|
||||
if (VIDEO_SUFFIXES.includes(suffix)) {
|
||||
return 'video';
|
||||
}
|
||||
if (AUDIO_SUFFIXES.includes(suffix)) {
|
||||
return 'audio';
|
||||
}
|
||||
return 'doc';
|
||||
}
|
||||
|
||||
/** 由文件记录判定大类 */
|
||||
export function mediaKindOfRecord(record?: FileRecord | null): MediaKind {
|
||||
return mediaKindOf(record?.contentType, record?.path || record?.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前校验,通过返回 null,不通过返回给用户看的错误文案。
|
||||
* 与后端同规则:图片按 MIME 认,其余按后缀白名单认。
|
||||
*/
|
||||
export function validateMediaFile(file: File): string | null {
|
||||
const mime = (file.type || '').toLowerCase();
|
||||
const suffix = mediaSuffix(file.name);
|
||||
const isImage = mime.startsWith('image/');
|
||||
if (!isImage) {
|
||||
const allowed =
|
||||
DOC_SUFFIXES.includes(suffix) ||
|
||||
VIDEO_SUFFIXES.includes(suffix) ||
|
||||
AUDIO_SUFFIXES.includes(suffix);
|
||||
if (!allowed) {
|
||||
return '不支持的文件格式,仅支持图片、文档、音频、视频';
|
||||
}
|
||||
}
|
||||
const maxMb = isImage ? MEDIA_MAX_MB_IMAGE : MEDIA_MAX_MB_OTHER;
|
||||
if (file.size / 1024 / 1024 > maxMb) {
|
||||
return `文件大小不能超过 ${maxMb}MB`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 文件大小格式化 */
|
||||
export function formatFileSize(length?: number | null): string {
|
||||
const size = Number(length ?? 0);
|
||||
if (!size || size < 0) {
|
||||
return '-';
|
||||
}
|
||||
if (size < 1024) {
|
||||
return size + 'B';
|
||||
}
|
||||
if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(1) + 'KB';
|
||||
}
|
||||
if (size < 1024 * 1024 * 1024) {
|
||||
return (size / 1024 / 1024).toFixed(1) + 'M';
|
||||
}
|
||||
return (size / 1024 / 1024 / 1024).toFixed(1) + 'G';
|
||||
}
|
||||
|
||||
/** 卡片/表格里显示的类型图标 */
|
||||
export function mediaIcon(kind: MediaKind, name?: string | null): Component {
|
||||
if (kind === 'audio') {
|
||||
return CustomerServiceOutlined;
|
||||
}
|
||||
if (kind === 'video') {
|
||||
return VideoCameraOutlined;
|
||||
}
|
||||
switch (mediaSuffix(name)) {
|
||||
case 'pdf':
|
||||
return FilePdfOutlined;
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return FileWordOutlined;
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
case 'csv':
|
||||
return FileExcelOutlined;
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return FilePptOutlined;
|
||||
case 'md':
|
||||
return FileMarkdownOutlined;
|
||||
case 'txt':
|
||||
return FileTextOutlined;
|
||||
default:
|
||||
return FileUnknownOutlined;
|
||||
}
|
||||
}
|
||||
|
||||
/** 图标色,按类型区分但都压在中低饱和,避免卡片花花绿绿 */
|
||||
export function mediaColor(kind: MediaKind, name?: string | null): string {
|
||||
if (kind === 'audio') {
|
||||
return '#805ad5';
|
||||
}
|
||||
if (kind === 'video') {
|
||||
return '#2b6cb0';
|
||||
}
|
||||
switch (mediaSuffix(name)) {
|
||||
case 'pdf':
|
||||
return '#e5484d';
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return '#2b6cb0';
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
case 'csv':
|
||||
return '#2f855a';
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return '#c05621';
|
||||
default:
|
||||
return '#718096';
|
||||
}
|
||||
}
|
||||
|
||||
/** 非图片卡片右下角的扩展名徽标文本 */
|
||||
export function mediaExtLabel(record?: FileRecord | null): string {
|
||||
const suffix = mediaSuffix(record?.path || record?.name);
|
||||
return suffix ? suffix.toUpperCase() : 'FILE';
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
:color="item.status === 1 ? 'success' : item.status === 0 ? 'error' : 'default'"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ item.status === 1 ? '通过' : item.status === 0 ? '不通过' : '-' }}
|
||||
{{ item.status === 1 ? '通过' : item.status === 0 ? '驳回' : '-' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div v-if="item.user?.realName" style="margin-bottom: 4px">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<a-tag color="green">通过</a-tag>
|
||||
</template>
|
||||
<template v-else-if="getLastReview(record)?.status === 2">
|
||||
<a-tag color="red">不通过</a-tag>
|
||||
<a-tag color="red">驳回</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>未审核</a-tag>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<!-- 音频 / 视频预览弹窗:素材库里非图片项的统一播放器 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="kind === 'video' ? 880 : 480"
|
||||
:footer="null"
|
||||
:visible="visible"
|
||||
:title="record?.name"
|
||||
:destroy-on-close="true"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<div v-if="visible && record" class="media-preview">
|
||||
<ele-xg-player
|
||||
v-if="kind === 'video'"
|
||||
:key="'v-' + record.id"
|
||||
:config="playerConfig"
|
||||
/>
|
||||
<div v-else class="media-preview-audio">
|
||||
<customer-service-outlined class="media-preview-audio-icon" />
|
||||
<div class="media-preview-audio-name ele-text-secondary">
|
||||
{{ record.name }}
|
||||
</div>
|
||||
<audio
|
||||
:key="'a-' + record.id"
|
||||
:src="record.url"
|
||||
controls
|
||||
autoplay
|
||||
class="media-preview-audio-player"
|
||||
></audio>
|
||||
</div>
|
||||
</div>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { CustomerServiceOutlined } from '@ant-design/icons-vue';
|
||||
import type { FileRecord } from '@/api/system/file/model';
|
||||
import { mediaKindOfRecord } from '@/utils/media';
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 当前预览的文件
|
||||
record?: FileRecord | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const kind = computed(() => mediaKindOfRecord(props.record));
|
||||
|
||||
// xgplayer 配置,key 变化时重建实例,避免沿用上一条的播放状态
|
||||
const playerConfig = computed(() => ({
|
||||
id: 'cmsPhotoPreviewPlayer',
|
||||
lang: 'zh-cn',
|
||||
fluid: true,
|
||||
url: props.record?.url,
|
||||
playbackRate: [0.5, 1, 1.5, 2],
|
||||
pip: true
|
||||
}));
|
||||
|
||||
/* 更新 visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.media-preview-audio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 0 8px;
|
||||
}
|
||||
|
||||
.media-preview-audio-icon {
|
||||
font-size: 48px;
|
||||
color: #805ad5;
|
||||
}
|
||||
|
||||
.media-preview-audio-name {
|
||||
margin: 12px 0 20px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.media-preview-audio-player {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,10 @@
|
||||
<!-- 角色编辑弹窗 -->
|
||||
<!-- 素材编辑弹窗:文件在列表页上传时已入库,这里只改元数据和分享代码 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="600"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '编辑' : '上传文件'"
|
||||
title="编辑素材"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
okText="保存"
|
||||
@update:visible="updateVisible"
|
||||
@@ -19,27 +19,22 @@
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="上传文件" name="fileName" v-if="!isUpdate">
|
||||
<span
|
||||
class="ele-text-success"
|
||||
v-if="fileName"
|
||||
style="margin-right: 10px"
|
||||
>
|
||||
{{ fileName }}
|
||||
</span>
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
:accept="'video/*'"
|
||||
v-if="!fileName"
|
||||
:customRequest="onUpload"
|
||||
>
|
||||
<a-button type="primary" class="ele-btn-icon">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>上传文件</span>
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<a-form-item label="文件">
|
||||
<div class="media-file">
|
||||
<component
|
||||
:is="fileIcon"
|
||||
class="media-file-icon"
|
||||
:style="{ color: fileColor }"
|
||||
/>
|
||||
<div class="media-file-info">
|
||||
<div class="media-file-name" :title="record?.name">
|
||||
{{ record?.name }}
|
||||
</div>
|
||||
<div class="ele-text-secondary media-file-sub">
|
||||
{{ kindLabel }} · {{ fileSize }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="文件名称" name="name">
|
||||
<a-input
|
||||
@@ -53,17 +48,17 @@
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="图片描述"
|
||||
placeholder="素材描述"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</a-form-item>
|
||||
<div style="margin-bottom: 22px">
|
||||
<a-divider orientation="left">图片分享</a-divider>
|
||||
<a-divider orientation="left">{{ shareTitle }}</a-divider>
|
||||
</div>
|
||||
<a-form-item label="图片URL链接">
|
||||
<a-form-item label="素材链接">
|
||||
<a-input v-model:value="share.url" @click="copyText(share.url)" />
|
||||
</a-form-item>
|
||||
<a-form-item label="网页代码(HTML)">
|
||||
<a-form-item label="网页代码(HTML)" v-if="share.html">
|
||||
<a-input v-model:value="share.html" @click="copyText(share.html)" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Markdown">
|
||||
@@ -72,38 +67,36 @@
|
||||
@click="copyText(share.Markdown)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="论坛BBCode">
|
||||
<a-form-item label="论坛BBCode" v-if="share.bbCode">
|
||||
<a-input v-model:value="share.bbCode" @click="copyText(share.bbCode)" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- <a-form-item label="封面图" name="images">-->
|
||||
<!-- <ele-image-upload-->
|
||||
<!-- v-model:value="images"-->
|
||||
<!-- :limit="1"-->
|
||||
<!-- :drag="true"-->
|
||||
<!-- :item-style="{ width: '60px', height: '60px' }"-->
|
||||
<!-- :accept="'image/*'"-->
|
||||
<!-- :upload-handler="uploadHandler"-->
|
||||
<!-- @upload="onUpload"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
<a-form-item label="下载地址">
|
||||
<a-input
|
||||
v-model:value="share.download"
|
||||
@click="copyText(share.download)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { computed, reactive, ref, 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 { FileRecord } from '@/api/system/file/model';
|
||||
import { messageLoading } from 'ele-admin-pro';
|
||||
import { addFiles, updateFiles, uploadFile } from '@/api/system/file';
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { RuleObject } from 'ant-design-vue/es/form';
|
||||
import { updateFiles } from '@/api/system/file';
|
||||
import { copyText } from '@/utils/common';
|
||||
import {
|
||||
formatFileSize,
|
||||
mediaColor,
|
||||
mediaIcon,
|
||||
mediaKindOfRecord,
|
||||
MEDIA_KIND_LABEL
|
||||
} from '@/utils/media';
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
@@ -121,20 +114,11 @@
|
||||
data?: FileRecord | null;
|
||||
}>();
|
||||
|
||||
//
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const fileName = ref('');
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
// 当前编辑的原始记录,分享代码依赖它的 url,不随表单变化
|
||||
const record = ref<FileRecord | null>(null);
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 图片资源分享
|
||||
const share = ref({
|
||||
url: '',
|
||||
Markdown: '',
|
||||
html: '',
|
||||
bbCode: ''
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const { form, resetFields, assignFields } = useFormData<FileRecord>({
|
||||
@@ -143,22 +127,49 @@
|
||||
comments: ''
|
||||
});
|
||||
|
||||
const kind = computed(() => mediaKindOfRecord(record.value));
|
||||
const kindLabel = computed(() => MEDIA_KIND_LABEL[kind.value]);
|
||||
const fileSize = computed(() => formatFileSize(record.value?.length));
|
||||
const fileIcon = computed(() =>
|
||||
mediaIcon(kind.value, record.value?.path || record.value?.name)
|
||||
);
|
||||
const fileColor = computed(() =>
|
||||
mediaColor(kind.value, record.value?.path || record.value?.name)
|
||||
);
|
||||
// 文档给的是链接与下载地址,图片/音视频给的是嵌入代码
|
||||
const shareTitle = computed(() =>
|
||||
kind.value === 'doc' ? '素材链接' : '素材分享'
|
||||
);
|
||||
|
||||
/**
|
||||
* 分享代码按类型生成:图片沿用 img 系列,音视频给 video/audio 标签,
|
||||
* 文档只给直链和下载地址(不提供 HTML 嵌入)。
|
||||
*/
|
||||
const share = computed(() => {
|
||||
const data = record.value;
|
||||
const url = data?.url || '';
|
||||
const name = data?.name || '';
|
||||
const result = {
|
||||
url,
|
||||
html: '',
|
||||
Markdown: `[${name}](${url})`,
|
||||
bbCode: `[url=${url}]${name}[/url]`,
|
||||
download: data?.downloadUrl || ''
|
||||
};
|
||||
if (kind.value === 'image') {
|
||||
result.html = `<a href="${url}"><img src="${url}" alt="${name}" border="0" /></a>`;
|
||||
result.Markdown = `[](${url})`;
|
||||
result.bbCode = `[url=${url}][img]${url}[/img][/url]`;
|
||||
} else if (kind.value === 'video') {
|
||||
result.html = `<video src="${url}" controls></video>`;
|
||||
} else if (kind.value === 'audio') {
|
||||
result.html = `<audio src="${url}" controls></audio>`;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<Record<string, Rule[]>>({
|
||||
fileName: [
|
||||
{
|
||||
required: true,
|
||||
message: '请上传文件',
|
||||
type: 'string',
|
||||
trigger: 'blur',
|
||||
validator: async (_rule: RuleObject) => {
|
||||
if (!isUpdate.value && fileName.value.length == 0) {
|
||||
return Promise.reject('请上传文件');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
],
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
@@ -169,39 +180,6 @@
|
||||
]
|
||||
});
|
||||
|
||||
// 上传文件
|
||||
const onUpload = (item) => {
|
||||
const { file } = item;
|
||||
if (!file.type.startsWith('video')) {
|
||||
message.error('文件格式不正确!');
|
||||
return;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 100) {
|
||||
message.error('大小不能超过 100MB');
|
||||
return;
|
||||
}
|
||||
const hide = messageLoading({
|
||||
content: '上传中..',
|
||||
duration: 0,
|
||||
mask: true
|
||||
});
|
||||
uploadFile(file)
|
||||
.then((data) => {
|
||||
hide();
|
||||
fileName.value = String(data.name);
|
||||
// images.value.push({
|
||||
// uid: data.id,
|
||||
// url: FILE_THUMBNAIL + data.path,
|
||||
// status: 'done'
|
||||
// });
|
||||
message.success('上传成功');
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
hide();
|
||||
});
|
||||
};
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
@@ -211,8 +189,7 @@
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateFiles : addFiles;
|
||||
saveOrUpdate(form)
|
||||
updateFiles(form)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
@@ -236,21 +213,44 @@
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
record.value = props.data ?? null;
|
||||
if (props.data) {
|
||||
assignFields(props.data);
|
||||
// 图片分享代码
|
||||
share.value.url = `${props.data.url}`;
|
||||
share.value.html = `<a href="${props.data.url}"><img src="${props.data.url}" alt="${props.data.name}" border="0" /></a>`;
|
||||
share.value.Markdown = `[](${props.data.url})`;
|
||||
share.value.bbCode = `[url=${props.data.url}][img]${props.data.url}[/img][/url]`;
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
record.value = null;
|
||||
resetFields();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.media-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.media-file-icon {
|
||||
flex: none;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.media-file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.media-file-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.media-file-sub {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+131
-28
@@ -1,9 +1,10 @@
|
||||
<!-- 素材库 · 网格视图 -->
|
||||
<template>
|
||||
<div class="page">
|
||||
<a-row :gutter="20">
|
||||
<a-col
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
v-for="item in data"
|
||||
:key="item.id"
|
||||
v-bind="
|
||||
styleResponsive
|
||||
? { xl: 4, lg: 8, md: 12, sm: 12, xs: 24 }
|
||||
@@ -12,10 +13,33 @@
|
||||
>
|
||||
<a-card :bordered="false" hoverable style="margin-top: 16px">
|
||||
<template #cover>
|
||||
<a-image :src="item.url" alt="" />
|
||||
<!-- 统一 4:3 封面盒子:图片裁剪填充,其余类型放类型图标 -->
|
||||
<div class="media-cover" @click="openPreview(item)">
|
||||
<a-image
|
||||
v-if="kindOf(item) === 'image'"
|
||||
class="media-cover-image"
|
||||
:src="item.thumbnail || item.url"
|
||||
:preview="{ src: item.url }"
|
||||
/>
|
||||
<template v-else>
|
||||
<component
|
||||
:is="iconOf(item)"
|
||||
class="media-cover-icon"
|
||||
:style="{ color: colorOf(item) }"
|
||||
/>
|
||||
<span class="media-cover-ext">{{ extOf(item) }}</span>
|
||||
<play-circle-filled
|
||||
v-if="kindOf(item) === 'video'"
|
||||
class="media-cover-play"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<a-card-meta :title="item.name" @click="openEdit(item)">
|
||||
<template #description>
|
||||
<div class="media-meta">
|
||||
{{ labelOf(item) }} · {{ sizeOf(item) }}
|
||||
</div>
|
||||
<div
|
||||
class="project-list-desc"
|
||||
v-if="item.comments"
|
||||
@@ -41,58 +65,85 @@
|
||||
</a-row>
|
||||
<!-- 编辑弹窗 -->
|
||||
<PhotoEdit v-model:visible="showEdit" :data="current" @done="query" />
|
||||
<!-- 音视频预览 -->
|
||||
<MediaPreview v-model:visible="showPreview" :record="current" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { PlayCircleFilled } from '@ant-design/icons-vue';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { FileRecord, FileRecordParam } from '@/api/system/file/model';
|
||||
import type { FileRecord, FileRecordParam } from '@/api/system/file/model';
|
||||
import PhotoEdit from './components/photo-edit.vue';
|
||||
import MediaPreview from './components/media-preview.vue';
|
||||
import { timeAgo } from 'ele-admin-pro';
|
||||
import {
|
||||
formatFileSize,
|
||||
mediaColor,
|
||||
mediaExtLabel,
|
||||
mediaIcon,
|
||||
mediaKindOfRecord,
|
||||
MEDIA_KIND_LABEL,
|
||||
type MediaKind
|
||||
} from '@/utils/media';
|
||||
|
||||
const props = defineProps<{
|
||||
// 搜索关键字
|
||||
searchText: string;
|
||||
defineProps<{
|
||||
// 列表数据
|
||||
data?: FileRecord[];
|
||||
// 搜索条件
|
||||
where?: FileRecordParam | null;
|
||||
// 修改回显的数据
|
||||
data?: FileRecord | null;
|
||||
}>();
|
||||
|
||||
// 当前编辑数据
|
||||
const current = ref<FileRecord | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
// 当前操作的数据(编辑 / 预览共用)
|
||||
const current = ref<FileRecord | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示音视频预览
|
||||
const showPreview = ref(false);
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const kindOf = (item: FileRecord): MediaKind => mediaKindOfRecord(item);
|
||||
const iconOf = (item: FileRecord) =>
|
||||
mediaIcon(kindOf(item), item.path || item.name);
|
||||
const colorOf = (item: FileRecord) =>
|
||||
mediaColor(kindOf(item), item.path || item.name);
|
||||
const extOf = (item: FileRecord) => mediaExtLabel(item);
|
||||
const labelOf = (item: FileRecord) => MEDIA_KIND_LABEL[kindOf(item)];
|
||||
const sizeOf = (item: FileRecord) => formatFileSize(item.length);
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: FileRecord) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
/* 点击封面:图片由 a-image 自己预览,音视频开播放器,文档新窗口打开 */
|
||||
const openPreview = (row: FileRecord) => {
|
||||
const kind = kindOf(row);
|
||||
if (kind === 'image') {
|
||||
return;
|
||||
}
|
||||
if (kind === 'doc') {
|
||||
window.open(row.url, '_blank');
|
||||
return;
|
||||
}
|
||||
current.value = row;
|
||||
showPreview.value = true;
|
||||
};
|
||||
|
||||
/* 刷新列表 */
|
||||
const query = () => {
|
||||
emit('done');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
console.log(props.data);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -102,6 +153,58 @@
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.media-cover {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// 所有类型共用 4:3,混排时卡片高度才对得齐
|
||||
aspect-ratio: 4 / 3;
|
||||
overflow: hidden;
|
||||
background: #f5f6f8;
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.ant-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-image-img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.media-cover-icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.media-cover-ext {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #718096;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.media-cover-play {
|
||||
position: absolute;
|
||||
font-size: 44px;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
.media-meta {
|
||||
margin-bottom: 2px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.project-list-desc {
|
||||
height: 44px;
|
||||
line-height: 22px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<a-page-header :ghost="false" title="图片库">
|
||||
<div class="ele-text-secondary"> 上传、分享有价值的图片素材 </div>
|
||||
<a-page-header :ghost="false" title="素材库">
|
||||
<div class="ele-text-secondary"> 上传、分享图片、文档与音视频素材 </div>
|
||||
<template #footer>
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="1" tab="网格模式" />
|
||||
@@ -10,16 +10,29 @@
|
||||
<div style="margin-bottom: 10px">
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
:accept="'image/*'"
|
||||
:accept="MEDIA_ACCEPT"
|
||||
:customRequest="onUpload"
|
||||
>
|
||||
<a-button key="1" type="primary" class="ele-btn-icon">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>上传图片</span>
|
||||
<span>上传素材</span>
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<a-select
|
||||
v-model:value="category"
|
||||
style="width: 120px; margin-left: 10px"
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in MEDIA_CATEGORY_OPTIONS"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
style="width: 360px; margin-left: 10px"
|
||||
@@ -68,15 +81,22 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { FileRecord, FileRecordParam } from '@/api/system/file/model';
|
||||
import type { FileRecord, FileRecordParam } from '@/api/system/file/model';
|
||||
import Image from './image.vue';
|
||||
import List from './list.vue';
|
||||
import { messageLoading } from 'ele-admin-pro';
|
||||
import { pageFiles, uploadFile } from '@/api/system/file';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import {
|
||||
MEDIA_ACCEPT,
|
||||
MEDIA_CATEGORY_OPTIONS,
|
||||
validateMediaFile
|
||||
} from '@/utils/media';
|
||||
|
||||
const type = ref('name');
|
||||
const searchText = ref('');
|
||||
// 媒体大类筛选,空串表示全部
|
||||
const category = ref('');
|
||||
const data = ref<FileRecord[]>([]);
|
||||
// 当前选项卡
|
||||
const activeKey = ref('1');
|
||||
@@ -94,7 +114,6 @@
|
||||
|
||||
/* 查询数据 */
|
||||
const query = () => {
|
||||
console.log('query()');
|
||||
if (type.value == 'name') {
|
||||
where.name = searchText.value;
|
||||
}
|
||||
@@ -103,8 +122,8 @@
|
||||
}
|
||||
where.page = page.value;
|
||||
where.limit = limit.value;
|
||||
where.contentType = 'image';
|
||||
console.log(where);
|
||||
// 空串会被当成一个真实的筛选值,必须置空
|
||||
where.mediaCategory = category.value || undefined;
|
||||
const hide = messageLoading('请求中..', 0);
|
||||
pageFiles(where).then((res) => {
|
||||
count.value = Number(res?.count);
|
||||
@@ -113,17 +132,20 @@
|
||||
});
|
||||
};
|
||||
|
||||
/* 切换类型回到第一页 */
|
||||
const onCategoryChange = () => {
|
||||
page.value = 1;
|
||||
query();
|
||||
};
|
||||
|
||||
query();
|
||||
|
||||
// 上传文件
|
||||
const onUpload = (item) => {
|
||||
const { file } = item;
|
||||
if (!file.type.startsWith('image')) {
|
||||
message.error('只能选择图片');
|
||||
return;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 10) {
|
||||
message.error('大小不能超过 10MB');
|
||||
const error = validateMediaFile(file);
|
||||
if (error) {
|
||||
message.error(error);
|
||||
return;
|
||||
}
|
||||
const hide = messageLoading({
|
||||
@@ -132,7 +154,7 @@
|
||||
mask: true
|
||||
});
|
||||
uploadFile(file)
|
||||
.then((data) => {
|
||||
.then(() => {
|
||||
hide();
|
||||
message.success('上传成功');
|
||||
query();
|
||||
@@ -149,15 +171,3 @@
|
||||
name: 'PhotoIndex'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.project-list-desc {
|
||||
height: 44px;
|
||||
line-height: 22px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
|
||||
+133
-31
@@ -1,3 +1,4 @@
|
||||
<!-- 素材库 · 列表视图 -->
|
||||
<template>
|
||||
<div class="page">
|
||||
<a-card :bordered="false">
|
||||
@@ -8,21 +9,21 @@
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
v-model:selection="selection"
|
||||
:scroll="{ x: 800 }"
|
||||
cache-key="proCmsVideoTable"
|
||||
:scroll="{ x: 860 }"
|
||||
cache-key="proCmsPhotoTable"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
:accept="'image/*'"
|
||||
:accept="MEDIA_ACCEPT"
|
||||
:customRequest="onUpload"
|
||||
>
|
||||
<a-button type="primary" class="ele-btn-icon">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>上传图片</span>
|
||||
<span>上传素材</span>
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<a-button
|
||||
@@ -37,6 +38,19 @@
|
||||
</template>
|
||||
<span>删除</span>
|
||||
</a-button>
|
||||
<a-select
|
||||
v-model:value="category"
|
||||
style="width: 120px"
|
||||
@change="reload()"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in MEDIA_CATEGORY_OPTIONS"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
@@ -60,7 +74,23 @@
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'path'">
|
||||
<a-image :src="record.url" :width="120" />
|
||||
<!-- 与网格视图同一套 4:3 缩略图规则 -->
|
||||
<div class="media-thumb" @click="openPreview(record)">
|
||||
<a-image
|
||||
v-if="kindOf(record) === 'image'"
|
||||
:src="record.thumbnail || record.url"
|
||||
:preview="{ src: record.url }"
|
||||
:width="120"
|
||||
/>
|
||||
<template v-else>
|
||||
<component
|
||||
:is="iconOf(record)"
|
||||
class="media-thumb-icon"
|
||||
:style="{ color: colorOf(record) }"
|
||||
/>
|
||||
<span class="media-thumb-ext">{{ extOf(record) }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'name'">
|
||||
<span>{{ record.name }}</span>
|
||||
@@ -78,7 +108,7 @@
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
placement="topRight"
|
||||
title="确定要删除此评价吗?"
|
||||
title="确定要删除此素材吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
@@ -90,6 +120,8 @@
|
||||
</a-card>
|
||||
<!-- 编辑弹窗 -->
|
||||
<PhotoEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
<!-- 音视频预览 -->
|
||||
<MediaPreview v-model:visible="showPreview" :record="current" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -109,6 +141,7 @@
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { messageLoading, toDateString } from 'ele-admin-pro/es';
|
||||
import PhotoEdit from './components/photo-edit.vue';
|
||||
import MediaPreview from './components/media-preview.vue';
|
||||
import {
|
||||
pageFiles,
|
||||
removeFile,
|
||||
@@ -119,17 +152,33 @@
|
||||
FileRecord,
|
||||
FileRecordParam
|
||||
} from '@/api/system/file/model/index';
|
||||
import {
|
||||
formatFileSize,
|
||||
mediaColor,
|
||||
mediaExtLabel,
|
||||
mediaIcon,
|
||||
mediaKindOfRecord,
|
||||
MEDIA_ACCEPT,
|
||||
MEDIA_CATEGORY_OPTIONS,
|
||||
MEDIA_KIND_LABEL,
|
||||
validateMediaFile,
|
||||
type MediaKind
|
||||
} from '@/utils/media';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
// 表格选中数据
|
||||
const selection = ref<FileRecord[]>([]);
|
||||
// 当前编辑数据
|
||||
// 当前操作的数据(编辑 / 预览共用)
|
||||
const current = ref<FileRecord | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示音视频预览
|
||||
const showPreview = ref(false);
|
||||
const type = ref('name');
|
||||
const searchText = ref('');
|
||||
// 媒体大类筛选,空串表示全部
|
||||
const category = ref('');
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
@@ -141,11 +190,17 @@
|
||||
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
|
||||
},
|
||||
{
|
||||
title: '图片',
|
||||
title: '缩略图',
|
||||
dataIndex: 'path',
|
||||
width: 180,
|
||||
width: 140,
|
||||
key: 'path'
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'contentType',
|
||||
width: 100,
|
||||
customRender: ({ record }) => MEDIA_KIND_LABEL[kindOf(record)]
|
||||
},
|
||||
{
|
||||
title: '文件名称',
|
||||
dataIndex: 'name'
|
||||
@@ -161,26 +216,16 @@
|
||||
sorter: true,
|
||||
showSorterTooltip: false,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => {
|
||||
if (text < 1024) {
|
||||
return text + 'B';
|
||||
} else if (text < 1024 * 1024) {
|
||||
return (text / 1024).toFixed(1) + 'KB';
|
||||
} else if (text < 1024 * 1024 * 1024) {
|
||||
return (text / 1024 / 1024).toFixed(1) + 'M';
|
||||
} else {
|
||||
return (text / 1024 / 1024 / 1024).toFixed(1) + 'G';
|
||||
}
|
||||
},
|
||||
customRender: ({ text }) => formatFileSize(text),
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '发布者',
|
||||
title: '上传人',
|
||||
width: 120,
|
||||
dataIndex: 'createNickname'
|
||||
},
|
||||
{
|
||||
title: '发布时间',
|
||||
title: '上传时间',
|
||||
dataIndex: 'createTime',
|
||||
sorter: true,
|
||||
width: 180,
|
||||
@@ -196,6 +241,13 @@
|
||||
}
|
||||
]);
|
||||
|
||||
const kindOf = (record: FileRecord): MediaKind => mediaKindOfRecord(record);
|
||||
const iconOf = (record: FileRecord) =>
|
||||
mediaIcon(kindOf(record), record.path || record.name);
|
||||
const colorOf = (record: FileRecord) =>
|
||||
mediaColor(kindOf(record), record.path || record.name);
|
||||
const extOf = (record: FileRecord) => mediaExtLabel(record);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
where = {};
|
||||
@@ -205,7 +257,8 @@
|
||||
if (type.value == 'createNickname') {
|
||||
where.createNickname = searchText.value;
|
||||
}
|
||||
where.contentType = 'image';
|
||||
// 空串会被当成一个真实的筛选值,必须置空
|
||||
where.mediaCategory = category.value || undefined;
|
||||
return pageFiles({ ...where, ...orders, page, limit });
|
||||
};
|
||||
|
||||
@@ -221,6 +274,20 @@
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 点击缩略图:音视频开播放器,文档新窗口打开 */
|
||||
const openPreview = (row: FileRecord) => {
|
||||
const kind = kindOf(row);
|
||||
if (kind === 'image') {
|
||||
return;
|
||||
}
|
||||
if (kind === 'doc') {
|
||||
window.open(row.url, '_blank');
|
||||
return;
|
||||
}
|
||||
current.value = row;
|
||||
showPreview.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: FileRecord) => {
|
||||
const hide = messageLoading('请求中..', 0);
|
||||
@@ -266,12 +333,9 @@
|
||||
// 上传文件
|
||||
const onUpload = (item) => {
|
||||
const { file } = item;
|
||||
if (!file.type.startsWith('image')) {
|
||||
message.error('只能选择图片');
|
||||
return;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 10) {
|
||||
message.error('大小不能超过 10MB');
|
||||
const error = validateMediaFile(file);
|
||||
if (error) {
|
||||
message.error(error);
|
||||
return;
|
||||
}
|
||||
const hide = messageLoading({
|
||||
@@ -280,7 +344,7 @@
|
||||
mask: true
|
||||
});
|
||||
uploadFile(file)
|
||||
.then((data) => {
|
||||
.then(() => {
|
||||
hide();
|
||||
message.success('上传成功');
|
||||
reload();
|
||||
@@ -294,6 +358,44 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'VideoIndex'
|
||||
name: 'PhotoList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.media-thumb {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
overflow: hidden;
|
||||
background: #f5f6f8;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.ant-image),
|
||||
:deep(.ant-image-img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.media-thumb-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.media-thumb-ext {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
padding: 0 4px;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
color: #718096;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="青马工程"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -363,4 +364,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="十佳青年岗位能手"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -386,4 +387,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="十佳团支部书记"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -376,4 +377,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -114,6 +114,7 @@
|
||||
module="gxmu_tygl_form"
|
||||
module-name="团员管理"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -510,4 +511,4 @@
|
||||
gap: 8px 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -84,6 +84,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="挑战杯"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -289,4 +290,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="五四红旗团委"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -444,4 +445,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="五四红旗团支部"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -478,4 +479,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -105,6 +105,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="未来学术之星"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -375,4 +376,4 @@
|
||||
gap: 8px 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="优秀共青团干部"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -409,4 +410,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -333,6 +333,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="优秀共青团员"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -867,4 +868,4 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="attachment-list">
|
||||
<a-empty
|
||||
v-if="!files.length"
|
||||
:description="emptyText"
|
||||
class="attachment-empty"
|
||||
/>
|
||||
<div
|
||||
v-for="(file, index) in files"
|
||||
:key="`${file.url}-${index}`"
|
||||
class="attachment-item"
|
||||
>
|
||||
<a-image
|
||||
v-if="isImageFile(file.url)"
|
||||
:src="file.url"
|
||||
:width="96"
|
||||
:height="96"
|
||||
class="attachment-image"
|
||||
/>
|
||||
<div class="attachment-body">
|
||||
<a
|
||||
:href="file.url"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="attachment-name"
|
||||
:title="file.name"
|
||||
>
|
||||
{{ file.name }}
|
||||
</a>
|
||||
<span class="attachment-tip">点击可预览</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
isImageFile,
|
||||
parseAttachments,
|
||||
type AttachmentFile
|
||||
} from '@/views/gxmu/utils/attachment';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 原始附件字段值
|
||||
value?: unknown;
|
||||
// 已解析好的文件列表(优先使用)
|
||||
files?: AttachmentFile[];
|
||||
emptyText?: string;
|
||||
}>(),
|
||||
{
|
||||
value: undefined,
|
||||
files: undefined,
|
||||
emptyText: '暂无材料'
|
||||
}
|
||||
);
|
||||
|
||||
const files = computed<AttachmentFile[]>(() => {
|
||||
if (props.files) {
|
||||
return props.files;
|
||||
}
|
||||
return parseAttachments(props.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'GxmuAttachmentList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #eef1f6;
|
||||
border-radius: 8px;
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.attachment-image {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.attachment-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
color: #1677ff;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attachment-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.attachment-empty {
|
||||
margin: 12px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -26,6 +26,21 @@
|
||||
</p>
|
||||
<p class="ant-upload-hint">将 Excel 文件拖到此处,或点击上传</p>
|
||||
</a-upload-dragger>
|
||||
<a-alert
|
||||
v-if="failures.length"
|
||||
type="warning"
|
||||
show-icon
|
||||
:message="`${successCount} 条导入成功,${failures.length} 条被跳过`"
|
||||
>
|
||||
<template #description>
|
||||
<div class="import-failures">
|
||||
<div v-for="failure in failures" :key="failure.row">
|
||||
第 {{ failure.row }} 行:{{ failure.reason }}
|
||||
</div>
|
||||
</div>
|
||||
<a-button size="small" @click="exportFailures">下载失败清单</a-button>
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</ele-modal>
|
||||
@@ -63,6 +78,8 @@
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const failures = ref<{ row: number; reason: string }[]>([]);
|
||||
const successCount = ref(0);
|
||||
|
||||
const description = computed(
|
||||
() =>
|
||||
@@ -141,13 +158,17 @@
|
||||
if (!rows.length) {
|
||||
throw new Error('导入文件内容为空');
|
||||
}
|
||||
const data = rows
|
||||
.map((row, index) => props.createPayload(row, index + 2))
|
||||
.filter((item) =>
|
||||
const entries = rows
|
||||
.map((row, index) => ({
|
||||
rowNo: index + 2,
|
||||
item: props.createPayload(row, index + 2)
|
||||
}))
|
||||
.filter(({ item }) =>
|
||||
props.templateFields.some((field) =>
|
||||
!!String(item[field.key] ?? '').trim()
|
||||
)
|
||||
);
|
||||
const data = entries.map((entry) => entry.item);
|
||||
if (!data.length) {
|
||||
throw new Error('未识别到有效数据');
|
||||
}
|
||||
@@ -155,10 +176,28 @@
|
||||
if (errors.length) {
|
||||
throw new Error(errors.slice(0, 5).join(';'));
|
||||
}
|
||||
for (const item of data) {
|
||||
await props.saveItem(item);
|
||||
failures.value = [];
|
||||
successCount.value = 0;
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
await props.saveItem(entry.item);
|
||||
successCount.value += 1;
|
||||
} catch (e: any) {
|
||||
failures.value.push({
|
||||
row: entry.rowNo,
|
||||
reason: e?.message || '保存失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
message.success(`成功导入 ${data.length} 条${props.moduleName}数据`);
|
||||
if (failures.value.length) {
|
||||
// 被跳过的是「同一申报项目已有进行中/已通过/被驳回」的记录,保留在清单里供人工处理
|
||||
message.warning(
|
||||
`成功 ${successCount.value} 条,跳过 ${failures.value.length} 条`
|
||||
);
|
||||
emit('done');
|
||||
return false;
|
||||
}
|
||||
message.success(`成功导入 ${successCount.value} 条${props.moduleName}数据`);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
@@ -169,7 +208,32 @@
|
||||
return false;
|
||||
};
|
||||
|
||||
const exportFailures = () => {
|
||||
if (!failures.value.length) {
|
||||
return;
|
||||
}
|
||||
const sheet = utils.aoa_to_sheet([
|
||||
['行号', '跳过原因'],
|
||||
...failures.value.map((item) => [item.row, item.reason])
|
||||
]);
|
||||
sheet['!cols'] = [{ wch: 10 }, { wch: 64 }];
|
||||
writeFile(
|
||||
{
|
||||
SheetNames: ['失败清单'],
|
||||
Sheets: { 失败清单: sheet }
|
||||
},
|
||||
`${props.moduleName}导入失败清单.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.import-failures {
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
:color="item.status === 1 ? 'success' : item.status === 2 ? 'error' : 'default'"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ item.status === 1 ? '通过' : item.status === 2 ? '不通过' : '待审核' }}
|
||||
{{ item.status === 1 ? '通过' : item.status === 2 ? '驳回' : '待审核' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div v-if="item.user?.realName" style="margin-bottom: 4px">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<a-tag color="green">通过</a-tag>
|
||||
</template>
|
||||
<template v-else-if="latest?.status === 2">
|
||||
<a-tag color="red">不通过</a-tag>
|
||||
<a-tag color="red">驳回</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>待审核</a-tag>
|
||||
|
||||
@@ -149,6 +149,19 @@
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item
|
||||
label="允许重复提交"
|
||||
name="allowRepeat"
|
||||
extra="关闭时,同一用户在同一申报项目下同时只能有一条待审核申报;开启后可配置最多同时几条"
|
||||
>
|
||||
<a-switch
|
||||
:checked="form.allowRepeat === 1"
|
||||
@change="(checked: boolean) => (form.allowRepeat = checked ? 1 : 0)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.allowRepeat === 1" label="最多同时几条" name="maxPending">
|
||||
<a-input-number :min="1" :max="20" class="ele-fluid" v-model:value="form.maxPending" />
|
||||
</a-form-item>
|
||||
<a-form-item label="开始时间" name="startTime">
|
||||
<a-date-picker
|
||||
allow-clear
|
||||
@@ -182,6 +195,10 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { DECLARE_MODULE_OPTIONS } from '../moduleOptions';
|
||||
import {
|
||||
TZBCY_PROJECT_GROUP_OPTIONS,
|
||||
TZBCY_PROJECT_TYPE_OPTIONS
|
||||
} from '../../TZBCYForm/options';
|
||||
import {
|
||||
normalizeDeclareRecord,
|
||||
stringifyDeclareMultiValueField
|
||||
@@ -244,6 +261,8 @@
|
||||
publicProjectGroup: [''],
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
allowRepeat: 0,
|
||||
maxPending: 1,
|
||||
userId: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
@@ -273,29 +292,68 @@
|
||||
const { resetFields } = useForm(form, rules);
|
||||
const isTzbcyModule = computed(() => form.module === 'gxmu_tzbcy_form');
|
||||
|
||||
/**
|
||||
* 挑战杯模块的默认配置
|
||||
* 与挑战杯申报表共用 TZBCYForm/options 中的同一份常量,避免两处维护产生漂移
|
||||
*/
|
||||
type ChallengeCupConfigKey =
|
||||
| 'formProjectType'
|
||||
| 'formProjectGroup'
|
||||
| 'publicProjectType'
|
||||
| 'publicProjectGroup';
|
||||
|
||||
const CHALLENGE_CUP_CONFIG_KEYS: ChallengeCupConfigKey[] = [
|
||||
'formProjectType',
|
||||
'formProjectGroup',
|
||||
'publicProjectType',
|
||||
'publicProjectGroup'
|
||||
];
|
||||
|
||||
const toOptionValues = (options: readonly { value: string }[]) =>
|
||||
options.map((item) => item.value);
|
||||
|
||||
const getChallengeCupDefaultConfig = (
|
||||
key: ChallengeCupConfigKey
|
||||
): string[] => {
|
||||
switch (key) {
|
||||
case 'formProjectType':
|
||||
case 'publicProjectType':
|
||||
return toOptionValues(TZBCY_PROJECT_TYPE_OPTIONS);
|
||||
default:
|
||||
return toOptionValues(TZBCY_PROJECT_GROUP_OPTIONS);
|
||||
}
|
||||
};
|
||||
|
||||
// 列表是否为空(没有任意一个有效值,空行不算)
|
||||
const isBlankConfigList = (list?: string[]) =>
|
||||
!(list || []).some((item) => !!item?.trim());
|
||||
|
||||
// 仅填充空配置,已有的配置(含用户手动编辑过的内容)保持不变
|
||||
const fillBlankConfigWithDefault = () => {
|
||||
CHALLENGE_CUP_CONFIG_KEYS.forEach((key) => {
|
||||
if (isBlankConfigList(form[key])) {
|
||||
form[key] = getChallengeCupDefaultConfig(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 选中挑战杯模块时套用默认配置
|
||||
const applyChallengeCupPreset = () => {
|
||||
if (isTzbcyModule.value) {
|
||||
fillBlankConfigWithDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeEditableList = (list?: string[]) => {
|
||||
const values = (list || []).map((item) => item?.trim() || '').filter(Boolean);
|
||||
return values.length ? values : [''];
|
||||
};
|
||||
|
||||
const addConfigRow = (
|
||||
key:
|
||||
| 'formProjectType'
|
||||
| 'formProjectGroup'
|
||||
| 'publicProjectType'
|
||||
| 'publicProjectGroup'
|
||||
) => {
|
||||
const addConfigRow = (key: ChallengeCupConfigKey) => {
|
||||
form[key].push('');
|
||||
};
|
||||
|
||||
const removeConfigRow = (
|
||||
key:
|
||||
| 'formProjectType'
|
||||
| 'formProjectGroup'
|
||||
| 'publicProjectType'
|
||||
| 'publicProjectGroup',
|
||||
index: number
|
||||
) => {
|
||||
const removeConfigRow = (key: ChallengeCupConfigKey, index: number) => {
|
||||
form[key].splice(index, 1);
|
||||
if (!form[key].length) {
|
||||
form[key].push('');
|
||||
@@ -418,6 +476,8 @@
|
||||
);
|
||||
yearValue.value = props.data.year ? String(props.data.year) : undefined;
|
||||
isUpdate.value = true;
|
||||
// 历史记录若未配置过挑战杯配置,补上默认值;已配置的记录保持原样
|
||||
applyChallengeCupPreset();
|
||||
} else {
|
||||
Object.assign(form, createDefaultForm());
|
||||
isUpdate.value = false;
|
||||
@@ -435,14 +495,15 @@
|
||||
watch(
|
||||
() => form.module,
|
||||
(value) => {
|
||||
if (value !== 'gxmu_tzbcy_form') {
|
||||
form.projectType = [];
|
||||
form.projectGroup = [];
|
||||
form.formProjectType = [''];
|
||||
form.formProjectGroup = [''];
|
||||
form.publicProjectType = [''];
|
||||
form.publicProjectGroup = [''];
|
||||
if (value === 'gxmu_tzbcy_form') {
|
||||
// 选中挑战杯模块时预填项目申报表 / 公开展示信息表的默认配置
|
||||
applyChallengeCupPreset();
|
||||
return;
|
||||
}
|
||||
// 切到其他模块时不清空配置,避免用户手滑切换后已编辑的内容丢失
|
||||
// (非挑战杯模块不会展示也不会提交这些字段)
|
||||
form.projectType = [];
|
||||
form.projectGroup = [];
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -471,4 +532,4 @@
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 1" color="green">通过</a-tag>
|
||||
<a-tag v-else-if="record.status === 2" color="red">不通过</a-tag>
|
||||
<a-tag v-else-if="record.status === 2" color="red">驳回</a-tag>
|
||||
<a-tag v-else>待审核</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'submitTime'">
|
||||
@@ -92,6 +92,7 @@
|
||||
:module="detailCurrent?.module"
|
||||
:module-name="getDeclareModuleLabel(detailCurrent?.module)"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="loadHistoryData"
|
||||
/>
|
||||
<ReviewFlowModal
|
||||
v-model:visible="showReviewFlow"
|
||||
@@ -499,4 +500,4 @@ export default {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -152,6 +152,13 @@
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="loading || !markdownContent"
|
||||
@click="formatRichText"
|
||||
>
|
||||
一键排版
|
||||
</a-button>
|
||||
<a-button :disabled="loading || !markdownContent" @click="exportWord">导出 Word</a-button>
|
||||
<a-button :disabled="!markdownContent" @click="copyGenerated">复制正文</a-button>
|
||||
<a-button :disabled="!generatedTitle" @click="copyGeneratedTitle">复制标题</a-button>
|
||||
@@ -171,17 +178,55 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="正文内容">
|
||||
<div class="markdown-shell">
|
||||
<div v-if="loading" class="markdown-streaming">
|
||||
{{ markdownContent || 'AI 正在生成内容...' }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="markdownContent"
|
||||
class="markdown-body markdown-viewer"
|
||||
v-html="renderedMarkdownHtml"
|
||||
></div>
|
||||
<div v-else class="markdown-empty">生成结果将显示在这里</div>
|
||||
</div>
|
||||
<a-tabs
|
||||
v-model:activeKey="contentTab"
|
||||
size="small"
|
||||
class="content-tabs"
|
||||
>
|
||||
<a-tab-pane key="markdown" tab="Markdown 预览">
|
||||
<div class="markdown-shell">
|
||||
<div v-if="loading" class="markdown-streaming">
|
||||
{{ markdownContent || 'AI 正在生成内容...' }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="markdownContent"
|
||||
class="markdown-body markdown-viewer"
|
||||
v-html="renderedMarkdownHtml"
|
||||
></div>
|
||||
<div v-else class="markdown-empty">生成结果将显示在这里</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="rich" tab="富文本排版">
|
||||
<div class="rich-shell">
|
||||
<div class="rich-bar">
|
||||
<span class="rich-bar-tip">
|
||||
一键排版按原文的标题层级、段落、列表、表格还原,可直接在编辑器里继续改
|
||||
</span>
|
||||
<a-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:disabled="saveDisabled"
|
||||
@click="saveAsManuscript"
|
||||
>
|
||||
{{ savedUpToDate ? '已保存为稿件' : '保存为稿件' }}
|
||||
</a-button>
|
||||
</div>
|
||||
<Toolbar
|
||||
class="rich-toolbar"
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
mode="default"
|
||||
/>
|
||||
<Editor
|
||||
v-model="richContentHtml"
|
||||
class="rich-editor"
|
||||
:defaultConfig="editorConfig"
|
||||
mode="default"
|
||||
@onCreated="handleEditorCreated"
|
||||
/>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
@@ -198,10 +243,10 @@
|
||||
import zh_HansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
|
||||
import highlight from '@bytemd/plugin-highlight-ssr';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
import { messageLoading } from 'ele-admin-pro';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { asBlob } from 'html-docx-js-typescript';
|
||||
@@ -215,6 +260,16 @@
|
||||
import type { FileRecord } from '@/api/system/file/model';
|
||||
import { copyText } from '@/utils/common';
|
||||
import { resolveDisplayText, isFailedPayload, pickDisplayable, FAILED_TEXT } from '@/utils/ai-result';
|
||||
import { markdownToHtml } from '@/utils/manuscript-content';
|
||||
import { addManuscript } from '@/api/cms/manuscript';
|
||||
import type { Manuscript } from '@/api/cms/manuscript/model';
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||
import type {
|
||||
IDomEditor,
|
||||
IEditorConfig,
|
||||
IToolbarConfig
|
||||
} from '@wangeditor/editor';
|
||||
import '@wangeditor/editor/dist/css/style.css';
|
||||
|
||||
interface FormState {
|
||||
/** 空串 = 自由创作(不套模板);非空 = 文稿模板,走「内容创作与宣传助手」 */
|
||||
@@ -248,6 +303,46 @@
|
||||
highlight()
|
||||
]);
|
||||
const loading = ref(false);
|
||||
/** 正文区域展示方式:默认看 Markdown 预览,一键排版后切到富文本 */
|
||||
const contentTab = ref<'markdown' | 'rich'>('markdown');
|
||||
/** 富文本编辑器里的 HTML(一键排版灌入,人工可继续改) */
|
||||
const richContentHtml = ref('');
|
||||
const savingManuscript = ref(false);
|
||||
/** 上次保存时的正文文本快照:内容没变就不再让点「保存为稿件」,避免存出重复草稿 */
|
||||
const savedSnapshot = ref('');
|
||||
const editorRef = shallowRef<IDomEditor>();
|
||||
|
||||
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 handleEditorCreated = (editor: IDomEditor) => {
|
||||
editorRef.value = editor;
|
||||
};
|
||||
|
||||
const generatedTitle = ref('');
|
||||
const generatedContent = ref('');
|
||||
const uploadedFiles = ref<Partial<FileRecord>[]>([]);
|
||||
@@ -569,6 +664,10 @@
|
||||
const clearGenerated = () => {
|
||||
generatedTitle.value = '';
|
||||
generatedContent.value = '';
|
||||
// 排版结果是这份 markdown 的产物,正文换了就不能留着旧的
|
||||
richContentHtml.value = '';
|
||||
savedSnapshot.value = '';
|
||||
contentTab.value = 'markdown';
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
@@ -627,6 +726,127 @@
|
||||
form.number = 600;
|
||||
};
|
||||
|
||||
/** 只留文本、丢掉标签与空白,用来判断两份富文本是不是同一份内容 */
|
||||
const normalizeForCompare = (html: string) =>
|
||||
String(html || '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/\s+/g, '');
|
||||
|
||||
/** 编辑器里的内容是否已经存成稿件(按文本比,编辑器规整过标签属性也算同一份) */
|
||||
const savedUpToDate = computed(
|
||||
() =>
|
||||
!!savedSnapshot.value &&
|
||||
savedSnapshot.value === normalizeForCompare(richContentHtml.value)
|
||||
);
|
||||
|
||||
/** 保存按钮的可用状态:没内容、正在存、这份已经存过,都不给点 */
|
||||
const saveDisabled = computed(
|
||||
() =>
|
||||
savingManuscript.value ||
|
||||
!hasRichText(richContentHtml.value) ||
|
||||
savedUpToDate.value
|
||||
);
|
||||
|
||||
/** 编辑器里的“空”:wangEditor 空内容是一段 `<p><br></p>`,不能只看字符串长度 */
|
||||
const hasRichText = (html: string) => {
|
||||
const text = String(html || '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /gi, '')
|
||||
.trim();
|
||||
return !!text || /<img\b/i.test(String(html || ''));
|
||||
};
|
||||
|
||||
/**
|
||||
* 一键排版:把生成结果按原文的标题层级、段落、列表、表格转成 HTML,
|
||||
* 灌进富文本编辑器。
|
||||
* - 用 gfm 插件,和页面上 Markdown 预览同一套解析(表格不会漏成管道符);
|
||||
* - 不把生成标题拼进正文:标题有独立字段,导出 Word 时也只在导出件里拼;
|
||||
* - 不落库:保存是用户在编辑器里确认过的第二次动作(见 saveAsManuscript)。
|
||||
*/
|
||||
const formatRichText = () => {
|
||||
if (loading.value) {
|
||||
message.warning('正在生成中,暂不能排版');
|
||||
return;
|
||||
}
|
||||
if (!markdownContent.value) {
|
||||
message.warning('暂无可排版的正文内容');
|
||||
return;
|
||||
}
|
||||
if (markdownContent.value === FAILED_TEXT) {
|
||||
message.error('生成失败的内容不能排版,请重新生成');
|
||||
return;
|
||||
}
|
||||
const nextHtml = markdownToHtml(markdownContent.value, [
|
||||
gfm({ locale: zh_HansGfm })
|
||||
]);
|
||||
if (!nextHtml.trim()) {
|
||||
message.warning('当前内容转换不出富文本,请检查正文');
|
||||
return;
|
||||
}
|
||||
const apply = () => {
|
||||
richContentHtml.value = nextHtml;
|
||||
// 排版换了内容,之前那次保存的状态就作废
|
||||
savedSnapshot.value = '';
|
||||
contentTab.value = 'rich';
|
||||
message.success('已按原文排版放进富文本编辑器,请检查后保存');
|
||||
};
|
||||
// 编辑器里有内容且不是同一份排版(用户改过),才需要确认覆盖。
|
||||
// 比的是「文本」而不是 HTML:wangEditor 会把 setHtml 进来的标签属性、
|
||||
// 空白重新规整一遍,按 HTML 比会把「原样再排一次」也判成要覆盖。
|
||||
if (
|
||||
hasRichText(richContentHtml.value) &&
|
||||
normalizeForCompare(richContentHtml.value) !== normalizeForCompare(nextHtml)
|
||||
) {
|
||||
Modal.confirm({
|
||||
title: '覆盖编辑器里的内容?',
|
||||
content: '富文本编辑器里已有内容,重新排版会覆盖掉当前改动。',
|
||||
okText: '覆盖',
|
||||
cancelText: '取消',
|
||||
onOk: apply
|
||||
});
|
||||
return;
|
||||
}
|
||||
apply();
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存为稿件:入库一律写 html,和「编辑稿件」弹窗的口径一致。
|
||||
* add 接口不回 id,所以这里只负责“存成一条新稿件”,不冒充“再编辑同一篇”:
|
||||
* 存过之后按钮置灰,改过正文才会重新可点(避免连点存出多条草稿)。
|
||||
*/
|
||||
const saveAsManuscript = async () => {
|
||||
const title = (generatedTitle.value || form.theme).trim();
|
||||
if (!title) {
|
||||
message.warning('请先填写生成标题,稿件需要标题');
|
||||
return;
|
||||
}
|
||||
if (!hasRichText(richContentHtml.value)) {
|
||||
message.warning('富文本里还没有内容,请先点「一键排版」');
|
||||
return;
|
||||
}
|
||||
if (savedUpToDate.value) {
|
||||
message.info('这份排版已经保存过了,改完再点可以另存一条');
|
||||
return;
|
||||
}
|
||||
savingManuscript.value = true;
|
||||
try {
|
||||
const msg = await addManuscript({
|
||||
title,
|
||||
content: richContentHtml.value,
|
||||
contentFormat: 'html',
|
||||
// 后端 status 是 Integer(列表里 0 待审核 / 1 显示),前端类型写成 string 是历史遗留
|
||||
status: 1 as unknown as Manuscript['status'],
|
||||
sortNumber: 0
|
||||
});
|
||||
savedSnapshot.value = normalizeForCompare(richContentHtml.value);
|
||||
message.success(msg || '已保存为稿件');
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '保存稿件失败');
|
||||
} finally {
|
||||
savingManuscript.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeUploadedFile = (index: number) => {
|
||||
uploadedFiles.value = uploadedFiles.value.filter((_, current) => current !== index);
|
||||
};
|
||||
@@ -852,6 +1072,7 @@
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearStreamResolver();
|
||||
editorRef.value?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1047,6 +1268,46 @@
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.content-tabs :deep(.ant-tabs-nav) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rich-shell {
|
||||
border: 1px solid #c7e8ff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
background: #fffdfb;
|
||||
}
|
||||
|
||||
.rich-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #eef4f9;
|
||||
background: #f5fbff;
|
||||
}
|
||||
|
||||
.rich-bar-tip {
|
||||
color: #5f7f9f;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.rich-toolbar {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.rich-editor {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.rich-editor :deep(.w-e-text-container) {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,904 @@
|
||||
/**
|
||||
* 审核列表「查看数据」详情的申报表配置。
|
||||
*
|
||||
* sections 决定申报表的字段分组与展示顺序,materialFields 声明需要在上传材料
|
||||
* 中展示的附件字段,未列出的字段会兜底追加到「其他信息」分组,避免数据丢失。
|
||||
*/
|
||||
|
||||
export interface ModuleSection {
|
||||
// 分组标题
|
||||
title: string;
|
||||
// 分组内字段(按展示顺序)
|
||||
fields: string[];
|
||||
}
|
||||
|
||||
export interface ModuleMaterialTable {
|
||||
// 明细数组字段
|
||||
field: string;
|
||||
// 明细中保存附件地址的字段
|
||||
fileField: string;
|
||||
// 明细标题字段
|
||||
titleField?: string;
|
||||
// 材料分组标题
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ModuleDetailConfig {
|
||||
// 模块名称
|
||||
title: string;
|
||||
// 申报表名称,用于拼接表单标题
|
||||
sheetName: string;
|
||||
// 字段中文名
|
||||
labels: Record<string, string>;
|
||||
// 申报表分组
|
||||
sections: ModuleSection[];
|
||||
// 附件字段(上传材料)
|
||||
materialFields: string[];
|
||||
// 明细数组中的附件
|
||||
materialTables?: ModuleMaterialTable[];
|
||||
// 额外的材料数据来源
|
||||
materialSource?: 'tzbcy';
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用字段中文名
|
||||
*/
|
||||
export const COMMON_LABELS: Record<string, string> = {
|
||||
year: '年份',
|
||||
id: 'ID',
|
||||
title: '标题',
|
||||
content: '内容',
|
||||
contentFormat: '正文格式',
|
||||
cover: '封面',
|
||||
createTime: '提交时间',
|
||||
updateTime: '更新时间',
|
||||
status: '状态',
|
||||
sortNumber: '排序',
|
||||
provinceCity: '省市',
|
||||
schoolName: '学校名称',
|
||||
projectName: '项目名称',
|
||||
projectType: '项目类型',
|
||||
projectGroup: '项目分组',
|
||||
publicProjectType: '公开展示项目类型',
|
||||
publicProjectGroup: '公开展示项目分组',
|
||||
leader: '负责人',
|
||||
phone: '联系电话',
|
||||
teamMembers: '团队成员',
|
||||
advisors: '指导老师',
|
||||
projectBrief: '项目简介',
|
||||
socialValue: '社会价值',
|
||||
practiceProcess: '实践过程',
|
||||
innovationMeaning: '创新意义',
|
||||
developmentProspect: '发展前景',
|
||||
teamCooperation: '团队协作情况',
|
||||
projectMaterials: '项目介绍材料',
|
||||
otherProofs: '其他相关证明材料',
|
||||
projectSummary: '项目总结',
|
||||
teamIntro: '团队介绍',
|
||||
teamSlogan: '团队口号',
|
||||
practiceLog: '实践日志',
|
||||
name: '姓名',
|
||||
gender: '性别',
|
||||
nation: '民族',
|
||||
photo: '电子版照片',
|
||||
birthMonth: '出生年月',
|
||||
politics: '政治面貌',
|
||||
nativePlace: '籍贯',
|
||||
wechat: '微信号',
|
||||
email: '邮箱',
|
||||
qq: 'QQ',
|
||||
idCardNo: '身份证号',
|
||||
hobby: '兴趣爱好',
|
||||
willingToWorkInGuangxi: '是否愿意留桂工作',
|
||||
schoolInfo: '学校信息',
|
||||
leaguePosition: '团内职务',
|
||||
resume: '个人简历',
|
||||
awards: '获得奖励',
|
||||
academicPerformance: '学习成绩',
|
||||
secondaryLeagueOpinion: '二级团组织意见',
|
||||
secondaryLeagueOpinionDate: '二级团组织意见日期',
|
||||
secondaryPartyOpinion: '二级党组织意见',
|
||||
secondaryPartyOpinionDate: '二级党组织意见日期',
|
||||
schoolLeagueOpinion: '学校团组织意见',
|
||||
schoolLeagueOpinionDate: '学校团组织意见日期',
|
||||
applyType: '申报类别',
|
||||
education: '学历',
|
||||
position: '职务',
|
||||
unit: '工作单位',
|
||||
experience: '主要经历',
|
||||
mainStory: '主要事迹',
|
||||
leagueOpinion: '团组织意见',
|
||||
partyOpinion: '党组织意见',
|
||||
schoolOpinion: '学校意见',
|
||||
collegeClass: '学院班级',
|
||||
branch: '团支部名称',
|
||||
eduEval: '教育评议情况',
|
||||
serialNo: '序号',
|
||||
archiveInCurrentOrg: '团籍是否在本组织',
|
||||
joinMonth: '入团时间',
|
||||
memberRecords: '团员记录',
|
||||
growthArchives: '成长档案',
|
||||
orgName: '组织名称',
|
||||
memberTotal: '团员总数',
|
||||
memberDeveloped2024: '上一年度发展团员数',
|
||||
smartSystemLogin: '智慧团建系统录入情况',
|
||||
committeeCount: '团委委员数',
|
||||
fulltimeCadreCount: '专职团干部数',
|
||||
parttimeCadreCount: '兼职团干部数',
|
||||
lastElectionTime: '最近一次换届时间',
|
||||
feeReceivable2024: '上一年度应收团费',
|
||||
feeReceived2024: '上一年度实收团费',
|
||||
feePayable2024: '上一年度应缴团费',
|
||||
feePaid2024: '上一年度实缴团费',
|
||||
branchCount: '团支部数量',
|
||||
standardizedWork2024: '上一年度标准化建设开展情况',
|
||||
recommendActivist2024: '上一年度推优入党人数',
|
||||
activistConfirmed: '入党积极分子确定情况',
|
||||
recommendDevTarget2024: '上一年度推荐发展对象人数',
|
||||
devTargetConfirmed: '发展对象确定情况',
|
||||
honorsFiveYears: '近五年获奖情况',
|
||||
workSummaryThreeYears: '近三年工作总结',
|
||||
branchName: '团支部名称',
|
||||
secondOrg: '所属二级党组织',
|
||||
secretary: '团支部书记',
|
||||
contact: '联系方式',
|
||||
establishTime: '成立时间',
|
||||
branchCommitteeMeetingCount: '支委会次数',
|
||||
branchMemberMeetingCount: '支部大会次数',
|
||||
eduEvalDone: '教育评议是否完成',
|
||||
annualRegDone: '年度团籍注册是否完成',
|
||||
classCount: '班级数量',
|
||||
smartSystem100: '智慧团建系统覆盖率',
|
||||
topicName: '课题名称',
|
||||
collegeGradeClass: '学院年级班级',
|
||||
advisor: '指导老师',
|
||||
topicType: '课题类别',
|
||||
guidanceTeachers: '指导教师组',
|
||||
applicants: '申请人信息',
|
||||
budgets: '经费预算',
|
||||
rationale: '立项依据',
|
||||
teacherOpinion: '指导教师意见',
|
||||
teacherSign: '指导教师签名',
|
||||
reviewOpinion: '评审意见',
|
||||
reviewScore: '评审成绩',
|
||||
reviewLeaderSign: '评审负责人签名',
|
||||
collegeOpinion: '学院意见',
|
||||
promiseSigner: '承诺签字人',
|
||||
promiseSignature: '承诺书签名',
|
||||
reviewCollege: '评审学院',
|
||||
reviewReporter: '汇报人',
|
||||
reviewDate: '评审日期',
|
||||
reviewMembers: '评审成员',
|
||||
fundingBudgetFile: '经费预算表',
|
||||
fundingAllocationFile: '经费划拨明细表',
|
||||
midtermRemark: '中期检查说明',
|
||||
midtermMaterials: '中期检查材料',
|
||||
finalRemark: '结题说明',
|
||||
finalMaterials: '结题材料',
|
||||
archiveAwards: '获奖情况',
|
||||
archiveProofs: '佐证材料',
|
||||
archiveRecords: '成果归档',
|
||||
grade: '年级',
|
||||
className: '班级',
|
||||
major: '专业',
|
||||
role: '角色',
|
||||
item: '项目',
|
||||
amount: '金额',
|
||||
reason: '原因',
|
||||
dept: '部门',
|
||||
sign: '签名',
|
||||
department: '部门',
|
||||
remark: '备注',
|
||||
type: '类型',
|
||||
recordTime: '时间',
|
||||
archiveTime: '时间',
|
||||
identity: '身份',
|
||||
organization: '所在组织',
|
||||
memberNo: '团员编号',
|
||||
currentDutyTime: '现任职务时间',
|
||||
cadreYears: '从事团学工作年限',
|
||||
assessment2024: '上一年度考核情况',
|
||||
volunteerRegTime: '志愿汇注册时间',
|
||||
cadreExperience: '团学工作经历',
|
||||
joinTime: '入团时间',
|
||||
collegeMajorClass: '学院专业班级',
|
||||
eduEval2024: '上一年度教育评议情况',
|
||||
smartSystem: '智慧团建系统录入情况',
|
||||
totalVolunteerHours: '累计志愿服务时长',
|
||||
volunteerHours2024: '上一年度志愿服务时长',
|
||||
duty: '职务',
|
||||
college: '学院',
|
||||
gradeMajor: '年级专业',
|
||||
birthDate: '出生日期',
|
||||
branchOrganizationName: '所属团支部',
|
||||
awardName: '获奖名称',
|
||||
awardTime: '获奖时间',
|
||||
proofFile: '证明材料',
|
||||
archiveOrder: '序号'
|
||||
};
|
||||
|
||||
/**
|
||||
* 五四类申报表的意见栏分组
|
||||
*/
|
||||
const OPINION_SECTION: ModuleSection = {
|
||||
title: '意见栏',
|
||||
fields: ['leagueOpinion', 'partyOpinion', 'schoolOpinion']
|
||||
};
|
||||
|
||||
export const MODULE_DETAIL_CONFIGS: Record<string, ModuleDetailConfig> = {
|
||||
gxmu_sjqn: {
|
||||
title: '十佳青年岗位能手',
|
||||
sheetName: '十佳青年岗位能手',
|
||||
labels: {
|
||||
applyType: '申报类别',
|
||||
education: '学历',
|
||||
position: '职务',
|
||||
title: '职称',
|
||||
unit: '所在单位',
|
||||
awards: '近三年曾获校级及以上奖励',
|
||||
experience: '工作(学习)经历',
|
||||
mainStory: '主要事迹',
|
||||
leagueOpinion: '所在二级团组织意见',
|
||||
partyOpinion: '所在单位党组织意见',
|
||||
schoolOpinion: '校团委意见'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
'year',
|
||||
'applyType',
|
||||
'name',
|
||||
'gender',
|
||||
'birthMonth',
|
||||
'nation',
|
||||
'politics',
|
||||
'education',
|
||||
'position',
|
||||
'title',
|
||||
'unit'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '主要事迹与经历',
|
||||
fields: ['awards', 'experience', 'mainStory']
|
||||
},
|
||||
OPINION_SECTION
|
||||
],
|
||||
materialFields: []
|
||||
},
|
||||
gxmu_sjtbzbsj: {
|
||||
title: '十佳团支部书记',
|
||||
sheetName: '十佳团支部书记',
|
||||
labels: {
|
||||
collegeClass: '学院班级/单位科室',
|
||||
branch: '所在团支部',
|
||||
eduEval: '上一年度团员教育评议等次',
|
||||
awards: '近三年曾获校级及以上奖励',
|
||||
experience: '工作(学习)经历',
|
||||
mainStory: '主要事迹',
|
||||
leagueOpinion: '所在二级团组织意见',
|
||||
partyOpinion: '所在单位党组织意见',
|
||||
schoolOpinion: '校团委意见'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
'year',
|
||||
'name',
|
||||
'gender',
|
||||
'birthMonth',
|
||||
'nation',
|
||||
'politics',
|
||||
'collegeClass',
|
||||
'branch',
|
||||
'eduEval'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '主要事迹与经历',
|
||||
fields: ['awards', 'experience', 'mainStory']
|
||||
},
|
||||
OPINION_SECTION
|
||||
],
|
||||
materialFields: []
|
||||
},
|
||||
gxmu_wshqtw: {
|
||||
title: '五四红旗团委',
|
||||
sheetName: '五四红旗团委',
|
||||
labels: {
|
||||
orgName: '二级团组织全称',
|
||||
leader: '负责人',
|
||||
phone: '联系电话',
|
||||
memberTotal: '现有团员总数',
|
||||
memberDeveloped2024: '上一年度发展团员人数',
|
||||
smartSystemLogin: '是否已登录智慧团建系统',
|
||||
committeeCount: '团委委员人数',
|
||||
fulltimeCadreCount: '专职团干部数',
|
||||
parttimeCadreCount: '兼职团干部数',
|
||||
lastElectionTime: '团委最近一次换届时间',
|
||||
feeReceivable2024: '上一年度应收团费',
|
||||
feeReceived2024: '上一年度实收团费',
|
||||
feePayable2024: '上一年度应上缴团费',
|
||||
feePaid2024: '上一年度实际上缴团费',
|
||||
branchCount: '团支部数',
|
||||
standardizedWork2024: '是否开展规范化建设工作',
|
||||
recommendActivist2024: '推荐入党积极分子人数',
|
||||
activistConfirmed: '确定为入党积极分子数',
|
||||
recommendDevTarget2024: '推荐党的发展对象人数',
|
||||
devTargetConfirmed: '确定为党的发展对象数',
|
||||
honorsFiveYears: '近五年获得校级及以上荣誉情况',
|
||||
workSummaryThreeYears: '近三年开展的主要工作及取得的效果',
|
||||
partyOpinion: '所在单位党组织意见',
|
||||
schoolOpinion: '校团委意见'
|
||||
},
|
||||
sections: [
|
||||
{ title: '组织信息', fields: ['year', 'orgName', 'leader', 'phone'] },
|
||||
{
|
||||
title: '团员与团干部情况',
|
||||
fields: [
|
||||
'memberTotal',
|
||||
'memberDeveloped2024',
|
||||
'smartSystemLogin',
|
||||
'committeeCount',
|
||||
'fulltimeCadreCount',
|
||||
'parttimeCadreCount',
|
||||
'lastElectionTime'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '团费收缴情况',
|
||||
fields: [
|
||||
'feeReceivable2024',
|
||||
'feeReceived2024',
|
||||
'feePayable2024',
|
||||
'feePaid2024'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '团支部与推优情况',
|
||||
fields: [
|
||||
'branchCount',
|
||||
'recommendActivist2024',
|
||||
'activistConfirmed',
|
||||
'recommendDevTarget2024',
|
||||
'devTargetConfirmed'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '工作与荣誉',
|
||||
fields: [
|
||||
'standardizedWork2024',
|
||||
'honorsFiveYears',
|
||||
'workSummaryThreeYears'
|
||||
]
|
||||
},
|
||||
{ title: '意见栏', fields: ['partyOpinion', 'schoolOpinion'] }
|
||||
],
|
||||
materialFields: []
|
||||
},
|
||||
gxmu_wshqtzb: {
|
||||
title: '五四红旗团支部',
|
||||
sheetName: '五四红旗团支部',
|
||||
labels: {
|
||||
branchName: '团支部全称',
|
||||
secondOrg: '所属二级团组织',
|
||||
secretary: '团支部书记',
|
||||
politics: '政治面貌',
|
||||
contact: '联系方式',
|
||||
establishTime: '成立时间',
|
||||
lastElectionTime: '最近一次换届时间',
|
||||
smartSystemLogin: '是否已登录智慧团建系统',
|
||||
memberTotal: '现有团员总数',
|
||||
memberDeveloped2024: '上一年度发展团员数',
|
||||
feeReceivable2024: '上一年度应收团费',
|
||||
feeReceived2024: '上一年度实收团费',
|
||||
feePayable2024: '上一年度应上缴团费',
|
||||
feePaid2024: '上一年度实际上缴团费',
|
||||
recommendActivist2024: '推荐入党积极分子人数',
|
||||
activistConfirmed: '确定为入党积极分子数',
|
||||
recommendDevTarget2024: '推荐党的发展对象人数',
|
||||
devTargetConfirmed: '确定为党的发展对象数',
|
||||
branchCommitteeMeetingCount: '团支部委员会会议召开次数',
|
||||
branchMemberMeetingCount: '团支部团员大会召开次数',
|
||||
eduEvalDone: '是否开展团员教育评议',
|
||||
annualRegDone: '是否开展团员年度团籍注册',
|
||||
classCount: '开展团课次数',
|
||||
smartSystem100: '是否100%录入智慧团建',
|
||||
honorsFiveYears: '近五年获得院级及以上荣誉情况',
|
||||
workSummaryThreeYears: '近三年开展的主要工作及取得的效果',
|
||||
leagueOpinion: '所在二级团组织意见',
|
||||
partyOpinion: '所在单位党组织意见',
|
||||
schoolOpinion: '校团委意见'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '支部信息',
|
||||
fields: [
|
||||
'year',
|
||||
'branchName',
|
||||
'secondOrg',
|
||||
'secretary',
|
||||
'politics',
|
||||
'contact',
|
||||
'establishTime',
|
||||
'lastElectionTime'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '团员与团费情况',
|
||||
fields: [
|
||||
'smartSystemLogin',
|
||||
'memberTotal',
|
||||
'memberDeveloped2024',
|
||||
'feeReceivable2024',
|
||||
'feeReceived2024',
|
||||
'feePayable2024',
|
||||
'feePaid2024'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '推优情况',
|
||||
fields: [
|
||||
'recommendActivist2024',
|
||||
'activistConfirmed',
|
||||
'recommendDevTarget2024',
|
||||
'devTargetConfirmed'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '组织生活',
|
||||
fields: [
|
||||
'branchCommitteeMeetingCount',
|
||||
'branchMemberMeetingCount',
|
||||
'eduEvalDone',
|
||||
'annualRegDone',
|
||||
'classCount',
|
||||
'smartSystem100'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '工作与荣誉',
|
||||
fields: ['honorsFiveYears', 'workSummaryThreeYears']
|
||||
},
|
||||
OPINION_SECTION
|
||||
],
|
||||
materialFields: []
|
||||
},
|
||||
gxmu_yxgqtdgb: {
|
||||
title: '优秀共青团干部',
|
||||
sheetName: '优秀共青团干部',
|
||||
labels: {
|
||||
identity: '身份',
|
||||
organization: '所在团组织',
|
||||
contact: '联系方式',
|
||||
memberNo: '发展团员编号',
|
||||
currentDutyTime: '任现团内职务时间',
|
||||
cadreYears: '担任团干部年限',
|
||||
assessment2024: '上一年度工作考核结果',
|
||||
volunteerRegTime: '成为注册志愿者时间',
|
||||
cadreExperience: '从事团干部经历',
|
||||
honorsFiveYears: '近五年获得校级及以上荣誉情况',
|
||||
mainStory: '主要事迹',
|
||||
leagueOpinion: '所在单位团组织意见',
|
||||
partyOpinion: '所在单位党组织意见'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
'year',
|
||||
'name',
|
||||
'gender',
|
||||
'nation',
|
||||
'birthMonth',
|
||||
'politics',
|
||||
'position',
|
||||
'identity',
|
||||
'organization',
|
||||
'contact',
|
||||
'memberNo'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '团干部履历',
|
||||
fields: [
|
||||
'currentDutyTime',
|
||||
'cadreYears',
|
||||
'assessment2024',
|
||||
'volunteerRegTime',
|
||||
'cadreExperience'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '主要事迹与荣誉',
|
||||
fields: ['honorsFiveYears', 'mainStory']
|
||||
},
|
||||
{ title: '意见栏', fields: ['leagueOpinion', 'partyOpinion'] }
|
||||
],
|
||||
materialFields: []
|
||||
},
|
||||
gxmu_yxgqty: {
|
||||
title: '优秀共青团员',
|
||||
sheetName: '优秀共青团员',
|
||||
labels: {
|
||||
joinTime: '入团时间',
|
||||
collegeMajorClass: '所在学院、专业、班级',
|
||||
position: '职务',
|
||||
volunteerRegTime: '成为注册志愿者时间',
|
||||
eduEval2024: '上一年度团员教育评议等次',
|
||||
smartSystem: '是否已登录智慧团建系统',
|
||||
contact: '联系电话',
|
||||
totalVolunteerHours: '累计志愿服务时长',
|
||||
volunteerHours2024: '上一年度志愿服务时长',
|
||||
memberNo: '发展团员编号',
|
||||
honorsFiveYears: '近五年获得荣誉情况',
|
||||
mainStory: '主要事迹',
|
||||
leagueOpinion: '学生社团指导教师意见/所在单位团委',
|
||||
partyOpinion: '业务指导单位意见/所在单位党组织',
|
||||
schoolOpinion: '校团委意见'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
'year',
|
||||
'name',
|
||||
'gender',
|
||||
'nation',
|
||||
'politics',
|
||||
'birthMonth',
|
||||
'joinTime',
|
||||
'collegeMajorClass',
|
||||
'position',
|
||||
'contact',
|
||||
'memberNo'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '志愿服务与教育评议',
|
||||
fields: [
|
||||
'volunteerRegTime',
|
||||
'eduEval2024',
|
||||
'smartSystem',
|
||||
'totalVolunteerHours',
|
||||
'volunteerHours2024'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '主要事迹与荣誉',
|
||||
fields: ['honorsFiveYears', 'mainStory']
|
||||
},
|
||||
OPINION_SECTION
|
||||
],
|
||||
materialFields: []
|
||||
},
|
||||
gxmu_tzbcy_form: {
|
||||
title: '挑战杯',
|
||||
sheetName: '挑战杯',
|
||||
labels: {
|
||||
projectMaterials: '项目介绍材料',
|
||||
otherProofs: '其他相关证明材料',
|
||||
teamMembers: '团队成员',
|
||||
advisors: '指导老师'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '项目信息',
|
||||
fields: [
|
||||
'year',
|
||||
'projectName',
|
||||
'provinceCity',
|
||||
'schoolName',
|
||||
'projectType',
|
||||
'projectGroup'
|
||||
]
|
||||
},
|
||||
{ title: '团队成员', fields: ['teamMembers'] },
|
||||
{ title: '指导老师', fields: ['advisors'] },
|
||||
{
|
||||
title: '项目说明',
|
||||
fields: [
|
||||
'projectBrief',
|
||||
'socialValue',
|
||||
'practiceProcess',
|
||||
'innovationMeaning',
|
||||
'developmentProspect',
|
||||
'teamCooperation',
|
||||
'projectMaterials',
|
||||
'otherProofs'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '公开展示信息',
|
||||
fields: [
|
||||
'publicProjectType',
|
||||
'publicProjectGroup',
|
||||
'projectSummary',
|
||||
'teamIntro',
|
||||
'teamSlogan',
|
||||
'practiceLog'
|
||||
]
|
||||
}
|
||||
],
|
||||
materialFields: [],
|
||||
materialSource: 'tzbcy'
|
||||
},
|
||||
gxmu_qmgc_form: {
|
||||
title: '青马工程',
|
||||
sheetName: '青马工程',
|
||||
labels: {
|
||||
hobby: '兴趣爱好',
|
||||
willingToWorkInGuangxi: '是否愿意留桂工作',
|
||||
schoolInfo: '学校信息',
|
||||
leaguePosition: '团内职务',
|
||||
resume: '个人简历',
|
||||
awards: '获得奖励',
|
||||
academicPerformance: '学习成绩',
|
||||
secondaryLeagueOpinion: '二级团组织意见',
|
||||
secondaryLeagueOpinionDate: '二级团组织意见日期',
|
||||
secondaryPartyOpinion: '二级党组织意见',
|
||||
secondaryPartyOpinionDate: '二级党组织意见日期',
|
||||
schoolLeagueOpinion: '学校团组织意见',
|
||||
schoolLeagueOpinionDate: '学校团组织意见日期'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '个人信息',
|
||||
fields: [
|
||||
'year',
|
||||
'name',
|
||||
'gender',
|
||||
'nation',
|
||||
'photo',
|
||||
'birthMonth',
|
||||
'politics',
|
||||
'nativePlace',
|
||||
'phone',
|
||||
'wechat',
|
||||
'email',
|
||||
'qq',
|
||||
'idCardNo',
|
||||
'hobby',
|
||||
'willingToWorkInGuangxi',
|
||||
'schoolInfo',
|
||||
'leaguePosition'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '学习与经历',
|
||||
fields: ['resume', 'awards', 'academicPerformance']
|
||||
},
|
||||
{
|
||||
title: '意见栏',
|
||||
fields: [
|
||||
'secondaryLeagueOpinion',
|
||||
'secondaryLeagueOpinionDate',
|
||||
'secondaryPartyOpinion',
|
||||
'secondaryPartyOpinionDate',
|
||||
'schoolLeagueOpinion',
|
||||
'schoolLeagueOpinionDate'
|
||||
]
|
||||
}
|
||||
],
|
||||
materialFields: []
|
||||
},
|
||||
gxmu_wxxzx_project: {
|
||||
title: '未来学术之星',
|
||||
sheetName: '未来学术之星',
|
||||
labels: {
|
||||
topicName: '课题名称',
|
||||
leader: '课题负责人',
|
||||
collegeGradeClass: '学院年级班级',
|
||||
phone: '联系电话',
|
||||
advisor: '指导老师',
|
||||
topicType: '课题类别',
|
||||
guidanceTeachers: '指导教师组',
|
||||
applicants: '申请人信息',
|
||||
budgets: '经费预算',
|
||||
rationale: '立项依据',
|
||||
teacherOpinion: '指导教师意见',
|
||||
teacherSign: '指导教师签名',
|
||||
reviewCollege: '评审学院',
|
||||
reviewReporter: '汇报人',
|
||||
reviewDate: '评审日期',
|
||||
reviewOpinion: '评审意见',
|
||||
reviewScore: '评审成绩',
|
||||
reviewLeaderSign: '评审负责人签名',
|
||||
collegeOpinion: '学院意见',
|
||||
schoolOpinion: '学校意见',
|
||||
reviewMembers: '评审成员',
|
||||
midtermRemark: '中期检查说明',
|
||||
midtermMaterials: '中期检查材料',
|
||||
finalRemark: '结题说明',
|
||||
finalMaterials: '结题材料',
|
||||
archiveRecords: '成果归档',
|
||||
archiveAwards: '获奖情况',
|
||||
archiveProofs: '佐证材料',
|
||||
promiseSigner: '承诺签字人',
|
||||
promiseSignature: '承诺书签名',
|
||||
fundingBudgetFile: '经费预算表',
|
||||
fundingAllocationFile: '经费划拨明细表',
|
||||
proofFile: '证明材料'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '课题信息',
|
||||
fields: [
|
||||
'year',
|
||||
'topicName',
|
||||
'leader',
|
||||
'collegeGradeClass',
|
||||
'phone',
|
||||
'advisor',
|
||||
'topicType'
|
||||
]
|
||||
},
|
||||
{ title: '指导教师组', fields: ['guidanceTeachers'] },
|
||||
{ title: '申请人信息', fields: ['applicants'] },
|
||||
{
|
||||
title: '经费预算',
|
||||
fields: ['budgets', 'fundingBudgetFile', 'fundingAllocationFile']
|
||||
},
|
||||
{ title: '立项依据', fields: ['rationale'] },
|
||||
{
|
||||
title: '审核评审',
|
||||
fields: [
|
||||
'teacherOpinion',
|
||||
'teacherSign',
|
||||
'reviewCollege',
|
||||
'reviewReporter',
|
||||
'reviewDate',
|
||||
'reviewOpinion',
|
||||
'reviewScore',
|
||||
'reviewLeaderSign',
|
||||
'collegeOpinion',
|
||||
'schoolOpinion'
|
||||
]
|
||||
},
|
||||
{ title: '评审成员', fields: ['reviewMembers'] },
|
||||
{
|
||||
title: '中期检查与结题',
|
||||
fields: [
|
||||
'midtermRemark',
|
||||
'midtermMaterials',
|
||||
'finalRemark',
|
||||
'finalMaterials'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '成果归档',
|
||||
fields: ['archiveRecords', 'archiveAwards', 'archiveProofs']
|
||||
},
|
||||
{
|
||||
title: '承诺书',
|
||||
fields: ['promiseSigner', 'promiseSignature']
|
||||
}
|
||||
],
|
||||
materialFields: [
|
||||
'fundingBudgetFile',
|
||||
'fundingAllocationFile',
|
||||
'midtermMaterials',
|
||||
'finalMaterials',
|
||||
'archiveProofs'
|
||||
],
|
||||
materialTables: [
|
||||
{
|
||||
field: 'archiveRecords',
|
||||
fileField: 'proofFile',
|
||||
titleField: 'awardName',
|
||||
label: '成果归档证明材料'
|
||||
}
|
||||
]
|
||||
},
|
||||
gxmu_tygl_form: {
|
||||
title: '团员管理',
|
||||
sheetName: '团员信息登记表',
|
||||
labels: {
|
||||
serialNo: '序号',
|
||||
college: '学院',
|
||||
className: '班级',
|
||||
phone: '手机号码',
|
||||
leaguePosition: '团内职务',
|
||||
branchOrganizationName: '所属团支部',
|
||||
idCardNo: '身份证号',
|
||||
birthDate: '出生日期',
|
||||
archiveInCurrentOrg: '团籍是否在本组织',
|
||||
joinMonth: '入团年月',
|
||||
memberRecords: '团员记录',
|
||||
growthArchives: '成长档案'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
'serialNo',
|
||||
'name',
|
||||
'gender',
|
||||
'nation',
|
||||
'politics',
|
||||
'college',
|
||||
'className',
|
||||
'phone',
|
||||
'leaguePosition',
|
||||
'branchOrganizationName',
|
||||
'idCardNo',
|
||||
'birthDate',
|
||||
'archiveInCurrentOrg',
|
||||
'joinMonth'
|
||||
]
|
||||
},
|
||||
{ title: '团员记录', fields: ['memberRecords'] },
|
||||
{ title: '成长档案', fields: ['growthArchives'] }
|
||||
],
|
||||
materialFields: []
|
||||
},
|
||||
cms_manuscript: {
|
||||
title: '稿件',
|
||||
sheetName: '稿件',
|
||||
labels: {
|
||||
title: '标题',
|
||||
content: '正文',
|
||||
contentFormat: '正文格式',
|
||||
cover: '封面',
|
||||
status: '状态',
|
||||
sortNumber: '排序'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: ['title', 'status', 'sortNumber']
|
||||
},
|
||||
{ title: '封面', fields: ['cover'] },
|
||||
{ title: '正文', fields: ['content'] }
|
||||
],
|
||||
materialFields: []
|
||||
}
|
||||
};
|
||||
|
||||
// 团员管理模块历史上存在两个模块标识
|
||||
MODULE_DETAIL_CONFIGS.gxmu_tygl = MODULE_DETAIL_CONFIGS.gxmu_tygl_form;
|
||||
|
||||
/**
|
||||
* 明细数组表头字段中文名
|
||||
*/
|
||||
export const TABLE_FIELD_LABELS: Record<string, string> = {
|
||||
name: '姓名',
|
||||
gender: '性别',
|
||||
nation: '民族',
|
||||
politics: '政治面貌',
|
||||
birthMonth: '出生年月',
|
||||
title: '职称',
|
||||
duty: '职务',
|
||||
department: '部门',
|
||||
dept: '部门',
|
||||
remark: '备注',
|
||||
college: '学院',
|
||||
gradeMajor: '年级专业',
|
||||
grade: '年级',
|
||||
className: '班级',
|
||||
major: '专业',
|
||||
role: '角色',
|
||||
phone: '联系电话',
|
||||
sign: '签名',
|
||||
item: '项目',
|
||||
amount: '金额',
|
||||
reason: '原因',
|
||||
type: '类型',
|
||||
content: '内容',
|
||||
recordTime: '时间',
|
||||
archiveTime: '时间',
|
||||
awardName: '获奖名称',
|
||||
awardTime: '获奖时间',
|
||||
archiveOrder: '序号',
|
||||
proofFile: '证明材料',
|
||||
year: '年份'
|
||||
};
|
||||
|
||||
export const getModuleDetailConfig = (module?: string) => {
|
||||
if (!module) {
|
||||
return undefined;
|
||||
}
|
||||
return MODULE_DETAIL_CONFIGS[module];
|
||||
};
|
||||
@@ -15,7 +15,9 @@
|
||||
<a-tag>{{ moduleName || module || '-' }}</a-tag>
|
||||
<span>业务ID:</span>
|
||||
<a-tag>{{ pk ?? '-' }}</a-tag>
|
||||
<a-button size="small" :loading="loading" @click="loadData">刷新</a-button>
|
||||
<a-button size="small" :loading="loading" @click="loadData"
|
||||
>刷新</a-button
|
||||
>
|
||||
</a-space>
|
||||
|
||||
<div v-if="flowList.length" class="flow-wrap">
|
||||
@@ -23,7 +25,9 @@
|
||||
<a-step v-for="item in flowList" :key="item.id">
|
||||
<template #title>
|
||||
<div class="flow-title">
|
||||
<span>{{ item.user?.realName || item.user?.username || '未分配审核人' }}</span>
|
||||
<span>{{
|
||||
item.user?.realName || item.user?.username || '未分配审核人'
|
||||
}}</span>
|
||||
<a-tag :color="getStatusColor(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</a-tag>
|
||||
@@ -31,14 +35,28 @@
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="flow-desc">
|
||||
<div>创建时间:{{ toDateString(item.createTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
|
||||
<div
|
||||
>创建时间:{{
|
||||
toDateString(item.createTime, 'yyyy-MM-dd HH:mm:ss')
|
||||
}}</div
|
||||
>
|
||||
<div v-if="item.content">审核意见:{{ item.content }}</div>
|
||||
<a-space v-if="canReview(item)" style="margin-top: 12px">
|
||||
<a-button type="primary" size="small" :loading="submitting" @click="openActionModal('approve', item)">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="submitting"
|
||||
@click="openActionModal('approve', item)"
|
||||
>
|
||||
通过
|
||||
</a-button>
|
||||
<a-button danger size="small" :loading="submitting" @click="openActionModal('reject', item)">
|
||||
拒绝
|
||||
<a-button
|
||||
danger
|
||||
size="small"
|
||||
:loading="submitting"
|
||||
@click="openActionModal('reject', item)"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
@@ -50,19 +68,26 @@
|
||||
|
||||
<a-modal
|
||||
v-model:visible="actionVisible"
|
||||
:title="actionType === 'approve' ? '审核通过' : '审核拒绝'"
|
||||
:title="actionType === 'approve' ? '审核通过' : '审核驳回'"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
:confirm-loading="submitting"
|
||||
@ok="submitReview"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="审核意见(非必填)">
|
||||
<a-form
|
||||
ref="actionFormRef"
|
||||
layout="vertical"
|
||||
:model="actionForm"
|
||||
:rules="actionRules"
|
||||
>
|
||||
<a-form-item label="审核意见" name="content">
|
||||
<a-textarea
|
||||
v-model:value="actionContent"
|
||||
v-model:value="actionForm.content"
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
allow-clear
|
||||
placeholder="请输入审核意见"
|
||||
placeholder="请输入审核意见(必填)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -71,163 +96,187 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Empty, message } from 'ant-design-vue';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import { listReviewList, updateReviewList } from '@/api/gxmu/reviewList';
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { Empty, message } from 'ant-design-vue';
|
||||
import type { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import { listReviewList, updateReviewList } from '@/api/gxmu/reviewList';
|
||||
import type { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
module?: string;
|
||||
moduleName?: string;
|
||||
pk?: number;
|
||||
}>();
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
module?: string;
|
||||
moduleName?: string;
|
||||
pk?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const loginUserId = computed(() => Number(userStore.info?.userId || 0));
|
||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const flowList = ref<ReviewList[]>([]);
|
||||
const actionVisible = ref(false);
|
||||
const actionType = ref<'approve' | 'reject'>('approve');
|
||||
const actionContent = ref('');
|
||||
const actionTarget = ref<ReviewList | null>(null);
|
||||
const userStore = useUserStore();
|
||||
const loginUserId = computed(() => Number(userStore.info?.userId || 0));
|
||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const flowList = ref<ReviewList[]>([]);
|
||||
const actionVisible = ref(false);
|
||||
const actionType = ref<'approve' | 'reject'>('approve');
|
||||
const actionFormRef = ref<FormInstance | null>(null);
|
||||
const actionForm = reactive<{ content: string }>({ content: '' });
|
||||
const actionTarget = ref<ReviewList | null>(null);
|
||||
|
||||
const currentStepIndex = computed(() => {
|
||||
const pendingIndex = flowList.value.findIndex((item) => item.status === 0);
|
||||
if (pendingIndex >= 0) {
|
||||
return pendingIndex;
|
||||
}
|
||||
return Math.max(flowList.value.length - 1, 0);
|
||||
});
|
||||
// 审核意见必填(仅空白字符视为未填写)
|
||||
const actionRules = {
|
||||
content: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule: unknown, value: string) => {
|
||||
const text = (value || '').trim();
|
||||
return text
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请输入审核意见'));
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
if (status === 1) return '通过';
|
||||
if (status === 2) return '拒绝';
|
||||
return '待审核';
|
||||
};
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
if (status === 1) return 'success';
|
||||
if (status === 2) return 'error';
|
||||
return 'default';
|
||||
};
|
||||
|
||||
const canReview = (item: ReviewList) => {
|
||||
return item.status === 0 && Number(item.userId || 0) === loginUserId.value;
|
||||
};
|
||||
|
||||
const loadData = () => {
|
||||
if (!props.module || !props.pk) {
|
||||
flowList.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
listReviewList({
|
||||
module: props.module,
|
||||
pk: props.pk
|
||||
})
|
||||
.then((data) => {
|
||||
flowList.value = [...(data || [])].sort(
|
||||
(a, b) => Number(b.id || 0) - Number(a.id || 0)
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
flowList.value = [];
|
||||
message.error(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const openActionModal = (type: 'approve' | 'reject', item: ReviewList) => {
|
||||
if (!canReview(item)) {
|
||||
message.error('当前记录不是由你审核');
|
||||
return;
|
||||
}
|
||||
actionType.value = type;
|
||||
actionTarget.value = item;
|
||||
actionContent.value = '';
|
||||
actionVisible.value = true;
|
||||
};
|
||||
|
||||
const submitReview = () => {
|
||||
if (!actionTarget.value?.id) {
|
||||
actionVisible.value = false;
|
||||
return;
|
||||
}
|
||||
if (!canReview(actionTarget.value)) {
|
||||
actionVisible.value = false;
|
||||
message.error('当前记录不是由你审核');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
updateReviewList({
|
||||
id: actionTarget.value.id,
|
||||
status: actionType.value === 'approve' ? 1 : 2,
|
||||
module: actionTarget.value.module,
|
||||
pk: actionTarget.value.pk,
|
||||
sortNumber: actionTarget.value.sortNumber,
|
||||
content: actionContent.value || undefined
|
||||
})
|
||||
.then((msg) => {
|
||||
message.success(msg);
|
||||
actionVisible.value = false;
|
||||
emit('done');
|
||||
loadData();
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
submitting.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
loadData();
|
||||
} else {
|
||||
flowList.value = [];
|
||||
actionVisible.value = false;
|
||||
actionTarget.value = null;
|
||||
actionContent.value = '';
|
||||
const currentStepIndex = computed(() => {
|
||||
const pendingIndex = flowList.value.findIndex((item) => item.status === 0);
|
||||
if (pendingIndex >= 0) {
|
||||
return pendingIndex;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
return Math.max(flowList.value.length - 1, 0);
|
||||
});
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
if (status === 1) return '通过';
|
||||
if (status === 2) return '驳回';
|
||||
return '待审核';
|
||||
};
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
if (status === 1) return 'success';
|
||||
if (status === 2) return 'error';
|
||||
return 'default';
|
||||
};
|
||||
|
||||
const canReview = (item: ReviewList) => {
|
||||
return item.status === 0 && Number(item.userId || 0) === loginUserId.value;
|
||||
};
|
||||
|
||||
const loadData = () => {
|
||||
if (!props.module || !props.pk) {
|
||||
flowList.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
listReviewList({
|
||||
module: props.module,
|
||||
pk: props.pk
|
||||
})
|
||||
.then((data) => {
|
||||
flowList.value = [...(data || [])].sort(
|
||||
(a, b) => Number(b.id || 0) - Number(a.id || 0)
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
flowList.value = [];
|
||||
message.error(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const openActionModal = (type: 'approve' | 'reject', item: ReviewList) => {
|
||||
if (!canReview(item)) {
|
||||
message.error('当前记录不是由你审核');
|
||||
return;
|
||||
}
|
||||
actionType.value = type;
|
||||
actionTarget.value = item;
|
||||
actionForm.content = '';
|
||||
actionVisible.value = true;
|
||||
};
|
||||
|
||||
const submitReview = () => {
|
||||
const target = actionTarget.value;
|
||||
if (!target?.id) {
|
||||
actionVisible.value = false;
|
||||
return;
|
||||
}
|
||||
if (!canReview(target)) {
|
||||
actionVisible.value = false;
|
||||
message.error('当前记录不是由你审核');
|
||||
return;
|
||||
}
|
||||
actionFormRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
submitting.value = true;
|
||||
return updateReviewList({
|
||||
id: target.id,
|
||||
status: actionType.value === 'approve' ? 1 : 2,
|
||||
module: target.module,
|
||||
pk: target.pk,
|
||||
sortNumber: target.sortNumber,
|
||||
content: actionForm.content.trim()
|
||||
})
|
||||
.then((msg) => {
|
||||
message.success(msg);
|
||||
actionVisible.value = false;
|
||||
emit('done');
|
||||
loadData();
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
submitting.value = false;
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
loadData();
|
||||
} else {
|
||||
flowList.value = [];
|
||||
actionVisible.value = false;
|
||||
actionTarget.value = null;
|
||||
actionForm.content = '';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.flow-wrap {
|
||||
max-height: 560px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.flow-wrap {
|
||||
max-height: 560px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.flow-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flow-desc {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.flow-desc {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 1.8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
v-model:value="form.pk"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态(0不通过 1通过)" name="status">
|
||||
<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>
|
||||
|
||||
+170
-150
@@ -23,7 +23,7 @@
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0">待审核</a-tag>
|
||||
<a-tag v-else-if="record.status === 1" color="green">通过</a-tag>
|
||||
<a-tag v-else-if="record.status === 2" color="red">拒绝</a-tag>
|
||||
<a-tag v-else-if="record.status === 2" color="red">驳回</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
@@ -36,12 +36,17 @@
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<ReviewListEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
<ReviewListEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDataVisible"
|
||||
:module="dataCurrent?.module"
|
||||
:module-name="dataCurrent?.moduleName"
|
||||
:pk="dataCurrent?.pk"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewFlowModal
|
||||
v-model:visible="showFlowVisible"
|
||||
@@ -55,162 +60,177 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type { DatasourceFunction, ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ReviewListEdit from './components/reviewListEdit.vue';
|
||||
import ModuleDataModal from './components/moduleDataModal.vue';
|
||||
import ReviewFlowModal from './components/reviewFlowModal.vue';
|
||||
import { pageGroupReviewList, removeBatchReviewList } from '@/api/gxmu/reviewList';
|
||||
import type { ReviewList, ReviewListParam } from '@/api/gxmu/reviewList/model';
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ReviewListEdit from './components/reviewListEdit.vue';
|
||||
import ModuleDataModal from './components/moduleDataModal.vue';
|
||||
import ReviewFlowModal from './components/reviewFlowModal.vue';
|
||||
import {
|
||||
pageGroupReviewList,
|
||||
removeBatchReviewList
|
||||
} from '@/api/gxmu/reviewList';
|
||||
import type {
|
||||
ReviewList,
|
||||
ReviewListParam
|
||||
} from '@/api/gxmu/reviewList/model';
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const selection = ref<ReviewList[]>([]);
|
||||
const current = ref<ReviewList | null>(null);
|
||||
const dataCurrent = ref<ReviewList | null>(null);
|
||||
const flowCurrent = ref<ReviewList | null>(null);
|
||||
const showEdit = ref(false);
|
||||
const showDataVisible = ref(false);
|
||||
const showFlowVisible = ref(false);
|
||||
const showMove = ref(false);
|
||||
const loading = ref(true);
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const selection = ref<ReviewList[]>([]);
|
||||
const current = ref<ReviewList | null>(null);
|
||||
const dataCurrent = ref<ReviewList | null>(null);
|
||||
const flowCurrent = ref<ReviewList | null>(null);
|
||||
const showEdit = ref(false);
|
||||
const showDataVisible = ref(false);
|
||||
const showFlowVisible = ref(false);
|
||||
const showMove = ref(false);
|
||||
const loading = ref(true);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders, filters }) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageGroupReviewList({
|
||||
...where,
|
||||
...orders,
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'moduleName',
|
||||
key: 'module',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'businessName',
|
||||
key: 'businessName',
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '最新审核意见',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '当前审核人',
|
||||
dataIndex: ['user', 'realName'],
|
||||
key: 'userId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
const reload = (where?: ReviewListParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where });
|
||||
};
|
||||
|
||||
const openEdit = (row?: ReviewList) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
const openDataModal = (row: ReviewList) => {
|
||||
dataCurrent.value = row;
|
||||
showDataVisible.value = true;
|
||||
};
|
||||
|
||||
const openFlowModal = (row: ReviewList) => {
|
||||
flowCurrent.value = row;
|
||||
showFlowVisible.value = true;
|
||||
};
|
||||
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchReviewList(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
});
|
||||
};
|
||||
return pageGroupReviewList({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'moduleName',
|
||||
key: 'module',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'businessName',
|
||||
key: 'businessName',
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '最新审核意见',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '当前审核人',
|
||||
dataIndex: ['user', 'realName'],
|
||||
key: 'userId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
query();
|
||||
const reload = (where?: ReviewListParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where });
|
||||
};
|
||||
|
||||
const openEdit = (row?: ReviewList) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
const openDataModal = (row: ReviewList) => {
|
||||
dataCurrent.value = row;
|
||||
showDataVisible.value = true;
|
||||
};
|
||||
|
||||
const openFlowModal = (row: ReviewList) => {
|
||||
flowCurrent.value = row;
|
||||
showFlowVisible.value = true;
|
||||
};
|
||||
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchReviewList(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ReviewList'
|
||||
};
|
||||
export default {
|
||||
name: 'ReviewList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,118 @@
|
||||
export interface AttachmentFile {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const FILE_EXT_PATTERN =
|
||||
/\.(pdf|docx?|xlsx?|pptx?|csv|txt|zip|rar|7z|jpe?g|png|gif|webp|bmp|svg|mp4|mp3|wav)$/i;
|
||||
|
||||
/**
|
||||
* 从文件地址中取出展示用的文件名
|
||||
*/
|
||||
export function getAttachmentName(url?: string) {
|
||||
if (!url) {
|
||||
return '未命名文件';
|
||||
}
|
||||
const cleanUrl = url.split('?')[0];
|
||||
const name = cleanUrl.split('/').pop() || cleanUrl;
|
||||
try {
|
||||
return decodeURIComponent(name);
|
||||
} catch (e) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否像文件地址(http(s)、协议相对地址、带扩展名的相对路径)
|
||||
*/
|
||||
export function isFileUrl(value?: string) {
|
||||
const text = value?.trim();
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
if (/^(https?:)?\/\//i.test(text)) {
|
||||
return true;
|
||||
}
|
||||
return FILE_EXT_PATTERN.test(text) && !/\s/.test(text);
|
||||
}
|
||||
|
||||
export function isImageFile(url?: string) {
|
||||
const text = (url || '').split('?')[0];
|
||||
return /\.(jpe?g|png|gif|webp|bmp|svg)$/i.test(text);
|
||||
}
|
||||
|
||||
const normalizeEntry = (item: unknown): AttachmentFile | null => {
|
||||
if (typeof item === 'string') {
|
||||
return isFileUrl(item)
|
||||
? { name: getAttachmentName(item), url: item.trim() }
|
||||
: null;
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
const record = item as Record<string, unknown>;
|
||||
const rawUrl = [
|
||||
record.url,
|
||||
record.fileUrl,
|
||||
record.downloadUrl,
|
||||
record.path
|
||||
].find((value) => typeof value === 'string' && isFileUrl(value as string));
|
||||
if (typeof rawUrl !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const rawName = [record.name, record.fileName, record.originalName].find(
|
||||
(value) => typeof value === 'string' && (value as string).trim()
|
||||
);
|
||||
return {
|
||||
name: (rawName as string | undefined) || getAttachmentName(rawUrl),
|
||||
url: rawUrl.trim()
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 把后端存储的附件字段解析成文件列表。
|
||||
* 兼容:单个地址、JSON 数组字符串、对象数组(name/url、fileName/fileUrl 等)
|
||||
*/
|
||||
export function parseAttachments(value: unknown): AttachmentFile[] {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return [];
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim();
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
if (text.startsWith('[') || text.startsWith('{')) {
|
||||
try {
|
||||
return parseAttachments(JSON.parse(text));
|
||||
} catch (e) {
|
||||
// 解析失败时按普通地址处理
|
||||
}
|
||||
}
|
||||
const single = normalizeEntry(text);
|
||||
return single ? [single] : [];
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => normalizeEntry(item))
|
||||
.filter((item): item is AttachmentFile => !!item);
|
||||
}
|
||||
const single = normalizeEntry(value);
|
||||
return single ? [single] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把附件字段解析结果转换成图片地址列表
|
||||
*/
|
||||
export function parseAttachmentImages(value: unknown) {
|
||||
const urls: string[] = [];
|
||||
parseAttachments(value).forEach((item) => {
|
||||
if (isImageFile(item.url)) {
|
||||
urls.push(item.url);
|
||||
}
|
||||
});
|
||||
if (!urls.length && typeof value === 'string' && isImageFile(value)) {
|
||||
urls.push(value);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
@@ -63,7 +63,7 @@ const router = useRouter();
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "AI+党建的双向赋能",
|
||||
title: "AI+团建的双向赋能",
|
||||
desc: "赋能广西医科大学团委的各项工作。项目遵循“先基础,后智能”的原则,信息化和智能化相结合,确保项目稳步推进,价值逐步释放。"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
class="ele-btn-icon"
|
||||
@click="openUrl('https://p.haoii123.com/')"
|
||||
>
|
||||
一键排版
|
||||
公众号排版
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user