commit
This commit is contained in:
@@ -233,6 +233,11 @@ public class MemberApplyRecord extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String sign;
|
||||
|
||||
@Column
|
||||
@Comment("照片")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String photo;
|
||||
|
||||
@Column
|
||||
@Comment("来源,高校编码")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||
|
||||
+118
-8
@@ -40,6 +40,15 @@ import com.deepoove.poi.XWPFTemplate;
|
||||
import com.deepoove.poi.config.Configure;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ddr.poi.html.HtmlRenderPolicy;
|
||||
import org.apache.poi.xwpf.usermodel.LineSpacingRule;
|
||||
import org.apache.poi.util.Units;
|
||||
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTable;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.*;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
@@ -52,10 +61,10 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Lang;
|
||||
import org.nutz.lang.random.R;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline;
|
||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDrawing;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -77,6 +86,13 @@ import java.util.zip.ZipOutputStream;
|
||||
@Slf4j
|
||||
public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implements MemberCommonService {
|
||||
|
||||
private static final int MEMBER_APPLY_PHOTO_WIDTH_PIXEL = 100;
|
||||
private static final int MEMBER_APPLY_PHOTO_HEIGHT_PIXEL = 140;
|
||||
private static final int MEMBER_APPLY_PHOTO_WIDTH_EMU = Units.pixelToEMU(MEMBER_APPLY_PHOTO_WIDTH_PIXEL);
|
||||
private static final int MEMBER_APPLY_PHOTO_HEIGHT_EMU = Units.pixelToEMU(MEMBER_APPLY_PHOTO_HEIGHT_PIXEL);
|
||||
private static final int MEMBER_APPLY_PHOTO_SIZE_TOLERANCE_EMU = Units.pixelToEMU(2);
|
||||
private static final double MEMBER_APPLY_PHOTO_PARAGRAPH_LINE_POINT = MEMBER_APPLY_PHOTO_HEIGHT_PIXEL * 0.75D;
|
||||
|
||||
@Inject
|
||||
private SysDictService sysDictService;
|
||||
@Inject
|
||||
@@ -624,7 +640,7 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
|
||||
|
||||
docData.put("nativePlace", member.getNativePlace());
|
||||
docData.put("marriage", member.getMarriage());
|
||||
docData.put("specialty", StrUtil.isNotBlank(member.getSpecialty()) ? HtmlUtil.cleanHtmlTag(member.getSpecialty()) : "");
|
||||
docData.put("specialty", buildMemberApplyDocHtml(member.getSpecialty()));
|
||||
|
||||
docData.put("jobCategory", member.getJobCategory());
|
||||
docData.put("idCard", member.getIdCard());
|
||||
@@ -633,7 +649,8 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
|
||||
docData.put("arrivalAtSchoolDate", StrUtil.isNotBlank(member.getArrivalAtSchoolDate()) ? DateUtil.parse(member.getArrivalAtSchoolDate()).toString("yyyy-MM-dd") : "");
|
||||
docData.put("homeAddress", member.getHomeAddress());
|
||||
|
||||
docData.put("personalData", sysOfficeTemplateUtil.convertRichTextToDocText(member.getPersonalData()));
|
||||
docData.put("personalData", buildMemberApplyDocHtml(member.getPersonalData()));
|
||||
docData.put("photo", sysOfficeTemplateUtil.createPictureRenderData(MEMBER_APPLY_PHOTO_WIDTH_PIXEL, MEMBER_APPLY_PHOTO_HEIGHT_PIXEL, member.getPhoto()));
|
||||
docData.put("sign", sysOfficeTemplateUtil.createPictureRenderData(member.getSign()));
|
||||
docData.put("applyDateTime", member.getApplyDateTime() != null ? DateUtil.format(member.getApplyDateTime(), "yyyy年MM月dd日") : "");
|
||||
|
||||
@@ -702,16 +719,109 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
|
||||
|
||||
Configure config = Configure.builder()
|
||||
.bind("personalData", htmlRenderPolicy)
|
||||
.bind("specialty", htmlRenderPolicy)
|
||||
.build();
|
||||
try {
|
||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("member_apply_form"), config)
|
||||
.render(docData)
|
||||
.write(response.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
XWPFTemplate template = XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("member_apply_form"), config)
|
||||
.render(docData);
|
||||
resizeMemberApplyPhotoRow(template.getXWPFDocument());
|
||||
template.write(response.getOutputStream());
|
||||
} catch (Exception e) {
|
||||
log.error("导出会员入会申请表失败,ID: {}, 错误信息: {}", id, e.getMessage());
|
||||
throw new RuntimeException("导出文件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 入会申请导出时统一转为 HTML 片段,历史富文本保留样式,普通文本保留换行。
|
||||
*/
|
||||
private String buildMemberApplyDocHtml(String text) {
|
||||
if (StrUtil.isBlank(text)) {
|
||||
return "";
|
||||
}
|
||||
String docText = sysOfficeTemplateUtil.convertRichTextToDocText(text);
|
||||
if (containsHtmlTag(docText)) {
|
||||
return docText;
|
||||
}
|
||||
String escapeText = HtmlUtil.escape(docText)
|
||||
.replace("\r\n", "\n")
|
||||
.replace("\r", "\n")
|
||||
.replace("\n", "<br/>");
|
||||
return "<p>" + escapeText + "</p>";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断内容是否包含 HTML 标签,避免把历史富文本当普通文本转义。
|
||||
*/
|
||||
private boolean containsHtmlTag(String text) {
|
||||
return StrUtil.isNotBlank(text) && text.matches("(?s).*<\\s*[a-zA-Z][^>]*>.*");
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the member photo row tall enough so Word does not clip the default inline picture.
|
||||
*/
|
||||
private void resizeMemberApplyPhotoRow(XWPFDocument document) {
|
||||
if (document == null) {
|
||||
return;
|
||||
}
|
||||
for (XWPFTable table : document.getTables()) {
|
||||
for (XWPFTableRow row : table.getRows()) {
|
||||
adjustMemberApplyPhotoParagraph(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match only the two-inch member photo by rendered picture size, avoiding signature pictures.
|
||||
*/
|
||||
private boolean adjustMemberApplyPhotoParagraph(XWPFTableRow row) {
|
||||
if (row == null) {
|
||||
return false;
|
||||
}
|
||||
boolean found = false;
|
||||
for (XWPFTableCell cell : row.getTableCells()) {
|
||||
for (XWPFParagraph paragraph : cell.getParagraphs()) {
|
||||
for (XWPFRun run : paragraph.getRuns()) {
|
||||
if (runContainsMemberApplyPhoto(run)) {
|
||||
adjustMemberApplyPhotoParagraphStyle(paragraph);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the inline photo visible without changing the merged cell vertical anchor.
|
||||
*/
|
||||
private void adjustMemberApplyPhotoParagraphStyle(XWPFParagraph paragraph) {
|
||||
paragraph.setAlignment(ParagraphAlignment.CENTER);
|
||||
paragraph.setSpacingBefore(0);
|
||||
paragraph.setSpacingAfter(0);
|
||||
paragraph.setSpacingBeforeLines(0);
|
||||
paragraph.setSpacingAfterLines(0);
|
||||
paragraph.setSpacingBetween(MEMBER_APPLY_PHOTO_PARAGRAPH_LINE_POINT, LineSpacingRule.AT_LEAST);
|
||||
}
|
||||
|
||||
private boolean runContainsMemberApplyPhoto(XWPFRun run) {
|
||||
if (run == null || run.getCTR() == null) {
|
||||
return false;
|
||||
}
|
||||
for (CTDrawing drawing : run.getCTR().getDrawingArray()) {
|
||||
for (CTInline inline : drawing.getInlineArray()) {
|
||||
if (inline.getExtent() != null && isMemberApplyPhotoSize(inline.getExtent().getCx(), inline.getExtent().getCy())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isMemberApplyPhotoSize(long widthEmu, long heightEmu) {
|
||||
return Math.abs(widthEmu - MEMBER_APPLY_PHOTO_WIDTH_EMU) <= MEMBER_APPLY_PHOTO_SIZE_TOLERANCE_EMU
|
||||
&& Math.abs(heightEmu - MEMBER_APPLY_PHOTO_HEIGHT_EMU) <= MEMBER_APPLY_PHOTO_SIZE_TOLERANCE_EMU;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -54,14 +54,19 @@ const INFO = {
|
||||
<el-empty v-else :image-size="50" description="暂无家庭成员信息"></el-empty>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="个人学习及工作经历" :span="3">
|
||||
<div v-if="viewData.personalData" class="text-left w-e-text member-apply-rich-text" v-html="viewData.personalData"></div>
|
||||
<div v-if="viewData.personalData" class="text-left w-e-text member-apply-rich-text" style="white-space: pre-wrap; word-break: break-all;" v-html="viewData.personalData"></div>
|
||||
<el-tag type="warning" v-else>暂无</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="特长及获奖情况" :span="3">
|
||||
<div v-if="viewData.specialty" class="text-left w-e-text member-apply-rich-text" v-html="viewData.specialty"></div>
|
||||
<div v-if="viewData.specialty" class="text-left w-e-text member-apply-rich-text" style="white-space: pre-wrap; word-break: break-all;" v-html="viewData.specialty"></div>
|
||||
<el-tag type="warning" v-else>暂无</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="照片" :span="3" v-if="viewData.photo">
|
||||
<el-image :src="viewData.photo"
|
||||
style="width: 120px;height: 150px"
|
||||
fit="cover"></el-image>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="签字信息" :span="3" v-if="viewData.sign">
|
||||
<el-image :src="viewData.sign"
|
||||
|
||||
@@ -193,13 +193,28 @@ layout("/layouts/platform.html"){
|
||||
|
||||
<el-descriptions-item label="个人学习及工作经历" :span="3">
|
||||
<el-form-item prop="personalData" label="个人学习及工作经历">
|
||||
<text-editor v-model="formData.personalData"></text-editor>
|
||||
<el-input type="textarea" :rows="4" v-model="formData.personalData"
|
||||
placeholder="请输入个人学习及工作经历"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="特长及获奖情况" :span="3">
|
||||
<el-form-item prop="specialty" label="特长及获奖情况">
|
||||
<text-editor v-model="formData.specialty"></text-editor>
|
||||
<el-input type="textarea" :rows="4" v-model="formData.specialty"
|
||||
maxlength="100" show-word-limit
|
||||
placeholder="请输入特长及获奖情况"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="照片" :span="3">
|
||||
<el-form-item prop="photo" label="照片">
|
||||
<file-upload
|
||||
:value.sync="formData.photo"
|
||||
:upload_number="1"
|
||||
upload_mode="image"
|
||||
upload_result_category="interval"
|
||||
upload_result_type="url"
|
||||
></file-upload>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -259,7 +274,29 @@ layout("/layouts/platform.html"){
|
||||
sex: [{ required: false, message: "必填", trigger: ["change", "blur"] }],
|
||||
isVoluntary: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
sign: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
photo: [{ required: true, message: "请上传照片", trigger: ["change", "blur"] }],
|
||||
homeAddress: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
families: [{
|
||||
validator: (rule, value, callback) => {
|
||||
// 提交时家庭主要成员至少填写一条,并校验核心成员信息。
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error("请添加家庭主要成员"))
|
||||
return
|
||||
}
|
||||
const hasIncomplete = value.some(item => {
|
||||
item = item || {}
|
||||
return !String(item.relation || "").trim()
|
||||
|| !String(item.name || "").trim()
|
||||
|| !String(item.unit || "").trim()
|
||||
})
|
||||
if (hasIncomplete) {
|
||||
callback(new Error("请完善家庭主要成员的关系、姓名、工作单位"))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: ["change", "blur"]
|
||||
}],
|
||||
personalData: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
specialty: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
marriage: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
@@ -388,23 +425,27 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
},
|
||||
onFinishTask() {
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
const loading = createLoading()
|
||||
this.$axios.post("/platform/member/apply/submit/submitAgain", {
|
||||
data: JSON.stringify(this.formData),
|
||||
taskId: this.taskId
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
commonUtil.pjaxPush("/platform/member/apply/mine")
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (valid) {
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
const loading = createLoading()
|
||||
this.$axios.post("/platform/member/apply/submit/submitAgain", {
|
||||
data: JSON.stringify(this.formData),
|
||||
taskId: this.taskId
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
commonUtil.pjaxPush("/platform/member/apply/mine")
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
async init() {
|
||||
|
||||
@@ -40,8 +40,19 @@
|
||||
</el-table>
|
||||
<el-empty v-else :image-size="50" description="暂无家庭成员信息"></el-empty>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="个人简况" :span="3">
|
||||
<div v-if="viewData.personalData" class="text-left" v-html="viewData.personalData"></div>
|
||||
<el-descriptions-item label="个人学习及工作经历" :span="3">
|
||||
<div v-if="viewData.personalData" class="text-left w-e-text member-apply-rich-text" style="white-space: pre-wrap; word-break: break-all;" v-html="viewData.personalData"></div>
|
||||
<el-tag type="warning" v-else>暂无</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="特长及获奖情况" :span="3">
|
||||
<div v-if="viewData.specialty" class="text-left w-e-text member-apply-rich-text" style="white-space: pre-wrap; word-break: break-all;" v-html="viewData.specialty"></div>
|
||||
<el-tag type="warning" v-else>暂无</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="照片" :span="3">
|
||||
<el-image v-if="viewData.photo" :src="viewData.photo"
|
||||
style="width: 120px;height: 150px"
|
||||
fit="cover"></el-image>
|
||||
<el-tag type="warning" v-else>暂无</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
|
||||
@@ -63,18 +63,24 @@ const INFO = {
|
||||
|
||||
<van-cell title="个人学习及工作经历">
|
||||
<template #label>
|
||||
<div v-if="viewData.personalData" class="text-left" v-html="viewData.personalData"></div>
|
||||
<div v-if="viewData.personalData" class="text-left w-e-text member-apply-rich-text" style="white-space: pre-wrap; word-break: break-all;" v-html="viewData.personalData"></div>
|
||||
<span style="font-size: 14px" v-else>无数据</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
|
||||
<van-cell title="特长及获奖情况">
|
||||
<template #label>
|
||||
<div v-if="viewData.specialty" class="text-left" v-html="viewData.specialty"></div>
|
||||
<div v-if="viewData.specialty" class="text-left w-e-text member-apply-rich-text" style="white-space: pre-wrap; word-break: break-all;" v-html="viewData.specialty"></div>
|
||||
<span style="font-size: 14px" v-else>无数据</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
|
||||
<van-cell title="照片" v-if="viewData.photo">
|
||||
<van-image :src="viewData.photo"
|
||||
style="width: 120px;height: 150px"
|
||||
fit="cover"></van-image>
|
||||
</van-cell>
|
||||
|
||||
<van-cell title="签字" >
|
||||
<van-image :src="viewData.sign"
|
||||
v-if="viewData.sign"
|
||||
|
||||
+65
-2
@@ -288,11 +288,48 @@ layout("/layouts/platform_h5.html"){
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group title="个人学习及工作经历" class="form-section">
|
||||
<text-editor v-model="formData.personalData"></text-editor>
|
||||
<van-field
|
||||
v-model="formData.personalData"
|
||||
name="personalData"
|
||||
rows="4"
|
||||
label=""
|
||||
type="textarea"
|
||||
placeholder="请输入个人学习及工作经历"
|
||||
required
|
||||
:rules="[{ required: true, message: '请填写个人学习及工作经历' }]"
|
||||
></van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group title="特长及获奖情况" class="form-section">
|
||||
<text-editor v-model="formData.specialty"></text-editor>
|
||||
<van-field
|
||||
v-model="formData.specialty"
|
||||
name="specialty"
|
||||
rows="4"
|
||||
label=""
|
||||
type="textarea"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
placeholder="请输入特长及获奖情况"
|
||||
required
|
||||
:rules="[{ required: true, message: '请填写特长及获奖情况' }]"
|
||||
></van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group title="照片" class="form-section">
|
||||
<van-field class="direction-column-field" name="photo" label=""
|
||||
required :rules="[{ required: true, message: '请上传照片' }]">
|
||||
<template #input>
|
||||
<h5-file-upload
|
||||
slot="input"
|
||||
:value.sync="formData.photo"
|
||||
:upload_number="1"
|
||||
upload_mode="image"
|
||||
upload_result_category="interval"
|
||||
upload_result_type="url"
|
||||
complete_result
|
||||
></h5-file-upload>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group title="签字" class="form-section">
|
||||
@@ -401,6 +438,9 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
onSubmit() {
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
if (!this.validateFamilies()) {
|
||||
return
|
||||
}
|
||||
if (!this.formData.personalData) {
|
||||
this.$toast.fail("请填写个人学习及工作经历")
|
||||
return
|
||||
@@ -442,6 +482,9 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
onFinishTask() {
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
if (!this.validateFamilies()) {
|
||||
return
|
||||
}
|
||||
if (!this.formData.personalData) {
|
||||
this.$toast.fail("请填写个人学习及工作经历")
|
||||
return
|
||||
@@ -482,6 +525,26 @@ layout("/layouts/platform_h5.html"){
|
||||
})
|
||||
},
|
||||
|
||||
validateFamilies() {
|
||||
// 提交时家庭主要成员至少填写一条,并校验核心成员信息。
|
||||
const families = this.formData.families || []
|
||||
if (families.length === 0) {
|
||||
this.$toast.fail("请添加家庭主要成员")
|
||||
return false
|
||||
}
|
||||
const hasIncomplete = families.some(item => {
|
||||
item = item || {}
|
||||
return !String(item.relation || "").trim()
|
||||
|| !String(item.name || "").trim()
|
||||
|| !String(item.unit || "").trim()
|
||||
})
|
||||
if (hasIncomplete) {
|
||||
this.$toast.fail("请完善家庭主要成员的关系、姓名、工作单位")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
onDateConfirm(value) {
|
||||
this.formData.birthday = value
|
||||
this.showDatePicker = false
|
||||
|
||||
Reference in New Issue
Block a user