【北大开源实践】增加数据导出功能 (#294)
* feat:添加了一个文件数据导出的功能和相应前端页面 * fix lint * fix conflict --------- Co-authored-by: dayou <853094838@qq.com>
This commit is contained in:
parent
2e1af4ae3a
commit
fd2ad752a0
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"node-cron": "^3.0.3"
|
||||
}
|
||||
}
|
@ -41,6 +41,7 @@ import { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
|
||||
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
||||
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
|
||||
import { Logger } from './logger';
|
||||
import { SurveyDownload } from './models/surveyDownload.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -81,6 +82,7 @@ import { Logger } from './logger';
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
Collaborator,
|
||||
SurveyDownload,
|
||||
],
|
||||
};
|
||||
},
|
||||
|
21
server/src/config/index.ts
Normal file
21
server/src/config/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
const mongo = {
|
||||
url: process.env.XIAOJU_SURVEY_MONGO_URL || 'mongodb://localhost:27017',
|
||||
dbName: process.env.XIAOJU_SURVER_MONGO_DBNAME || 'xiaojuSurvey',
|
||||
};
|
||||
|
||||
const session = {
|
||||
expireTime:
|
||||
parseInt(process.env.XIAOJU_SURVEY_JWT_EXPIRES_IN) || 8 * 3600 * 1000,
|
||||
};
|
||||
|
||||
const encrypt = {
|
||||
type: process.env.XIAOJU_SURVEY_ENCRYPT_TYPE || 'aes',
|
||||
aesCodelength: parseInt(process.env.XIAOJU_SURVEY_ENCRYPT_TYPE_LEN) || 10, //aes密钥长度
|
||||
};
|
||||
|
||||
const jwt = {
|
||||
secret: process.env.XIAOJU_SURVEY_JWT_SECRET || 'xiaojuSurveyJwtSecret',
|
||||
expiresIn: process.env.XIAOJU_SURVEY_JWT_EXPIRES_IN || '8h',
|
||||
};
|
||||
|
||||
export { mongo, session, encrypt, jwt };
|
@ -6,6 +6,7 @@ export enum RECORD_STATUS {
|
||||
PUBLISHED = 'published', // 发布
|
||||
REMOVED = 'removed', // 删除
|
||||
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
||||
COMOPUTETING = 'computing', // 计算中
|
||||
}
|
||||
|
||||
// 历史类型
|
||||
|
46
server/src/models/surveyDownload.entity.ts
Normal file
46
server/src/models/surveyDownload.entity.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Entity, Column, BeforeInsert, AfterLoad } from 'typeorm';
|
||||
import pluginManager from '../securityPlugin/pluginManager';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'surveyDownload' })
|
||||
export class SurveyDownload extends BaseEntity {
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@Column()
|
||||
surveyPath: string;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
filePath: string;
|
||||
|
||||
@Column()
|
||||
onwer: string;
|
||||
|
||||
@Column()
|
||||
filename: string;
|
||||
|
||||
@Column()
|
||||
fileSize: string;
|
||||
|
||||
@Column()
|
||||
fileType: string;
|
||||
|
||||
// @Column()
|
||||
// ownerId: string;
|
||||
|
||||
@Column()
|
||||
downloadTime: string;
|
||||
|
||||
@BeforeInsert()
|
||||
async onDataInsert() {
|
||||
return await pluginManager.triggerHook('beforeResponseDataCreate', this);
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
async onDataLoaded() {
|
||||
return await pluginManager.triggerHook('afterResponseDataReaded', this);
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { SurveyDownloadService } from '../services/surveyDownload.service';
|
||||
|
||||
@ApiTags('survey')
|
||||
@ApiBearerAuth()
|
||||
@ -30,6 +31,8 @@ export class DataStatisticController {
|
||||
private readonly dataStatisticService: DataStatisticService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly logger: Logger,
|
||||
//
|
||||
private readonly surveyDownloadService: SurveyDownloadService,
|
||||
) {}
|
||||
|
||||
@Get('/dataTable')
|
||||
|
@ -0,0 +1,219 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
Res,
|
||||
// Response,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
//后添加
|
||||
import { SurveyDownloadService } from '../services/surveyDownload.service';
|
||||
import {
|
||||
DownloadFileByNameDto,
|
||||
GetDownloadDto,
|
||||
GetDownloadListDto,
|
||||
} from '../dto/getdownload.dto';
|
||||
import { join } from 'path';
|
||||
import * as util from 'util';
|
||||
import * as fs from 'fs';
|
||||
import { Response } from 'express';
|
||||
import moment from 'moment';
|
||||
import { MessageService } from '../services/message.service';
|
||||
|
||||
@ApiTags('survey')
|
||||
@ApiBearerAuth()
|
||||
@Controller('/api/survey/surveyDownload')
|
||||
export class SurveyDownloadController {
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly surveyDownloadService: SurveyDownloadService,
|
||||
private readonly logger: Logger,
|
||||
private readonly messageService: MessageService,
|
||||
) {}
|
||||
|
||||
@Get('/download')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'query.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async download(
|
||||
@Query()
|
||||
queryInfo: GetDownloadDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = GetDownloadDto.validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { surveyId, isDesensitive } = value;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const id = await this.surveyDownloadService.createDownload({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
});
|
||||
this.messageService.addMessage({
|
||||
responseSchema,
|
||||
surveyId,
|
||||
isDesensitive,
|
||||
id,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: { message: '正在生成下载文件,请稍后查看' },
|
||||
};
|
||||
}
|
||||
@Get('/getdownloadList')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'query.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async downloadList(
|
||||
@Query()
|
||||
queryInfo: GetDownloadListDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = GetDownloadListDto.validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { ownerId, page, pageSize } = value;
|
||||
const { total, listBody } =
|
||||
await this.surveyDownloadService.getDownloadList({
|
||||
ownerId,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
total: total,
|
||||
listBody: listBody.map((data) => {
|
||||
const fmt = 'YYYY-MM-DD HH:mm:ss';
|
||||
const units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
let unitIndex = 0;
|
||||
let size = Number(data.fileSize);
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
data.downloadTime = moment(Number(data.downloadTime)).format(fmt);
|
||||
data.fileSize = `${size.toFixed()} ${units[unitIndex]}`;
|
||||
return data;
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/getdownloadfileByName')
|
||||
// @HttpCode(200)
|
||||
// @UseGuards(SurveyGuard)
|
||||
// @SetMetadata('surveyId', 'query.surveyId')
|
||||
// @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||
// @UseGuards(Authentication)
|
||||
async getDownloadfileByName(
|
||||
@Query() queryInfo: DownloadFileByNameDto,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const { value, error } = DownloadFileByNameDto.validate(queryInfo);
|
||||
if (error) {
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const { owner, fileName } = value;
|
||||
const rootDir = process.cwd(); // 获取当前工作目录
|
||||
const filePath = join(rootDir, 'download', owner, fileName);
|
||||
|
||||
// 使用 util.promisify 将 fs.access 转换为返回 Promise 的函数
|
||||
const access = util.promisify(fs.access);
|
||||
try {
|
||||
console.log('检查文件路径:', filePath);
|
||||
await access(filePath, fs.constants.F_OK);
|
||||
|
||||
// 文件存在,设置响应头并流式传输文件
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
console.log('文件存在,设置响应头');
|
||||
const encodedFileName = encodeURIComponent(fileName);
|
||||
const contentDisposition = `attachment; filename="${encodedFileName}"; filename*=UTF-8''${encodedFileName}`;
|
||||
res.setHeader('Content-Disposition', contentDisposition);
|
||||
console.log('设置响应头成功,文件名:', encodedFileName);
|
||||
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
console.log('创建文件流成功');
|
||||
fileStream.pipe(res);
|
||||
|
||||
fileStream.on('end', () => {
|
||||
console.log('文件传输完成');
|
||||
});
|
||||
|
||||
fileStream.on('error', (streamErr) => {
|
||||
console.error('文件流错误:', streamErr);
|
||||
res.status(500).send('文件传输中出现错误');
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('文件不存在:', filePath);
|
||||
res.status(404).send('文件不存在');
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/deletefileByName')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'query.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async deleteFileByName(
|
||||
@Query() queryInfo: DownloadFileByNameDto,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const { value, error } = DownloadFileByNameDto.validate(queryInfo);
|
||||
if (error) {
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { owner, fileName } = value;
|
||||
|
||||
try {
|
||||
const result = await this.surveyDownloadService.deleteDownloadFile({
|
||||
owner,
|
||||
fileName,
|
||||
});
|
||||
|
||||
// 根据 deleteDownloadFile 的返回值执行不同操作
|
||||
if (result === 0) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '文件状态已删除或文件不存在',
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
code: 200,
|
||||
message: '文件删除成功',
|
||||
data: {},
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
code: 500,
|
||||
message: '删除文件时出错',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
43
server/src/modules/survey/dto/getdownload.dto.ts
Normal file
43
server/src/modules/survey/dto/getdownload.dto.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import Joi from 'joi';
|
||||
|
||||
export class GetDownloadDto {
|
||||
@ApiProperty({ description: '问卷id', required: true })
|
||||
surveyId: string;
|
||||
@ApiProperty({ description: '是否脱密', required: true })
|
||||
isDesensitive: boolean;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
isDesensitive: Joi.boolean().default(true), // 默认true就是需要脱敏
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
export class GetDownloadListDto {
|
||||
@ApiProperty({ description: '拥有者id', required: true })
|
||||
ownerId: string;
|
||||
@ApiProperty({ description: '当前页', required: false })
|
||||
page: number;
|
||||
@ApiProperty({ description: '一页大小', required: false })
|
||||
pageSize: number;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
ownerId: Joi.string().required(),
|
||||
page: Joi.number().default(1),
|
||||
pageSize: Joi.number().default(20),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
export class DownloadFileByNameDto {
|
||||
@ApiProperty({ description: '文件名', required: true })
|
||||
fileName: string;
|
||||
owner: string;
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
fileName: Joi.string().required(),
|
||||
owner: Joi.string().required(),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import { keyBy } from 'lodash';
|
||||
import { DataItem } from 'src/interfaces/survey';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { getListHeadByDataList } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
export class DataStatisticService {
|
||||
private radioType = ['radio-star', 'radio-nps'];
|
||||
|
87
server/src/modules/survey/services/message.service.ts
Normal file
87
server/src/modules/survey/services/message.service.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { SurveyDownloadService } from './surveyDownload.service';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
|
||||
interface QueueItem {
|
||||
surveyId: string;
|
||||
responseSchema: ResponseSchema;
|
||||
isDesensitive: boolean;
|
||||
id: object;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MessageService extends EventEmitter {
|
||||
private queue: QueueItem[];
|
||||
private concurrency: number;
|
||||
private processing: number;
|
||||
|
||||
constructor(
|
||||
@Inject('NumberToken') concurrency: number,
|
||||
private readonly surveyDownloadService: SurveyDownloadService,
|
||||
) {
|
||||
super();
|
||||
this.queue = [];
|
||||
this.concurrency = concurrency;
|
||||
this.processing = 0;
|
||||
this.on('messageAdded', this.processMessages);
|
||||
}
|
||||
|
||||
public addMessage({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
isDesensitive,
|
||||
id,
|
||||
}: {
|
||||
surveyId: string;
|
||||
responseSchema: ResponseSchema;
|
||||
isDesensitive: boolean;
|
||||
id: object;
|
||||
}) {
|
||||
const message = {
|
||||
surveyId,
|
||||
responseSchema,
|
||||
isDesensitive,
|
||||
id,
|
||||
};
|
||||
this.queue.push(message);
|
||||
this.emit('messageAdded');
|
||||
}
|
||||
|
||||
private processMessages = async (): Promise<void> => {
|
||||
if (this.processing >= this.concurrency || this.queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messagesToProcess = Math.min(
|
||||
this.queue.length,
|
||||
this.concurrency - this.processing,
|
||||
);
|
||||
const messages = this.queue.splice(0, messagesToProcess);
|
||||
|
||||
this.processing += messagesToProcess;
|
||||
|
||||
await Promise.all(
|
||||
messages.map(async (message) => {
|
||||
console.log(`开始计算: ${message}`);
|
||||
await this.handleMessage(message);
|
||||
this.emit('messageProcessed', message);
|
||||
}),
|
||||
);
|
||||
|
||||
this.processing -= messagesToProcess;
|
||||
if (this.queue.length > 0) {
|
||||
setImmediate(() => this.processMessages());
|
||||
}
|
||||
};
|
||||
|
||||
async handleMessage(message: QueueItem) {
|
||||
const { surveyId, responseSchema, isDesensitive, id } = message;
|
||||
await this.surveyDownloadService.getDownloadPath({
|
||||
responseSchema,
|
||||
surveyId,
|
||||
isDesensitive,
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
365
server/src/modules/survey/services/surveyDownload.service.ts
Normal file
365
server/src/modules/survey/services/surveyDownload.service.ts
Normal file
@ -0,0 +1,365 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
|
||||
import moment from 'moment';
|
||||
import { keyBy } from 'lodash';
|
||||
import { DataItem } from 'src/interfaces/survey';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { getListHeadByDataList } from '../utils';
|
||||
//后添加
|
||||
import { promises } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { SurveyDownload } from 'src/models/surveyDownload.entity';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import * as cron from 'node-cron';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
@Injectable()
|
||||
export class SurveyDownloadService implements OnModuleInit {
|
||||
private radioType = ['radio-star', 'radio-nps'];
|
||||
|
||||
constructor(
|
||||
@InjectRepository(SurveyResponse)
|
||||
private readonly surveyResponseRepository: MongoRepository<SurveyResponse>,
|
||||
@InjectRepository(SurveyDownload)
|
||||
private readonly SurveyDownloadRepository: MongoRepository<SurveyDownload>,
|
||||
@InjectRepository(SurveyMeta)
|
||||
private readonly SurveyDmetaRepository: MongoRepository<SurveyMeta>,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
) {}
|
||||
//初始化一个自动删除过期文件的方法
|
||||
async onModuleInit() {
|
||||
cron.schedule('0 0 * * *', async () => {
|
||||
try {
|
||||
const files = await this.SurveyDownloadRepository.find({
|
||||
where: {
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
});
|
||||
const now = Date.now();
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.downloadTime || !file.filePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileSaveDate = Number(file.downloadTime);
|
||||
const diffDays = (now - fileSaveDate) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (diffDays > 10) {
|
||||
this.deleteDownloadFile({
|
||||
owner: file.onwer,
|
||||
fileName: file.filename,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('删除文件错误', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createDownload({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
}: {
|
||||
surveyId: string;
|
||||
responseSchema: ResponseSchema;
|
||||
}) {
|
||||
const [surveyMeta] = await this.SurveyDmetaRepository.find({
|
||||
where: {
|
||||
surveyPath: responseSchema.surveyPath,
|
||||
},
|
||||
});
|
||||
const newSurveyDownload = this.SurveyDownloadRepository.create({
|
||||
pageId: surveyId,
|
||||
surveyPath: responseSchema.surveyPath,
|
||||
title: responseSchema.title,
|
||||
fileSize: '计算中',
|
||||
downloadTime: String(Date.now()),
|
||||
onwer: surveyMeta.owner,
|
||||
});
|
||||
newSurveyDownload.curStatus = {
|
||||
status: RECORD_STATUS.COMOPUTETING,
|
||||
date: Date.now(),
|
||||
};
|
||||
return (await this.SurveyDownloadRepository.save(newSurveyDownload))._id;
|
||||
}
|
||||
|
||||
private formatHead(listHead = []) {
|
||||
const head = [];
|
||||
|
||||
listHead.forEach((headItem) => {
|
||||
head.push({
|
||||
field: headItem.field,
|
||||
title: headItem.title,
|
||||
});
|
||||
|
||||
if (headItem.othersCode?.length) {
|
||||
headItem.othersCode.forEach((item) => {
|
||||
head.push({
|
||||
field: item.code,
|
||||
title: `${headItem.title}-${item.option}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return head;
|
||||
}
|
||||
async getDownloadPath({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
isDesensitive,
|
||||
id,
|
||||
}: {
|
||||
surveyId: string;
|
||||
responseSchema: ResponseSchema;
|
||||
isDesensitive: boolean;
|
||||
id: object;
|
||||
}) {
|
||||
const dataList = responseSchema?.code?.dataConf?.dataList || [];
|
||||
const Head = getListHeadByDataList(dataList);
|
||||
const listHead = this.formatHead(Head);
|
||||
const dataListMap = keyBy(dataList, 'field');
|
||||
const where = {
|
||||
pageId: surveyId,
|
||||
'curStatus.status': {
|
||||
$ne: 'removed',
|
||||
},
|
||||
};
|
||||
const [surveyResponseList] =
|
||||
await this.surveyResponseRepository.findAndCount({
|
||||
where,
|
||||
order: {
|
||||
createDate: -1,
|
||||
},
|
||||
});
|
||||
const [surveyMeta] = await this.SurveyDmetaRepository.find({
|
||||
where: {
|
||||
surveyPath: responseSchema.surveyPath,
|
||||
},
|
||||
});
|
||||
const listBody = surveyResponseList.map((submitedData) => {
|
||||
const data = submitedData.data;
|
||||
const dataKeys = Object.keys(data);
|
||||
|
||||
for (const itemKey of dataKeys) {
|
||||
if (typeof itemKey !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (itemKey.indexOf('data') !== 0) {
|
||||
continue;
|
||||
}
|
||||
// 获取题目id
|
||||
const itemConfigKey = itemKey.split('_')[0];
|
||||
// 获取题目
|
||||
const itemConfig: DataItem = dataListMap[itemConfigKey];
|
||||
// 题目删除会出现,数据列表报错
|
||||
if (!itemConfig) {
|
||||
continue;
|
||||
}
|
||||
// 处理选项的更多输入框
|
||||
if (
|
||||
this.radioType.includes(itemConfig.type) &&
|
||||
!data[`${itemConfigKey}_custom`]
|
||||
) {
|
||||
data[`${itemConfigKey}_custom`] =
|
||||
data[`${itemConfigKey}_${data[itemConfigKey]}`];
|
||||
}
|
||||
// 将选项id还原成选项文案
|
||||
if (
|
||||
Array.isArray(itemConfig.options) &&
|
||||
itemConfig.options.length > 0
|
||||
) {
|
||||
const optionTextMap = keyBy(itemConfig.options, 'hash');
|
||||
data[itemKey] = Array.isArray(data[itemKey])
|
||||
? data[itemKey]
|
||||
.map((item) => optionTextMap[item]?.text || item)
|
||||
.join(',')
|
||||
: optionTextMap[data[itemKey]]?.text || data[itemKey];
|
||||
}
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
difTime: (submitedData.difTime / 1000).toFixed(2),
|
||||
createDate: moment(submitedData.createDate).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
),
|
||||
};
|
||||
});
|
||||
if (isDesensitive) {
|
||||
// 脱敏
|
||||
listBody.forEach((item) => {
|
||||
this.pluginManager.triggerHook('desensitiveData', item);
|
||||
});
|
||||
}
|
||||
|
||||
let titlesCsv =
|
||||
listHead
|
||||
.map((question) => `"${question.title.replace(/<[^>]*>/g, '')}"`)
|
||||
.join(',') + '\n';
|
||||
// 获取工作区根目录的路径
|
||||
const rootDir = process.cwd();
|
||||
const timestamp = Date.now();
|
||||
|
||||
const filePath = join(
|
||||
rootDir,
|
||||
'download',
|
||||
`${surveyMeta.owner}`,
|
||||
`${surveyMeta.title}_${timestamp}.csv`,
|
||||
);
|
||||
const dirPath = path.dirname(filePath);
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
listBody.forEach((row) => {
|
||||
const rowValues = listHead.map((head) => {
|
||||
const value = row[head.field];
|
||||
if (typeof value === 'string') {
|
||||
// 处理字符串中的特殊字符
|
||||
return `"${value.replace(/"/g, '""').replace(/<[^>]*>/g, '')}"`;
|
||||
}
|
||||
return `"${value}"`; // 其他类型的值(数字、布尔等)直接转换为字符串
|
||||
});
|
||||
titlesCsv += rowValues.join(',') + '\n';
|
||||
});
|
||||
const BOM = '\uFEFF';
|
||||
let size = 0;
|
||||
const newSurveyDownload = await this.SurveyDownloadRepository.findOne({
|
||||
where: {
|
||||
_id: id,
|
||||
},
|
||||
});
|
||||
fs.writeFile(filePath, BOM + titlesCsv, { encoding: 'utf8' }, (err) => {
|
||||
if (err) {
|
||||
console.error('保存文件时出错:', err);
|
||||
} else {
|
||||
console.log('文件已保存:', filePath);
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err) {
|
||||
console.error('获取文件大小时出错:', err);
|
||||
} else {
|
||||
console.log('文件大小:', stats.size);
|
||||
size = stats.size;
|
||||
const filename = `${surveyMeta.title}_${timestamp}.csv`;
|
||||
const fileType = 'csv';
|
||||
(newSurveyDownload.pageId = surveyId),
|
||||
(newSurveyDownload.surveyPath = responseSchema.surveyPath),
|
||||
(newSurveyDownload.title = responseSchema.title),
|
||||
(newSurveyDownload.filePath = filePath),
|
||||
(newSurveyDownload.filename = filename),
|
||||
(newSurveyDownload.fileType = fileType),
|
||||
(newSurveyDownload.fileSize = String(size)),
|
||||
(newSurveyDownload.downloadTime = String(Date.now())),
|
||||
(newSurveyDownload.onwer = surveyMeta.owner);
|
||||
newSurveyDownload.curStatus = {
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: Date.now(),
|
||||
};
|
||||
|
||||
this.SurveyDownloadRepository.save(newSurveyDownload);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
filePath,
|
||||
};
|
||||
}
|
||||
|
||||
async getDownloadList({
|
||||
ownerId,
|
||||
page,
|
||||
pageSize,
|
||||
}: {
|
||||
ownerId: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}) {
|
||||
const where = {
|
||||
onwer: ownerId,
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
};
|
||||
const [surveyDownloadList, total] =
|
||||
await this.SurveyDownloadRepository.findAndCount({
|
||||
where,
|
||||
take: pageSize,
|
||||
skip: (page - 1) * pageSize,
|
||||
order: {
|
||||
createDate: -1,
|
||||
},
|
||||
});
|
||||
const listBody = surveyDownloadList.map((data) => {
|
||||
return {
|
||||
_id: data._id,
|
||||
filename: data.filename,
|
||||
fileType: data.fileType,
|
||||
fileSize: data.fileSize,
|
||||
downloadTime: data.downloadTime,
|
||||
curStatus: data.curStatus.status,
|
||||
owner: data.onwer,
|
||||
};
|
||||
});
|
||||
return {
|
||||
total,
|
||||
listBody,
|
||||
};
|
||||
}
|
||||
async test({}: { fileName: string }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteDownloadFile({
|
||||
owner,
|
||||
fileName,
|
||||
}: {
|
||||
owner: string;
|
||||
fileName: string;
|
||||
}) {
|
||||
const where = {
|
||||
filename: fileName,
|
||||
};
|
||||
|
||||
const [surveyDownloadList] = await this.SurveyDownloadRepository.find({
|
||||
where,
|
||||
});
|
||||
if (surveyDownloadList.curStatus.status === RECORD_STATUS.REMOVED) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const newStatusInfo = {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: Date.now(),
|
||||
};
|
||||
surveyDownloadList.curStatus = newStatusInfo;
|
||||
// if (Array.isArray(survey.statusList)) {
|
||||
// survey.statusList.push(newStatusInfo);
|
||||
// } else {
|
||||
// survey.statusList = [newStatusInfo];
|
||||
// }
|
||||
const rootDir = process.cwd(); // 获取当前工作目录
|
||||
const filePath = join(rootDir, 'download', owner, fileName);
|
||||
try {
|
||||
await promises.unlink(filePath);
|
||||
console.log(`File at ${filePath} has been successfully deleted.`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete file at ${filePath}:`, error);
|
||||
}
|
||||
await this.SurveyDownloadRepository.save(surveyDownloadList);
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
message: '删除成功',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@ -29,6 +29,11 @@ import { SurveyHistoryService } from './services/surveyHistory.service';
|
||||
import { SurveyMetaService } from './services/surveyMeta.service';
|
||||
import { ContentSecurityService } from './services/contentSecurity.service';
|
||||
import { CollaboratorService } from './services/collaborator.service';
|
||||
//后添加
|
||||
import { SurveyDownload } from 'src/models/surveyDownload.entity';
|
||||
import { SurveyDownloadService } from './services/surveyDownload.service';
|
||||
import { SurveyDownloadController } from './controllers/surveyDownload.controller';
|
||||
import { MessageService } from './services/message.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -39,6 +44,8 @@ import { CollaboratorService } from './services/collaborator.service';
|
||||
SurveyResponse,
|
||||
Word,
|
||||
Collaborator,
|
||||
//后添加
|
||||
SurveyDownload,
|
||||
]),
|
||||
ConfigModule,
|
||||
SurveyResponseModule,
|
||||
@ -52,6 +59,8 @@ import { CollaboratorService } from './services/collaborator.service';
|
||||
SurveyMetaController,
|
||||
SurveyUIController,
|
||||
CollaboratorController,
|
||||
//后添加
|
||||
SurveyDownloadController,
|
||||
],
|
||||
providers: [
|
||||
DataStatisticService,
|
||||
@ -62,6 +71,13 @@ import { CollaboratorService } from './services/collaborator.service';
|
||||
ContentSecurityService,
|
||||
CollaboratorService,
|
||||
LoggerProvider,
|
||||
//后添加
|
||||
SurveyDownloadService,
|
||||
MessageService,
|
||||
{
|
||||
provide: 'NumberToken', // 使用一个唯一的标识符
|
||||
useValue: 10, // 假设这是你想提供的值
|
||||
},
|
||||
],
|
||||
})
|
||||
export class SurveyModule {}
|
||||
|
2
web/components.d.ts
vendored
2
web/components.d.ts
vendored
@ -28,7 +28,6 @@ declare module 'vue' {
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
@ -51,7 +50,6 @@ declare module 'vue' {
|
||||
IEpMinus: typeof import('~icons/ep/minus')['default']
|
||||
IEpMonitor: typeof import('~icons/ep/monitor')['default']
|
||||
IEpMore: typeof import('~icons/ep/more')['default']
|
||||
IEpPlus: typeof import('~icons/ep/plus')['default']
|
||||
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
|
||||
IEpRank: typeof import('~icons/ep/rank')['default']
|
||||
IEpRemove: typeof import('~icons/ep/remove')['default']
|
||||
|
@ -8,3 +8,12 @@ export const getRecycleList = (data) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
//问卷下载
|
||||
export const downloadSurvey = ({ surveyId, isDesensitive }) => {
|
||||
return axios.get('/survey/surveyDownload/download', {
|
||||
params: {
|
||||
surveyId,
|
||||
isDesensitive
|
||||
}
|
||||
})
|
||||
}
|
||||
|
34
web/src/management/api/download.js
Normal file
34
web/src/management/api/download.js
Normal file
@ -0,0 +1,34 @@
|
||||
import axios from './base'
|
||||
|
||||
//问卷列表
|
||||
export const getDownloadList = ({ ownerId, page, pageSize }) => {
|
||||
return axios.get('/survey/surveyDownload/getdownloadList', {
|
||||
params: {
|
||||
ownerId,
|
||||
page,
|
||||
pageSize
|
||||
}
|
||||
})
|
||||
}
|
||||
//问卷下载
|
||||
export const getDownloadFileByName = (fileName) => {
|
||||
return axios
|
||||
.get('/survey/surveyDownload/getdownloadfileByName', {
|
||||
params: {
|
||||
fileName
|
||||
},
|
||||
responseType: 'blob'
|
||||
})
|
||||
.then((res) => {
|
||||
return res
|
||||
})
|
||||
}
|
||||
//问卷删除
|
||||
export const deleteDownloadFile = (owner, fileName) => {
|
||||
return axios.get('/survey/surveyDownload/deletefileByName', {
|
||||
params: {
|
||||
owner,
|
||||
fileName
|
||||
}
|
||||
})
|
||||
}
|
@ -11,6 +11,17 @@
|
||||
@input="onIsShowOriginChange"
|
||||
>
|
||||
</el-switch>
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<el-switch
|
||||
:model-value="isDownloadDesensitive"
|
||||
active-text="是否下载脱敏数据"
|
||||
@input="onisDownloadDesensitive"
|
||||
style="margin-right: 20px"
|
||||
>
|
||||
</el-switch>
|
||||
<el-button type="primary" @click="onDownload">导出数据</el-button>
|
||||
</div>
|
||||
<!-- <el-button type="primary" @click="exportData">导出数据</el-button> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -38,7 +49,7 @@ import 'element-plus/theme-chalk/src/message.scss'
|
||||
|
||||
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
||||
import LeftMenu from '@/management/components/LeftMenu.vue'
|
||||
import { getRecycleList } from '@/management/api/analysis'
|
||||
import { getRecycleList, downloadSurvey } from '@/management/api/analysis'
|
||||
|
||||
import DataTable from './components/DataTable.vue'
|
||||
|
||||
@ -59,7 +70,8 @@ export default {
|
||||
},
|
||||
currentPage: 1,
|
||||
isShowOriginData: false,
|
||||
tmpIsShowOriginData: false
|
||||
tmpIsShowOriginData: false,
|
||||
isDownloadDesensitive: true
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
@ -89,6 +101,34 @@ export default {
|
||||
ElMessage.error('查询回收数据失败,请重试')
|
||||
}
|
||||
},
|
||||
async onDownload() {
|
||||
try {
|
||||
await ElMessageBox.confirm('是否确认下载?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('取消下载')
|
||||
return
|
||||
}
|
||||
this.exportData()
|
||||
this.gotoDownloadList()
|
||||
},
|
||||
async gotoDownloadList() {
|
||||
try {
|
||||
await ElMessageBox.confirm('计算中,是否前往下载中心?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('取消跳转')
|
||||
return
|
||||
}
|
||||
|
||||
this.$router.push('/survey/download')
|
||||
},
|
||||
handleCurrentChange(current) {
|
||||
if (this.mainTableLoading) {
|
||||
return
|
||||
@ -125,6 +165,29 @@ export default {
|
||||
this.tmpIsShowOriginData = data
|
||||
await this.init()
|
||||
this.isShowOriginData = data
|
||||
},
|
||||
async onisDownloadDesensitive() {
|
||||
if (this.isDownloadDesensitive) {
|
||||
this.isDownloadDesensitive = false
|
||||
} else {
|
||||
this.isDownloadDesensitive = true
|
||||
}
|
||||
},
|
||||
|
||||
async exportData() {
|
||||
try {
|
||||
const res = await downloadSurvey({
|
||||
surveyId: String(this.$route.params.id),
|
||||
isDesensitive: this.isDownloadDesensitive
|
||||
})
|
||||
console.log(this.$route.params.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('下载成功')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('下载失败')
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -158,6 +221,8 @@ export default {
|
||||
|
||||
.menus {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
|
129
web/src/management/pages/download/SurveyDownloadPage.vue
Normal file
129
web/src/management/pages/download/SurveyDownloadPage.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="question-list-root">
|
||||
<div class="top-nav">
|
||||
<div class="left">
|
||||
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
|
||||
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
|
||||
<el-menu-item index="1" @click="handleSurvey">问卷列表</el-menu-item>
|
||||
<el-menu-item index="2">下载页面</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="login-info">
|
||||
您好,{{ userInfo?.username }}
|
||||
<img class="login-info-img" src="/imgs/avatar.webp" />
|
||||
<span class="logout" @click="handleLogout">退出</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<DownloadList
|
||||
:loading="loading"
|
||||
:data="surveyList"
|
||||
:total="surveyTotal"
|
||||
@reflush="fetchSurveyList"
|
||||
></DownloadList>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useRouter } from 'vue-router'
|
||||
import DownloadList from './components/DownloadList.vue'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const userInfo = computed(() => {
|
||||
return store.state.user.userInfo
|
||||
})
|
||||
const surveyList = computed(() => {
|
||||
return store.state.download.surveyList
|
||||
})
|
||||
const surveyTotal = computed(() => {
|
||||
return store.state.download.surveyTotal
|
||||
})
|
||||
const handleSurvey = () => {
|
||||
router.push('/survey')
|
||||
}
|
||||
const handleLogout = () => {
|
||||
store.dispatch('user/logout')
|
||||
router.replace({ name: 'login' })
|
||||
}
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
fetchSurveyList()
|
||||
})
|
||||
const fetchSurveyList = async (params?: any) => {
|
||||
if (!params) {
|
||||
params = {
|
||||
pageSize: 15,
|
||||
curPage: 1
|
||||
}
|
||||
}
|
||||
;(params.ownerId = store.state.user.userInfo.username), (loading.value = true)
|
||||
await store.dispatch('download/getDownloadList', params)
|
||||
loading.value = false
|
||||
}
|
||||
const activeIndex = ref('2')
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.question-list-root {
|
||||
height: 100%;
|
||||
background-color: #f6f7f9;
|
||||
.top-nav {
|
||||
background: #fff;
|
||||
color: #4a4c5b;
|
||||
padding: 0 20px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.04);
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 200px);
|
||||
.logo-img {
|
||||
width: 90px;
|
||||
height: fit-content;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.el-menu {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
border: none !important;
|
||||
:deep(.el-menu-item, .is-active) {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.login-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.login-info-img {
|
||||
margin-left: 10px;
|
||||
height: 30px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.logout {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
color: #faa600;
|
||||
}
|
||||
}
|
||||
.table-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%; /* 确保容器宽度为100% */
|
||||
}
|
||||
}
|
||||
</style>
|
273
web/src/management/pages/download/components/DownloadList.vue
Normal file
273
web/src/management/pages/download/components/DownloadList.vue
Normal file
@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div class="list-wrapper" v-if="total">
|
||||
<el-table
|
||||
v-if="total"
|
||||
ref="multipleListTable"
|
||||
class="list-table"
|
||||
:data="dataList"
|
||||
empty-text="暂无数据"
|
||||
row-key="_id"
|
||||
header-row-class-name="tableview-header"
|
||||
row-class-name="tableview-row"
|
||||
cell-class-name="tableview-cell"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="field in fieldList"
|
||||
:key="field.key"
|
||||
:prop="field.key"
|
||||
:label="field.title"
|
||||
:width="field.width"
|
||||
class-name="link"
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template v-slot="{ row }">
|
||||
<el-button type="text" size="small" @click="handleDownload(row)"> 下载 </el-button>
|
||||
<el-button type="text" size="small" @click="openDeleteDialog(row)"> 删除 </el-button>
|
||||
<el-dialog v-model="centerDialogVisible" title="Warning" width="500" align-center>
|
||||
<span>确认删除文件吗?</span>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="centerDialogVisible = false"> 取消 </el-button>
|
||||
<el-button type="primary" @click="confirmDelete"> 确认 </el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="small-text">文件有效期为十天,过期或删除将从下载页面移除,请及时下载.</div>
|
||||
<div class="list-pagination" v-if="total">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { get, map } from 'lodash-es'
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||
|
||||
import moment from 'moment'
|
||||
// 引入中文
|
||||
import 'moment/locale/zh-cn'
|
||||
// 设置中文
|
||||
moment.locale('zh-cn')
|
||||
|
||||
import { deleteDownloadFile } from '@/management/api/download'
|
||||
import axios from 'axios'
|
||||
|
||||
interface DownloadItem {
|
||||
downloadTime: number // 根据实际情况可能需要调整类型
|
||||
[key: string]: any // 允许其他任意属性
|
||||
}
|
||||
const store = useStore()
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
const centerDialogVisible = ref(false)
|
||||
// 下载文件
|
||||
const handleDownload = async (row: any) => {
|
||||
if (row.curStatus == 'removed') {
|
||||
ElMessage.error('文件已删除')
|
||||
return
|
||||
}
|
||||
const fileName = row.filename
|
||||
const owner = row.owner
|
||||
axios({
|
||||
method: 'get',
|
||||
url:
|
||||
'/api/survey/surveyDownload/getdownloadfileByName?fileName=' + fileName + '&owner=' + owner,
|
||||
responseType: 'blob' // 设置响应类型为 Blob
|
||||
})
|
||||
.then((response: { data: BlobPart }) => {
|
||||
const blob = new Blob([response.data])
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.download = fileName
|
||||
|
||||
// 添加到文档中
|
||||
document.body.appendChild(link)
|
||||
|
||||
// 模拟点击链接来触发文件下载
|
||||
link.click()
|
||||
|
||||
// 清理资源
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('下载文件时出错:', error)
|
||||
})
|
||||
// try {
|
||||
// // 获取文件内容
|
||||
// const response = await getDownloadFileByName(fileName);
|
||||
// console.log('Response from server:', response);
|
||||
|
||||
// // 解析文件名获取 MIME 类型
|
||||
// let mimeType = '';
|
||||
// if (fileName.endsWith('.csv')) {
|
||||
// mimeType = 'text/csv; charset=utf-8'; // 指定编码方式为 UTF-8
|
||||
// } else if (fileName.endsWith('.xls')) {
|
||||
// mimeType = 'application/vnd.ms-excel';
|
||||
// } else if (fileName.endsWith('.xlsx')) {
|
||||
// mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
// } else {
|
||||
// throw new Error('不支持的文件类型');
|
||||
// }
|
||||
|
||||
// const blob = new Blob(response.data, { type: mimeType });
|
||||
// console.log('Blob:', blob); // Check the Blob object
|
||||
|
||||
// const url = window.URL.createObjectURL(blob);
|
||||
// const link = document.createElement('a');
|
||||
// link.href = url;
|
||||
// link.download = fileName;
|
||||
// link.click();
|
||||
// window.URL.revokeObjectURL(url);
|
||||
// if (link.parentNode)
|
||||
// link.parentNode.removeChild(link);
|
||||
// } catch (error) {
|
||||
// console.error('下载文件时出错:', error);
|
||||
// }
|
||||
}
|
||||
// 删除文件
|
||||
const openDeleteDialog = (row: any) => {
|
||||
centerDialogVisible.value = true
|
||||
store.dispatch('download/setRow', row)
|
||||
}
|
||||
const handleDelete = async (row: any, callback: { (): void; (): void }) => {
|
||||
try {
|
||||
console.log('Delete file:', row.filename)
|
||||
const fileName = row.filename
|
||||
const owner = row.owner
|
||||
await deleteDownloadFile(owner, fileName)
|
||||
row.curStatus = 'removed'
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除文件时出错:', error)
|
||||
}
|
||||
}
|
||||
// 确认删除文件
|
||||
const confirmDelete = () => {
|
||||
handleDelete(store.state.download.currentRow, () => {
|
||||
centerDialogVisible.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const fields = ['filename', 'fileType', 'fileSize', 'downloadTime', 'curStatus']
|
||||
const total = computed(() => {
|
||||
return props.total
|
||||
})
|
||||
const data = computed(() => {
|
||||
return props.data
|
||||
})
|
||||
|
||||
const dataList = computed(() => {
|
||||
return (data.value as DownloadItem[]).map((item: DownloadItem) => {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
return {
|
||||
...item
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
const fieldList = computed(() => {
|
||||
return map(fields, (f) => {
|
||||
return get(downloadListConfig, f)
|
||||
})
|
||||
})
|
||||
const downloadListConfig = {
|
||||
filename: {
|
||||
title: '文件名称',
|
||||
key: 'filename',
|
||||
width: 340,
|
||||
tip: true
|
||||
},
|
||||
fileType: {
|
||||
title: '格式',
|
||||
key: 'fileType',
|
||||
width: 200,
|
||||
tip: true
|
||||
},
|
||||
fileSize: {
|
||||
title: '预估大小',
|
||||
key: 'fileSize',
|
||||
width: 140
|
||||
},
|
||||
downloadTime: {
|
||||
title: '下载时间',
|
||||
key: 'downloadTime',
|
||||
width: 240
|
||||
},
|
||||
curStatus: {
|
||||
title: '状态',
|
||||
key: 'curStatus',
|
||||
comp: 'StateModule'
|
||||
}
|
||||
}
|
||||
const handleCurrentChange = (val: number) => {
|
||||
const params = {
|
||||
pageSize: 15,
|
||||
page: val,
|
||||
ownerId: store.state.user.userInfo.username
|
||||
}
|
||||
|
||||
store.dispatch('download/getDownloadList', params)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.question-list-root {
|
||||
height: 100%;
|
||||
background-color: #f6f7f9;
|
||||
|
||||
.list-wrapper {
|
||||
padding: 10px 20px;
|
||||
background: #fff;
|
||||
|
||||
.list-table {
|
||||
min-height: 620px;
|
||||
|
||||
.cell {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.small-text {
|
||||
color: red;
|
||||
}
|
||||
.list-pagination {
|
||||
margin-top: 20px;
|
||||
:deep(.el-pagination) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -92,7 +92,7 @@ export const statusMaps = {
|
||||
new: '未发布',
|
||||
editing: '修改中',
|
||||
published: '已发布',
|
||||
removed: '',
|
||||
removed: '已删除',
|
||||
pausing: ''
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
|
||||
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
|
||||
<el-menu-item index="1">问卷列表</el-menu-item>
|
||||
<el-menu-item index="2" @click="handleDownload">下载页面</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="login-info">
|
||||
@ -155,6 +156,10 @@ const handleLogout = () => {
|
||||
store.dispatch('user/logout')
|
||||
router.replace({ name: 'login' })
|
||||
}
|
||||
// 下载页面
|
||||
const handleDownload = () => {
|
||||
router.push('/survey/download')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -19,6 +19,14 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '问卷列表'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/survey/download/',
|
||||
name: 'download',
|
||||
component: () => import('../pages/download/SurveyDownloadPage.vue'),
|
||||
meta: {
|
||||
needLogin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/survey/:id/edit',
|
||||
meta: {
|
||||
|
54
web/src/management/store/download/index.js
Normal file
54
web/src/management/store/download/index.js
Normal file
@ -0,0 +1,54 @@
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
import { getDownloadList, deleteDownloadFile } from '@/management/api/download'
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
surveyList: [],
|
||||
surveyTotal: 0,
|
||||
currentRow: []
|
||||
},
|
||||
mutations: {
|
||||
setSurveyList(state, list) {
|
||||
state.surveyList = list
|
||||
},
|
||||
setSurveyTotal(state, total) {
|
||||
state.surveyTotal = total
|
||||
},
|
||||
setCurrentRow(state, row) {
|
||||
state.currentRow = row
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async setRow({ commit }, payload) {
|
||||
commit('setCurrentRow', payload)
|
||||
},
|
||||
async getDownloadList({ commit }, payload) {
|
||||
let params = {
|
||||
ownerId: payload.ownerId,
|
||||
page: payload.page ? payload.page : 1,
|
||||
pageSize: payload.pageSize ? payload.pageSize : 15
|
||||
}
|
||||
try {
|
||||
const { data } = await getDownloadList(params)
|
||||
console.log(data)
|
||||
commit('setSurveyList', data.listBody)
|
||||
commit('setSurveyTotal', data.total)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
async DownloadFileByName(payload) {
|
||||
let params = {
|
||||
fileName: payload.fileName
|
||||
}
|
||||
try {
|
||||
const { data } = await deleteDownloadFile(params)
|
||||
console.log(data)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
getters: {}
|
||||
}
|
@ -64,5 +64,8 @@ export default {
|
||||
},
|
||||
setQuestionDataList(state, data) {
|
||||
state.schema.questionDataList = data
|
||||
},
|
||||
setDownloadPath(state, data) {
|
||||
state.downloadPath = data
|
||||
}
|
||||
}
|
||||
|
@ -56,5 +56,6 @@ export default {
|
||||
logicConf: {
|
||||
showLogicConf: []
|
||||
}
|
||||
}
|
||||
},
|
||||
downloadPath: ''
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import list from './list'
|
||||
import actions from './actions'
|
||||
import mutations from './mutations'
|
||||
import state from './state'
|
||||
import download from './download'
|
||||
|
||||
export default createStore({
|
||||
state,
|
||||
@ -14,6 +15,7 @@ export default createStore({
|
||||
modules: {
|
||||
edit,
|
||||
user,
|
||||
list
|
||||
list,
|
||||
download
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user