优化
This commit is contained in:
parent
52fd2e7560
commit
e0b089fbf8
@ -62,6 +62,11 @@
|
|||||||
<artifactId>poi-ooxml</artifactId>
|
<artifactId>poi-ooxml</artifactId>
|
||||||
<version>5.2.5</version>
|
<version>5.2.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.belerweb</groupId>
|
||||||
|
<artifactId>pinyin4j</artifactId>
|
||||||
|
<version>2.5.1</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-api</artifactId>
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
|||||||
@ -12,6 +12,11 @@ import com.writeoff.module.expert.model.ExpertBankCardInfo;
|
|||||||
import com.writeoff.module.expert.model.ExpertInfo;
|
import com.writeoff.module.expert.model.ExpertInfo;
|
||||||
import com.writeoff.module.system.service.DataPermissionService;
|
import com.writeoff.module.system.service.DataPermissionService;
|
||||||
import com.writeoff.security.AuthContext;
|
import com.writeoff.security.AuthContext;
|
||||||
|
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||||
|
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
|
||||||
|
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
|
||||||
|
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
|
||||||
|
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.jdbc.core.RowMapper;
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -114,12 +119,31 @@ public class ExpertService {
|
|||||||
StringBuilder whereClause = new StringBuilder(
|
StringBuilder whereClause = new StringBuilder(
|
||||||
"WHERE e.tenant_id=" + PLATFORM_TENANT_ID + " AND e.is_deleted=0"
|
"WHERE e.tenant_id=" + PLATFORM_TENANT_ID + " AND e.is_deleted=0"
|
||||||
);
|
);
|
||||||
List<Object> countArgs = new ArrayList<>();
|
|
||||||
if (keyword != null && !keyword.trim().isEmpty()) {
|
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||||
String kw = keyword.trim().replace("'", "''");
|
String kw = keyword.trim().replace("'", "''");
|
||||||
whereClause.append(" AND (e.expert_name LIKE '%").append(kw).append("%' OR e.id_no LIKE '%").append(kw).append("%')");
|
whereClause.append(" AND (e.expert_name LIKE '%").append(kw).append("%' OR e.id_no LIKE '%").append(kw).append("%')");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
com.writeoff.module.system.service.DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope();
|
||||||
|
if (!scope.isExpertAll()) {
|
||||||
|
boolean hasIdScope = !scope.getExpertIds().isEmpty();
|
||||||
|
boolean hasOwnerScope = scope.isExpertOwnerOnly();
|
||||||
|
if (!hasIdScope && !hasOwnerScope) {
|
||||||
|
return new PageResult<ExpertInfo>(new ArrayList<>(), 0, safePage, safeSize);
|
||||||
|
}
|
||||||
|
whereClause.append(" AND (1=0");
|
||||||
|
if (hasIdScope) {
|
||||||
|
whereClause.append(" OR e.id IN (").append(scope.getExpertIds().stream().map(String::valueOf).collect(Collectors.joining(","))).append(")");
|
||||||
|
}
|
||||||
|
if (hasOwnerScope) {
|
||||||
|
Long userId = AuthContext.userId();
|
||||||
|
if (userId != null) {
|
||||||
|
whereClause.append(" OR e.created_by=").append(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
whereClause.append(")");
|
||||||
|
}
|
||||||
|
|
||||||
Integer total = jdbcTemplate.queryForObject(
|
Integer total = jdbcTemplate.queryForObject(
|
||||||
"SELECT COUNT(1) FROM expert e " + whereClause,
|
"SELECT COUNT(1) FROM expert e " + whereClause,
|
||||||
Integer.class
|
Integer.class
|
||||||
@ -138,7 +162,6 @@ public class ExpertService {
|
|||||||
sql.append(whereClause);
|
sql.append(whereClause);
|
||||||
sql.append(" ORDER BY e.id DESC LIMIT ").append(safeSize).append(" OFFSET ").append(offset);
|
sql.append(" ORDER BY e.id DESC LIMIT ").append(safeSize).append(" OFFSET ").append(offset);
|
||||||
List<ExpertInfo> list = jdbcTemplate.query(sql.toString(), EXPERT_ROW_MAPPER);
|
List<ExpertInfo> list = jdbcTemplate.query(sql.toString(), EXPERT_ROW_MAPPER);
|
||||||
list = filterByExpertScope(list);
|
|
||||||
List<ExpertInfo> maskedList = new java.util.ArrayList<ExpertInfo>(list.size());
|
List<ExpertInfo> maskedList = new java.util.ArrayList<ExpertInfo>(list.size());
|
||||||
for (ExpertInfo item : list) {
|
for (ExpertInfo item : list) {
|
||||||
maskedList.add(maskSensitiveFields(item));
|
maskedList.add(maskSensitiveFields(item));
|
||||||
@ -195,11 +218,13 @@ public class ExpertService {
|
|||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public ExpertInfo create(CreateExpertRequest request) {
|
public ExpertInfo create(CreateExpertRequest request) {
|
||||||
|
String idNo = request.getIdNo() == null ? "" : request.getIdNo().trim().toUpperCase();
|
||||||
|
request.setIdNo(idNo);
|
||||||
Integer count = jdbcTemplate.queryForObject(
|
Integer count = jdbcTemplate.queryForObject(
|
||||||
"SELECT COUNT(1) FROM expert WHERE tenant_id=? AND id_no=? AND is_deleted=0",
|
"SELECT COUNT(1) FROM expert WHERE tenant_id=? AND id_no=? AND is_deleted=0",
|
||||||
Integer.class,
|
Integer.class,
|
||||||
PLATFORM_TENANT_ID,
|
PLATFORM_TENANT_ID,
|
||||||
request.getIdNo()
|
idNo
|
||||||
);
|
);
|
||||||
if (count != null && count > 0) {
|
if (count != null && count > 0) {
|
||||||
throw new BusinessException(10001, "身份证号已存在");
|
throw new BusinessException(10001, "身份证号已存在");
|
||||||
@ -617,11 +642,76 @@ public class ExpertService {
|
|||||||
name
|
name
|
||||||
);
|
);
|
||||||
if (byName.isEmpty()) {
|
if (byName.isEmpty()) {
|
||||||
throw new BusinessException(10001, label + "不在平台字典内,请先在平台字典中维护");
|
String pinyinCode = getPinyin(name);
|
||||||
|
if (pinyinCode.length() > 40) {
|
||||||
|
pinyinCode = pinyinCode.substring(0, 40);
|
||||||
|
}
|
||||||
|
Integer count = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(1) FROM platform_dictionary_item WHERE dict_type=? AND dict_code=?",
|
||||||
|
Integer.class,
|
||||||
|
dictType,
|
||||||
|
pinyinCode
|
||||||
|
);
|
||||||
|
if (count != null && count > 0) {
|
||||||
|
int i = 1;
|
||||||
|
while(true) {
|
||||||
|
String newCode = pinyinCode + "_" + i;
|
||||||
|
if (newCode.length() > 50) {
|
||||||
|
newCode = newCode.substring(newCode.length() - 50);
|
||||||
|
}
|
||||||
|
Integer c = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(1) FROM platform_dictionary_item WHERE dict_type=? AND dict_code=?",
|
||||||
|
Integer.class, dictType, newCode
|
||||||
|
);
|
||||||
|
if (c == null || c == 0) {
|
||||||
|
pinyinCode = newCode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Integer maxSort = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT MAX(sort_no) FROM platform_dictionary_item WHERE dict_type=?",
|
||||||
|
Integer.class,
|
||||||
|
dictType
|
||||||
|
);
|
||||||
|
int nextSort = (maxSort == null ? 0 : maxSort) + 10;
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"INSERT INTO platform_dictionary_item (dict_type, dict_code, dict_name, sort_no, status, remark, created_by, updated_by) " +
|
||||||
|
"VALUES (?, ?, ?, ?, 'ENABLED', 'Auto-imported', ?, ?)",
|
||||||
|
dictType, pinyinCode, name, nextSort, safeUserId(), safeUserId()
|
||||||
|
);
|
||||||
|
return new DictionaryItem(pinyinCode, name);
|
||||||
}
|
}
|
||||||
return byName.get(0);
|
return byName.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getPinyin(String src) {
|
||||||
|
if (src == null || src.trim().isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||||
|
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
|
||||||
|
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
||||||
|
format.setVCharType(HanyuPinyinVCharType.WITH_V);
|
||||||
|
for (char c : src.trim().toCharArray()) {
|
||||||
|
if (Character.toString(c).matches("[\\u4E00-\\u9FA5]+")) {
|
||||||
|
try {
|
||||||
|
String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c, format);
|
||||||
|
if (pinyins != null && pinyins.length > 0) {
|
||||||
|
result.append(pinyins[0]);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.append(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.append(Character.toUpperCase(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private void validateImportExpert(CreateExpertRequest request, Set<String> batchIdNos, Set<String> batchPhones) {
|
private void validateImportExpert(CreateExpertRequest request, Set<String> batchIdNos, Set<String> batchPhones) {
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
throw new BusinessException(10001, "导入行不能为空");
|
throw new BusinessException(10001, "导入行不能为空");
|
||||||
|
|||||||
@ -11,6 +11,11 @@ import com.writeoff.module.file.service.OssService;
|
|||||||
import com.writeoff.module.expert.model.ExpertBankCardInfo;
|
import com.writeoff.module.expert.model.ExpertBankCardInfo;
|
||||||
import com.writeoff.module.expert.model.ExpertInfo;
|
import com.writeoff.module.expert.model.ExpertInfo;
|
||||||
import com.writeoff.security.AuthContext;
|
import com.writeoff.security.AuthContext;
|
||||||
|
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||||
|
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
|
||||||
|
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
|
||||||
|
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
|
||||||
|
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.jdbc.core.RowMapper;
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -188,11 +193,13 @@ public class PlatformExpertService {
|
|||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public ExpertInfo create(CreateExpertRequest request) {
|
public ExpertInfo create(CreateExpertRequest request) {
|
||||||
|
String idNo = request.getIdNo() == null ? "" : request.getIdNo().trim().toUpperCase();
|
||||||
|
request.setIdNo(idNo);
|
||||||
Integer count = jdbcTemplate.queryForObject(
|
Integer count = jdbcTemplate.queryForObject(
|
||||||
"SELECT COUNT(1) FROM expert WHERE tenant_id=? AND id_no=? AND is_deleted=0",
|
"SELECT COUNT(1) FROM expert WHERE tenant_id=? AND id_no=? AND is_deleted=0",
|
||||||
Integer.class,
|
Integer.class,
|
||||||
PLATFORM_TENANT_ID,
|
PLATFORM_TENANT_ID,
|
||||||
request.getIdNo()
|
idNo
|
||||||
);
|
);
|
||||||
if (count != null && count > 0) {
|
if (count != null && count > 0) {
|
||||||
throw new BusinessException(10001, "身份证号已存在");
|
throw new BusinessException(10001, "身份证号已存在");
|
||||||
@ -614,11 +621,76 @@ public class PlatformExpertService {
|
|||||||
name
|
name
|
||||||
);
|
);
|
||||||
if (byName.isEmpty()) {
|
if (byName.isEmpty()) {
|
||||||
throw new BusinessException(10001, label + "不在平台字典内,请先在平台字典中维护");
|
String pinyinCode = getPinyin(name);
|
||||||
|
if (pinyinCode.length() > 40) {
|
||||||
|
pinyinCode = pinyinCode.substring(0, 40);
|
||||||
|
}
|
||||||
|
Integer count = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(1) FROM platform_dictionary_item WHERE dict_type=? AND dict_code=?",
|
||||||
|
Integer.class,
|
||||||
|
dictType,
|
||||||
|
pinyinCode
|
||||||
|
);
|
||||||
|
if (count != null && count > 0) {
|
||||||
|
int i = 1;
|
||||||
|
while(true) {
|
||||||
|
String newCode = pinyinCode + "_" + i;
|
||||||
|
if (newCode.length() > 50) {
|
||||||
|
newCode = newCode.substring(newCode.length() - 50);
|
||||||
|
}
|
||||||
|
Integer c = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(1) FROM platform_dictionary_item WHERE dict_type=? AND dict_code=?",
|
||||||
|
Integer.class, dictType, newCode
|
||||||
|
);
|
||||||
|
if (c == null || c == 0) {
|
||||||
|
pinyinCode = newCode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Integer maxSort = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT MAX(sort_no) FROM platform_dictionary_item WHERE dict_type=?",
|
||||||
|
Integer.class,
|
||||||
|
dictType
|
||||||
|
);
|
||||||
|
int nextSort = (maxSort == null ? 0 : maxSort) + 10;
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"INSERT INTO platform_dictionary_item (dict_type, dict_code, dict_name, sort_no, status, remark, created_by, updated_by) " +
|
||||||
|
"VALUES (?, ?, ?, ?, 'ENABLED', 'Auto-imported', ?, ?)",
|
||||||
|
dictType, pinyinCode, name, nextSort, safeUserId(), safeUserId()
|
||||||
|
);
|
||||||
|
return new DictionaryItem(pinyinCode, name);
|
||||||
}
|
}
|
||||||
return byName.get(0);
|
return byName.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getPinyin(String src) {
|
||||||
|
if (src == null || src.trim().isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||||
|
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
|
||||||
|
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
||||||
|
format.setVCharType(HanyuPinyinVCharType.WITH_V);
|
||||||
|
for (char c : src.trim().toCharArray()) {
|
||||||
|
if (Character.toString(c).matches("[\\u4E00-\\u9FA5]+")) {
|
||||||
|
try {
|
||||||
|
String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c, format);
|
||||||
|
if (pinyins != null && pinyins.length > 0) {
|
||||||
|
result.append(pinyins[0]);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.append(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.append(Character.toUpperCase(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private DictionaryItem resolveOptionalDictionaryItem(String dictType, String dictCode, String dictName, String label) {
|
private DictionaryItem resolveOptionalDictionaryItem(String dictType, String dictCode, String dictName, String label) {
|
||||||
boolean hasCode = dictCode != null && !dictCode.trim().isEmpty();
|
boolean hasCode = dictCode != null && !dictCode.trim().isEmpty();
|
||||||
boolean hasName = dictName != null && !dictName.trim().isEmpty();
|
boolean hasName = dictName != null && !dictName.trim().isEmpty();
|
||||||
|
|||||||
@ -172,7 +172,7 @@ public class MeetingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@RequirePermission(value = "meeting.create", dataScope = DataScopeType.MEETING, auditAction = "MEETING_UPDATE")
|
@RequirePermission(value = "meeting.update", dataScope = DataScopeType.MEETING, auditAction = "MEETING_UPDATE")
|
||||||
public ApiResponse<Meeting> update(@PathVariable("id") Long id,
|
public ApiResponse<Meeting> update(@PathVariable("id") Long id,
|
||||||
@RequestBody @Valid CreateMeetingRequest request) {
|
@RequestBody @Valid CreateMeetingRequest request) {
|
||||||
return ApiResponse.success(meetingService.update(id, request));
|
return ApiResponse.success(meetingService.update(id, request));
|
||||||
|
|||||||
@ -2050,6 +2050,8 @@ public class MeetingMaterialService {
|
|||||||
requireMeetingInvoiceAttachmentList(map.get("attachments"));
|
requireMeetingInvoiceAttachmentList(map.get("attachments"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new BusinessException(10001, "资料内容格式错误或缺少必填字段");
|
throw new BusinessException(10001, "资料内容格式错误或缺少必填字段");
|
||||||
}
|
}
|
||||||
@ -3170,7 +3172,7 @@ public class MeetingMaterialService {
|
|||||||
}
|
}
|
||||||
Collection<?> list = (Collection<?>) value;
|
Collection<?> list = (Collection<?>) value;
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
throw new BusinessException(10001, "提交失败,缺少必填字段: invoices");
|
return;
|
||||||
}
|
}
|
||||||
for (Object item : list) {
|
for (Object item : list) {
|
||||||
if (!(item instanceof Map)) {
|
if (!(item instanceof Map)) {
|
||||||
|
|||||||
@ -230,8 +230,12 @@ public class MeetingService {
|
|||||||
double cateringRatio = request.getCateringRatio() == null ? project.getCateringFeeRatio() : normalizeRatio(request.getCateringRatio(), "餐费占比");
|
double cateringRatio = request.getCateringRatio() == null ? project.getCateringFeeRatio() : normalizeRatio(request.getCateringRatio(), "餐费占比");
|
||||||
assertMeetingRatiosWithinProject(project, laborRatio, cateringRatio);
|
assertMeetingRatiosWithinProject(project, laborRatio, cateringRatio);
|
||||||
long defaultBudgetCent = calculateDefaultMeetingBudgetCent(project);
|
long defaultBudgetCent = calculateDefaultMeetingBudgetCent(project);
|
||||||
if (defaultBudgetCent <= 0L) {
|
long initialBudgetCent = request.getBudgetCent() != null ? request.getBudgetCent() : defaultBudgetCent;
|
||||||
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "默认会议预算必须大于 0");
|
if (!project.isAllowMeetingOverBudget() && initialBudgetCent > defaultBudgetCent) {
|
||||||
|
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "当前项目不允许会议预算超出默认分配额度");
|
||||||
|
}
|
||||||
|
if (initialBudgetCent <= 0L) {
|
||||||
|
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "会议预算必须大于 0");
|
||||||
}
|
}
|
||||||
String now = nowIsoSeconds();
|
String now = nowIsoSeconds();
|
||||||
Meeting meeting = new Meeting(
|
Meeting meeting = new Meeting(
|
||||||
@ -243,7 +247,7 @@ public class MeetingService {
|
|||||||
request.getLocation(),
|
request.getLocation(),
|
||||||
request.getStartTime(),
|
request.getStartTime(),
|
||||||
request.getEndTime(),
|
request.getEndTime(),
|
||||||
defaultBudgetCent,
|
initialBudgetCent,
|
||||||
laborRatio,
|
laborRatio,
|
||||||
cateringRatio,
|
cateringRatio,
|
||||||
MeetingStatus.NOT_STARTED,
|
MeetingStatus.NOT_STARTED,
|
||||||
@ -401,6 +405,15 @@ public class MeetingService {
|
|||||||
double laborRatio = request.getLaborRatio() == null ? existing.getLaborRatio() : normalizeRatio(request.getLaborRatio(), "劳务占比");
|
double laborRatio = request.getLaborRatio() == null ? existing.getLaborRatio() : normalizeRatio(request.getLaborRatio(), "劳务占比");
|
||||||
double cateringRatio = request.getCateringRatio() == null ? existing.getCateringRatio() : normalizeRatio(request.getCateringRatio(), "餐费占比");
|
double cateringRatio = request.getCateringRatio() == null ? existing.getCateringRatio() : normalizeRatio(request.getCateringRatio(), "餐费占比");
|
||||||
assertMeetingRatiosWithinProject(project, laborRatio, cateringRatio);
|
assertMeetingRatiosWithinProject(project, laborRatio, cateringRatio);
|
||||||
|
|
||||||
|
long newBudgetCent = request.getBudgetCent() != null ? request.getBudgetCent() : existing.getBudgetCent();
|
||||||
|
if (!project.isAllowMeetingOverBudget() && newBudgetCent > existing.getBudgetCent()) {
|
||||||
|
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "当前项目不允许调高会议预算");
|
||||||
|
}
|
||||||
|
if (newBudgetCent <= 0L) {
|
||||||
|
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "会议预算必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
Meeting updated = new Meeting(
|
Meeting updated = new Meeting(
|
||||||
existing.getId(),
|
existing.getId(),
|
||||||
existing.getProjectId(),
|
existing.getProjectId(),
|
||||||
@ -410,7 +423,7 @@ public class MeetingService {
|
|||||||
request.getLocation(),
|
request.getLocation(),
|
||||||
request.getStartTime(),
|
request.getStartTime(),
|
||||||
request.getEndTime(),
|
request.getEndTime(),
|
||||||
request.getBudgetCent(),
|
newBudgetCent,
|
||||||
laborRatio,
|
laborRatio,
|
||||||
cateringRatio,
|
cateringRatio,
|
||||||
existing.getStatus(),
|
existing.getStatus(),
|
||||||
|
|||||||
@ -144,8 +144,8 @@ public class MeetingSummaryExportService {
|
|||||||
projectName,
|
projectName,
|
||||||
queryTenantName(tenantId)
|
queryTenantName(tenantId)
|
||||||
);
|
);
|
||||||
String meetingCategory = stringValue(meeting.get("meeting_category"));
|
String meetingCategory = resolveDictName("MEETING_CATEGORY", stringValue(meeting.get("meeting_category")));
|
||||||
String location = stringValue(meeting.get("location"));
|
String location = resolveDictName("MEETING_LOCATION", stringValue(meeting.get("location")));
|
||||||
String startTime = stringValue(meeting.get("start_time"));
|
String startTime = stringValue(meeting.get("start_time"));
|
||||||
String endTime = stringValue(meeting.get("end_time"));
|
String endTime = stringValue(meeting.get("end_time"));
|
||||||
String guestCountText = formatNumber(basicInfo.get("guestCount"));
|
String guestCountText = formatNumber(basicInfo.get("guestCount"));
|
||||||
@ -240,6 +240,26 @@ public class MeetingSummaryExportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveDictName(String dictType, String dictCode) {
|
||||||
|
if (dictCode == null || dictCode.trim().isEmpty()) {
|
||||||
|
return dictCode;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<String> names = jdbcTemplate.queryForList(
|
||||||
|
"SELECT dict_name FROM platform_dictionary_item WHERE dict_type=? AND dict_code=? AND is_deleted=0 LIMIT 1",
|
||||||
|
String.class,
|
||||||
|
dictType,
|
||||||
|
dictCode
|
||||||
|
);
|
||||||
|
if (!names.isEmpty() && names.get(0) != null) {
|
||||||
|
return names.get(0);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return dictCode;
|
||||||
|
}
|
||||||
|
|
||||||
private List<Long> parseIdList(Object value) {
|
private List<Long> parseIdList(Object value) {
|
||||||
if (!(value instanceof List)) {
|
if (!(value instanceof List)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|||||||
@ -93,9 +93,9 @@ public class UserController {
|
|||||||
|
|
||||||
@PostMapping("/{id}/reset-password")
|
@PostMapping("/{id}/reset-password")
|
||||||
@RequirePermission(value = "user.password.reset", dataScope = DataScopeType.TENANT, auditAction = "USER_RESET_PASSWORD")
|
@RequirePermission(value = "user.password.reset", dataScope = DataScopeType.TENANT, auditAction = "USER_RESET_PASSWORD")
|
||||||
public ApiResponse<String> resetPassword(@PathVariable("id") Long id, @RequestBody @Valid ResetPasswordRequest request) {
|
public ApiResponse<String> resetPassword(@PathVariable("id") Long id) {
|
||||||
systemUserService.resetPassword(id, request);
|
systemUserService.resetPassword(id);
|
||||||
return ApiResponse.success("OK");
|
return ApiResponse.success("密码已重置");
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/role-history")
|
@GetMapping("/{id}/role-history")
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import com.writeoff.module.system.model.UserRoleHistory;
|
|||||||
import com.writeoff.security.AuthContext;
|
import com.writeoff.security.AuthContext;
|
||||||
import com.writeoff.security.PasswordCodecService;
|
import com.writeoff.security.PasswordCodecService;
|
||||||
import com.writeoff.security.PasswordPolicyService;
|
import com.writeoff.security.PasswordPolicyService;
|
||||||
|
import com.writeoff.security.PasswordSetupService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
@ -55,7 +56,9 @@ public class SystemUserService {
|
|||||||
private final PasswordPolicyService passwordPolicyService;
|
private final PasswordPolicyService passwordPolicyService;
|
||||||
private final PasswordCodecService passwordCodecService;
|
private final PasswordCodecService passwordCodecService;
|
||||||
private final TransactionTemplate transactionTemplate;
|
private final TransactionTemplate transactionTemplate;
|
||||||
|
private final PasswordSetupService passwordSetupService;
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private final String frontendBaseUrl;
|
||||||
|
|
||||||
private static final RowMapper<SystemUser> USER_ROW_MAPPER = (rs, n) -> new SystemUser(
|
private static final RowMapper<SystemUser> USER_ROW_MAPPER = (rs, n) -> new SystemUser(
|
||||||
rs.getLong("id"),
|
rs.getLong("id"),
|
||||||
@ -97,13 +100,17 @@ public class SystemUserService {
|
|||||||
NotificationDispatchService notificationDispatchService,
|
NotificationDispatchService notificationDispatchService,
|
||||||
PasswordPolicyService passwordPolicyService,
|
PasswordPolicyService passwordPolicyService,
|
||||||
PasswordCodecService passwordCodecService,
|
PasswordCodecService passwordCodecService,
|
||||||
PlatformTransactionManager transactionManager) {
|
PlatformTransactionManager transactionManager,
|
||||||
|
PasswordSetupService passwordSetupService,
|
||||||
|
@org.springframework.beans.factory.annotation.Value("${app.frontend-base-url:http://localhost:5173}") String frontendBaseUrl) {
|
||||||
this.jdbcTemplate = jdbcTemplate;
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
this.dataPermissionService = dataPermissionService;
|
this.dataPermissionService = dataPermissionService;
|
||||||
this.notificationDispatchService = notificationDispatchService;
|
this.notificationDispatchService = notificationDispatchService;
|
||||||
this.passwordPolicyService = passwordPolicyService;
|
this.passwordPolicyService = passwordPolicyService;
|
||||||
this.passwordCodecService = passwordCodecService;
|
this.passwordCodecService = passwordCodecService;
|
||||||
this.transactionTemplate = new TransactionTemplate(transactionManager);
|
this.transactionTemplate = new TransactionTemplate(transactionManager);
|
||||||
|
this.passwordSetupService = passwordSetupService;
|
||||||
|
this.frontendBaseUrl = frontendBaseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<SystemUser> listUsers(int pageNo, int pageSize, Boolean includeDeleted) {
|
public PageResult<SystemUser> listUsers(int pageNo, int pageSize, Boolean includeDeleted) {
|
||||||
@ -179,10 +186,8 @@ public class SystemUserService {
|
|||||||
final String phone = request.getPhone() == null ? "" : request.getPhone().trim();
|
final String phone = request.getPhone() == null ? "" : request.getPhone().trim();
|
||||||
final String email = request.getEmail() == null ? "" : request.getEmail().trim();
|
final String email = request.getEmail() == null ? "" : request.getEmail().trim();
|
||||||
final String rawPassword = request.getPassword() == null ? "" : request.getPassword().trim();
|
final String rawPassword = request.getPassword() == null ? "" : request.getPassword().trim();
|
||||||
if (rawPassword.isEmpty()) {
|
final String finalPassword = rawPassword.isEmpty() ? ("Tmp@" + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8) + "aA1") : rawPassword;
|
||||||
throw new BusinessException(10001, "\u5bc6\u7801\u4e0d\u80fd\u4e3a\u7a7a");
|
passwordPolicyService.validate(finalPassword);
|
||||||
}
|
|
||||||
passwordPolicyService.validate(rawPassword);
|
|
||||||
final String validFrom = request.getValidFrom() == null || request.getValidFrom().trim().isEmpty()
|
final String validFrom = request.getValidFrom() == null || request.getValidFrom().trim().isEmpty()
|
||||||
? LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
? LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
||||||
: normalizeDateTimeString(request.getValidFrom());
|
: normalizeDateTimeString(request.getValidFrom());
|
||||||
@ -191,7 +196,7 @@ public class SystemUserService {
|
|||||||
: normalizeDateTimeString(request.getValidTo());
|
: normalizeDateTimeString(request.getValidTo());
|
||||||
return transactionTemplate.execute(status -> {
|
return transactionTemplate.execute(status -> {
|
||||||
assertPhoneAvailable(phone, null);
|
assertPhoneAvailable(phone, null);
|
||||||
String passwordHash = passwordCodecService.encode(rawPassword);
|
String passwordHash = passwordCodecService.encode(finalPassword);
|
||||||
String tenantSwitchAccountKey = resolveTenantSwitchAccountKeyByPhone(phone, null);
|
String tenantSwitchAccountKey = resolveTenantSwitchAccountKeyByPhone(phone, null);
|
||||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||||
jdbcTemplate.update(connection -> {
|
jdbcTemplate.update(connection -> {
|
||||||
@ -217,7 +222,8 @@ public class SystemUserService {
|
|||||||
}, keyHolder);
|
}, keyHolder);
|
||||||
Long id = keyHolder.getKey() == null ? null : keyHolder.getKey().longValue();
|
Long id = keyHolder.getKey() == null ? null : keyHolder.getKey().longValue();
|
||||||
autoAssignExecutorRoleWhenCreatorIsProjectExecutor(id);
|
autoAssignExecutorRoleWhenCreatorIsProjectExecutor(id);
|
||||||
sendUserCreatedMail(id, userName, phone, email, validFrom, validTo);
|
String setupLink = passwordSetupService.issueUserSetupLink(tenantId(), id, safeUserId());
|
||||||
|
sendUserCreatedMail(id, userName, phone, email, finalPassword, validFrom, validTo, setupLink);
|
||||||
return new SystemUser(id, userName, phone, email, "ENABLED", validFrom, validTo, "", "");
|
return new SystemUser(id, userName, phone, email, "ENABLED", validFrom, validTo, "", "");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -248,8 +254,8 @@ public class SystemUserService {
|
|||||||
ps.setTimestamp(idx++, validTo == null ? Timestamp.valueOf(LocalDateTime.of(2099, 12, 31, 23, 59, 59)) : validTo);
|
ps.setTimestamp(idx++, validTo == null ? Timestamp.valueOf(LocalDateTime.of(2099, 12, 31, 23, 59, 59)) : validTo);
|
||||||
ps.setLong(idx++, operator);
|
ps.setLong(idx++, operator);
|
||||||
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
|
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
|
||||||
passwordPolicyService.validate(request.getPassword());
|
passwordPolicyService.validate(request.getPassword().trim());
|
||||||
ps.setString(idx++, passwordCodecService.encode(request.getPassword()));
|
ps.setString(idx++, passwordCodecService.encode(request.getPassword().trim()));
|
||||||
}
|
}
|
||||||
ps.setLong(idx++, tenantId());
|
ps.setLong(idx++, tenantId());
|
||||||
ps.setLong(idx, userId);
|
ps.setLong(idx, userId);
|
||||||
@ -413,14 +419,54 @@ public class SystemUserService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetPassword(Long userId, ResetPasswordRequest request) {
|
public void resetPassword(Long userId) {
|
||||||
assertUserExists(userId);
|
assertUserExists(userId);
|
||||||
passwordPolicyService.validate(request.getNewPassword());
|
|
||||||
jdbcTemplate.update(
|
SystemUser user = jdbcTemplate.queryForObject(
|
||||||
"UPDATE sys_user SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
"SELECT id, user_name, phone, email, status, valid_from, valid_to FROM sys_user WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
|
||||||
passwordCodecService.encode(request.getNewPassword()),
|
(rs, rowNum) -> new SystemUser(
|
||||||
|
rs.getLong("id"),
|
||||||
|
rs.getString("user_name"),
|
||||||
|
rs.getString("phone"),
|
||||||
|
rs.getString("email"),
|
||||||
|
rs.getString("status"),
|
||||||
|
rs.getString("valid_from"),
|
||||||
|
rs.getString("valid_to")
|
||||||
|
),
|
||||||
|
tenantId(),
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
String setupLink = passwordSetupService.issueUserSetupLink(tenantId(), userId, safeUserId());
|
||||||
|
|
||||||
|
List<Map<String, Object>> tenants = jdbcTemplate.queryForList(
|
||||||
|
"SELECT tenant_code, tenant_name FROM tenant WHERE id=? LIMIT 1",
|
||||||
|
tenantId()
|
||||||
|
);
|
||||||
|
String tenantCode = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_code"));
|
||||||
|
String tenantName = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_name"));
|
||||||
|
|
||||||
|
Map<String, Object> variables = new LinkedHashMap<String, Object>();
|
||||||
|
variables.put("userId", userId);
|
||||||
|
variables.put("targetUserId", userId);
|
||||||
|
variables.put("userName", user.getUserName());
|
||||||
|
variables.put("phone", user.getPhone());
|
||||||
|
variables.put("email", user.getEmail());
|
||||||
|
variables.put("tenantCode", tenantCode);
|
||||||
|
variables.put("tenantName", tenantName);
|
||||||
|
variables.put("setupLink", setupLink);
|
||||||
|
|
||||||
|
DispatchNotificationRequest dispatchRequest = new DispatchNotificationRequest();
|
||||||
|
dispatchRequest.setIdempotencyKey("user-password-reset-" + tenantId() + "-" + userId + "-" + System.currentTimeMillis());
|
||||||
|
dispatchRequest.setEventCode("USER_PASSWORD_RESET");
|
||||||
|
dispatchRequest.setBizType("USER");
|
||||||
|
dispatchRequest.setBizId("user-" + userId);
|
||||||
|
try {
|
||||||
|
dispatchRequest.setVariablesJson(objectMapper.writeValueAsString(variables));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to write variables json", e);
|
||||||
|
}
|
||||||
|
notificationDispatchService.dispatch(dispatchRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeMyPassword(Long userId, String oldPassword, String newPassword) {
|
public void changeMyPassword(Long userId, String oldPassword, String newPassword) {
|
||||||
@ -707,8 +753,8 @@ public class SystemUserService {
|
|||||||
if (item.getUserName() == null || item.getUserName().trim().isEmpty()) {
|
if (item.getUserName() == null || item.getUserName().trim().isEmpty()) {
|
||||||
throw new BusinessException(10001, "用户名不能为空");
|
throw new BusinessException(10001, "用户名不能为空");
|
||||||
}
|
}
|
||||||
if (item.getPassword() == null || item.getPassword().trim().isEmpty()) {
|
if (item.getPassword() != null && !item.getPassword().trim().isEmpty()) {
|
||||||
throw new BusinessException(10001, "密码不能为空");
|
passwordPolicyService.validate(item.getPassword().trim());
|
||||||
}
|
}
|
||||||
ImportValidationUtils.validatePhone(item.getPhone());
|
ImportValidationUtils.validatePhone(item.getPhone());
|
||||||
ImportValidationUtils.validateRequiredEmail(item.getEmail());
|
ImportValidationUtils.validateRequiredEmail(item.getEmail());
|
||||||
@ -717,7 +763,6 @@ public class SystemUserService {
|
|||||||
if (!batchPhones.add(phone)) {
|
if (!batchPhones.add(phone)) {
|
||||||
throw new BusinessException(10001, "批次内手机号重复");
|
throw new BusinessException(10001, "批次内手机号重复");
|
||||||
}
|
}
|
||||||
passwordPolicyService.validate(item.getPassword().trim());
|
|
||||||
String roleCode = ImportValidationUtils.trim(item.getRoleCode());
|
String roleCode = ImportValidationUtils.trim(item.getRoleCode());
|
||||||
if (roleCode.isEmpty()) {
|
if (roleCode.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
@ -750,7 +795,7 @@ public class SystemUserService {
|
|||||||
: ex.getMessage();
|
: ex.getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendUserCreatedMail(Long userId, String userName, String phone, String email, String validFrom, String validTo) {
|
private void sendUserCreatedMail(Long userId, String userName, String phone, String email, String rawPassword, String validFrom, String validTo, String setupLink) {
|
||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -761,18 +806,26 @@ public class SystemUserService {
|
|||||||
);
|
);
|
||||||
String tenantCode = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_code"));
|
String tenantCode = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_code"));
|
||||||
String tenantName = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_name"));
|
String tenantName = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_name"));
|
||||||
String loginPath = "/" + tenantCode + "/login";
|
String baseUrl = frontendBaseUrl == null ? "" : frontendBaseUrl;
|
||||||
|
while (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||||
|
}
|
||||||
|
String loginPath = baseUrl.isEmpty() ? "/" + tenantCode + "/login" : baseUrl + "/" + tenantCode + "/login";
|
||||||
|
String displayValidTo = (validTo != null && validTo.contains("2099-12-31")) ? "长期有效" : validTo;
|
||||||
|
|
||||||
Map<String, Object> variables = new LinkedHashMap<String, Object>();
|
Map<String, Object> variables = new LinkedHashMap<String, Object>();
|
||||||
variables.put("userId", userId);
|
variables.put("userId", userId);
|
||||||
variables.put("targetUserId", userId);
|
variables.put("targetUserId", userId);
|
||||||
variables.put("userName", userName);
|
variables.put("userName", userName);
|
||||||
variables.put("phone", phone);
|
variables.put("phone", phone);
|
||||||
variables.put("email", email);
|
variables.put("email", email);
|
||||||
|
variables.put("password", rawPassword);
|
||||||
variables.put("validFrom", validFrom);
|
variables.put("validFrom", validFrom);
|
||||||
variables.put("validTo", validTo);
|
variables.put("validTo", displayValidTo);
|
||||||
variables.put("tenantCode", tenantCode);
|
variables.put("tenantCode", tenantCode);
|
||||||
variables.put("tenantName", tenantName);
|
variables.put("tenantName", tenantName);
|
||||||
variables.put("loginPath", loginPath);
|
variables.put("loginPath", setupLink != null ? setupLink : loginPath);
|
||||||
|
variables.put("setupLink", setupLink != null ? setupLink : "");
|
||||||
DispatchNotificationRequest request = new DispatchNotificationRequest();
|
DispatchNotificationRequest request = new DispatchNotificationRequest();
|
||||||
request.setIdempotencyKey("user-created-" + tenantId() + "-" + userId);
|
request.setIdempotencyKey("user-created-" + tenantId() + "-" + userId);
|
||||||
request.setEventCode("USER_CREATED");
|
request.setEventCode("USER_CREATED");
|
||||||
|
|||||||
@ -270,7 +270,8 @@ public class TenantService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
String setupLink = passwordSetupService.issueTenantAdminSetupLink(tenantId, uid, safeUserId());
|
String setupLink = passwordSetupService.issueTenantAdminSetupLink(tenantId, uid, safeUserId());
|
||||||
sendTenantAdminMail(tenantId, request, action, setupLink);
|
// 移除了发送邮件功能
|
||||||
|
// sendTenantAdminMail(tenantId, request, action, setupLink);
|
||||||
|
|
||||||
java.util.Map<String, Object> data = new java.util.LinkedHashMap<String, Object>();
|
java.util.Map<String, Object> data = new java.util.LinkedHashMap<String, Object>();
|
||||||
data.put("tenantId", tenantId);
|
data.put("tenantId", tenantId);
|
||||||
@ -278,6 +279,7 @@ public class TenantService {
|
|||||||
data.put("roleId", roleId);
|
data.put("roleId", roleId);
|
||||||
data.put("roleCode", roleCode);
|
data.put("roleCode", roleCode);
|
||||||
data.put("action", action);
|
data.put("action", action);
|
||||||
|
data.put("setupLink", setupLink);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,11 +39,23 @@ public class TemplateController {
|
|||||||
@RequestParam(value = "status", required = false) String status,
|
@RequestParam(value = "status", required = false) String status,
|
||||||
@RequestParam(value = "scopeType", required = false) String scopeType,
|
@RequestParam(value = "scopeType", required = false) String scopeType,
|
||||||
@RequestParam(value = "bizScene", required = false) String bizScene,
|
@RequestParam(value = "bizScene", required = false) String bizScene,
|
||||||
@RequestParam(value = "watermarkEnabled", required = false) Boolean watermarkEnabled,
|
|
||||||
@RequestParam(value = "effectiveStatus", required = false) String effectiveStatus,
|
@RequestParam(value = "effectiveStatus", required = false) String effectiveStatus,
|
||||||
@RequestParam(value = "pageNo", defaultValue = "1") int pageNo,
|
@RequestParam(value = "pageNo", defaultValue = "1") int pageNo,
|
||||||
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize) {
|
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize) {
|
||||||
return ApiResponse.success(templateService.list(templateName, templateType, status, scopeType, bizScene, watermarkEnabled, effectiveStatus, pageNo, pageSize));
|
return ApiResponse.success(templateService.list(templateName, templateType, status, scopeType, bizScene, effectiveStatus, pageNo, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/view-list")
|
||||||
|
@RequirePermission(value = "template.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VIEW_LIST")
|
||||||
|
public ApiResponse<PageResult<TemplateInfo>> viewList(
|
||||||
|
@RequestParam(value = "templateName", required = false) String templateName,
|
||||||
|
@RequestParam(value = "templateType", required = false) String templateType,
|
||||||
|
@RequestParam(value = "scopeType", required = false) String scopeType,
|
||||||
|
@RequestParam(value = "bizScene", required = false) String bizScene,
|
||||||
|
@RequestParam(value = "effectiveStatus", required = false) String effectiveStatus,
|
||||||
|
@RequestParam(value = "pageNo", defaultValue = "1") int pageNo,
|
||||||
|
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize) {
|
||||||
|
return ApiResponse.success(templateService.listView(templateName, templateType, scopeType, bizScene, effectiveStatus, pageNo, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/published-options")
|
@GetMapping("/published-options")
|
||||||
@ -98,6 +110,12 @@ public class TemplateController {
|
|||||||
return ApiResponse.success(templateService.create(request));
|
return ApiResponse.success(templateService.create(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/update")
|
||||||
|
@RequirePermission(value = "template.update", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_UPDATE")
|
||||||
|
public ApiResponse<TemplateInfo> update(@PathVariable("id") Long id, @RequestBody @Valid com.writeoff.module.template.dto.UpdateTemplateRequest request) {
|
||||||
|
return ApiResponse.success(templateService.update(id, request));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/upload-sign")
|
@PostMapping("/upload-sign")
|
||||||
@RequirePermission(value = "template.create", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_UPLOAD_SIGN")
|
@RequirePermission(value = "template.create", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_UPLOAD_SIGN")
|
||||||
public ApiResponse<Map<String, Object>> uploadSign(@RequestBody @Valid TemplateUploadSignRequest request) {
|
public ApiResponse<Map<String, Object>> uploadSign(@RequestBody @Valid TemplateUploadSignRequest request) {
|
||||||
@ -105,7 +123,7 @@ public class TemplateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/versions")
|
@GetMapping("/{id}/versions")
|
||||||
@RequirePermission(value = "template.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VERSIONS")
|
@RequirePermission(value = "template.detail.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VERSIONS")
|
||||||
public ApiResponse<List<TemplateVersionInfo>> versions(@PathVariable("id") Long id) {
|
public ApiResponse<List<TemplateVersionInfo>> versions(@PathVariable("id") Long id) {
|
||||||
return ApiResponse.success(templateService.versions(id));
|
return ApiResponse.success(templateService.versions(id));
|
||||||
}
|
}
|
||||||
@ -148,16 +166,10 @@ public class TemplateController {
|
|||||||
return ApiResponse.success(templateService.download(id, request.getRemoteAddr(), request.getHeader("User-Agent")));
|
return ApiResponse.success(templateService.download(id, request.getRemoteAddr(), request.getHeader("User-Agent")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/download-watermark")
|
|
||||||
@RequirePermission(value = "template.download", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_DOWNLOAD_WATERMARK")
|
|
||||||
public ApiResponse<Map<String, Object>> downloadWatermark(@PathVariable("id") Long id,
|
|
||||||
@RequestParam(value = "watermarkText", required = false) String watermarkText,
|
|
||||||
HttpServletRequest request) {
|
|
||||||
return ApiResponse.success(templateService.downloadWatermark(id, watermarkText, request.getRemoteAddr(), request.getHeader("User-Agent")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}/versions/diff")
|
@GetMapping("/{id}/versions/diff")
|
||||||
@RequirePermission(value = "template.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VERSION_DIFF")
|
@RequirePermission(value = "template.detail.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VERSION_DIFF")
|
||||||
public ApiResponse<Map<String, Object>> versionDiff(@PathVariable("id") Long id,
|
public ApiResponse<Map<String, Object>> versionDiff(@PathVariable("id") Long id,
|
||||||
@RequestParam(value = "leftVersionNo", required = false) Integer leftVersionNo,
|
@RequestParam(value = "leftVersionNo", required = false) Integer leftVersionNo,
|
||||||
@RequestParam(value = "rightVersionNo", required = false) Integer rightVersionNo) {
|
@RequestParam(value = "rightVersionNo", required = false) Integer rightVersionNo) {
|
||||||
@ -172,7 +184,6 @@ public class TemplateController {
|
|||||||
@RequestParam(value = "userId", required = false) Long userId,
|
@RequestParam(value = "userId", required = false) Long userId,
|
||||||
@RequestParam(value = "userKeyword", required = false) String userKeyword,
|
@RequestParam(value = "userKeyword", required = false) String userKeyword,
|
||||||
@RequestParam(value = "versionNo", required = false) Integer versionNo,
|
@RequestParam(value = "versionNo", required = false) Integer versionNo,
|
||||||
@RequestParam(value = "downloadType", required = false) String downloadType,
|
|
||||||
@RequestParam(value = "ip", required = false) String ip,
|
@RequestParam(value = "ip", required = false) String ip,
|
||||||
@RequestParam(value = "downloadedFrom", required = false) String downloadedFrom,
|
@RequestParam(value = "downloadedFrom", required = false) String downloadedFrom,
|
||||||
@RequestParam(value = "downloadedTo", required = false) String downloadedTo,
|
@RequestParam(value = "downloadedTo", required = false) String downloadedTo,
|
||||||
@ -184,7 +195,6 @@ public class TemplateController {
|
|||||||
userId,
|
userId,
|
||||||
userKeyword,
|
userKeyword,
|
||||||
versionNo,
|
versionNo,
|
||||||
downloadType,
|
|
||||||
ip,
|
ip,
|
||||||
downloadedFrom,
|
downloadedFrom,
|
||||||
downloadedTo,
|
downloadedTo,
|
||||||
|
|||||||
@ -18,7 +18,6 @@ public class CreateTemplateRequest {
|
|||||||
private String changeLog;
|
private String changeLog;
|
||||||
private String effectiveFrom;
|
private String effectiveFrom;
|
||||||
private String effectiveTo;
|
private String effectiveTo;
|
||||||
private Boolean watermarkEnabled;
|
|
||||||
private Integer downloadRateLimitPerHour;
|
private Integer downloadRateLimitPerHour;
|
||||||
|
|
||||||
public String getTemplateName() {
|
public String getTemplateName() {
|
||||||
@ -109,13 +108,6 @@ public class CreateTemplateRequest {
|
|||||||
this.effectiveTo = effectiveTo;
|
this.effectiveTo = effectiveTo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getWatermarkEnabled() {
|
|
||||||
return watermarkEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWatermarkEnabled(Boolean watermarkEnabled) {
|
|
||||||
this.watermarkEnabled = watermarkEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getDownloadRateLimitPerHour() {
|
public Integer getDownloadRateLimitPerHour() {
|
||||||
return downloadRateLimitPerHour;
|
return downloadRateLimitPerHour;
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
package com.writeoff.module.template.dto;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public class UpdateTemplateRequest {
|
||||||
|
@NotBlank(message = "模板名称不能为空")
|
||||||
|
private String templateName;
|
||||||
|
@NotBlank(message = "模板类型不能为空")
|
||||||
|
private String templateType;
|
||||||
|
@NotBlank(message = "适用范围不能为空")
|
||||||
|
private String scopeType;
|
||||||
|
private Long projectId;
|
||||||
|
private Long meetingId;
|
||||||
|
private Long scopeId;
|
||||||
|
private String bizScene;
|
||||||
|
private String effectiveFrom;
|
||||||
|
private String effectiveTo;
|
||||||
|
private Integer downloadRateLimitPerHour;
|
||||||
|
|
||||||
|
public String getTemplateName() {
|
||||||
|
return templateName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTemplateName(String templateName) {
|
||||||
|
this.templateName = templateName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTemplateType() {
|
||||||
|
return templateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTemplateType(String templateType) {
|
||||||
|
this.templateType = templateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScopeType() {
|
||||||
|
return scopeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScopeType(String scopeType) {
|
||||||
|
this.scopeType = scopeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getProjectId() {
|
||||||
|
return projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProjectId(Long projectId) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getMeetingId() {
|
||||||
|
return meetingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMeetingId(Long meetingId) {
|
||||||
|
this.meetingId = meetingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getScopeId() {
|
||||||
|
return scopeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScopeId(Long scopeId) {
|
||||||
|
this.scopeId = scopeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBizScene() {
|
||||||
|
return bizScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBizScene(String bizScene) {
|
||||||
|
this.bizScene = bizScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEffectiveFrom() {
|
||||||
|
return effectiveFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEffectiveFrom(String effectiveFrom) {
|
||||||
|
this.effectiveFrom = effectiveFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEffectiveTo() {
|
||||||
|
return effectiveTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEffectiveTo(String effectiveTo) {
|
||||||
|
this.effectiveTo = effectiveTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Integer getDownloadRateLimitPerHour() {
|
||||||
|
return downloadRateLimitPerHour;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDownloadRateLimitPerHour(Integer downloadRateLimitPerHour) {
|
||||||
|
this.downloadRateLimitPerHour = downloadRateLimitPerHour;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,6 @@ public class TemplateDownloadLogInfo {
|
|||||||
private String userPhone;
|
private String userPhone;
|
||||||
private String objectKey;
|
private String objectKey;
|
||||||
private String downloadType;
|
private String downloadType;
|
||||||
private String watermarkText;
|
|
||||||
private Long projectId;
|
private Long projectId;
|
||||||
private Long meetingId;
|
private Long meetingId;
|
||||||
private String ip;
|
private String ip;
|
||||||
@ -18,7 +17,7 @@ public class TemplateDownloadLogInfo {
|
|||||||
private String downloadedAt;
|
private String downloadedAt;
|
||||||
|
|
||||||
public TemplateDownloadLogInfo(Long id, Long templateId, String templateName, Integer versionNo, Long userId, String userName, String userPhone,
|
public TemplateDownloadLogInfo(Long id, Long templateId, String templateName, Integer versionNo, Long userId, String userName, String userPhone,
|
||||||
String objectKey, String downloadType, String watermarkText, Long projectId, Long meetingId,
|
String objectKey, String downloadType, Long projectId, Long meetingId,
|
||||||
String ip, String userAgent, String downloadedAt) {
|
String ip, String userAgent, String downloadedAt) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.templateId = templateId;
|
this.templateId = templateId;
|
||||||
@ -29,7 +28,6 @@ public class TemplateDownloadLogInfo {
|
|||||||
this.userPhone = userPhone;
|
this.userPhone = userPhone;
|
||||||
this.objectKey = objectKey;
|
this.objectKey = objectKey;
|
||||||
this.downloadType = downloadType;
|
this.downloadType = downloadType;
|
||||||
this.watermarkText = watermarkText;
|
|
||||||
this.projectId = projectId;
|
this.projectId = projectId;
|
||||||
this.meetingId = meetingId;
|
this.meetingId = meetingId;
|
||||||
this.ip = ip;
|
this.ip = ip;
|
||||||
@ -73,9 +71,7 @@ public class TemplateDownloadLogInfo {
|
|||||||
return downloadType;
|
return downloadType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getWatermarkText() {
|
|
||||||
return watermarkText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getProjectId() {
|
public Long getProjectId() {
|
||||||
return projectId;
|
return projectId;
|
||||||
|
|||||||
@ -14,13 +14,12 @@ public class TemplateInfo {
|
|||||||
private String currentObjectKey;
|
private String currentObjectKey;
|
||||||
private String effectiveFrom;
|
private String effectiveFrom;
|
||||||
private String effectiveTo;
|
private String effectiveTo;
|
||||||
private Boolean watermarkEnabled;
|
|
||||||
private Integer downloadRateLimitPerHour;
|
private Integer downloadRateLimitPerHour;
|
||||||
private String createdAt;
|
private String createdAt;
|
||||||
private String updatedAt;
|
private String updatedAt;
|
||||||
|
|
||||||
public TemplateInfo(Long id, String templateName, String templateType, String scopeType, Long projectId, Long meetingId, Long scopeId, String bizScene,
|
public TemplateInfo(Long id, String templateName, String templateType, String scopeType, Long projectId, Long meetingId, Long scopeId, String bizScene,
|
||||||
String status, Integer currentVersionNo, String currentObjectKey, String effectiveFrom, String effectiveTo, Boolean watermarkEnabled, Integer downloadRateLimitPerHour,
|
String status, Integer currentVersionNo, String currentObjectKey, String effectiveFrom, String effectiveTo, Integer downloadRateLimitPerHour,
|
||||||
String createdAt, String updatedAt) {
|
String createdAt, String updatedAt) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.templateName = templateName;
|
this.templateName = templateName;
|
||||||
@ -35,7 +34,6 @@ public class TemplateInfo {
|
|||||||
this.currentObjectKey = currentObjectKey;
|
this.currentObjectKey = currentObjectKey;
|
||||||
this.effectiveFrom = effectiveFrom;
|
this.effectiveFrom = effectiveFrom;
|
||||||
this.effectiveTo = effectiveTo;
|
this.effectiveTo = effectiveTo;
|
||||||
this.watermarkEnabled = watermarkEnabled;
|
|
||||||
this.downloadRateLimitPerHour = downloadRateLimitPerHour;
|
this.downloadRateLimitPerHour = downloadRateLimitPerHour;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
@ -93,9 +91,6 @@ public class TemplateInfo {
|
|||||||
return effectiveTo;
|
return effectiveTo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getWatermarkEnabled() {
|
|
||||||
return watermarkEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getDownloadRateLimitPerHour() {
|
public Integer getDownloadRateLimitPerHour() {
|
||||||
return downloadRateLimitPerHour;
|
return downloadRateLimitPerHour;
|
||||||
|
|||||||
@ -59,7 +59,6 @@ public class TemplateService {
|
|||||||
rs.getString("current_object_key"),
|
rs.getString("current_object_key"),
|
||||||
rs.getString("effective_from"),
|
rs.getString("effective_from"),
|
||||||
rs.getString("effective_to"),
|
rs.getString("effective_to"),
|
||||||
rs.getInt("watermark_enabled") == 1,
|
|
||||||
rs.getInt("download_rate_limit_per_hour"),
|
rs.getInt("download_rate_limit_per_hour"),
|
||||||
rs.getString("created_at"),
|
rs.getString("created_at"),
|
||||||
rs.getString("updated_at")
|
rs.getString("updated_at")
|
||||||
@ -86,7 +85,6 @@ public class TemplateService {
|
|||||||
rs.getString("user_phone"),
|
rs.getString("user_phone"),
|
||||||
rs.getString("object_key"),
|
rs.getString("object_key"),
|
||||||
rs.getString("download_type"),
|
rs.getString("download_type"),
|
||||||
rs.getString("watermark_text"),
|
|
||||||
rs.getObject("project_id") == null ? null : rs.getLong("project_id"),
|
rs.getObject("project_id") == null ? null : rs.getLong("project_id"),
|
||||||
rs.getObject("meeting_id") == null ? null : rs.getLong("meeting_id"),
|
rs.getObject("meeting_id") == null ? null : rs.getLong("meeting_id"),
|
||||||
rs.getString("ip"),
|
rs.getString("ip"),
|
||||||
@ -114,7 +112,6 @@ public class TemplateService {
|
|||||||
String status,
|
String status,
|
||||||
String scopeType,
|
String scopeType,
|
||||||
String bizScene,
|
String bizScene,
|
||||||
Boolean watermarkEnabled,
|
|
||||||
String effectiveStatus,
|
String effectiveStatus,
|
||||||
int pageNo,
|
int pageNo,
|
||||||
int pageSize) {
|
int pageSize) {
|
||||||
@ -150,10 +147,7 @@ public class TemplateService {
|
|||||||
whereSql.append(" AND t.biz_scene=?");
|
whereSql.append(" AND t.biz_scene=?");
|
||||||
whereArgs.add(normalizedBizScene);
|
whereArgs.add(normalizedBizScene);
|
||||||
}
|
}
|
||||||
if (watermarkEnabled != null) {
|
|
||||||
whereSql.append(" AND t.watermark_enabled=?");
|
|
||||||
whereArgs.add(Boolean.TRUE.equals(watermarkEnabled) ? 1 : 0);
|
|
||||||
}
|
|
||||||
appendEffectiveStatusFilter(whereSql, normalizedEffectiveStatus);
|
appendEffectiveStatusFilter(whereSql, normalizedEffectiveStatus);
|
||||||
|
|
||||||
Integer total = jdbcTemplate.queryForObject(
|
Integer total = jdbcTemplate.queryForObject(
|
||||||
@ -176,6 +170,16 @@ public class TemplateService {
|
|||||||
return new PageResult<>(list, totalCount, safePage, safeSize);
|
return new PageResult<>(list, totalCount, safePage, safeSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PageResult<TemplateInfo> listView(String templateName,
|
||||||
|
String templateType,
|
||||||
|
String scopeType,
|
||||||
|
String bizScene,
|
||||||
|
String effectiveStatus,
|
||||||
|
int pageNo,
|
||||||
|
int pageSize) {
|
||||||
|
return list(templateName, templateType, "PUBLISHED", scopeType, bizScene, effectiveStatus, pageNo, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
public List<TemplateInfo> listPublishedOptions(String bizScene) {
|
public List<TemplateInfo> listPublishedOptions(String bizScene) {
|
||||||
String normalizedBizScene = normalizeOptionalBizScene(bizScene);
|
String normalizedBizScene = normalizeOptionalBizScene(bizScene);
|
||||||
StringBuilder sql = new StringBuilder(templateSelectSql())
|
StringBuilder sql = new StringBuilder(templateSelectSql())
|
||||||
@ -248,8 +252,8 @@ public class TemplateService {
|
|||||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||||
jdbcTemplate.update(connection -> {
|
jdbcTemplate.update(connection -> {
|
||||||
PreparedStatement ps = connection.prepareStatement(
|
PreparedStatement ps = connection.prepareStatement(
|
||||||
"INSERT INTO template (tenant_id, template_name, template_type, scope_type, project_id, meeting_id, scope_id, biz_scene, status, current_version_no, effective_from, effective_to, watermark_enabled, download_rate_limit_per_hour, created_by, updated_by) " +
|
"INSERT INTO template (tenant_id, template_name, template_type, scope_type, project_id, meeting_id, scope_id, biz_scene, status, current_version_no, effective_from, effective_to, download_rate_limit_per_hour, created_by, updated_by) " +
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'DRAFT', 1, STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), ?, ?, ?, ?)",
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'DRAFT', 1, STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), ?, ?, ?)",
|
||||||
Statement.RETURN_GENERATED_KEYS
|
Statement.RETURN_GENERATED_KEYS
|
||||||
);
|
);
|
||||||
ps.setLong(1, tenantId());
|
ps.setLong(1, tenantId());
|
||||||
@ -262,10 +266,9 @@ public class TemplateService {
|
|||||||
ps.setString(8, normalizeBizScene(request.getBizScene()));
|
ps.setString(8, normalizeBizScene(request.getBizScene()));
|
||||||
ps.setString(9, effectiveFrom);
|
ps.setString(9, effectiveFrom);
|
||||||
ps.setString(10, effectiveTo);
|
ps.setString(10, effectiveTo);
|
||||||
ps.setInt(11, Boolean.TRUE.equals(request.getWatermarkEnabled()) ? 1 : 0);
|
ps.setInt(11, downloadRateLimitPerHour);
|
||||||
ps.setInt(12, downloadRateLimitPerHour);
|
ps.setLong(12, userId);
|
||||||
ps.setLong(13, userId);
|
ps.setLong(13, userId);
|
||||||
ps.setLong(14, userId);
|
|
||||||
return ps;
|
return ps;
|
||||||
}, keyHolder);
|
}, keyHolder);
|
||||||
Long templateId = keyHolder.getKey() == null ? null : keyHolder.getKey().longValue();
|
Long templateId = keyHolder.getKey() == null ? null : keyHolder.getKey().longValue();
|
||||||
@ -282,6 +285,36 @@ public class TemplateService {
|
|||||||
return findById(validTemplateId);
|
return findById(validTemplateId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public TemplateInfo update(Long templateId, com.writeoff.module.template.dto.UpdateTemplateRequest request) {
|
||||||
|
TemplateInfo template = findById(templateId);
|
||||||
|
assertTemplateEditable(template);
|
||||||
|
String scopeType = normalizeScope(request.getScopeType());
|
||||||
|
String templateType = normalizeTemplateType(request.getTemplateType());
|
||||||
|
String effectiveFrom = normalizeOptionalDateTime(request.getEffectiveFrom(), "effectiveFrom");
|
||||||
|
String effectiveTo = normalizeOptionalDateTime(request.getEffectiveTo(), "effectiveTo");
|
||||||
|
Integer downloadRateLimitPerHour = normalizeDownloadRateLimit(request.getDownloadRateLimitPerHour());
|
||||||
|
assertEffectiveRangeValid(effectiveFrom, effectiveTo);
|
||||||
|
assertTypeOptionEnabled(templateType);
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"UPDATE template SET template_name=?, template_type=?, scope_type=?, project_id=?, meeting_id=?, scope_id=?, biz_scene=?, effective_from=STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), effective_to=STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), download_rate_limit_per_hour=?, updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
|
||||||
|
request.getTemplateName(),
|
||||||
|
templateType,
|
||||||
|
scopeType,
|
||||||
|
request.getProjectId(),
|
||||||
|
request.getMeetingId(),
|
||||||
|
request.getScopeId(),
|
||||||
|
normalizeBizScene(request.getBizScene()),
|
||||||
|
effectiveFrom,
|
||||||
|
effectiveTo,
|
||||||
|
downloadRateLimitPerHour,
|
||||||
|
safeUserId(),
|
||||||
|
tenantId(),
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
return findById(templateId);
|
||||||
|
}
|
||||||
|
|
||||||
public List<TemplateVersionInfo> versions(Long templateId) {
|
public List<TemplateVersionInfo> versions(Long templateId) {
|
||||||
assertTemplateExists(templateId);
|
assertTemplateExists(templateId);
|
||||||
return jdbcTemplate.query(
|
return jdbcTemplate.query(
|
||||||
@ -451,15 +484,14 @@ public class TemplateService {
|
|||||||
assertTemplateEffectiveNow(template, "下载");
|
assertTemplateEffectiveNow(template, "下载");
|
||||||
assertDownloadRateLimit(template, userId);
|
assertDownloadRateLimit(template, userId);
|
||||||
jdbcTemplate.update(
|
jdbcTemplate.update(
|
||||||
"INSERT INTO template_download_log (tenant_id, template_id, version_no, user_id, object_key, download_type, watermark_text, project_id, meeting_id, ip, user_agent) " +
|
"INSERT INTO template_download_log (tenant_id, template_id, version_no, user_id, object_key, download_type, project_id, meeting_id, ip, user_agent) " +
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
tenantId(),
|
tenantId(),
|
||||||
template.getId(),
|
template.getId(),
|
||||||
template.getCurrentVersionNo(),
|
template.getCurrentVersionNo(),
|
||||||
userId,
|
userId,
|
||||||
template.getCurrentObjectKey(),
|
template.getCurrentObjectKey(),
|
||||||
"NORMAL",
|
"NORMAL",
|
||||||
null,
|
|
||||||
template.getProjectId(),
|
template.getProjectId(),
|
||||||
template.getMeetingId(),
|
template.getMeetingId(),
|
||||||
ip,
|
ip,
|
||||||
@ -473,46 +505,6 @@ public class TemplateService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public Map<String, Object> downloadWatermark(Long templateId, String watermarkText, String ip, String userAgent) {
|
|
||||||
TemplateInfo template = findById(templateId);
|
|
||||||
if ("DISABLED".equalsIgnoreCase(template.getStatus()) || "ARCHIVED".equalsIgnoreCase(template.getStatus())) {
|
|
||||||
throw new BusinessException(10003, "模板已停用,无法下载");
|
|
||||||
}
|
|
||||||
if (template.getCurrentObjectKey() == null || template.getCurrentObjectKey().trim().isEmpty()) {
|
|
||||||
throw new BusinessException(10003, "模板当前版本文件不存在");
|
|
||||||
}
|
|
||||||
String signedUrl = ossService.generateDownloadUrl(template.getCurrentObjectKey());
|
|
||||||
Long userId = safeUserId();
|
|
||||||
assertTemplateDownloadAllowed(template);
|
|
||||||
if (!template.getWatermarkEnabled()) {
|
|
||||||
throw new BusinessException(10003, "模板未启用水印下载");
|
|
||||||
}
|
|
||||||
assertTemplateEffectiveNow(template, "水印下载");
|
|
||||||
assertDownloadRateLimit(template, userId);
|
|
||||||
jdbcTemplate.update(
|
|
||||||
"INSERT INTO template_download_log (tenant_id, template_id, version_no, user_id, object_key, download_type, watermark_text, project_id, meeting_id, ip, user_agent) " +
|
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
tenantId(),
|
|
||||||
template.getId(),
|
|
||||||
template.getCurrentVersionNo(),
|
|
||||||
userId,
|
|
||||||
template.getCurrentObjectKey(),
|
|
||||||
"WATERMARK",
|
|
||||||
watermarkText == null ? null : watermarkText.trim(),
|
|
||||||
template.getProjectId(),
|
|
||||||
template.getMeetingId(),
|
|
||||||
ip,
|
|
||||||
userAgent
|
|
||||||
);
|
|
||||||
Map<String, Object> result = new LinkedHashMap<String, Object>();
|
|
||||||
result.put("templateId", template.getId());
|
|
||||||
result.put("versionNo", template.getCurrentVersionNo());
|
|
||||||
result.put("objectKey", template.getCurrentObjectKey());
|
|
||||||
result.put("signedUrl", signedUrl);
|
|
||||||
result.put("watermarkText", watermarkText == null ? "" : watermarkText.trim());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> versionDiff(Long templateId, Integer leftVersionNo, Integer rightVersionNo) {
|
public Map<String, Object> versionDiff(Long templateId, Integer leftVersionNo, Integer rightVersionNo) {
|
||||||
assertTemplateExists(templateId);
|
assertTemplateExists(templateId);
|
||||||
@ -548,7 +540,6 @@ public class TemplateService {
|
|||||||
Long userId,
|
Long userId,
|
||||||
String userKeyword,
|
String userKeyword,
|
||||||
Integer versionNo,
|
Integer versionNo,
|
||||||
String downloadType,
|
|
||||||
String ip,
|
String ip,
|
||||||
String downloadedFrom,
|
String downloadedFrom,
|
||||||
String downloadedTo,
|
String downloadedTo,
|
||||||
@ -559,7 +550,6 @@ public class TemplateService {
|
|||||||
int offset = (safePage - 1) * safeSize;
|
int offset = (safePage - 1) * safeSize;
|
||||||
String normalizedTemplateName = trimToNull(templateName);
|
String normalizedTemplateName = trimToNull(templateName);
|
||||||
String normalizedUserKeyword = trimToNull(userKeyword);
|
String normalizedUserKeyword = trimToNull(userKeyword);
|
||||||
String normalizedDownloadType = normalizeOptionalDownloadType(downloadType);
|
|
||||||
String normalizedIp = trimToNull(ip);
|
String normalizedIp = trimToNull(ip);
|
||||||
String normalizedDownloadedFrom = trimToNull(downloadedFrom);
|
String normalizedDownloadedFrom = trimToNull(downloadedFrom);
|
||||||
String normalizedDownloadedTo = trimToNull(downloadedTo);
|
String normalizedDownloadedTo = trimToNull(downloadedTo);
|
||||||
@ -598,10 +588,7 @@ public class TemplateService {
|
|||||||
whereSql.append(" AND l.version_no=?");
|
whereSql.append(" AND l.version_no=?");
|
||||||
whereArgs.add(versionNo);
|
whereArgs.add(versionNo);
|
||||||
}
|
}
|
||||||
if (normalizedDownloadType != null) {
|
|
||||||
whereSql.append(" AND l.download_type=?");
|
|
||||||
whereArgs.add(normalizedDownloadType);
|
|
||||||
}
|
|
||||||
if (normalizedIp != null) {
|
if (normalizedIp != null) {
|
||||||
whereSql.append(" AND l.ip LIKE ?");
|
whereSql.append(" AND l.ip LIKE ?");
|
||||||
whereArgs.add("%" + normalizedIp + "%");
|
whereArgs.add("%" + normalizedIp + "%");
|
||||||
@ -628,7 +615,7 @@ public class TemplateService {
|
|||||||
List<TemplateDownloadLogInfo> list = jdbcTemplate.query(
|
List<TemplateDownloadLogInfo> list = jdbcTemplate.query(
|
||||||
"SELECT l.id, l.template_id, COALESCE(t.template_name, '') AS template_name, " +
|
"SELECT l.id, l.template_id, COALESCE(t.template_name, '') AS template_name, " +
|
||||||
"l.version_no, l.user_id, COALESCE(u.user_name, '') AS user_name, COALESCE(u.phone, '') AS user_phone, " +
|
"l.version_no, l.user_id, COALESCE(u.user_name, '') AS user_name, COALESCE(u.phone, '') AS user_phone, " +
|
||||||
"l.object_key, l.download_type, COALESCE(l.watermark_text, '') AS watermark_text, " +
|
"l.object_key, l.download_type, " +
|
||||||
"l.project_id, l.meeting_id, l.ip, l.user_agent, DATE_FORMAT(l.downloaded_at, '%Y-%m-%d %H:%i:%s') AS downloaded_at" +
|
"l.project_id, l.meeting_id, l.ip, l.user_agent, DATE_FORMAT(l.downloaded_at, '%Y-%m-%d %H:%i:%s') AS downloaded_at" +
|
||||||
whereSql +
|
whereSql +
|
||||||
" ORDER BY l.downloaded_at DESC, l.id DESC LIMIT ? OFFSET ?",
|
" ORDER BY l.downloaded_at DESC, l.id DESC LIMIT ? OFFSET ?",
|
||||||
@ -709,7 +696,7 @@ public class TemplateService {
|
|||||||
"t.current_version_no, tv.object_key AS current_object_key, " +
|
"t.current_version_no, tv.object_key AS current_object_key, " +
|
||||||
"DATE_FORMAT(t.effective_from, '%Y-%m-%d %H:%i:%s') AS effective_from, " +
|
"DATE_FORMAT(t.effective_from, '%Y-%m-%d %H:%i:%s') AS effective_from, " +
|
||||||
"DATE_FORMAT(t.effective_to, '%Y-%m-%d %H:%i:%s') AS effective_to, " +
|
"DATE_FORMAT(t.effective_to, '%Y-%m-%d %H:%i:%s') AS effective_to, " +
|
||||||
"t.watermark_enabled, t.download_rate_limit_per_hour, " +
|
"t.download_rate_limit_per_hour, " +
|
||||||
"DATE_FORMAT(t.created_at, '%Y-%m-%d %H:%i:%s') AS created_at, " +
|
"DATE_FORMAT(t.created_at, '%Y-%m-%d %H:%i:%s') AS created_at, " +
|
||||||
"DATE_FORMAT(t.updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at " +
|
"DATE_FORMAT(t.updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at " +
|
||||||
"FROM template t " +
|
"FROM template t " +
|
||||||
@ -806,17 +793,7 @@ public class TemplateService {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeOptionalDownloadType(String downloadType) {
|
|
||||||
String value = trimToNull(downloadType);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String normalized = value.toUpperCase();
|
|
||||||
if (!"NORMAL".equals(normalized) && !"WATERMARK".equals(normalized)) {
|
|
||||||
throw new BusinessException(10003, "downloadType仅支持NORMAL/WATERMARK");
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeContentType(String contentType) {
|
private String normalizeContentType(String contentType) {
|
||||||
if (contentType == null || contentType.trim().isEmpty()) {
|
if (contentType == null || contentType.trim().isEmpty()) {
|
||||||
@ -936,7 +913,7 @@ public class TemplateService {
|
|||||||
|
|
||||||
private void assertTemplateEditable(TemplateInfo template) {
|
private void assertTemplateEditable(TemplateInfo template) {
|
||||||
if ("ARCHIVED".equalsIgnoreCase(template.getStatus())) {
|
if ("ARCHIVED".equalsIgnoreCase(template.getStatus())) {
|
||||||
throw new BusinessException(10003, "已归档模板不允许新增版本");
|
throw new BusinessException(10003, "已归档模板不允许编辑或新增版本");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import java.util.Map;
|
|||||||
@Service
|
@Service
|
||||||
public class PasswordSetupService {
|
public class PasswordSetupService {
|
||||||
private static final String SCENARIO_TENANT_ADMIN_SETUP = "TENANT_ADMIN_SETUP";
|
private static final String SCENARIO_TENANT_ADMIN_SETUP = "TENANT_ADMIN_SETUP";
|
||||||
|
private static final String SCENARIO_USER_SETUP = "USER_SETUP";
|
||||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
@ -71,6 +72,36 @@ public class PasswordSetupService {
|
|||||||
return buildSetupLink(tenantCode, rawToken);
|
return buildSetupLink(tenantCode, rawToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public String issueUserSetupLink(Long tenantId, Long userId, Long operatorUserId) {
|
||||||
|
Map<String, Object> user = loadSystemUser(tenantId, userId);
|
||||||
|
String tenantCode = String.valueOf(user.get("tenant_code"));
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"UPDATE auth_password_setup_token " +
|
||||||
|
"SET is_deleted=1, updated_by=?, updated_at=CURRENT_TIMESTAMP " +
|
||||||
|
"WHERE tenant_id=? AND user_id=? AND scenario=? AND is_deleted=0 AND used_at IS NULL",
|
||||||
|
safeOperator(operatorUserId),
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
SCENARIO_USER_SETUP
|
||||||
|
);
|
||||||
|
|
||||||
|
String rawToken = generateToken();
|
||||||
|
LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(Math.max(passwordSetupExpireMinutes, 10L));
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"INSERT INTO auth_password_setup_token (tenant_id, user_id, scenario, token_hash, expires_at, created_by, updated_by) " +
|
||||||
|
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
SCENARIO_USER_SETUP,
|
||||||
|
hashToken(rawToken),
|
||||||
|
Timestamp.valueOf(expiresAt),
|
||||||
|
safeOperator(operatorUserId),
|
||||||
|
safeOperator(operatorUserId)
|
||||||
|
);
|
||||||
|
return buildSetupLink(tenantCode, rawToken);
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Object> verifyTenantPasswordSetupToken(String tenantCode, String rawToken) {
|
public Map<String, Object> verifyTenantPasswordSetupToken(String tenantCode, String rawToken) {
|
||||||
Map<String, Object> tokenRecord = loadAvailableTokenRecord(tenantCode, rawToken);
|
Map<String, Object> tokenRecord = loadAvailableTokenRecord(tenantCode, rawToken);
|
||||||
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
||||||
@ -112,11 +143,12 @@ public class PasswordSetupService {
|
|||||||
jdbcTemplate.update(
|
jdbcTemplate.update(
|
||||||
"UPDATE auth_password_setup_token " +
|
"UPDATE auth_password_setup_token " +
|
||||||
"SET is_deleted=1, updated_by=?, updated_at=CURRENT_TIMESTAMP " +
|
"SET is_deleted=1, updated_by=?, updated_at=CURRENT_TIMESTAMP " +
|
||||||
"WHERE tenant_id=? AND user_id=? AND scenario=? AND id<>? AND is_deleted=0 AND used_at IS NULL",
|
"WHERE tenant_id=? AND user_id=? AND scenario IN (?, ?) AND id<>? AND is_deleted=0 AND used_at IS NULL",
|
||||||
userId,
|
userId,
|
||||||
tenantId,
|
tenantId,
|
||||||
userId,
|
userId,
|
||||||
SCENARIO_TENANT_ADMIN_SETUP,
|
SCENARIO_TENANT_ADMIN_SETUP,
|
||||||
|
SCENARIO_USER_SETUP,
|
||||||
tokenId
|
tokenId
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -139,11 +171,12 @@ public class PasswordSetupService {
|
|||||||
"FROM auth_password_setup_token tkn " +
|
"FROM auth_password_setup_token tkn " +
|
||||||
"JOIN tenant t ON tkn.tenant_id=t.id " +
|
"JOIN tenant t ON tkn.tenant_id=t.id " +
|
||||||
"JOIN sys_user u ON tkn.user_id=u.id AND tkn.tenant_id=u.tenant_id " +
|
"JOIN sys_user u ON tkn.user_id=u.id AND tkn.tenant_id=u.tenant_id " +
|
||||||
"WHERE tkn.token_hash=? AND tkn.scenario=? AND tkn.is_deleted=0 " +
|
"WHERE tkn.token_hash=? AND tkn.scenario IN (?, ?) AND tkn.is_deleted=0 " +
|
||||||
"AND t.is_deleted=0 AND u.is_deleted=0 " +
|
"AND t.is_deleted=0 AND u.is_deleted=0 " +
|
||||||
"LIMIT 1",
|
"LIMIT 1",
|
||||||
hashToken(normalizedToken),
|
hashToken(normalizedToken),
|
||||||
SCENARIO_TENANT_ADMIN_SETUP
|
SCENARIO_TENANT_ADMIN_SETUP,
|
||||||
|
SCENARIO_USER_SETUP
|
||||||
);
|
);
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
throw new BusinessException(10001, "设置链接无效或已过期");
|
throw new BusinessException(10001, "设置链接无效或已过期");
|
||||||
@ -181,6 +214,22 @@ public class PasswordSetupService {
|
|||||||
return rows.get(0);
|
return rows.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> loadSystemUser(Long tenantId, Long userId) {
|
||||||
|
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||||
|
"SELECT u.id, u.tenant_id, t.tenant_code " +
|
||||||
|
"FROM sys_user u " +
|
||||||
|
"JOIN tenant t ON u.tenant_id=t.id " +
|
||||||
|
"WHERE u.tenant_id=? AND u.id=? AND u.is_deleted=0 AND t.is_deleted=0 " +
|
||||||
|
"LIMIT 1",
|
||||||
|
tenantId,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw new BusinessException(10003, "用户不存在");
|
||||||
|
}
|
||||||
|
return rows.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
private String buildSetupLink(String tenantCode, String rawToken) {
|
private String buildSetupLink(String tenantCode, String rawToken) {
|
||||||
String baseUrl = normalizeBaseUrl(frontendBaseUrl);
|
String baseUrl = normalizeBaseUrl(frontendBaseUrl);
|
||||||
String path = "/" + tenantCode + "/setup-password?token=" + urlEncode(rawToken);
|
String path = "/" + tenantCode + "/setup-password?token=" + urlEncode(rawToken);
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
-- 添加模板编辑权限
|
||||||
|
INSERT INTO permission (id, permission_code, permission_name, module)
|
||||||
|
SELECT t.next_id, 'template.update', '编辑模板', 'template'
|
||||||
|
FROM (SELECT IFNULL(MAX(id), 0) + 1 AS next_id FROM permission) t
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM permission WHERE permission_code = 'template.update'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 授予角色编辑模板权限(通常跟随 template.create 一起授权)
|
||||||
|
-- 找出所有拥有 template.create 权限的角色,给他们也加上 template.update 权限
|
||||||
|
SET @next_rp_id = (SELECT IFNULL(MAX(id), 0) FROM role_permission);
|
||||||
|
|
||||||
|
INSERT INTO role_permission (id, tenant_id, role_id, permission_id)
|
||||||
|
SELECT
|
||||||
|
(@next_rp_id := @next_rp_id + 1) AS id,
|
||||||
|
rp.tenant_id,
|
||||||
|
rp.role_id,
|
||||||
|
p.id
|
||||||
|
FROM role_permission rp
|
||||||
|
JOIN permission source_p ON source_p.id = rp.permission_id AND source_p.permission_code = 'template.create'
|
||||||
|
JOIN permission p ON p.permission_code = 'template.update'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM role_permission existing_rp
|
||||||
|
WHERE existing_rp.tenant_id = rp.tenant_id
|
||||||
|
AND existing_rp.role_id = rp.role_id
|
||||||
|
AND existing_rp.permission_id = p.id
|
||||||
|
);
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
-- 添加查看详情和水印下载权限
|
||||||
|
INSERT INTO permission (id, permission_code, permission_name, module)
|
||||||
|
SELECT t.next_id, 'template.detail.read', '查看模板详情', 'template'
|
||||||
|
FROM (SELECT IFNULL(MAX(id), 0) + 1 AS next_id FROM permission) t
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM permission WHERE permission_code = 'template.detail.read'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO permission (id, permission_code, permission_name, module)
|
||||||
|
SELECT t.next_id, 'template.watermark.download', '水印下载模板', 'template'
|
||||||
|
FROM (SELECT IFNULL(MAX(id), 0) + 1 AS next_id FROM permission) t
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM permission WHERE permission_code = 'template.watermark.download'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 授予角色新权限(继承现有权限)
|
||||||
|
SET @next_rp_id = (SELECT IFNULL(MAX(id), 0) FROM role_permission);
|
||||||
|
|
||||||
|
-- 给有 template.read 的加上 template.detail.read
|
||||||
|
INSERT INTO role_permission (id, tenant_id, role_id, permission_id)
|
||||||
|
SELECT
|
||||||
|
(@next_rp_id := @next_rp_id + 1) AS id,
|
||||||
|
rp.tenant_id,
|
||||||
|
rp.role_id,
|
||||||
|
p.id
|
||||||
|
FROM role_permission rp
|
||||||
|
JOIN permission source_p ON source_p.id = rp.permission_id AND source_p.permission_code = 'template.read'
|
||||||
|
JOIN permission p ON p.permission_code = 'template.detail.read'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM role_permission existing_rp
|
||||||
|
WHERE existing_rp.tenant_id = rp.tenant_id
|
||||||
|
AND existing_rp.role_id = rp.role_id
|
||||||
|
AND existing_rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 给有 template.download 的加上 template.watermark.download
|
||||||
|
INSERT INTO role_permission (id, tenant_id, role_id, permission_id)
|
||||||
|
SELECT
|
||||||
|
(@next_rp_id := @next_rp_id + 1) AS id,
|
||||||
|
rp.tenant_id,
|
||||||
|
rp.role_id,
|
||||||
|
p.id
|
||||||
|
FROM role_permission rp
|
||||||
|
JOIN permission source_p ON source_p.id = rp.permission_id AND source_p.permission_code = 'template.download'
|
||||||
|
JOIN permission p ON p.permission_code = 'template.watermark.download'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM role_permission existing_rp
|
||||||
|
WHERE existing_rp.tenant_id = rp.tenant_id
|
||||||
|
AND existing_rp.role_id = rp.role_id
|
||||||
|
AND existing_rp.permission_id = p.id
|
||||||
|
);
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
-- 移除水印相关字段
|
||||||
|
ALTER TABLE template DROP COLUMN watermark_enabled;
|
||||||
|
ALTER TABLE template_download_log DROP COLUMN watermark_text;
|
||||||
|
|
||||||
|
-- 删除之前新增的 template.watermark.download 权限
|
||||||
|
DELETE FROM role_permission WHERE permission_id IN (SELECT id FROM permission WHERE permission_code = 'template.watermark.download');
|
||||||
|
DELETE FROM permission WHERE permission_code = 'template.watermark.download';
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
SET @next_permission_id := (SELECT IFNULL(MAX(id), 0) + 1 FROM permission);
|
||||||
|
INSERT INTO permission (id, permission_code, permission_name, module)
|
||||||
|
SELECT @next_permission_id, 'meeting.update', '编辑会议', 'meeting'
|
||||||
|
FROM dual
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM permission
|
||||||
|
WHERE permission_code = 'meeting.update'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @next_role_permission_id := (SELECT IFNULL(MAX(id), 0) FROM role_permission);
|
||||||
|
INSERT INTO role_permission (id, tenant_id, role_id, permission_id)
|
||||||
|
SELECT
|
||||||
|
(@next_role_permission_id := @next_role_permission_id + 1) AS id,
|
||||||
|
rp.tenant_id,
|
||||||
|
rp.role_id,
|
||||||
|
p.id AS permission_id
|
||||||
|
FROM role_permission rp
|
||||||
|
JOIN permission source_p ON source_p.id = rp.permission_id AND source_p.permission_code = 'meeting.create'
|
||||||
|
JOIN permission p ON p.permission_code = 'meeting.update'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM role_permission exist_rp
|
||||||
|
WHERE exist_rp.tenant_id = rp.tenant_id
|
||||||
|
AND exist_rp.role_id = rp.role_id
|
||||||
|
AND exist_rp.permission_id = p.id
|
||||||
|
);
|
||||||
Binary file not shown.
2354
frontend/package-lock.json
generated
2354
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,9 +6,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"compressorjs": "^1.3.0",
|
"compressorjs": "^1.3.0",
|
||||||
"element-plus": "^2.8.4",
|
"element-plus": "^2.8.4",
|
||||||
@ -18,8 +20,13 @@
|
|||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.9.1",
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"@vue/test-utils": "^2.4.11",
|
||||||
|
"happy-dom": "^20.10.1",
|
||||||
|
"jsdom": "^27.0.1",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.4.8"
|
"vite": "^5.4.8",
|
||||||
|
"vitest": "^4.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -716,12 +716,23 @@ export const fetchTemplates = (params?: {
|
|||||||
status?: string;
|
status?: string;
|
||||||
scopeType?: string;
|
scopeType?: string;
|
||||||
bizScene?: string;
|
bizScene?: string;
|
||||||
watermarkEnabled?: boolean;
|
|
||||||
effectiveStatus?: string;
|
effectiveStatus?: string;
|
||||||
pageNo?: number;
|
pageNo?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}) =>
|
}) =>
|
||||||
http.get("/templates", { params });
|
http.get("/templates", { params });
|
||||||
|
export const fetchTemplateViewList = (params?: {
|
||||||
|
templateName?: string;
|
||||||
|
templateType?: string;
|
||||||
|
scopeType?: string;
|
||||||
|
bizScene?: string;
|
||||||
|
|
||||||
|
effectiveStatus?: string;
|
||||||
|
pageNo?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}) =>
|
||||||
|
http.get("/templates/view-list", { params });
|
||||||
export const fetchPublishedTemplateOptions = (params?: { bizScene?: string }) =>
|
export const fetchPublishedTemplateOptions = (params?: { bizScene?: string }) =>
|
||||||
http.get("/templates/published-options", { params });
|
http.get("/templates/published-options", { params });
|
||||||
export const fetchTemplateTypeOptions = () => http.get("/templates/type-options");
|
export const fetchTemplateTypeOptions = () => http.get("/templates/type-options");
|
||||||
@ -742,9 +753,21 @@ export const createTemplate = (payload: {
|
|||||||
changeLog?: string;
|
changeLog?: string;
|
||||||
effectiveFrom?: string;
|
effectiveFrom?: string;
|
||||||
effectiveTo?: string;
|
effectiveTo?: string;
|
||||||
watermarkEnabled?: boolean;
|
|
||||||
downloadRateLimitPerHour?: number;
|
downloadRateLimitPerHour?: number;
|
||||||
}) => http.post("/templates", payload);
|
}) => http.post("/templates", payload);
|
||||||
|
export const updateTemplate = (id: number, payload: {
|
||||||
|
templateName: string;
|
||||||
|
templateType: string;
|
||||||
|
scopeType: "ALL" | "PROJECT" | "MEETING";
|
||||||
|
projectId?: number;
|
||||||
|
meetingId?: number;
|
||||||
|
bizScene?: "MEETING_RECOMMEND" | "AUDIT_NOTIFY" | "SETTLEMENT";
|
||||||
|
effectiveFrom?: string;
|
||||||
|
effectiveTo?: string;
|
||||||
|
|
||||||
|
downloadRateLimitPerHour?: number;
|
||||||
|
}) => http.post(`/templates/${id}/update`, payload);
|
||||||
export const fetchTemplateUploadSign = (payload: {
|
export const fetchTemplateUploadSign = (payload: {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
@ -764,8 +787,6 @@ export const archiveTemplate = (id: number) => http.post(`/templates/${id}/archi
|
|||||||
export const rollbackTemplate = (id: number, payload: { versionNo: number; rollbackReason: string }) =>
|
export const rollbackTemplate = (id: number, payload: { versionNo: number; rollbackReason: string }) =>
|
||||||
http.post(`/templates/${id}/rollback`, payload);
|
http.post(`/templates/${id}/rollback`, payload);
|
||||||
export const downloadTemplate = (id: number) => http.get(`/templates/${id}/download`);
|
export const downloadTemplate = (id: number) => http.get(`/templates/${id}/download`);
|
||||||
export const downloadTemplateWatermark = (id: number, params?: { watermarkText?: string }) =>
|
|
||||||
http.get(`/templates/${id}/download-watermark`, { params });
|
|
||||||
export const fetchTemplateVersionDiff = (id: number, params?: { leftVersionNo?: number; rightVersionNo?: number }) =>
|
export const fetchTemplateVersionDiff = (id: number, params?: { leftVersionNo?: number; rightVersionNo?: number }) =>
|
||||||
http.get(`/templates/${id}/versions/diff`, { params });
|
http.get(`/templates/${id}/versions/diff`, { params });
|
||||||
export const fetchTemplateDownloadLogs = (params?: {
|
export const fetchTemplateDownloadLogs = (params?: {
|
||||||
@ -774,7 +795,7 @@ export const fetchTemplateDownloadLogs = (params?: {
|
|||||||
userId?: number;
|
userId?: number;
|
||||||
userKeyword?: string;
|
userKeyword?: string;
|
||||||
versionNo?: number;
|
versionNo?: number;
|
||||||
downloadType?: "NORMAL" | "WATERMARK";
|
|
||||||
ip?: string;
|
ip?: string;
|
||||||
downloadedFrom?: string;
|
downloadedFrom?: string;
|
||||||
downloadedTo?: string;
|
downloadedTo?: string;
|
||||||
|
|||||||
@ -76,6 +76,7 @@ const handleMarkRead = () => {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notif-detail-title {
|
.notif-detail-title {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export const PERMS = {
|
|||||||
read: "meeting.read",
|
read: "meeting.read",
|
||||||
manage: "meeting.manage",
|
manage: "meeting.manage",
|
||||||
create: "meeting.create",
|
create: "meeting.create",
|
||||||
|
update: "meeting.update",
|
||||||
delete: "meeting.delete",
|
delete: "meeting.delete",
|
||||||
cancel: "meeting.cancel",
|
cancel: "meeting.cancel",
|
||||||
submit: "meeting.submit",
|
submit: "meeting.submit",
|
||||||
@ -83,6 +84,7 @@ export const PERMS = {
|
|||||||
read: "template.read",
|
read: "template.read",
|
||||||
manage: "template.manage",
|
manage: "template.manage",
|
||||||
create: "template.create",
|
create: "template.create",
|
||||||
|
update: "template.update",
|
||||||
publish: "template.publish",
|
publish: "template.publish",
|
||||||
disable: "template.disable",
|
disable: "template.disable",
|
||||||
archive: "template.archive",
|
archive: "template.archive",
|
||||||
@ -90,6 +92,7 @@ export const PERMS = {
|
|||||||
download: "template.download",
|
download: "template.download",
|
||||||
downloadLogReadAll: "template.download.log.read.all",
|
downloadLogReadAll: "template.download.log.read.all",
|
||||||
flowLink: "template.flow.link",
|
flowLink: "template.flow.link",
|
||||||
|
detailRead: "template.detail.read",
|
||||||
},
|
},
|
||||||
expert: {
|
expert: {
|
||||||
read: "expert.read",
|
read: "expert.read",
|
||||||
|
|||||||
@ -1,11 +1,47 @@
|
|||||||
|
import * as xlsx from "xlsx";
|
||||||
|
|
||||||
export const readTextFile = (file: File): Promise<string> =>
|
export const readTextFile = (file: File): Promise<string> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => resolve(String(reader.result || ""));
|
reader.onload = () => {
|
||||||
|
const buffer = reader.result as ArrayBuffer;
|
||||||
|
try {
|
||||||
|
const decoderUtf8 = new TextDecoder("utf-8", { fatal: true });
|
||||||
|
resolve(decoderUtf8.decode(buffer));
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
const decoderGbk = new TextDecoder("gbk");
|
||||||
|
resolve(decoderGbk.decode(buffer));
|
||||||
|
} catch (e2) {
|
||||||
|
reject(new Error("文件编码无法识别"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
reader.onerror = () => reject(reader.error || new Error("文件读取失败"));
|
reader.onerror = () => reject(reader.error || new Error("文件读取失败"));
|
||||||
reader.readAsText(file, "utf-8");
|
reader.readAsArrayBuffer(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const readXlsxFile = async (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = e.target?.result;
|
||||||
|
// codepage 936 tells xlsx to use GBK for CSVs misnamed as XLS
|
||||||
|
const workbook = xlsx.read(data, { type: "array", codepage: 936 });
|
||||||
|
const firstSheetName = workbook.SheetNames[0];
|
||||||
|
const worksheet = workbook.Sheets[firstSheetName];
|
||||||
|
const csv = xlsx.utils.sheet_to_csv(worksheet);
|
||||||
|
resolve(csv);
|
||||||
|
} catch (err) {
|
||||||
|
reject(new Error("读取 Excel 文件失败"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = () => reject(reader.error || new Error("文件读取失败"));
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const downloadCsvTemplate = (fileName: string, headers: string[], sampleRows: string[][] = []) => {
|
export const downloadCsvTemplate = (fileName: string, headers: string[], sampleRows: string[][] = []) => {
|
||||||
downloadCsvRows(fileName, [headers, ...sampleRows]);
|
downloadCsvRows(fileName, [headers, ...sampleRows]);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -65,6 +65,7 @@ const STATUS_TEXT_MAP: Record<string, string> = {
|
|||||||
AUDIT_RETURNED: "审核退回",
|
AUDIT_RETURNED: "审核退回",
|
||||||
FINANCE_CONFIRMED: "财务已确认",
|
FINANCE_CONFIRMED: "财务已确认",
|
||||||
USER_CREATED: "用户创建",
|
USER_CREATED: "用户创建",
|
||||||
|
USER_PASSWORD_RESET: "密码重置",
|
||||||
DELIVERED: "已送达",
|
DELIVERED: "已送达",
|
||||||
UNREAD: "未读",
|
UNREAD: "未读",
|
||||||
READ: "已读",
|
READ: "已读",
|
||||||
|
|||||||
@ -64,7 +64,8 @@
|
|||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item command="dashboard">工作台</el-dropdown-item>
|
<el-dropdown-item command="dashboard">工作台</el-dropdown-item>
|
||||||
<el-dropdown-item command="profile">个人设置</el-dropdown-item>
|
<el-dropdown-item command="profile">个人设置</el-dropdown-item>
|
||||||
<el-dropdown-item divided command="logout">退出全部设备</el-dropdown-item>
|
<el-dropdown-item command="refreshAuth">刷新权限</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@ -159,7 +160,8 @@
|
|||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item command="dashboard">工作台</el-dropdown-item>
|
<el-dropdown-item command="dashboard">工作台</el-dropdown-item>
|
||||||
<el-dropdown-item command="profile">个人设置</el-dropdown-item>
|
<el-dropdown-item command="profile">个人设置</el-dropdown-item>
|
||||||
<el-dropdown-item divided command="logout">退出全部设备</el-dropdown-item>
|
<el-dropdown-item command="refreshAuth">刷新权限</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@ -281,7 +283,7 @@ import { useRoute, useRouter } from "vue-router";
|
|||||||
import BreadcrumbNav from "../../components/BreadcrumbNav.vue";
|
import BreadcrumbNav from "../../components/BreadcrumbNav.vue";
|
||||||
import GlobalSearchLauncher from "../../components/GlobalSearchLauncher.vue";
|
import GlobalSearchLauncher from "../../components/GlobalSearchLauncher.vue";
|
||||||
import InAppNotificationDetailDialog from "../../components/InAppNotificationDetailDialog.vue";
|
import InAppNotificationDetailDialog from "../../components/InAppNotificationDetailDialog.vue";
|
||||||
import { fetchInAppNotifications, markAllInAppNotificationsRead, markInAppNotificationRead, logoutAllAuth, switchTenant } from "../../api/modules";
|
import { fetchInAppNotifications, markAllInAppNotificationsRead, markInAppNotificationRead, logoutAllAuth, switchTenant, refreshAuth } from "../../api/modules";
|
||||||
import { PERMS } from "../../constants/permissions";
|
import { PERMS } from "../../constants/permissions";
|
||||||
import { useAppearanceStore } from "../../stores/appearance";
|
import { useAppearanceStore } from "../../stores/appearance";
|
||||||
import { useAuthStore } from "../../stores/auth";
|
import { useAuthStore } from "../../stores/auth";
|
||||||
@ -554,6 +556,19 @@ const handleUserCommand = async (command: string) => {
|
|||||||
await router.push(profileRoute.value);
|
await router.push(profileRoute.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (command === "refreshAuth") {
|
||||||
|
try {
|
||||||
|
const resp = await refreshAuth();
|
||||||
|
withSuppressedAuthRefresh(() => {
|
||||||
|
authStore.saveAuthPayload(resp?.data || null);
|
||||||
|
});
|
||||||
|
await refreshLayoutState();
|
||||||
|
ElMessage.success("权限刷新成功");
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore, handled by global interceptor
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (command === "logout") {
|
if (command === "logout") {
|
||||||
await logoutAll();
|
await logoutAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="300">
|
<el-table-column label="操作" width="380" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button v-if="canManage" size="small" @click="handleCopy(row.id)">复制</el-button>
|
<el-button v-if="canManage" size="small" @click="handleCopy(row.id)">复制</el-button>
|
||||||
<el-button v-if="canManage" size="small" @click="handleEdit(row)">编辑节点</el-button>
|
<el-button v-if="canManage" size="small" @click="handleEdit(row)">编辑节点</el-button>
|
||||||
|
|||||||
@ -1266,7 +1266,7 @@ const resetMaterialViews = () => {
|
|||||||
meetingEffect: "",
|
meetingEffect: "",
|
||||||
};
|
};
|
||||||
docView.value = { agendas: [], signInName: "", signInOssKey: "", themePhotoName: "", themePhotoOssKey: "", invitations: [] };
|
docView.value = { agendas: [], signInName: "", signInOssKey: "", themePhotoName: "", themePhotoOssKey: "", invitations: [] };
|
||||||
photoView.value = { photos: [], summary: "" };
|
photoView.value = { photos: [], summary: "", expertSummaries: [] };
|
||||||
laborView.value = { details: [] };
|
laborView.value = { details: [] };
|
||||||
invoiceView.value = { invoices: [], summary: "" };
|
invoiceView.value = { invoices: [], summary: "" };
|
||||||
expertProfileView.value = { fileName: "", ossKey: "" };
|
expertProfileView.value = { fileName: "", ossKey: "" };
|
||||||
@ -2084,6 +2084,7 @@ const loadMaterial = async () => {
|
|||||||
originIndex: index + 1,
|
originIndex: index + 1,
|
||||||
})),
|
})),
|
||||||
summary: onsitePhoto.summary || "",
|
summary: onsitePhoto.summary || "",
|
||||||
|
expertSummaries: Array.isArray(onsitePhoto.expertSummaries) ? onsitePhoto.expertSummaries : [],
|
||||||
};
|
};
|
||||||
laborView.value = {
|
laborView.value = {
|
||||||
details: (Array.isArray(laborProtocol.details) ? laborProtocol.details : []).map((x: any) => {
|
details: (Array.isArray(laborProtocol.details) ? laborProtocol.details : []).map((x: any) => {
|
||||||
|
|||||||
@ -198,13 +198,13 @@
|
|||||||
|
|
||||||
<el-dialog v-model="importVisible" title="批量导入专家" :width="DIALOG_WIDTH.lg" destroy-on-close>
|
<el-dialog v-model="importVisible" title="批量导入专家" :width="DIALOG_WIDTH.lg" destroy-on-close>
|
||||||
<div class="text-secondary mb-sm">
|
<div class="text-secondary mb-sm">
|
||||||
支持 `.csv` 或制表符文本。模板字段顺序为:
|
支持 `.xlsx`, `.xls`, `.csv` 或制表符文本。模板字段顺序为:
|
||||||
`expertName, idNo, phone, titleCode, hospitalCode`
|
`专家姓名, 身份证号, 手机号, 职称, 医院`
|
||||||
</div>
|
</div>
|
||||||
<el-upload
|
<el-upload
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
accept=".csv,.txt"
|
accept=".xlsx,.xls,.csv,.txt"
|
||||||
:on-change="handleImportFileChange"
|
:on-change="handleImportFileChange"
|
||||||
>
|
>
|
||||||
<el-button>选择文件</el-button>
|
<el-button>选择文件</el-button>
|
||||||
@ -221,11 +221,11 @@
|
|||||||
<div v-if="importPreview.length" class="mt-md">
|
<div v-if="importPreview.length" class="mt-md">
|
||||||
<div class="mb-sm">预览前 {{ importPreview.length }} 行:</div>
|
<div class="mb-sm">预览前 {{ importPreview.length }} 行:</div>
|
||||||
<el-table :data="importPreview" size="small" max-height="240">
|
<el-table :data="importPreview" size="small" max-height="240">
|
||||||
<el-table-column prop="expertName" label="姓名" />
|
<el-table-column prop="expertName" label="专家姓名" />
|
||||||
<el-table-column prop="idNo" label="身份证" />
|
<el-table-column prop="idNo" label="身份证号" />
|
||||||
<el-table-column prop="phone" label="手机号" />
|
<el-table-column prop="phone" label="手机号" />
|
||||||
<el-table-column prop="titleCode" label="职称编码" />
|
<el-table-column prop="title" label="职称" />
|
||||||
<el-table-column prop="hospitalCode" label="医院编码" />
|
<el-table-column prop="organization" label="医院" />
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="lastImportResult" class="mt-md">
|
<div v-if="lastImportResult" class="mt-md">
|
||||||
@ -298,7 +298,7 @@ import {
|
|||||||
} from "../../api/modules";
|
} from "../../api/modules";
|
||||||
import { PERMS } from "../../constants/permissions";
|
import { PERMS } from "../../constants/permissions";
|
||||||
import { useAuthStore } from "../../stores/auth";
|
import { useAuthStore } from "../../stores/auth";
|
||||||
import { downloadCsvRows, downloadCsvTemplate, findMissingHeaders, parseDelimitedText, readTextFile } from "../../utils/batchImport";
|
import { downloadCsvRows, downloadCsvTemplate, findMissingHeaders, parseDelimitedText, readTextFile, readXlsxFile } from "../../utils/batchImport";
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const isPlatform = computed(() => authStore.scope === "PLATFORM");
|
const isPlatform = computed(() => authStore.scope === "PLATFORM");
|
||||||
@ -320,14 +320,14 @@ const importText = ref("");
|
|||||||
const importVisible = ref(false);
|
const importVisible = ref(false);
|
||||||
const importLoading = ref(false);
|
const importLoading = ref(false);
|
||||||
const importFileName = ref("");
|
const importFileName = ref("");
|
||||||
const importPreview = ref<Array<{ expertName: string; idNo: string; phone: string; titleCode: string; hospitalCode: string }>>([]);
|
const importPreview = ref<Array<{ expertName: string; idNo: string; phone: string; title: string; organization: string }>>([]);
|
||||||
const lastImportResult = ref<any | null>(null);
|
const lastImportResult = ref<any | null>(null);
|
||||||
const lastImportedExperts = ref<Array<{
|
const lastImportedExperts = ref<Array<{
|
||||||
expertName: string;
|
expertName: string;
|
||||||
idNo: string;
|
idNo: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
titleCode: string;
|
title: string;
|
||||||
hospitalCode: string;
|
organization: string;
|
||||||
}> | null>(null);
|
}> | null>(null);
|
||||||
const titleOptions = ref<any[]>([]);
|
const titleOptions = ref<any[]>([]);
|
||||||
const hospitalOptions = ref<any[]>([]);
|
const hospitalOptions = ref<any[]>([]);
|
||||||
@ -456,13 +456,13 @@ const openImportDialog = () => {
|
|||||||
|
|
||||||
const downloadExpertTemplate = () => {
|
const downloadExpertTemplate = () => {
|
||||||
downloadCsvTemplate("expert-import-template.csv", [
|
downloadCsvTemplate("expert-import-template.csv", [
|
||||||
"expertName",
|
"专家姓名",
|
||||||
"idNo",
|
"身份证号",
|
||||||
"phone",
|
"手机号",
|
||||||
"titleCode",
|
"职称",
|
||||||
"hospitalCode",
|
"医院",
|
||||||
], [
|
], [
|
||||||
["张教授", "110101199001011234", "13800138000", "CHIEF_PHYSICIAN", "BEIJING_HOSPITAL"],
|
["张教授", "110101199001011234", "13800138000", "主任医师", "北京医院"],
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -472,7 +472,12 @@ const handleImportFileChange = async (uploadFile: any) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
importFileName.value = file.name;
|
importFileName.value = file.name;
|
||||||
importText.value = await readTextFile(file);
|
const nameLower = file.name.toLowerCase();
|
||||||
|
if (nameLower.endsWith(".xlsx") || nameLower.endsWith(".xls")) {
|
||||||
|
importText.value = await readXlsxFile(file);
|
||||||
|
} else {
|
||||||
|
importText.value = await readTextFile(file);
|
||||||
|
}
|
||||||
syncImportPreview();
|
syncImportPreview();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -482,7 +487,7 @@ const parseExpertImportRows = (text: string) => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [header, ...dataRows] = rows;
|
const [header, ...dataRows] = rows;
|
||||||
const missingHeaders = findMissingHeaders(header, ["expertName", "idNo", "phone"]);
|
const missingHeaders = findMissingHeaders(header, ["专家姓名", "身份证号", "手机号"]);
|
||||||
if (missingHeaders.length) {
|
if (missingHeaders.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -493,19 +498,19 @@ const parseExpertImportRows = (text: string) => {
|
|||||||
return dataRows
|
return dataRows
|
||||||
.filter((row) => row.some((cell) => String(cell || "").trim().length > 0))
|
.filter((row) => row.some((cell) => String(cell || "").trim().length > 0))
|
||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
expertName: row[headerIndex.get("expertName") ?? 0] || "",
|
expertName: row[headerIndex.get("专家姓名") ?? 0] || "",
|
||||||
idNo: row[headerIndex.get("idNo") ?? 1] || "",
|
idNo: row[headerIndex.get("身份证号") ?? 1] || "",
|
||||||
phone: row[headerIndex.get("phone") ?? 2] || "",
|
phone: row[headerIndex.get("手机号") ?? 2] || "",
|
||||||
titleCode: row[headerIndex.get("titleCode") ?? 3] || "",
|
title: row[headerIndex.get("职称") ?? 3] || "",
|
||||||
hospitalCode: row[headerIndex.get("hospitalCode") ?? 4] || "",
|
organization: row[headerIndex.get("医院") ?? 4] || "",
|
||||||
}))
|
}))
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
expertName: item.expertName.trim(),
|
expertName: item.expertName.trim(),
|
||||||
idNo: item.idNo.trim(),
|
idNo: item.idNo.trim(),
|
||||||
phone: item.phone.trim(),
|
phone: item.phone.trim(),
|
||||||
titleCode: item.titleCode.trim(),
|
title: item.title.trim(),
|
||||||
hospitalCode: item.hospitalCode.trim(),
|
organization: item.organization.trim(),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -516,10 +521,10 @@ const syncImportPreview = () => {
|
|||||||
const validateExpertImportHeaders = (text: string) => {
|
const validateExpertImportHeaders = (text: string) => {
|
||||||
const rows = parseDelimitedText(text);
|
const rows = parseDelimitedText(text);
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
return { ok: false, missingHeaders: ["expertName", "idNo", "phone"] };
|
return { ok: false, missingHeaders: ["专家姓名", "身份证号", "手机号"] };
|
||||||
}
|
}
|
||||||
const [header] = rows;
|
const [header] = rows;
|
||||||
const missingHeaders = findMissingHeaders(header, ["expertName", "idNo", "phone"]);
|
const missingHeaders = findMissingHeaders(header, ["专家姓名", "身份证号", "手机号"]);
|
||||||
return { ok: missingHeaders.length === 0, missingHeaders };
|
return { ok: missingHeaders.length === 0, missingHeaders };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -944,11 +949,11 @@ const downloadExpertErrorSnapshot = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rows: string[][] = [[
|
const rows: string[][] = [[
|
||||||
"expertName",
|
"专家姓名",
|
||||||
"idNo",
|
"身份证号",
|
||||||
"phone",
|
"手机号",
|
||||||
"titleCode",
|
"职称",
|
||||||
"hospitalCode",
|
"医院",
|
||||||
"__error",
|
"__error",
|
||||||
]];
|
]];
|
||||||
errors.forEach((error: any) => {
|
errors.forEach((error: any) => {
|
||||||
@ -961,9 +966,9 @@ const downloadExpertErrorSnapshot = () => {
|
|||||||
item.expertName || "",
|
item.expertName || "",
|
||||||
item.idNo || "",
|
item.idNo || "",
|
||||||
item.phone || "",
|
item.phone || "",
|
||||||
item.titleCode || "",
|
item.title || "",
|
||||||
item.hospitalCode || "",
|
item.organization || "",
|
||||||
String(error?.message || ""),
|
error?.message || "未知错误",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
downloadCsvRows("expert-import-error-snapshot.csv", rows);
|
downloadCsvRows("expert-import-error-snapshot.csv", rows);
|
||||||
|
|||||||
@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
<el-table :data="rows" class="mt-md" empty-text="暂无数据">
|
<el-table :data="rows" class="mt-md" empty-text="暂无数据">
|
||||||
<el-table-column prop="taskCode" label="任务编码" width="180" :formatter="taskCodeFormatter" />
|
<el-table-column prop="taskCode" label="任务编码" width="180" :formatter="taskCodeFormatter" />
|
||||||
<el-table-column prop="bizType" label="业务类型" width="140" :formatter="bizTypeFormatter" />
|
<!-- <el-table-column prop="bizType" label="业务类型" width="140" :formatter="bizTypeFormatter" /> -->
|
||||||
<el-table-column prop="bizId" label="业务ID" width="120" />
|
<!-- <el-table-column prop="bizId" label="业务ID" width="120" /> -->
|
||||||
<el-table-column prop="fileName" label="文件名" width="180" />
|
<el-table-column prop="fileName" label="文件名" width="350" />
|
||||||
<el-table-column prop="fileOssKey" label="文件OSS" />
|
<!-- <el-table-column prop="fileOssKey" label="文件OSS" /> -->
|
||||||
<el-table-column prop="status" label="状态" width="100" :formatter="statusFormatter" />
|
<el-table-column prop="status" label="状态" width="100" :formatter="statusFormatter" />
|
||||||
<el-table-column prop="retryCount" label="重试" width="80" />
|
<el-table-column prop="retryCount" label="重试" width="80" />
|
||||||
<el-table-column prop="downloadCount" label="下载次数" width="90" />
|
<el-table-column prop="downloadCount" label="下载次数" width="90" />
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
:rows="rows"
|
:rows="rows"
|
||||||
:summary-task-map="summaryTaskMap"
|
:summary-task-map="summaryTaskMap"
|
||||||
:can-create="canCreate"
|
:can-create="canCreate"
|
||||||
|
:can-edit="canEdit"
|
||||||
:can-submit="canSubmit"
|
:can-submit="canSubmit"
|
||||||
:can-submit-meeting="canSubmitMeeting"
|
:can-submit-meeting="canSubmitMeeting"
|
||||||
:can-withdraw="canWithdraw"
|
:can-withdraw="canWithdraw"
|
||||||
@ -392,6 +393,7 @@ const meetingPageSize = ref(20);
|
|||||||
const meetingTotalCount = ref(0);
|
const meetingTotalCount = ref(0);
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const canCreate = computed(() => authStore.hasPermission(PERMS.meeting.create));
|
const canCreate = computed(() => authStore.hasPermission(PERMS.meeting.create));
|
||||||
|
const canEdit = computed(() => authStore.hasPermission(PERMS.meeting.update));
|
||||||
const canSubmit = computed(() => authStore.hasPermission(PERMS.meeting.submit));
|
const canSubmit = computed(() => authStore.hasPermission(PERMS.meeting.submit));
|
||||||
const canWithdraw = computed(() => authStore.hasPermission(PERMS.meeting.withdraw));
|
const canWithdraw = computed(() => authStore.hasPermission(PERMS.meeting.withdraw));
|
||||||
const canMaterialSave = computed(() => authStore.hasPermission(PERMS.meeting.materialSave));
|
const canMaterialSave = computed(() => authStore.hasPermission(PERMS.meeting.materialSave));
|
||||||
@ -1080,6 +1082,7 @@ const materialBudgetBase = ref({
|
|||||||
laborTotalCent: 0,
|
laborTotalCent: 0,
|
||||||
invoiceTotalCent: 0,
|
invoiceTotalCent: 0,
|
||||||
meetingInvoiceTotalCent: 0,
|
meetingInvoiceTotalCent: 0,
|
||||||
|
cateringTotalCent: 0,
|
||||||
ready: false,
|
ready: false,
|
||||||
});
|
});
|
||||||
const materialModuleLoading = ref(false);
|
const materialModuleLoading = ref(false);
|
||||||
@ -2894,7 +2897,7 @@ const handleGenerateSummary = async (meetingId: number) => {
|
|||||||
});
|
});
|
||||||
scheduleSummaryTaskPolling(meetingId, taskId);
|
scheduleSummaryTaskPolling(meetingId, taskId);
|
||||||
}
|
}
|
||||||
ElMessage.success(taskId > 0 ? `会议总结任务已创建:${taskId},当前状态为待处理` : "会议总结任务已创建");
|
ElMessage.success("会议总结生成任务已提交,系统正在后台处理,请稍后可在导出任务中心查看");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckSummaryStatus = async (meetingId: number) => {
|
const handleCheckSummaryStatus = async (meetingId: number) => {
|
||||||
@ -5187,14 +5190,15 @@ const onMeetingInvoiceSectionFileChange = async (
|
|||||||
}
|
}
|
||||||
await putToSignedUrl(uploadUrl, file, signResp?.data?.contentType || file.type);
|
await putToSignedUrl(uploadUrl, file, signResp?.data?.contentType || file.type);
|
||||||
const section = ensureMeetingInvoiceSection(sectionCode);
|
const section = ensureMeetingInvoiceSection(sectionCode);
|
||||||
|
const shouldRunOcr = fieldKey === "invoiceFile" || sectionCode === "OTHER";
|
||||||
section.files[fieldKey].push({
|
section.files[fieldKey].push({
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
ossKey: objectKey,
|
ossKey: objectKey,
|
||||||
ocr: fieldKey === "invoiceFile" ? { status: "idle" } : undefined,
|
ocr: shouldRunOcr ? { status: "idle" } : undefined,
|
||||||
});
|
});
|
||||||
await cacheDocPreviewUrl(objectKey);
|
await cacheDocPreviewUrl(objectKey);
|
||||||
ElMessage.success("会议发票附件上传成功");
|
ElMessage.success("会议发票附件上传成功");
|
||||||
if (fieldKey === "invoiceFile") {
|
if (shouldRunOcr) {
|
||||||
await runMeetingInvoiceOcr(sectionCode, fieldKey);
|
await runMeetingInvoiceOcr(sectionCode, fieldKey);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -5615,6 +5619,7 @@ const resetMaterialForms = () => {
|
|||||||
laborTotalCent: 0,
|
laborTotalCent: 0,
|
||||||
invoiceTotalCent: 0,
|
invoiceTotalCent: 0,
|
||||||
meetingInvoiceTotalCent: 0,
|
meetingInvoiceTotalCent: 0,
|
||||||
|
cateringTotalCent: 0,
|
||||||
ready: false,
|
ready: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -6157,127 +6162,7 @@ const loadMaterialModule = async (
|
|||||||
// ignore when current material does not exist
|
// ignore when current material does not exist
|
||||||
}
|
}
|
||||||
if (moduleCode === "EXPERT_LIST") {
|
if (moduleCode === "EXPERT_LIST") {
|
||||||
void (async () => {
|
// 移除预加载逻辑,改为点击预览时按需加载 (handleViewOss 中已实现)
|
||||||
const tasks: Array<Promise<void>> = [];
|
|
||||||
Object.keys(photoByExpert.value).forEach((id) => {
|
|
||||||
const expertId = Number(id || 0);
|
|
||||||
(photoByExpert.value[expertId] || []).forEach((item) => {
|
|
||||||
const nameOrKey = item.name || item.ossKey || "";
|
|
||||||
if (!item.ossKey || !isImageFile(nameOrKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tasks.push(
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const resp = await fetchFilePresignDownload({ objectKey: item.ossKey });
|
|
||||||
if (typeof requestId === "number" && !isMaterialLoadActive(requestId, meetingId, moduleCode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
item.previewUrl = resp?.data?.signedUrl || "";
|
|
||||||
} catch (_e) {
|
|
||||||
if (typeof requestId === "number" && !isMaterialLoadActive(requestId, meetingId, moduleCode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
item.previewUrl = "";
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (tasks.length > 0) {
|
|
||||||
await Promise.allSettled(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(laborByExpert.value).forEach((id) => {
|
|
||||||
const expertId = Number(id || 0);
|
|
||||||
const row = laborByExpert.value[expertId];
|
|
||||||
if (!row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const invoiceFiles = Array.isArray(row.invoiceFiles) ? row.invoiceFiles : [];
|
|
||||||
if (invoiceFiles.length === 0 && row.invoiceOssKey) {
|
|
||||||
invoiceFiles.push({
|
|
||||||
fileName: String(row.invoiceFileName || "").trim(),
|
|
||||||
ossKey: String(row.invoiceOssKey || "").trim(),
|
|
||||||
previewUrl: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (invoiceFiles.length === 0) {
|
|
||||||
row.invoicePreviewUrl = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
invoiceFiles.forEach((file, index) => {
|
|
||||||
const key = String(file?.ossKey || "").trim();
|
|
||||||
const nameOrKey = String(file?.fileName || key || "").trim();
|
|
||||||
if (!key || !isImageFile(nameOrKey)) {
|
|
||||||
file.previewUrl = "";
|
|
||||||
if (index === 0) {
|
|
||||||
row.invoicePreviewUrl = "";
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tasks.push(
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const resp = await fetchFilePresignDownload({ objectKey: key });
|
|
||||||
if (typeof requestId === "number" && !isMaterialLoadActive(requestId, meetingId, moduleCode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
file.previewUrl = resp?.data?.signedUrl || "";
|
|
||||||
if (index === 0) {
|
|
||||||
row.invoicePreviewUrl = file.previewUrl;
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
if (typeof requestId === "number" && !isMaterialLoadActive(requestId, meetingId, moduleCode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
file.previewUrl = "";
|
|
||||||
if (index === 0) {
|
|
||||||
row.invoicePreviewUrl = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (tasks.length > 0) {
|
|
||||||
await Promise.allSettled(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
const protocolPreviewTasks: Array<Promise<void>> = [];
|
|
||||||
Object.keys(laborByExpert.value).forEach((id) => {
|
|
||||||
const expertId = Number(id || 0);
|
|
||||||
const row = laborByExpert.value[expertId];
|
|
||||||
if (!row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const key = String(row.protocolOssKey || "").trim();
|
|
||||||
const nameOrKey = String(row.protocolFileName || key || "").trim();
|
|
||||||
if (!key || !isImageFile(nameOrKey)) {
|
|
||||||
row.protocolPreviewUrl = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
protocolPreviewTasks.push(
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const resp = await fetchFilePresignDownload({ objectKey: key });
|
|
||||||
if (typeof requestId === "number" && !isMaterialLoadActive(requestId, meetingId, moduleCode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
row.protocolPreviewUrl = resp?.data?.signedUrl || "";
|
|
||||||
} catch (_e) {
|
|
||||||
if (typeof requestId === "number" && !isMaterialLoadActive(requestId, meetingId, moduleCode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
row.protocolPreviewUrl = "";
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (protocolPreviewTasks.length > 0) {
|
|
||||||
await Promise.allSettled(protocolPreviewTasks);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
if (moduleCode === "EXPERT_PROFILE") {
|
if (moduleCode === "EXPERT_PROFILE") {
|
||||||
const profileKey = String(expertProfileForm.value.ossKey || "").trim();
|
const profileKey = String(expertProfileForm.value.ossKey || "").trim();
|
||||||
@ -6447,6 +6332,16 @@ const resolveSelectedMeetingCateringRatio = () => {
|
|||||||
return Math.max(0, Number(currentRow?.cateringRatio || 0));
|
return Math.max(0, Number(currentRow?.cateringRatio || 0));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolveSelectedMeetingLaborRatio = () => {
|
||||||
|
const meetingId = Number(selectedMeetingId.value || 0);
|
||||||
|
const currentDetailId = Number(currentMeetingDetail.value?.id || 0);
|
||||||
|
if (meetingId > 0 && currentDetailId === meetingId) {
|
||||||
|
return Math.max(0, Number(currentMeetingDetail.value?.laborRatio || 0));
|
||||||
|
}
|
||||||
|
const currentRow = rows.value.find((item: any) => Number(item?.id || 0) === meetingId);
|
||||||
|
return Math.max(0, Number(currentRow?.laborRatio || 0));
|
||||||
|
};
|
||||||
|
|
||||||
const validateMeetingInvoiceRequired = () => {
|
const validateMeetingInvoiceRequired = () => {
|
||||||
if (selectedModuleCode.value !== "MEETING_INVOICE") {
|
if (selectedModuleCode.value !== "MEETING_INVOICE") {
|
||||||
return true;
|
return true;
|
||||||
@ -6552,6 +6447,22 @@ const calcMeetingInvoiceUsageFromContentJson = (contentJson: string) => {
|
|||||||
return calcMeetingInvoiceUsageFromForm(normalized);
|
return calcMeetingInvoiceUsageFromForm(normalized);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calcMeetingCateringUsageFromForm = (formState: MeetingInvoiceFormState) => {
|
||||||
|
const sectionState = formState.sections?.["CATERING_DETAIL"];
|
||||||
|
if (!sectionState) return 0;
|
||||||
|
const amountYuan = Number(sectionState.totalAmountYuan);
|
||||||
|
if (!Number.isFinite(amountYuan) || amountYuan < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return toCent(amountYuan);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcMeetingCateringUsageFromContentJson = (contentJson: string) => {
|
||||||
|
const parsed = safeParse(contentJson || "{}");
|
||||||
|
const normalized = normalizeMeetingInvoiceForm(parsed);
|
||||||
|
return calcMeetingCateringUsageFromForm(normalized);
|
||||||
|
};
|
||||||
|
|
||||||
const materialBudgetSummary = computed(() => {
|
const materialBudgetSummary = computed(() => {
|
||||||
const base = materialBudgetBase.value;
|
const base = materialBudgetBase.value;
|
||||||
if (!base.ready) {
|
if (!base.ready) {
|
||||||
@ -6563,12 +6474,15 @@ const materialBudgetSummary = computed(() => {
|
|||||||
usedTotalCent: 0,
|
usedTotalCent: 0,
|
||||||
remainCent: 0,
|
remainCent: 0,
|
||||||
overCent: 0,
|
overCent: 0,
|
||||||
|
remainLaborCent: 0,
|
||||||
|
remainCateringCent: 0,
|
||||||
ready: false,
|
ready: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let laborTotalCent = base.laborTotalCent;
|
let laborTotalCent = base.laborTotalCent;
|
||||||
let invoiceTotalCent = base.invoiceTotalCent;
|
let invoiceTotalCent = base.invoiceTotalCent;
|
||||||
let meetingInvoiceTotalCent = base.meetingInvoiceTotalCent;
|
let meetingInvoiceTotalCent = base.meetingInvoiceTotalCent;
|
||||||
|
let cateringTotalCent = base.cateringTotalCent || 0;
|
||||||
if (selectedModuleCode.value === "EXPERT_LIST") {
|
if (selectedModuleCode.value === "EXPERT_LIST") {
|
||||||
const currentExpertUsage = calcExpertUsageFromCurrentForm();
|
const currentExpertUsage = calcExpertUsageFromCurrentForm();
|
||||||
laborTotalCent = currentExpertUsage.laborTotalCent;
|
laborTotalCent = currentExpertUsage.laborTotalCent;
|
||||||
@ -6576,10 +6490,20 @@ const materialBudgetSummary = computed(() => {
|
|||||||
}
|
}
|
||||||
if (selectedModuleCode.value === "MEETING_INVOICE") {
|
if (selectedModuleCode.value === "MEETING_INVOICE") {
|
||||||
meetingInvoiceTotalCent = calcMeetingInvoiceUsageFromForm(meetingInvoiceForm.value);
|
meetingInvoiceTotalCent = calcMeetingInvoiceUsageFromForm(meetingInvoiceForm.value);
|
||||||
|
cateringTotalCent = calcMeetingCateringUsageFromForm(meetingInvoiceForm.value);
|
||||||
}
|
}
|
||||||
const usedTotalCent = laborTotalCent + invoiceTotalCent + meetingInvoiceTotalCent;
|
const usedTotalCent = laborTotalCent + invoiceTotalCent + meetingInvoiceTotalCent;
|
||||||
const remainCent = Math.max(0, base.budgetCent - usedTotalCent);
|
const remainCent = Math.max(0, base.budgetCent - usedTotalCent);
|
||||||
const overCent = Math.max(0, usedTotalCent - base.budgetCent);
|
const overCent = Math.max(0, usedTotalCent - base.budgetCent);
|
||||||
|
|
||||||
|
const laborRatio = resolveSelectedMeetingLaborRatio();
|
||||||
|
const maxLaborCent = Math.floor(base.budgetCent * laborRatio);
|
||||||
|
const remainLaborCent = maxLaborCent - laborTotalCent;
|
||||||
|
|
||||||
|
const cateringRatio = resolveSelectedMeetingCateringRatio();
|
||||||
|
const maxCateringCent = Math.floor(base.budgetCent * cateringRatio);
|
||||||
|
const remainCateringCent = maxCateringCent - cateringTotalCent;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
budgetCent: base.budgetCent,
|
budgetCent: base.budgetCent,
|
||||||
laborTotalCent,
|
laborTotalCent,
|
||||||
@ -6588,6 +6512,8 @@ const materialBudgetSummary = computed(() => {
|
|||||||
usedTotalCent,
|
usedTotalCent,
|
||||||
remainCent,
|
remainCent,
|
||||||
overCent,
|
overCent,
|
||||||
|
remainLaborCent,
|
||||||
|
remainCateringCent,
|
||||||
ready: true,
|
ready: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -6633,6 +6559,7 @@ const refreshMaterialBudgetBase = async (meetingIdInput?: number, requestId?: nu
|
|||||||
laborTotalCent: 0,
|
laborTotalCent: 0,
|
||||||
invoiceTotalCent: 0,
|
invoiceTotalCent: 0,
|
||||||
meetingInvoiceTotalCent: 0,
|
meetingInvoiceTotalCent: 0,
|
||||||
|
cateringTotalCent: 0,
|
||||||
ready: false,
|
ready: false,
|
||||||
};
|
};
|
||||||
materialBudgetLoading.value = false;
|
materialBudgetLoading.value = false;
|
||||||
@ -6650,11 +6577,13 @@ const refreshMaterialBudgetBase = async (meetingIdInput?: number, requestId?: nu
|
|||||||
}
|
}
|
||||||
const expertUsage = calcExpertUsageFromContentJson(expertContentJson);
|
const expertUsage = calcExpertUsageFromContentJson(expertContentJson);
|
||||||
const meetingInvoiceTotalCent = calcMeetingInvoiceUsageFromContentJson(meetingInvoiceContentJson);
|
const meetingInvoiceTotalCent = calcMeetingInvoiceUsageFromContentJson(meetingInvoiceContentJson);
|
||||||
|
const cateringTotalCent = calcMeetingCateringUsageFromContentJson(meetingInvoiceContentJson);
|
||||||
materialBudgetBase.value = {
|
materialBudgetBase.value = {
|
||||||
budgetCent,
|
budgetCent,
|
||||||
laborTotalCent: expertUsage.laborTotalCent,
|
laborTotalCent: expertUsage.laborTotalCent,
|
||||||
invoiceTotalCent: expertUsage.invoiceTotalCent,
|
invoiceTotalCent: expertUsage.invoiceTotalCent,
|
||||||
meetingInvoiceTotalCent,
|
meetingInvoiceTotalCent,
|
||||||
|
cateringTotalCent,
|
||||||
ready: true,
|
ready: true,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
@ -6670,11 +6599,19 @@ const validateMeetingBudgetLimit = async () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const summary = materialBudgetSummary.value;
|
const summary = materialBudgetSummary.value;
|
||||||
if (summary.overCent <= 0) {
|
if (summary.overCent > 0) {
|
||||||
return true;
|
ElMessage.warning(`预算校验不通过:当前会议预算${toYuan(summary.budgetCent)}元,已用${toYuan(summary.usedTotalCent)}元,超预算${toYuan(summary.overCent)}元`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
ElMessage.warning(`预算校验不通过:当前会议预算${toYuan(summary.budgetCent)}元,已用${toYuan(summary.usedTotalCent)}元,超预算${toYuan(summary.overCent)}元`);
|
if (summary.remainLaborCent < 0) {
|
||||||
return false;
|
ElMessage.warning(`预算校验不通过:劳务费已超出占比允许的最大额度,超额${toYuan(-summary.remainLaborCent)}元`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (summary.remainCateringCent < 0) {
|
||||||
|
ElMessage.warning(`预算校验不通过:餐费已超出占比允许的最大额度,超额${toYuan(-summary.remainCateringCent)}元`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveMaterial = async () => {
|
const handleSaveMaterial = async () => {
|
||||||
|
|||||||
@ -94,6 +94,7 @@
|
|||||||
<el-option label="审核退回" value="AUDIT_RETURNED" />
|
<el-option label="审核退回" value="AUDIT_RETURNED" />
|
||||||
<el-option label="财务已确认" value="FINANCE_CONFIRMED" />
|
<el-option label="财务已确认" value="FINANCE_CONFIRMED" />
|
||||||
<el-option label="用户创建" value="USER_CREATED" />
|
<el-option label="用户创建" value="USER_CREATED" />
|
||||||
|
<el-option label="密码重置" value="USER_PASSWORD_RESET" />
|
||||||
<el-option label="审核通过(旧)" value="AUDIT_APPROVED" />
|
<el-option label="审核通过(旧)" value="AUDIT_APPROVED" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -153,6 +154,7 @@
|
|||||||
<el-option label="审核退回" value="AUDIT_RETURNED" />
|
<el-option label="审核退回" value="AUDIT_RETURNED" />
|
||||||
<el-option label="财务已确认" value="FINANCE_CONFIRMED" />
|
<el-option label="财务已确认" value="FINANCE_CONFIRMED" />
|
||||||
<el-option label="用户创建" value="USER_CREATED" />
|
<el-option label="用户创建" value="USER_CREATED" />
|
||||||
|
<el-option label="密码重置" value="USER_PASSWORD_RESET" />
|
||||||
<el-option label="审核通过(旧)" value="AUDIT_APPROVED" />
|
<el-option label="审核通过(旧)" value="AUDIT_APPROVED" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -217,6 +219,7 @@
|
|||||||
<el-option label="审核退回" value="AUDIT_RETURNED" />
|
<el-option label="审核退回" value="AUDIT_RETURNED" />
|
||||||
<el-option label="财务已确认" value="FINANCE_CONFIRMED" />
|
<el-option label="财务已确认" value="FINANCE_CONFIRMED" />
|
||||||
<el-option label="用户创建" value="USER_CREATED" />
|
<el-option label="用户创建" value="USER_CREATED" />
|
||||||
|
<el-option label="密码重置" value="USER_PASSWORD_RESET" />
|
||||||
<el-option label="审核通过(旧)" value="AUDIT_APPROVED" />
|
<el-option label="审核通过(旧)" value="AUDIT_APPROVED" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -350,9 +353,10 @@ const dispatchFormRules = ref<FormRules>({
|
|||||||
const statusFormatter = (_row: any, _column: any, value: unknown) => toZhStatus(value);
|
const statusFormatter = (_row: any, _column: any, value: unknown) => toZhStatus(value);
|
||||||
const normalizeUpper = (value: unknown) => String(value || "").trim().toUpperCase();
|
const normalizeUpper = (value: unknown) => String(value || "").trim().toUpperCase();
|
||||||
const isUserCreatedEvent = (value: unknown) => String(value || "").trim().toUpperCase() === "USER_CREATED";
|
const isUserCreatedEvent = (value: unknown) => String(value || "").trim().toUpperCase() === "USER_CREATED";
|
||||||
|
const isUserPasswordResetEvent = (value: unknown) => String(value || "").trim().toUpperCase() === "USER_PASSWORD_RESET";
|
||||||
const resolveReceiverTypeByEvent = (eventCode: unknown, currentReceiverType: unknown) => {
|
const resolveReceiverTypeByEvent = (eventCode: unknown, currentReceiverType: unknown) => {
|
||||||
const normalizedEventCode = normalizeUpper(eventCode);
|
const normalizedEventCode = normalizeUpper(eventCode);
|
||||||
if (isUserCreatedEvent(eventCode)) {
|
if (isUserCreatedEvent(eventCode) || isUserPasswordResetEvent(eventCode)) {
|
||||||
return "TARGET_USER";
|
return "TARGET_USER";
|
||||||
}
|
}
|
||||||
if (normalizedEventCode === "AUDIT_TASK_ASSIGNED") {
|
if (normalizedEventCode === "AUDIT_TASK_ASSIGNED") {
|
||||||
@ -370,7 +374,7 @@ const resolveReceiverTypeByEvent = (eventCode: unknown, currentReceiverType: unk
|
|||||||
: String(currentReceiverType || "SUBMITTER");
|
: String(currentReceiverType || "SUBMITTER");
|
||||||
};
|
};
|
||||||
const resolveBizTypeByEvent = (eventCode: unknown, currentBizType: unknown) => {
|
const resolveBizTypeByEvent = (eventCode: unknown, currentBizType: unknown) => {
|
||||||
if (isUserCreatedEvent(eventCode)) {
|
if (isUserCreatedEvent(eventCode) || isUserPasswordResetEvent(eventCode)) {
|
||||||
return "USER";
|
return "USER";
|
||||||
}
|
}
|
||||||
return String(currentBizType || "").trim().toUpperCase() === "USER"
|
return String(currentBizType || "").trim().toUpperCase() === "USER"
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="card-title">SMTP / 邮件发送配置</div>
|
<div class="card-title">SMTP / 邮件发送配置</div>
|
||||||
<div class="card-subtitle">配置完成后,通知邮件和租户管理员账号邮件都会优先读取这里的网关参数。</div>
|
<div class="card-subtitle">配置完成后,通知邮件会优先读取这里的网关参数。</div>
|
||||||
</div>
|
</div>
|
||||||
<el-tag :type="emailForm.status === 'ENABLED' ? 'success' : 'info'">{{ emailForm.status === "ENABLED" ? "已启用" : "未启用" }}</el-tag>
|
<el-tag :type="emailForm.status === 'ENABLED' ? 'success' : 'info'">{{ emailForm.status === "ENABLED" ? "已启用" : "未启用" }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<PageContainer title="项目管理">
|
<PageContainer title="项目管理">
|
||||||
<QueryToolbar>
|
<QueryToolbar>
|
||||||
<el-form :inline="true" @submit.prevent class="project-page-toolbar">
|
<el-form :inline="true" @submit.prevent class="project-page-toolbar">
|
||||||
@ -34,16 +34,16 @@
|
|||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column> -->
|
</el-table-column> -->
|
||||||
<el-table-column prop="budgetCent" label="预算(元)" :formatter="budgetFormatter" />
|
<el-table-column prop="budgetCent" label="预算(元)" :formatter="budgetFormatter" width="100"/>
|
||||||
<el-table-column label="费用合计(元)" :formatter="projectFeeTotalFormatter" />
|
<el-table-column label="费用合计(元)" :formatter="projectFeeTotalFormatter" width="115"/>
|
||||||
<!-- <el-table-column prop="budgetExecutionRatio" label="预算执行率" :formatter="executionRatioFormatter" /> -->
|
<!-- <el-table-column prop="budgetExecutionRatio" label="预算执行率" :formatter="executionRatioFormatter" /> -->
|
||||||
<el-table-column prop="meetingTotal" label="会议总期数" width="100"/>
|
<el-table-column prop="meetingTotal" label="会议总期数" width="90"/>
|
||||||
<el-table-column label="项目周期" width="200">
|
<el-table-column label="项目周期" width="200">
|
||||||
<template #default="{ row }">{{ row.startDate || "-" }} ~ {{ row.endDate || "-" }}</template>
|
<template #default="{ row }">{{ row.startDate || "-" }} ~ {{ row.endDate || "-" }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column prop="status" label="状态" :formatter="statusFormatter" width="100"/>
|
<el-table-column prop="status" label="状态" :formatter="statusFormatter" width="100"/>
|
||||||
<el-table-column label="操作" width="370">
|
<el-table-column label="操作" width="370" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button v-if="canCreate" size="small" type="primary" @click="openCreateDrawer(row)">添加子项目</el-button>
|
<el-button v-if="canCreate" size="small" type="primary" @click="openCreateDrawer(row)">添加子项目</el-button>
|
||||||
<el-button size="small" @click="openDetailDrawer(row)">详情</el-button>
|
<el-button size="small" @click="openDetailDrawer(row)">详情</el-button>
|
||||||
@ -120,13 +120,13 @@
|
|||||||
<el-table-column prop="afterValue" label="变更后" />
|
<el-table-column prop="afterValue" label="变更后" />
|
||||||
<el-table-column prop="changeReason" label="变更原因" width="150" />
|
<el-table-column prop="changeReason" label="变更原因" width="150" />
|
||||||
<el-table-column prop="operatorUserName" label="操作人" width="140" />
|
<el-table-column prop="operatorUserName" label="操作人" width="140" />
|
||||||
<el-table-column prop="handoverAt" label="交接时间" width="180" />
|
<!-- <el-table-column prop="handoverAt" label="交接时间" width="180" /> -->
|
||||||
<el-table-column prop="createdAt" label="记录时间" width="180" />
|
<el-table-column prop="createdAt" label="记录时间" width="180" />
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
<el-dialog v-model="bindingVisible" title="绑定项目人员" :width="DIALOG_WIDTH.lg">
|
<el-dialog v-model="bindingVisible" title="绑定项目人员" :width="DIALOG_WIDTH.lg">
|
||||||
<el-form label-position="left" :label-width="LABEL_WIDTH.lg">
|
<el-form label-position="left" :label-width="LABEL_WIDTH.lg" style="margin-top: 10px;">
|
||||||
<el-form-item v-if="!isProjectExecutorRole" label="项目负责人">
|
<el-form-item v-if="!isProjectExecutorRole" label="项目负责人">
|
||||||
<div class="binding-select-field">
|
<div class="binding-select-field">
|
||||||
<div v-if="ownerReadonlyBoundUsers.length" class="binding-readonly-block">
|
<div v-if="ownerReadonlyBoundUsers.length" class="binding-readonly-block">
|
||||||
|
|||||||
@ -16,16 +16,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="模板状态">
|
|
||||||
<el-select v-model="templateQuery.status" clearable placeholder="全部" class="w-input-md">
|
|
||||||
<el-option v-for="item in templateStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="水印下载">
|
|
||||||
<el-select v-model="templateQuery.watermarkEnabled" clearable placeholder="全部" class="w-input-md">
|
|
||||||
<el-option v-for="item in watermarkFilterOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button v-if="canRead" type="primary" @click="handleTemplateSearch">查询</el-button>
|
<el-button v-if="canRead" type="primary" @click="handleTemplateSearch">查询</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -47,23 +38,19 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="bizScene" label="业务场景" width="140" :formatter="bizSceneFormatter" />
|
<el-table-column prop="bizScene" label="业务场景" width="140" :formatter="bizSceneFormatter" />
|
||||||
<el-table-column prop="currentVersionNo" label="当前版本" width="100" />
|
<!-- <el-table-column prop="currentVersionNo" label="当前版本" width="100" /> -->
|
||||||
<el-table-column prop="status" label="状态" width="110" :formatter="statusFormatter" />
|
<el-table-column prop="status" label="状态" width="110" :formatter="statusFormatter" />
|
||||||
<el-table-column label="生效时间" min-width="220">
|
<el-table-column label="生效时间" min-width="220">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatEffectiveTime(row) }}
|
{{ formatEffectiveTime(row) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="水印" width="90">
|
|
||||||
<template #default="{ row }">
|
<!-- <el-table-column prop="downloadRateLimitPerHour" label="限流/小时" width="110" /> -->
|
||||||
{{ row.watermarkEnabled ? "开启" : "关闭" }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="downloadRateLimitPerHour" label="限流/小时" width="110" />
|
|
||||||
<el-table-column prop="updatedAt" label="最近更新" width="180" />
|
<el-table-column prop="updatedAt" label="最近更新" width="180" />
|
||||||
<el-table-column label="操作" min-width="280" fixed="right">
|
<el-table-column label="操作" min-width="300" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" v-if="canRead" @click="openDetailDrawer(row)">查看详情</el-button>
|
<el-button size="small" v-if="canDetailRead" @click="openDetailDrawer(row)">查看详情</el-button>
|
||||||
<el-button size="small" v-if="canRead" @click="focusLogs(row)">
|
<el-button size="small" v-if="canRead" @click="focusLogs(row)">
|
||||||
{{ canReadAllLogs ? "查看下载日志" : "查看我的下载日志" }}
|
{{ canReadAllLogs ? "查看下载日志" : "查看我的下载日志" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -76,15 +63,7 @@
|
|||||||
>
|
>
|
||||||
下载
|
下载
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
v-if="canDownload"
|
|
||||||
:disabled="isWatermarkDownloadBlocked(row)"
|
|
||||||
@click="handleDownloadWatermark(row)"
|
|
||||||
>
|
|
||||||
水印下载
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -123,12 +102,7 @@
|
|||||||
<el-form-item label="版本号">
|
<el-form-item label="版本号">
|
||||||
<el-input-number v-model="logQuery.versionNo" :min="1" class="w-input-sm" />
|
<el-input-number v-model="logQuery.versionNo" :min="1" class="w-input-sm" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="下载方式">
|
|
||||||
<el-select v-model="logQuery.downloadType" clearable placeholder="全部" class="w-input-md">
|
|
||||||
<el-option label="普通下载" value="NORMAL" />
|
|
||||||
<el-option label="水印下载" value="WATERMARK" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="canReadAllLogs" label="IP">
|
<el-form-item v-if="canReadAllLogs" label="IP">
|
||||||
<el-input v-model="logQuery.ip" clearable placeholder="请输入 IP" class="w-input-md" />
|
<el-input v-model="logQuery.ip" clearable placeholder="请输入 IP" class="w-input-md" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -157,11 +131,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="versionNo" label="版本号" width="90" />
|
<el-table-column prop="versionNo" label="版本号" width="90" />
|
||||||
<el-table-column label="下载方式" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ getDownloadTypeLabel(row.downloadType) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column v-if="canReadAllLogs" label="下载人" min-width="180">
|
<el-table-column v-if="canReadAllLogs" label="下载人" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.userName || row.userPhone">
|
<span v-if="row.userName || row.userPhone">
|
||||||
@ -170,7 +140,7 @@
|
|||||||
<span v-else>{{ `用户#${row.userId}` }}</span>
|
<span v-else>{{ `用户#${row.userId}` }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="watermarkText" label="水印文案" min-width="160" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="objectKey" label="ObjectKey" min-width="260" show-overflow-tooltip />
|
<el-table-column prop="objectKey" label="ObjectKey" min-width="260" show-overflow-tooltip />
|
||||||
<el-table-column prop="ip" label="IP" width="140" />
|
<el-table-column prop="ip" label="IP" width="140" />
|
||||||
<el-table-column prop="downloadedAt" label="下载时间" width="180" />
|
<el-table-column prop="downloadedAt" label="下载时间" width="180" />
|
||||||
@ -201,7 +171,7 @@
|
|||||||
<el-descriptions-item label="适用范围">{{ getScopeTypeLabel(detailRow.scopeType) }}</el-descriptions-item>
|
<el-descriptions-item label="适用范围">{{ getScopeTypeLabel(detailRow.scopeType) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="当前版本">V{{ detailRow.currentVersionNo || 1 }}</el-descriptions-item>
|
<el-descriptions-item label="当前版本">V{{ detailRow.currentVersionNo || 1 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="生效时间">{{ formatEffectiveTime(detailRow) }}</el-descriptions-item>
|
<el-descriptions-item label="生效时间">{{ formatEffectiveTime(detailRow) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="水印下载">{{ detailRow.watermarkEnabled ? "开启" : "关闭" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="下载限流/小时">{{ detailRow.downloadRateLimitPerHour ?? "-" }}</el-descriptions-item>
|
<el-descriptions-item label="下载限流/小时">{{ detailRow.downloadRateLimitPerHour ?? "-" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="最近更新">{{ detailRow.updatedAt || "-" }}</el-descriptions-item>
|
<el-descriptions-item label="最近更新">{{ detailRow.updatedAt || "-" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="下载状态">
|
<el-descriptions-item label="下载状态">
|
||||||
@ -216,14 +186,6 @@
|
|||||||
<el-button v-if="canDownload" type="primary" :disabled="isDownloadBlocked(detailRow)" @click="handleDownload(detailRow)">
|
<el-button v-if="canDownload" type="primary" :disabled="isDownloadBlocked(detailRow)" @click="handleDownload(detailRow)">
|
||||||
下载
|
下载
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
|
||||||
v-if="canDownload"
|
|
||||||
type="primary"
|
|
||||||
:disabled="isWatermarkDownloadBlocked(detailRow)"
|
|
||||||
@click="handleDownloadWatermark(detailRow)"
|
|
||||||
>
|
|
||||||
水印下载
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-descriptions v-if="currentVersionDetail" class="mt-md" :column="2" border>
|
<el-descriptions v-if="currentVersionDetail" class="mt-md" :column="2" border>
|
||||||
@ -261,11 +223,7 @@
|
|||||||
<el-tab-pane :label="canReadAllLogs ? '最近下载' : '我的最近下载'">
|
<el-tab-pane :label="canReadAllLogs ? '最近下载' : '我的最近下载'">
|
||||||
<el-table :data="detailDownloadRows" empty-text="暂无下载记录">
|
<el-table :data="detailDownloadRows" empty-text="暂无下载记录">
|
||||||
<el-table-column prop="downloadedAt" label="下载时间" width="180" />
|
<el-table-column prop="downloadedAt" label="下载时间" width="180" />
|
||||||
<el-table-column label="下载方式" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ getDownloadTypeLabel(row.downloadType) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column v-if="canReadAllLogs" label="下载人" min-width="180">
|
<el-table-column v-if="canReadAllLogs" label="下载人" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.userName || row.userPhone">
|
<span v-if="row.userName || row.userPhone">
|
||||||
@ -274,7 +232,7 @@
|
|||||||
<span v-else>{{ `用户#${row.userId}` }}</span>
|
<span v-else>{{ `用户#${row.userId}` }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="watermarkText" label="水印文案" min-width="160" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="ip" label="IP" width="140" />
|
<el-table-column prop="ip" label="IP" width="140" />
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@ -291,11 +249,11 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
|||||||
import PageContainer from "../../components/PageContainer.vue";
|
import PageContainer from "../../components/PageContainer.vue";
|
||||||
import {
|
import {
|
||||||
downloadTemplate,
|
downloadTemplate,
|
||||||
downloadTemplateWatermark,
|
|
||||||
fetchTemplateDownloadLogs,
|
fetchTemplateViewList,
|
||||||
fetchTemplateTypeOptions,
|
fetchTemplateTypeOptions,
|
||||||
|
fetchTemplateDownloadLogs,
|
||||||
fetchTemplateVersions,
|
fetchTemplateVersions,
|
||||||
fetchTemplates,
|
|
||||||
} from "../../api/modules";
|
} from "../../api/modules";
|
||||||
import { PERMS } from "../../constants/permissions";
|
import { PERMS } from "../../constants/permissions";
|
||||||
import { DRAWER_SIZE } from "../../constants/ui";
|
import { DRAWER_SIZE } from "../../constants/ui";
|
||||||
@ -323,6 +281,7 @@ const authStore = useAuthStore();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const canRead = computed(() => authStore.hasPermission(PERMS.template.read));
|
const canRead = computed(() => authStore.hasPermission(PERMS.template.read));
|
||||||
|
const canDetailRead = computed(() => authStore.hasPermission(PERMS.template.detailRead));
|
||||||
const canDownload = computed(() => authStore.hasPermission(PERMS.template.download));
|
const canDownload = computed(() => authStore.hasPermission(PERMS.template.download));
|
||||||
const canReadAllLogs = computed(() => authStore.hasPermission(PERMS.template.downloadLogReadAll));
|
const canReadAllLogs = computed(() => authStore.hasPermission(PERMS.template.downloadLogReadAll));
|
||||||
|
|
||||||
@ -345,12 +304,7 @@ const detailDownloadRows = ref<any[]>([]);
|
|||||||
|
|
||||||
const templateTypeOptions = ref<Array<{ typeCode: TemplateTypeCode; typeName: string; status: "ENABLED" | "DISABLED" }>>([]);
|
const templateTypeOptions = ref<Array<{ typeCode: TemplateTypeCode; typeName: string; status: "ENABLED" | "DISABLED" }>>([]);
|
||||||
|
|
||||||
const templateStatusOptions = [
|
|
||||||
{ label: "草稿", value: "DRAFT" },
|
|
||||||
{ label: "已发布", value: "PUBLISHED" },
|
|
||||||
{ label: "已停用", value: "DISABLED" },
|
|
||||||
{ label: "已归档", value: "ARCHIVED" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const scopeTypeOptions = [
|
const scopeTypeOptions = [
|
||||||
{ label: "全部", value: "ALL" },
|
{ label: "全部", value: "ALL" },
|
||||||
@ -358,16 +312,9 @@ const scopeTypeOptions = [
|
|||||||
{ label: "会议级", value: "MEETING" },
|
{ label: "会议级", value: "MEETING" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const watermarkFilterOptions = [
|
|
||||||
{ label: "开启", value: "true" },
|
|
||||||
{ label: "关闭", value: "false" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const templateQuery = ref({
|
const templateQuery = ref({
|
||||||
templateName: "",
|
templateName: "",
|
||||||
templateType: "" as TemplateTypeCode | "",
|
templateType: "" as TemplateTypeCode | "",
|
||||||
status: "",
|
|
||||||
watermarkEnabled: "" as "" | "true" | "false",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const logQuery = ref({
|
const logQuery = ref({
|
||||||
@ -376,7 +323,6 @@ const logQuery = ref({
|
|||||||
userId: undefined as number | undefined,
|
userId: undefined as number | undefined,
|
||||||
userKeyword: "",
|
userKeyword: "",
|
||||||
versionNo: undefined as number | undefined,
|
versionNo: undefined as number | undefined,
|
||||||
downloadType: "" as "" | "NORMAL" | "WATERMARK",
|
|
||||||
ip: "",
|
ip: "",
|
||||||
dateRange: [] as string[],
|
dateRange: [] as string[],
|
||||||
});
|
});
|
||||||
@ -409,7 +355,7 @@ const getTemplateTypeLabel = (code: string) =>
|
|||||||
const getScopeTypeLabel = (code: ScopeTypeCode | string) =>
|
const getScopeTypeLabel = (code: ScopeTypeCode | string) =>
|
||||||
scopeTypeOptions.find((item) => item.value === code)?.label || code || "-";
|
scopeTypeOptions.find((item) => item.value === code)?.label || code || "-";
|
||||||
|
|
||||||
const getDownloadTypeLabel = (value?: unknown) => (String(value ?? "").trim() === "WATERMARK" ? "水印下载" : "普通下载");
|
|
||||||
|
|
||||||
const statusFormatter = (_row: any, _column: any, value: unknown) => getStatusLabel(value);
|
const statusFormatter = (_row: any, _column: any, value: unknown) => getStatusLabel(value);
|
||||||
const bizSceneFormatter = (_row: any, _column: any, value: unknown) => getBizSceneLabel(value);
|
const bizSceneFormatter = (_row: any, _column: any, value: unknown) => getBizSceneLabel(value);
|
||||||
@ -425,7 +371,6 @@ const formatEffectiveTime = (row: any) => {
|
|||||||
|
|
||||||
const buildTemplateName = (row: any) => row?.templateName || `模板#${row?.id ?? "-"}`;
|
const buildTemplateName = (row: any) => row?.templateName || `模板#${row?.id ?? "-"}`;
|
||||||
const isDownloadBlocked = (row?: any) => ["DISABLED", "ARCHIVED"].includes(String(row?.status || "").trim().toUpperCase());
|
const isDownloadBlocked = (row?: any) => ["DISABLED", "ARCHIVED"].includes(String(row?.status || "").trim().toUpperCase());
|
||||||
const isWatermarkDownloadBlocked = (row?: any) => isDownloadBlocked(row) || !row?.watermarkEnabled;
|
|
||||||
|
|
||||||
const confirmAction = async (message: string, title: string) => {
|
const confirmAction = async (message: string, title: string) => {
|
||||||
try {
|
try {
|
||||||
@ -443,8 +388,7 @@ const confirmAction = async (message: string, title: string) => {
|
|||||||
const buildTemplateParams = () => ({
|
const buildTemplateParams = () => ({
|
||||||
templateName: templateQuery.value.templateName || undefined,
|
templateName: templateQuery.value.templateName || undefined,
|
||||||
templateType: templateQuery.value.templateType || undefined,
|
templateType: templateQuery.value.templateType || undefined,
|
||||||
status: templateQuery.value.status || undefined,
|
status: "PUBLISHED",
|
||||||
watermarkEnabled: templateQuery.value.watermarkEnabled ? templateQuery.value.watermarkEnabled === "true" : undefined,
|
|
||||||
pageNo: templatePageNo.value,
|
pageNo: templatePageNo.value,
|
||||||
pageSize: templatePageSize.value,
|
pageSize: templatePageSize.value,
|
||||||
});
|
});
|
||||||
@ -455,7 +399,6 @@ const buildLogParams = () => ({
|
|||||||
userId: canReadAllLogs.value ? logQuery.value.userId : undefined,
|
userId: canReadAllLogs.value ? logQuery.value.userId : undefined,
|
||||||
userKeyword: canReadAllLogs.value ? logQuery.value.userKeyword || undefined : undefined,
|
userKeyword: canReadAllLogs.value ? logQuery.value.userKeyword || undefined : undefined,
|
||||||
versionNo: logQuery.value.versionNo,
|
versionNo: logQuery.value.versionNo,
|
||||||
downloadType: logQuery.value.downloadType || undefined,
|
|
||||||
ip: canReadAllLogs.value ? logQuery.value.ip || undefined : undefined,
|
ip: canReadAllLogs.value ? logQuery.value.ip || undefined : undefined,
|
||||||
downloadedFrom: logQuery.value.dateRange?.[0] || undefined,
|
downloadedFrom: logQuery.value.dateRange?.[0] || undefined,
|
||||||
downloadedTo: logQuery.value.dateRange?.[1] || undefined,
|
downloadedTo: logQuery.value.dateRange?.[1] || undefined,
|
||||||
@ -490,7 +433,7 @@ const loadTemplates = async () => {
|
|||||||
templateTotalCount.value = 0;
|
templateTotalCount.value = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resp = await fetchTemplates(buildTemplateParams());
|
const resp = await fetchTemplateViewList(buildTemplateParams());
|
||||||
templateRows.value = resp?.data?.list || [];
|
templateRows.value = resp?.data?.list || [];
|
||||||
templateTotalCount.value = Number(resp?.data?.total || 0);
|
templateTotalCount.value = Number(resp?.data?.total || 0);
|
||||||
};
|
};
|
||||||
@ -544,8 +487,6 @@ const resetTemplateQuery = async () => {
|
|||||||
templateQuery.value = {
|
templateQuery.value = {
|
||||||
templateName: "",
|
templateName: "",
|
||||||
templateType: "",
|
templateType: "",
|
||||||
status: "",
|
|
||||||
watermarkEnabled: "",
|
|
||||||
};
|
};
|
||||||
templatePageNo.value = 1;
|
templatePageNo.value = 1;
|
||||||
templatePageSize.value = 20;
|
templatePageSize.value = 20;
|
||||||
@ -559,7 +500,6 @@ const resetLogQuery = async () => {
|
|||||||
userId: undefined,
|
userId: undefined,
|
||||||
userKeyword: "",
|
userKeyword: "",
|
||||||
versionNo: undefined,
|
versionNo: undefined,
|
||||||
downloadType: "",
|
|
||||||
ip: "",
|
ip: "",
|
||||||
dateRange: [],
|
dateRange: [],
|
||||||
};
|
};
|
||||||
@ -594,28 +534,7 @@ const handleDownload = async (row: any) => {
|
|||||||
await Promise.all([loadLogs(), refreshDetailData()]);
|
await Promise.all([loadLogs(), refreshDetailData()]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadWatermark = async (row: any) => {
|
|
||||||
if (isWatermarkDownloadBlocked(row)) {
|
|
||||||
ElMessage.warning(isDownloadBlocked(row) ? "该模板已停用或归档,当前不可下载" : "该模板未开启水印下载");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const confirmed = await confirmAction(
|
|
||||||
`确认对「${buildTemplateName(row)}」执行水印下载?系统会记录本次下载日志。`,
|
|
||||||
"水印下载",
|
|
||||||
);
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const resp = await downloadTemplateWatermark(row.id, { watermarkText: "仅限内部使用" });
|
|
||||||
const url = resp?.data?.signedUrl;
|
|
||||||
if (!url) {
|
|
||||||
ElMessage.error("获取水印下载链接失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.open(url, "_blank");
|
|
||||||
ElMessage.success("已生成水印下载链接并记录下载日志");
|
|
||||||
await Promise.all([loadLogs(), refreshDetailData()]);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
initQueryFromRoute();
|
initQueryFromRoute();
|
||||||
|
|||||||
@ -39,11 +39,7 @@
|
|||||||
<el-option v-for="item in effectiveStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
<el-option v-for="item in effectiveStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="水印下载">
|
|
||||||
<el-select v-model="listQuery.watermarkEnabled" clearable placeholder="全部" class="w-input-md">
|
|
||||||
<el-option v-for="item in watermarkFilterOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button v-if="canRead" type="primary" @click="handleSearch">查询</el-button>
|
<el-button v-if="canRead" type="primary" @click="handleSearch">查询</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -122,9 +118,6 @@
|
|||||||
class="w-input-md"
|
class="w-input-md"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="开启水印下载">
|
|
||||||
<el-switch v-model="form.watermarkEnabled" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="下载限流/小时">
|
<el-form-item label="下载限流/小时">
|
||||||
<el-input-number v-model="form.downloadRateLimitPerHour" :min="1" :max="9999" />
|
<el-input-number v-model="form.downloadRateLimitPerHour" :min="1" :max="9999" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -194,16 +187,20 @@
|
|||||||
{{ formatEffectiveTime(row) }}
|
{{ formatEffectiveTime(row) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="水印" width="90">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.watermarkEnabled ? "开启" : "关闭" }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="downloadRateLimitPerHour" label="限流/小时" width="110" />
|
<el-table-column prop="downloadRateLimitPerHour" label="限流/小时" width="110" />
|
||||||
<el-table-column prop="updatedAt" label="最近更新" width="180" />
|
<el-table-column prop="updatedAt" label="最近更新" width="180" />
|
||||||
<el-table-column label="操作" min-width="420" fixed="right">
|
<el-table-column label="操作" min-width="350" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" v-if="canRead" @click="openDetailDrawer(row)">版本管理</el-button>
|
<el-button size="small" v-if="canDetailRead" @click="openDetailDrawer(row)">版本管理</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
v-if="canUpdate && row.status !== 'ARCHIVED'"
|
||||||
|
@click="openEditDrawer(row)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
type="success"
|
type="success"
|
||||||
@ -235,6 +232,68 @@
|
|||||||
@current-change="load"
|
@current-change="load"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<el-drawer v-model="editVisible" title="编辑模板" :size="DRAWER_SIZE.md" destroy-on-close>
|
||||||
|
<el-form ref="editFormRef" :model="editForm" :rules="formRules" label-width="120px" class="pr-md">
|
||||||
|
<el-form-item label="模板名称" prop="templateName">
|
||||||
|
<el-input v-model="editForm.templateName" placeholder="如:XX大会报名签到码" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模板类型" prop="templateType">
|
||||||
|
<el-select v-model="editForm.templateType" class="w-input-md">
|
||||||
|
<el-option
|
||||||
|
v-for="item in templateTypeOptions.filter(i => i.status === 'ENABLED' || i.typeCode === editForm.templateType)"
|
||||||
|
:key="item.typeCode"
|
||||||
|
:label="item.typeName"
|
||||||
|
:value="item.typeCode"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="适用范围" prop="scopeType">
|
||||||
|
<el-select v-model="editForm.scopeType" class="w-input-md">
|
||||||
|
<el-option v-for="item in scopeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="editForm.scopeType === 'PROJECT'" label="关联项目ID" prop="projectId">
|
||||||
|
<el-input-number v-model="editForm.projectId" :min="1" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="editForm.scopeType === 'MEETING'" label="关联会议ID" prop="meetingId">
|
||||||
|
<el-input-number v-model="editForm.meetingId" :min="1" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="业务场景" prop="bizScene">
|
||||||
|
<el-select v-model="editForm.bizScene" class="w-input-md">
|
||||||
|
<el-option
|
||||||
|
v-for="item in flowSceneOptions"
|
||||||
|
:key="item.sceneCode"
|
||||||
|
:label="item.sceneName"
|
||||||
|
:value="item.sceneCode"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="生效开始时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="editForm.effectiveFrom"
|
||||||
|
type="datetime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
class="w-input-md"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="生效结束时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="editForm.effectiveTo"
|
||||||
|
type="datetime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
class="w-input-md"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="下载限流/小时">
|
||||||
|
<el-input-number v-model="editForm.downloadRateLimitPerHour" :min="1" :max="9999" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="editVisible = false">取消</el-button>
|
||||||
|
<el-button v-if="canUpdate" type="primary" @click="handleEdit">保存修改</el-button>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
<el-drawer v-model="detailVisible" title="模板管理详情" :size="DRAWER_SIZE.lg" destroy-on-close>
|
<el-drawer v-model="detailVisible" title="模板管理详情" :size="DRAWER_SIZE.lg" destroy-on-close>
|
||||||
<template v-if="detailRow">
|
<template v-if="detailRow">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
@ -245,7 +304,7 @@
|
|||||||
<el-descriptions-item label="适用范围">{{ getScopeTypeLabel(detailRow.scopeType) }}</el-descriptions-item>
|
<el-descriptions-item label="适用范围">{{ getScopeTypeLabel(detailRow.scopeType) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="当前版本">V{{ detailRow.currentVersionNo || 1 }}</el-descriptions-item>
|
<el-descriptions-item label="当前版本">V{{ detailRow.currentVersionNo || 1 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="生效时间">{{ formatEffectiveTime(detailRow) }}</el-descriptions-item>
|
<el-descriptions-item label="生效时间">{{ formatEffectiveTime(detailRow) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="水印下载">{{ detailRow.watermarkEnabled ? "开启" : "关闭" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="下载限流/小时">{{ detailRow.downloadRateLimitPerHour ?? "-" }}</el-descriptions-item>
|
<el-descriptions-item label="下载限流/小时">{{ detailRow.downloadRateLimitPerHour ?? "-" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="最近更新">{{ detailRow.updatedAt || "-" }}</el-descriptions-item>
|
<el-descriptions-item label="最近更新">{{ detailRow.updatedAt || "-" }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
@ -458,6 +517,8 @@ import {
|
|||||||
createTemplate,
|
createTemplate,
|
||||||
disableTemplate,
|
disableTemplate,
|
||||||
disableTemplateTypeOption,
|
disableTemplateTypeOption,
|
||||||
|
downloadTemplate,
|
||||||
|
|
||||||
enableTemplateTypeOption,
|
enableTemplateTypeOption,
|
||||||
fetchPublishedTemplateOptions,
|
fetchPublishedTemplateOptions,
|
||||||
fetchTemplateFlowLinks,
|
fetchTemplateFlowLinks,
|
||||||
@ -469,6 +530,7 @@ import {
|
|||||||
fetchTemplates,
|
fetchTemplates,
|
||||||
publishTemplate,
|
publishTemplate,
|
||||||
rollbackTemplate,
|
rollbackTemplate,
|
||||||
|
updateTemplate,
|
||||||
} from "../../api/modules";
|
} from "../../api/modules";
|
||||||
import { PERMS } from "../../constants/permissions";
|
import { PERMS } from "../../constants/permissions";
|
||||||
import { DRAWER_SIZE, DIALOG_WIDTH, LABEL_WIDTH } from "../../constants/ui";
|
import { DRAWER_SIZE, DIALOG_WIDTH, LABEL_WIDTH } from "../../constants/ui";
|
||||||
@ -495,7 +557,9 @@ const BIZ_SCENE_LABELS: Record<string, string> = {
|
|||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
const canRead = computed(() => authStore.hasPermission(PERMS.template.read));
|
const canRead = computed(() => authStore.hasPermission(PERMS.template.read));
|
||||||
|
const canDetailRead = computed(() => authStore.hasPermission(PERMS.template.detailRead));
|
||||||
const canCreate = computed(() => authStore.hasPermission(PERMS.template.create));
|
const canCreate = computed(() => authStore.hasPermission(PERMS.template.create));
|
||||||
|
const canUpdate = computed(() => authStore.hasPermission(PERMS.template.update));
|
||||||
const canPublish = computed(() => authStore.hasPermission(PERMS.template.publish));
|
const canPublish = computed(() => authStore.hasPermission(PERMS.template.publish));
|
||||||
const canDisable = computed(() => authStore.hasPermission(PERMS.template.disable));
|
const canDisable = computed(() => authStore.hasPermission(PERMS.template.disable));
|
||||||
const canArchive = computed(() => authStore.hasPermission(PERMS.template.archive));
|
const canArchive = computed(() => authStore.hasPermission(PERMS.template.archive));
|
||||||
@ -508,6 +572,7 @@ const pageSize = ref(20);
|
|||||||
const totalCount = ref(0);
|
const totalCount = ref(0);
|
||||||
|
|
||||||
const createVisible = ref(false);
|
const createVisible = ref(false);
|
||||||
|
const editVisible = ref(false);
|
||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
const flowBindingVisible = ref(false);
|
const flowBindingVisible = ref(false);
|
||||||
const rollbackDialogVisible = ref(false);
|
const rollbackDialogVisible = ref(false);
|
||||||
@ -549,10 +614,7 @@ const effectiveStatusOptions = [
|
|||||||
{ label: "已过期", value: "EXPIRED" },
|
{ label: "已过期", value: "EXPIRED" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const watermarkFilterOptions = [
|
|
||||||
{ label: "开启", value: "true" },
|
|
||||||
{ label: "关闭", value: "false" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const listQuery = ref({
|
const listQuery = ref({
|
||||||
templateName: "",
|
templateName: "",
|
||||||
@ -561,7 +623,20 @@ const listQuery = ref({
|
|||||||
scopeType: "" as ScopeTypeCode | "",
|
scopeType: "" as ScopeTypeCode | "",
|
||||||
bizScene: "" as BizSceneCode | "",
|
bizScene: "" as BizSceneCode | "",
|
||||||
effectiveStatus: "",
|
effectiveStatus: "",
|
||||||
watermarkEnabled: "" as "" | "true" | "false",
|
});
|
||||||
|
|
||||||
|
const editFormRef = ref<FormInstance>();
|
||||||
|
const editForm = ref({
|
||||||
|
id: 0,
|
||||||
|
templateName: "",
|
||||||
|
templateType: "AGENDA" as TemplateTypeCode,
|
||||||
|
scopeType: "ALL" as ScopeTypeCode,
|
||||||
|
projectId: undefined as number | undefined,
|
||||||
|
meetingId: undefined as number | undefined,
|
||||||
|
bizScene: "MEETING_RECOMMEND" as BizSceneCode,
|
||||||
|
effectiveFrom: "",
|
||||||
|
effectiveTo: "",
|
||||||
|
downloadRateLimitPerHour: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
@ -575,7 +650,6 @@ const form = ref({
|
|||||||
changeLog: "",
|
changeLog: "",
|
||||||
effectiveFrom: "",
|
effectiveFrom: "",
|
||||||
effectiveTo: "",
|
effectiveTo: "",
|
||||||
watermarkEnabled: false,
|
|
||||||
downloadRateLimitPerHour: 100,
|
downloadRateLimitPerHour: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -666,7 +740,6 @@ const buildListParams = () => ({
|
|||||||
scopeType: listQuery.value.scopeType || undefined,
|
scopeType: listQuery.value.scopeType || undefined,
|
||||||
bizScene: listQuery.value.bizScene || undefined,
|
bizScene: listQuery.value.bizScene || undefined,
|
||||||
effectiveStatus: listQuery.value.effectiveStatus || undefined,
|
effectiveStatus: listQuery.value.effectiveStatus || undefined,
|
||||||
watermarkEnabled: listQuery.value.watermarkEnabled ? listQuery.value.watermarkEnabled === "true" : undefined,
|
|
||||||
pageNo: pageNo.value,
|
pageNo: pageNo.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
});
|
});
|
||||||
@ -717,7 +790,6 @@ const resetCreateForm = () => {
|
|||||||
changeLog: "",
|
changeLog: "",
|
||||||
effectiveFrom: "",
|
effectiveFrom: "",
|
||||||
effectiveTo: "",
|
effectiveTo: "",
|
||||||
watermarkEnabled: false,
|
|
||||||
downloadRateLimitPerHour: 100,
|
downloadRateLimitPerHour: 100,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -728,6 +800,23 @@ const openCreateDrawer = () => {
|
|||||||
createVisible.value = true;
|
createVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditDrawer = (row: any) => {
|
||||||
|
selectedTemplateId.value = row.id;
|
||||||
|
editForm.value = {
|
||||||
|
id: row.id,
|
||||||
|
templateName: row.templateName || "",
|
||||||
|
templateType: (row.templateType || "AGENDA") as TemplateTypeCode,
|
||||||
|
scopeType: (row.scopeType || "ALL") as ScopeTypeCode,
|
||||||
|
projectId: row.projectId || undefined,
|
||||||
|
meetingId: row.meetingId || undefined,
|
||||||
|
bizScene: (row.bizScene || "MEETING_RECOMMEND") as BizSceneCode,
|
||||||
|
effectiveFrom: row.effectiveFrom || "",
|
||||||
|
effectiveTo: row.effectiveTo || "",
|
||||||
|
downloadRateLimitPerHour: Number(row.downloadRateLimitPerHour || 100),
|
||||||
|
};
|
||||||
|
editVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
const openFlowBindingDrawer = async () => {
|
const openFlowBindingDrawer = async () => {
|
||||||
if (!canRead.value) {
|
if (!canRead.value) {
|
||||||
return;
|
return;
|
||||||
@ -884,7 +973,6 @@ const handleCreate = async () => {
|
|||||||
changeLog: form.value.changeLog,
|
changeLog: form.value.changeLog,
|
||||||
effectiveFrom: form.value.effectiveFrom || undefined,
|
effectiveFrom: form.value.effectiveFrom || undefined,
|
||||||
effectiveTo: form.value.effectiveTo || undefined,
|
effectiveTo: form.value.effectiveTo || undefined,
|
||||||
watermarkEnabled: form.value.watermarkEnabled,
|
|
||||||
downloadRateLimitPerHour: form.value.downloadRateLimitPerHour,
|
downloadRateLimitPerHour: form.value.downloadRateLimitPerHour,
|
||||||
});
|
});
|
||||||
ElMessage.success("模板创建成功");
|
ElMessage.success("模板创建成功");
|
||||||
@ -892,6 +980,45 @@ const handleCreate = async () => {
|
|||||||
await load();
|
await load();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEdit = async () => {
|
||||||
|
if (!editFormRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valid = await editFormRef.value.validate().catch(() => false);
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (editForm.value.scopeType === "PROJECT" && !editForm.value.projectId) {
|
||||||
|
ElMessage.warning("项目级模板需要填写项目ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (editForm.value.scopeType === "MEETING" && !editForm.value.meetingId) {
|
||||||
|
ElMessage.warning("会议级模板需要填写会议ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (editForm.value.effectiveFrom && editForm.value.effectiveTo && editForm.value.effectiveFrom > editForm.value.effectiveTo) {
|
||||||
|
ElMessage.warning("生效开始时间不能晚于生效结束时间");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateTemplate(editForm.value.id, {
|
||||||
|
templateName: editForm.value.templateName,
|
||||||
|
templateType: editForm.value.templateType,
|
||||||
|
scopeType: editForm.value.scopeType,
|
||||||
|
projectId: editForm.value.projectId,
|
||||||
|
meetingId: editForm.value.meetingId,
|
||||||
|
bizScene: editForm.value.bizScene,
|
||||||
|
effectiveFrom: editForm.value.effectiveFrom || undefined,
|
||||||
|
effectiveTo: editForm.value.effectiveTo || undefined,
|
||||||
|
downloadRateLimitPerHour: editForm.value.downloadRateLimitPerHour,
|
||||||
|
});
|
||||||
|
ElMessage.success("模板修改成功");
|
||||||
|
editVisible.value = false;
|
||||||
|
await load();
|
||||||
|
if (detailVisible.value && Number(detailRow.value?.id) === Number(editForm.value.id)) {
|
||||||
|
await refreshDetailData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const enableType = async (typeCode: string) => {
|
const enableType = async (typeCode: string) => {
|
||||||
await enableTemplateTypeOption(typeCode);
|
await enableTemplateTypeOption(typeCode);
|
||||||
ElMessage.success(`模板类型 ${typeCode} 已启用`);
|
ElMessage.success(`模板类型 ${typeCode} 已启用`);
|
||||||
@ -1116,7 +1243,6 @@ const resetListQuery = async () => {
|
|||||||
scopeType: "",
|
scopeType: "",
|
||||||
bizScene: "",
|
bizScene: "",
|
||||||
effectiveStatus: "",
|
effectiveStatus: "",
|
||||||
watermarkEnabled: "",
|
|
||||||
};
|
};
|
||||||
pageNo.value = 1;
|
pageNo.value = 1;
|
||||||
pageSize.value = 20;
|
pageSize.value = 20;
|
||||||
|
|||||||
@ -74,24 +74,17 @@
|
|||||||
stripe
|
stripe
|
||||||
:empty-text="loading ? '加载中...' : '暂无待审批任务'"
|
:empty-text="loading ? '加载中...' : '暂无待审批任务'"
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="任务ID" width="80" />
|
<el-table-column label="会议信息" min-width="180">
|
||||||
<el-table-column prop="meetingId" label="关联会议ID" width="100" />
|
|
||||||
<el-table-column prop="moduleCode" label="模块" min-width="120">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag size="small">{{ formatModuleCode(row.moduleCode) }}</el-tag>
|
{{ meetingNameMap[row.meetingId] || `会议#${row.meetingId}` }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="nodeStatus" label="状态" width="100">
|
<el-table-column prop="node" label="审批节点" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag
|
<el-tag size="small">{{ formatAuditNode(row.node) }}</el-tag>
|
||||||
size="small"
|
|
||||||
:type="row.nodeStatus === 'PENDING' ? 'warning' : 'info'"
|
|
||||||
>
|
|
||||||
{{ row.nodeStatus === 'PENDING' ? '待处理' : row.nodeStatus }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="到达时间" width="180" />
|
<el-table-column prop="submittedAt" label="到达时间" width="180" />
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button
|
||||||
@ -111,10 +104,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted } from "vue";
|
import { computed, ref, onMounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { fetchAuditTasks, fetchDashboardStats } from "../../api/modules";
|
import { fetchAuditTasks, fetchDashboardStats, fetchMeetings } from "../../api/modules";
|
||||||
import { PERMS } from "../../constants/permissions";
|
import { PERMS } from "../../constants/permissions";
|
||||||
import { useAuthStore } from "../../stores/auth";
|
import { useAuthStore } from "../../stores/auth";
|
||||||
import { toZhModuleCode } from "../../utils/status";
|
import { toZhAuditNode } from "../../utils/status";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@ -127,8 +120,9 @@ const myPendingTasks = ref<any[]>([]);
|
|||||||
const pendingAuditCount = ref(0);
|
const pendingAuditCount = ref(0);
|
||||||
const activeMeetingCount = ref(0);
|
const activeMeetingCount = ref(0);
|
||||||
const pendingFinanceCount = ref(0);
|
const pendingFinanceCount = ref(0);
|
||||||
|
const meetingNameMap = ref<Record<number, string>>({});
|
||||||
|
|
||||||
const formatModuleCode = (code: string) => toZhModuleCode(code);
|
const formatAuditNode = (code: string) => toZhAuditNode(code);
|
||||||
|
|
||||||
const goToAudit = (meetingId?: number) => {
|
const goToAudit = (meetingId?: number) => {
|
||||||
if (meetingId) {
|
if (meetingId) {
|
||||||
@ -139,17 +133,19 @@ const goToAudit = (meetingId?: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadMyTasks = async () => {
|
const loadMyTasks = async () => {
|
||||||
if (!authStore.hasPermission(PERMS.audit.read)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const resp = await fetchAuditTasks({ mine: true, pageNo: 1, pageSize: 50 });
|
const resp = await fetchAuditTasks({ mine: true, scope: "PENDING_MINE", pageNo: 1, pageSize: 50 });
|
||||||
const list = resp?.data?.list || [];
|
const list = resp?.data?.list || [];
|
||||||
// 过滤出PENDING的任务以供展示
|
// 过滤出PENDING的任务以供展示
|
||||||
const pendings = list.filter((x: any) => x.nodeStatus === "PENDING" || x.nodeStatus === "CLAIMED");
|
const pendings = list.filter((x: any) => x.status === "PENDING" || x.status === "CLAIMED");
|
||||||
myPendingTasks.value = pendings.slice(0, 10); // 工作台只展示Top 10
|
myPendingTasks.value = pendings.slice(0, 10); // 工作台只展示Top 10
|
||||||
pendingAuditCount.value = pendings.length;
|
pendingAuditCount.value = pendings.length;
|
||||||
|
|
||||||
|
// 如果有待办任务,获取会议名称
|
||||||
|
if (pendings.length > 0) {
|
||||||
|
await loadNameMaps();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
@ -172,6 +168,25 @@ const loadDashboardStats = async () => {
|
|||||||
// 接口未就绪时静默失败
|
// 接口未就绪时静默失败
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadNameMaps = async () => {
|
||||||
|
try {
|
||||||
|
const meetingResp = await fetchMeetings({ pageNo: 1, pageSize: 200 });
|
||||||
|
const meetings = meetingResp?.data?.list || [];
|
||||||
|
const meetingMap: Record<number, string> = {};
|
||||||
|
meetings.forEach((item: any) => {
|
||||||
|
const id = Number(item?.id || 0);
|
||||||
|
if (id > 0) {
|
||||||
|
const projectName = String(item?.projectName || "").trim();
|
||||||
|
const topic = String(item?.topic || "").trim();
|
||||||
|
meetingMap[id] = projectName && topic ? `${projectName} / ${topic}` : topic || `会议#${id}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
meetingNameMap.value = meetingMap;
|
||||||
|
} catch (_e) {
|
||||||
|
meetingNameMap.value = {};
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
</QueryToolbar>
|
</QueryToolbar>
|
||||||
|
|
||||||
<el-table :data="rows" class="mt-md" empty-text="暂无数据">
|
<el-table :data="rows" class="mt-md" empty-text="暂无数据">
|
||||||
<el-table-column prop="tenantCode" label="租户编码" width="220" />
|
<el-table-column prop="tenantCode" label="租户编码" width="200" />
|
||||||
<el-table-column prop="tenantName" label="租户名称" />
|
<el-table-column prop="tenantName" label="租户名称" width="220"/>
|
||||||
<el-table-column label="Logo" width="180">
|
<el-table-column label="Logo" width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<img
|
<img
|
||||||
@ -21,7 +21,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="状态" width="120" :formatter="statusFormatter" />
|
<el-table-column prop="status" label="状态" width="120" :formatter="statusFormatter" />
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="190" />
|
<el-table-column prop="createdAt" label="创建时间" width="190" />
|
||||||
<el-table-column label="操作" width="420">
|
<el-table-column label="操作" width="380">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button v-if="canManage" size="small" @click="openEdit(row)">编辑</el-button>
|
<el-button v-if="canManage" size="small" @click="openEdit(row)">编辑</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
@ -121,10 +121,10 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="adminDialogVisible" title="设置租户管理员" :width="DIALOG_WIDTH.md">
|
<el-dialog v-model="adminDialogVisible" title="设置租户管理员" :width="DIALOG_WIDTH.md">
|
||||||
<div class="mb-md text-regular">
|
<div class="mb-md text-regular" style="margin-top: 10px;">
|
||||||
当前租户:{{ selectedTenant?.tenantName }}({{ selectedTenant?.tenantCode }})
|
当前租户:{{ selectedTenant?.tenantName }}({{ selectedTenant?.tenantCode }})
|
||||||
</div>
|
</div>
|
||||||
<el-form ref="adminFormRef" :model="adminForm" label-position="left" :label-width="LABEL_WIDTH.md" :rules="adminFormRules">
|
<el-form ref="adminFormRef" :model="adminForm" label-position="left" :label-width="LABEL_WIDTH.lg" :rules="adminFormRules">
|
||||||
<el-form-item label="管理员姓名" prop="userName">
|
<el-form-item label="管理员姓名" prop="userName">
|
||||||
<el-input v-model="adminForm.userName" />
|
<el-input v-model="adminForm.userName" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -141,7 +141,7 @@
|
|||||||
type="info"
|
type="info"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
title="系统会向该邮箱发送一次性首次设密链接,不再发送明文初始密码。"
|
title="创建成功后将直接在页面显示一次性设密链接,请注意保存以便发给管理员。"
|
||||||
/>
|
/>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -469,12 +469,17 @@ const handleSetAdmin = async () => {
|
|||||||
roleCode: adminForm.value.roleCode.trim() || undefined,
|
roleCode: adminForm.value.roleCode.trim() || undefined,
|
||||||
});
|
});
|
||||||
const action = String(resp?.data?.action || "");
|
const action = String(resp?.data?.action || "");
|
||||||
ElMessage.success(
|
const setupLink = String(resp?.data?.setupLink || "");
|
||||||
action === "UPDATED"
|
const isUpdate = action === "UPDATED";
|
||||||
? "租户管理员更新成功,首次设密链接已发送到邮箱"
|
ElMessage.success(isUpdate ? "租户管理员更新成功" : "租户管理员创建成功");
|
||||||
: "租户管理员创建成功,首次设密链接已发送到邮箱",
|
|
||||||
);
|
|
||||||
adminDialogVisible.value = false;
|
adminDialogVisible.value = false;
|
||||||
|
if (setupLink) {
|
||||||
|
ElMessageBox.alert(
|
||||||
|
`<div style="word-break: break-all;">管理员设密链接:<br/><br/><a href="${setupLink}" target="_blank">${setupLink}</a></div>`,
|
||||||
|
"请保存并发送给管理员",
|
||||||
|
{ dangerouslyUseHTMLString: true, confirmButtonText: "确定" }
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInitBaseline = async (row: any) => {
|
const handleInitBaseline = async (row: any) => {
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<div class="setup-card">
|
<div class="setup-card">
|
||||||
<div class="setup-header">
|
<div class="setup-header">
|
||||||
<div class="setup-badge">首次设密</div>
|
<div class="setup-badge">首次设密</div>
|
||||||
<h1>租户管理员密码设置</h1>
|
<h1>账号密码设置</h1>
|
||||||
<p>完成一次性密码配置后即可前往登录页登录。</p>
|
<p>完成一次性密码配置后即可前往登录页登录。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -29,7 +29,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="setup-info">
|
<div class="setup-info">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span>管理员</span>
|
<span>用户姓名</span>
|
||||||
<strong>{{ verifyData.userName || "-" }}</strong>
|
<strong>{{ verifyData.userName || "-" }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
|
|||||||
@ -134,10 +134,6 @@
|
|||||||
<el-form-item label="手机号" prop="phone">
|
<el-form-item label="手机号" prop="phone">
|
||||||
<el-input v-model="form.phone" />
|
<el-input v-model="form.phone" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="密码" prop="password">
|
|
||||||
<el-input v-model="form.password" type="password" show-password />
|
|
||||||
<PasswordStrengthBar :password="form.password" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="邮箱">
|
<el-form-item label="邮箱">
|
||||||
<el-input v-model="form.email" />
|
<el-input v-model="form.email" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -307,7 +303,6 @@
|
|||||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||||
import PageContainer from "../../components/PageContainer.vue";
|
import PageContainer from "../../components/PageContainer.vue";
|
||||||
import QueryToolbar from "../../components/QueryToolbar.vue";
|
import QueryToolbar from "../../components/QueryToolbar.vue";
|
||||||
import PasswordStrengthBar from "../../components/PasswordStrengthBar.vue";
|
|
||||||
import { DRAWER_SIZE, DIALOG_WIDTH, LABEL_WIDTH } from "../../constants/ui";
|
import { DRAWER_SIZE, DIALOG_WIDTH, LABEL_WIDTH } from "../../constants/ui";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import type { FormInstance, FormRules } from "element-plus";
|
import type { FormInstance, FormRules } from "element-plus";
|
||||||
@ -384,7 +379,6 @@ const delegationForm = ref({
|
|||||||
const form = ref({
|
const form = ref({
|
||||||
userName: "测试用户",
|
userName: "测试用户",
|
||||||
phone: "13800000000",
|
phone: "13800000000",
|
||||||
password: "123456",
|
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
validFrom: "",
|
validFrom: "",
|
||||||
validTo: "",
|
validTo: "",
|
||||||
@ -399,7 +393,6 @@ const router = useRouter();
|
|||||||
const formRules = computed<FormRules>(() => ({
|
const formRules = computed<FormRules>(() => ({
|
||||||
userName: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
userName: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||||
phone: [{ required: true, message: "请输入手机号", trigger: "blur" }],
|
phone: [{ required: true, message: "请输入手机号", trigger: "blur" }],
|
||||||
password: [{ required: !isEditMode.value, message: "请输入密码", trigger: "blur" }],
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const delegationFormRules = reactive<FormRules>({
|
const delegationFormRules = reactive<FormRules>({
|
||||||
@ -429,7 +422,6 @@ const openCreateDrawer = () => {
|
|||||||
form.value = {
|
form.value = {
|
||||||
userName: "测试用户",
|
userName: "测试用户",
|
||||||
phone: "13800000000",
|
phone: "13800000000",
|
||||||
password: "123456",
|
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
validFrom: "",
|
validFrom: "",
|
||||||
validTo: "",
|
validTo: "",
|
||||||
@ -453,7 +445,6 @@ const openEditDrawer = (row: any) => {
|
|||||||
form.value = {
|
form.value = {
|
||||||
userName: row.userName,
|
userName: row.userName,
|
||||||
phone: row.phone,
|
phone: row.phone,
|
||||||
password: "",
|
|
||||||
email: row.email,
|
email: row.email,
|
||||||
validFrom: row.validFrom,
|
validFrom: row.validFrom,
|
||||||
validTo: row.validTo,
|
validTo: row.validTo,
|
||||||
@ -541,7 +532,7 @@ const downloadUserTemplate = () => {
|
|||||||
"validTo",
|
"validTo",
|
||||||
"roleCode",
|
"roleCode",
|
||||||
], [
|
], [
|
||||||
["张三", "13800138000", "Abc12345!", "zhangsan@example.com", "2026-04-20 09:00:00", "2099-12-31 23:59:59", "TENANT_ADMIN"],
|
["张三", "13800138000", "zhangsan@example.com", "2026-04-20 09:00:00", "2099-12-31 23:59:59", "TENANT_ADMIN"],
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -571,7 +562,7 @@ const parseUserImportRows = (text: string) => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [header, ...dataRows] = rows;
|
const [header, ...dataRows] = rows;
|
||||||
const missingHeaders = findMissingHeaders(header, ["userName", "phone", "password"]);
|
const missingHeaders = findMissingHeaders(header, ["userName", "phone"]);
|
||||||
if (missingHeaders.length) {
|
if (missingHeaders.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -584,17 +575,14 @@ const parseUserImportRows = (text: string) => {
|
|||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
userName: row[headerIndex.get("userName") ?? 0] || "",
|
userName: row[headerIndex.get("userName") ?? 0] || "",
|
||||||
phone: row[headerIndex.get("phone") ?? 1] || "",
|
phone: row[headerIndex.get("phone") ?? 1] || "",
|
||||||
password: row[headerIndex.get("password") ?? 2] || "",
|
email: row[headerIndex.get("email") ?? 2] || "",
|
||||||
email: row[headerIndex.get("email") ?? 3] || "",
|
validFrom: row[headerIndex.get("validFrom") ?? 3] || "",
|
||||||
validFrom: row[headerIndex.get("validFrom") ?? 4] || "",
|
validTo: row[headerIndex.get("validTo") ?? 4] || "",
|
||||||
validTo: row[headerIndex.get("validTo") ?? 5] || "",
|
roleCode: row[headerIndex.get("roleCode") ?? 5] || "",
|
||||||
roleCode: row[headerIndex.get("roleCode") ?? 6] || "",
|
|
||||||
}))
|
}))
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
...item,
|
|
||||||
userName: item.userName.trim(),
|
userName: item.userName.trim(),
|
||||||
phone: item.phone.trim(),
|
phone: item.phone.trim(),
|
||||||
password: item.password.trim(),
|
|
||||||
email: item.email.trim(),
|
email: item.email.trim(),
|
||||||
validFrom: item.validFrom.trim(),
|
validFrom: item.validFrom.trim(),
|
||||||
validTo: item.validTo.trim(),
|
validTo: item.validTo.trim(),
|
||||||
@ -605,10 +593,10 @@ const parseUserImportRows = (text: string) => {
|
|||||||
const validateUserImportHeaders = (text: string) => {
|
const validateUserImportHeaders = (text: string) => {
|
||||||
const rows = parseDelimitedText(text);
|
const rows = parseDelimitedText(text);
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
return { ok: false, missingHeaders: ["userName", "phone", "password"] };
|
return { ok: false, missingHeaders: ["userName", "phone"] };
|
||||||
}
|
}
|
||||||
const [header] = rows;
|
const [header] = rows;
|
||||||
const missingHeaders = findMissingHeaders(header, ["userName", "phone", "password"]);
|
const missingHeaders = findMissingHeaders(header, ["userName", "phone"]);
|
||||||
return { ok: missingHeaders.length === 0, missingHeaders };
|
return { ok: missingHeaders.length === 0, missingHeaders };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -649,7 +637,6 @@ const downloadUserErrorSnapshot = () => {
|
|||||||
const rows: string[][] = [[
|
const rows: string[][] = [[
|
||||||
"userName",
|
"userName",
|
||||||
"phone",
|
"phone",
|
||||||
"password",
|
|
||||||
"email",
|
"email",
|
||||||
"validFrom",
|
"validFrom",
|
||||||
"validTo",
|
"validTo",
|
||||||
@ -665,7 +652,6 @@ const downloadUserErrorSnapshot = () => {
|
|||||||
rows.push([
|
rows.push([
|
||||||
item.userName || "",
|
item.userName || "",
|
||||||
item.phone || "",
|
item.phone || "",
|
||||||
item.password || "",
|
|
||||||
item.email || "",
|
item.email || "",
|
||||||
item.validFrom || "",
|
item.validFrom || "",
|
||||||
item.validTo || "",
|
item.validTo || "",
|
||||||
@ -696,9 +682,6 @@ const handleSubmitUser = async () => {
|
|||||||
validFrom: form.value.validFrom,
|
validFrom: form.value.validFrom,
|
||||||
validTo: form.value.validTo,
|
validTo: form.value.validTo,
|
||||||
};
|
};
|
||||||
if (form.value.password) {
|
|
||||||
payload.password = form.value.password;
|
|
||||||
}
|
|
||||||
await updateUser(editingUserId.value, payload);
|
await updateUser(editingUserId.value, payload);
|
||||||
if (form.value.roleId) {
|
if (form.value.roleId) {
|
||||||
await assignUserRole({ userId: editingUserId.value, roleId: form.value.roleId });
|
await assignUserRole({ userId: editingUserId.value, roleId: form.value.roleId });
|
||||||
@ -708,7 +691,6 @@ const handleSubmitUser = async () => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
userName: form.value.userName,
|
userName: form.value.userName,
|
||||||
phone: form.value.phone,
|
phone: form.value.phone,
|
||||||
password: form.value.password,
|
|
||||||
email: form.value.email,
|
email: form.value.email,
|
||||||
validFrom: form.value.validFrom,
|
validFrom: form.value.validFrom,
|
||||||
validTo: form.value.validTo,
|
validTo: form.value.validTo,
|
||||||
@ -748,16 +730,13 @@ const handleDeleteUser = async (userId: number, userName: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleResetPassword = async (userId: number) => {
|
const handleResetPassword = async (userId: number) => {
|
||||||
const dialog = await ElMessageBox.prompt("请输入新密码", "重置密码", {
|
await ElMessageBox.confirm("确认要重置该用户的密码吗?系统将发送设密链接到该用户的邮箱。", "重置密码", {
|
||||||
confirmButtonText: "确定",
|
type: "warning",
|
||||||
|
confirmButtonText: "确认重置",
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
inputValue: "123456",
|
});
|
||||||
}).catch(() => null);
|
await resetUserPassword(userId, {});
|
||||||
if (!dialog) {
|
ElMessage.success("已发送密码重置邮件");
|
||||||
return;
|
|
||||||
}
|
|
||||||
await resetUserPassword(userId, { newPassword: dialog.value });
|
|
||||||
ElMessage.success("密码已重置");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewRoleHistory = async (userId: number) => {
|
const handleViewRoleHistory = async (userId: number) => {
|
||||||
|
|||||||
@ -470,9 +470,9 @@ const previewLaborInvoice = (row: Record<string, unknown>) => {
|
|||||||
<div class="audit-attachment-panel__head">劳务金额</div>
|
<div class="audit-attachment-panel__head">劳务金额</div>
|
||||||
<div class="audit-attachment-panel__body audit-amount-panel">
|
<div class="audit-attachment-panel__body audit-amount-panel">
|
||||||
<div class="audit-amount-panel__value">
|
<div class="audit-amount-panel__value">
|
||||||
{{ formatYuan(currentLaborPreTaxAmountCent) }} / {{ formatYuan(currentLaborAfterTaxAmountCent) }}
|
应发:{{ formatYuan(currentLaborPreTaxAmountCent) }} 元<br/><br/>实发:{{ formatYuan(currentLaborAfterTaxAmountCent) }} 元
|
||||||
</div>
|
</div>
|
||||||
<div class="audit-amount-panel__unit">元</div>
|
<!-- <div class="audit-amount-panel__unit">元</div> -->
|
||||||
<div class="audit-amount-panel__remark">
|
<div class="audit-amount-panel__remark">
|
||||||
{{ currentLaborRow.remark || "无备注" }}
|
{{ currentLaborRow.remark || "无备注" }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,7 +33,7 @@ defineEmits<{
|
|||||||
<el-descriptions-item label="会议形式">{{ currentMeetingDetail.meetingForm || "-" }}</el-descriptions-item>
|
<el-descriptions-item label="会议形式">{{ currentMeetingDetail.meetingForm || "-" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="会议地点">{{ currentMeetingDetail.location || "-" }}</el-descriptions-item>
|
<el-descriptions-item label="会议地点">{{ currentMeetingDetail.location || "-" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="会议时间">{{ currentMeetingDetail.startTime || "-" }} ~ {{ currentMeetingDetail.endTime || "-" }}</el-descriptions-item>
|
<el-descriptions-item label="会议时间">{{ currentMeetingDetail.startTime || "-" }} ~ {{ currentMeetingDetail.endTime || "-" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="预算(元)">{{ toYuan(currentMeetingDetail.budgetCent) }}</el-descriptions-item>
|
<el-descriptions-item label="预算(元)">{{ toYuan(currentMeetingDetail.budgetCent) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="劳务占比">{{ formatPercent(currentMeetingDetail.laborRatio) }}</el-descriptions-item>
|
<el-descriptions-item label="劳务占比">{{ formatPercent(currentMeetingDetail.laborRatio) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="餐费占比">{{ formatPercent(currentMeetingDetail.cateringRatio) }}</el-descriptions-item>
|
<el-descriptions-item label="餐费占比">{{ formatPercent(currentMeetingDetail.cateringRatio) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="会议状态">{{ toZhStatus(currentMeetingDetail.status) }}</el-descriptions-item>
|
<el-descriptions-item label="会议状态">{{ toZhStatus(currentMeetingDetail.status) }}</el-descriptions-item>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed, ref, watch, onBeforeUnmount } from "vue";
|
||||||
|
import { ZoomIn, ZoomOut, RefreshLeft, RefreshRight } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const visible = defineModel<boolean>({ required: true });
|
const visible = defineModel<boolean>({ required: true });
|
||||||
|
|
||||||
@ -16,6 +17,90 @@ const dialogTitle = computed(() => {
|
|||||||
if (kind.value === "excel") return "Excel 预览";
|
if (kind.value === "excel") return "Excel 预览";
|
||||||
return "图片预览";
|
return "图片预览";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 图片缩放和旋转状态
|
||||||
|
const scale = ref(1);
|
||||||
|
const rotation = ref(0);
|
||||||
|
const translateX = ref(0);
|
||||||
|
const translateY = ref(0);
|
||||||
|
|
||||||
|
// 拖拽相关状态
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const startX = ref(0);
|
||||||
|
const startY = ref(0);
|
||||||
|
|
||||||
|
// 当弹窗打开或图片更换时重置状态
|
||||||
|
watch([visible, () => props.docPreviewDialogImageUrl], ([newVisible]) => {
|
||||||
|
if (newVisible) {
|
||||||
|
scale.value = 1;
|
||||||
|
rotation.value = 0;
|
||||||
|
translateX.value = 0;
|
||||||
|
translateY.value = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleZoomIn = () => {
|
||||||
|
scale.value += 0.2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleZoomOut = () => {
|
||||||
|
if (scale.value > 0.2) {
|
||||||
|
scale.value -= 0.2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRotateLeft = () => {
|
||||||
|
rotation.value -= 90;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRotateRight = () => {
|
||||||
|
rotation.value += 90;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 鼠标滚轮缩放事件处理
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// 使用较小的步长以保证滚轮缩放的平滑度
|
||||||
|
const zoomStep = 0.1;
|
||||||
|
if (e.deltaY < 0) {
|
||||||
|
// 向上滚动,放大
|
||||||
|
scale.value += zoomStep;
|
||||||
|
} else {
|
||||||
|
// 向下滚动,缩小
|
||||||
|
if (scale.value > 0.2) {
|
||||||
|
scale.value -= zoomStep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 鼠标拖拽事件处理
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
isDragging.value = true;
|
||||||
|
startX.value = e.clientX - translateX.value;
|
||||||
|
startY.value = e.clientY - translateY.value;
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isDragging.value) return;
|
||||||
|
translateX.value = e.clientX - startX.value;
|
||||||
|
translateY.value = e.clientY - startY.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时清理事件监听器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -23,6 +108,7 @@ const dialogTitle = computed(() => {
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
width="80%"
|
width="80%"
|
||||||
|
top="5vh"
|
||||||
append-to-body
|
append-to-body
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
@ -33,12 +119,28 @@ const dialogTitle = computed(() => {
|
|||||||
class="doc-preview-pdf-frame"
|
class="doc-preview-pdf-frame"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="kind === 'excel'" class="doc-preview-excel-container" v-html="docPreviewExcelHtml"></div>
|
<div v-else-if="kind === 'excel'" class="doc-preview-excel-container" v-html="docPreviewExcelHtml"></div>
|
||||||
<div v-else-if="docPreviewDialogImageUrl" class="doc-preview-image-container">
|
<div v-else-if="docPreviewDialogImageUrl" class="doc-preview-image-wrapper">
|
||||||
<img
|
<div class="image-toolbar">
|
||||||
:src="docPreviewDialogImageUrl"
|
<el-button-group>
|
||||||
alt="preview"
|
<el-button :icon="ZoomIn" @click="handleZoomIn" title="放大" />
|
||||||
class="doc-preview-image"
|
<el-button :icon="ZoomOut" @click="handleZoomOut" title="缩小" />
|
||||||
/>
|
<el-button :icon="RefreshLeft" @click="handleRotateLeft" title="向左旋转" />
|
||||||
|
<el-button :icon="RefreshRight" @click="handleRotateRight" title="向右旋转" />
|
||||||
|
</el-button-group>
|
||||||
|
</div>
|
||||||
|
<div class="doc-preview-image-container" @wheel="handleWheel">
|
||||||
|
<img
|
||||||
|
:src="docPreviewDialogImageUrl"
|
||||||
|
alt="preview"
|
||||||
|
class="doc-preview-image"
|
||||||
|
@mousedown="handleMouseDown"
|
||||||
|
:style="{
|
||||||
|
transform: `translate(${translateX}px, ${translateY}px) scale(${scale}) rotate(${rotation}deg)`,
|
||||||
|
transition: isDragging ? 'none' : 'transform 0.3s ease',
|
||||||
|
cursor: isDragging ? 'grabbing' : 'grab'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@ -46,10 +148,21 @@ const dialogTitle = computed(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.doc-preview-pdf-frame {
|
.doc-preview-pdf-frame {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 75vh;
|
height: 80vh;
|
||||||
border: none;
|
border: none;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.doc-preview-image-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.image-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
.doc-preview-image-container {
|
.doc-preview-image-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -58,15 +171,20 @@ const dialogTitle = computed(() => {
|
|||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: 60vh;
|
||||||
|
max-height: 80vh;
|
||||||
}
|
}
|
||||||
.doc-preview-image {
|
.doc-preview-image {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 75vh;
|
max-height: 75vh;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
transform-origin: center center;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
.doc-preview-excel-container {
|
.doc-preview-excel-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 75vh;
|
height: 80vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #dcdfe6;
|
border: 1px solid #dcdfe6;
|
||||||
|
|||||||
@ -257,7 +257,7 @@ const handleSave = async () => {
|
|||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="预算(元)">
|
<el-form-item label="预算(元)">
|
||||||
<el-input-number v-model="meetingForm.budgetYuan" :min="0.01" :step="0.01" :precision="2" />
|
<el-input-number v-model="meetingForm.budgetYuan" :min="0.01" :step="0.01" :precision="2" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="劳务占比">
|
<el-form-item label="劳务占比">
|
||||||
|
|||||||
@ -12,6 +12,7 @@ const props = defineProps<{
|
|||||||
finishedAt?: string;
|
finishedAt?: string;
|
||||||
}>;
|
}>;
|
||||||
canCreate: boolean;
|
canCreate: boolean;
|
||||||
|
canEdit?: boolean;
|
||||||
canSubmit: boolean;
|
canSubmit: boolean;
|
||||||
canSubmitMeeting: (row: any) => boolean;
|
canSubmitMeeting: (row: any) => boolean;
|
||||||
canWithdraw: boolean;
|
canWithdraw: boolean;
|
||||||
@ -90,12 +91,12 @@ const summaryStatusText = (rowId: number, statusFormatter: (row: any, column: an
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<!-- <el-table-column prop="currentAuditNode" label="当前审核节点" width="160" :formatter="auditNodeFormatter" />
|
<!-- <el-table-column prop="currentAuditNode" label="当前审核节点" width="160" :formatter="auditNodeFormatter" />
|
||||||
<el-table-column prop="currentAuditorUserId" label="当前审核节点" width="160" :formatter="auditorNameFormatter" /> -->
|
<el-table-column prop="currentAuditorUserId" label="当前审核节点" width="160" :formatter="auditorNameFormatter" /> -->
|
||||||
<el-table-column label="操作" width="320">
|
<el-table-column label="操作" width="320" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<el-button size="small" @click="$emit('openDetail', row)">详情</el-button>
|
<el-button size="small" @click="$emit('openDetail', row)">详情</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="canCreate"
|
v-if="canEdit"
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="isMeetingAuditApproved(row)"
|
:disabled="isMeetingAuditApproved(row)"
|
||||||
@ -148,24 +149,6 @@ const summaryStatusText = (rowId: number, statusFormatter: (row: any, column: an
|
|||||||
<el-dropdown-item v-if="canMaterialExport" divided @click="$emit('generateSummary', row.id)">
|
<el-dropdown-item v-if="canMaterialExport" divided @click="$emit('generateSummary', row.id)">
|
||||||
生成总结
|
生成总结
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item v-if="canMaterialExport" @click="$emit('checkSummaryStatus', row.id)">
|
|
||||||
查看总结状态
|
|
||||||
{{ summaryTaskMap[row.id]?.taskId ? `(${summaryStatusText(row.id, statusFormatter, summaryTaskMap)})` : "" }}
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item
|
|
||||||
v-if="canMaterialExport"
|
|
||||||
:disabled="!summaryTaskMap[row.id]?.taskId || summaryTaskMap[row.id]?.status !== 'SUCCESS'"
|
|
||||||
@click="$emit('refreshSummaryToken', row.id)"
|
|
||||||
>
|
|
||||||
刷新总结令牌
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item
|
|
||||||
v-if="canMaterialExport"
|
|
||||||
:disabled="summaryTaskMap[row.id]?.status !== 'SUCCESS' || !summaryTaskMap[row.id]?.taskId || !summaryTaskMap[row.id]?.token"
|
|
||||||
@click="$emit('downloadSummary', row.id)"
|
|
||||||
>
|
|
||||||
下载总结
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
|
|||||||
@ -677,9 +677,17 @@ watch(
|
|||||||
<div class="budget-item"><span class="text-secondary">已用额度</span> <span>¥ {{ toYuan(materialBudgetSummary.usedTotalCent) }}</span></div>
|
<div class="budget-item"><span class="text-secondary">已用额度</span> <span>¥ {{ toYuan(materialBudgetSummary.usedTotalCent) }}</span></div>
|
||||||
<div class="budget-divider"></div>
|
<div class="budget-divider"></div>
|
||||||
<div class="budget-item" :class="materialBudgetSummary.overCent > 0 ? 'text-danger fw-bold' : 'text-success fw-bold'">
|
<div class="budget-item" :class="materialBudgetSummary.overCent > 0 ? 'text-danger fw-bold' : 'text-success fw-bold'">
|
||||||
<span>{{ materialBudgetSummary.overCent > 0 ? '超发预算' : '剩余额度' }}</span>
|
<span>{{ materialBudgetSummary.overCent > 0 ? '超发预算' : '剩余总额度' }}</span>
|
||||||
<span>¥ {{ toYuan(materialBudgetSummary.overCent > 0 ? materialBudgetSummary.overCent : materialBudgetSummary.remainCent) }}</span>
|
<span>¥ {{ toYuan(materialBudgetSummary.overCent > 0 ? materialBudgetSummary.overCent : materialBudgetSummary.remainCent) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="budget-item" :class="materialBudgetSummary.remainLaborCent < 0 ? 'text-danger fw-bold' : 'text-success fw-bold'">
|
||||||
|
<span>剩余劳务费额度</span>
|
||||||
|
<span>¥ {{ toYuan(materialBudgetSummary.remainLaborCent) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="budget-item" :class="materialBudgetSummary.remainCateringCent < 0 ? 'text-danger fw-bold' : 'text-success fw-bold'">
|
||||||
|
<span>剩余餐费额度</span>
|
||||||
|
<span>¥ {{ toYuan(materialBudgetSummary.remainCateringCent) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="materialBudgetLoading" class="sidebar-budget-card sidebar-budget-card--loading">
|
<div v-else-if="materialBudgetLoading" class="sidebar-budget-card sidebar-budget-card--loading">
|
||||||
<div class="budget-title">预算看板</div>
|
<div class="budget-title">预算看板</div>
|
||||||
@ -1657,7 +1665,7 @@ watch(
|
|||||||
:on-change="(f, l) => handleUploadChangeWithCompression(f, l, buildMeetingInvoiceUploadChangeHandler(section.code, uploadField.key))"
|
:on-change="(f, l) => handleUploadChangeWithCompression(f, l, buildMeetingInvoiceUploadChangeHandler(section.code, uploadField.key))"
|
||||||
:on-preview="handleMeetingInvoiceSectionPicturePreview"
|
:on-preview="handleMeetingInvoiceSectionPicturePreview"
|
||||||
:on-remove="buildMeetingInvoicePictureRemoveHandler(section.code, uploadField.key)"
|
:on-remove="buildMeetingInvoicePictureRemoveHandler(section.code, uploadField.key)"
|
||||||
accept=".jpg,.jpeg,.png,.webp,.gif,.bmp,.pdf"
|
:accept="uploadField.key === 'settlementFile' ? '.jpg,.jpeg,.png,.webp,.gif,.bmp,.pdf,.xls,.xlsx' : '.jpg,.jpeg,.png,.webp,.gif,.bmp,.pdf'"
|
||||||
>
|
>
|
||||||
<template #file="{ file }">
|
<template #file="{ file }">
|
||||||
<MaterialPictureCardFileItem
|
<MaterialPictureCardFileItem
|
||||||
@ -2121,6 +2129,8 @@ watch(
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-module-card {
|
.material-module-card {
|
||||||
@ -2136,6 +2146,10 @@ watch(
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Segmented Control */
|
/* Custom Segmented Control */
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import MeetingQueryToolbar from './MeetingQueryToolbar.vue';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
|
||||||
|
describe('MeetingQueryToolbar.vue', () => {
|
||||||
|
it('should render form items properly', () => {
|
||||||
|
const wrapper = mount(MeetingQueryToolbar, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus]
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
queryForm: {
|
||||||
|
projectName: '',
|
||||||
|
topic: '',
|
||||||
|
meetingStatus: '',
|
||||||
|
auditStatus: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('项目名称');
|
||||||
|
expect(wrapper.text()).toContain('会议主题');
|
||||||
|
expect(wrapper.text()).toContain('会议状态');
|
||||||
|
expect(wrapper.text()).toContain('审核状态');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit load event when search button is clicked', async () => {
|
||||||
|
const wrapper = mount(MeetingQueryToolbar, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus]
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
queryForm: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = wrapper.findAll('.el-button');
|
||||||
|
const searchBtn = buttons.find(b => b.text().includes('查询'));
|
||||||
|
expect(searchBtn).toBeDefined();
|
||||||
|
|
||||||
|
await searchBtn!.trigger('click');
|
||||||
|
expect(wrapper.emitted()).toHaveProperty('load');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit resetQuery event when reset button is clicked', async () => {
|
||||||
|
const wrapper = mount(MeetingQueryToolbar, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus]
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
queryForm: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = wrapper.findAll('.el-button');
|
||||||
|
const resetBtn = buttons.find(b => b.text().includes('重置'));
|
||||||
|
expect(resetBtn).toBeDefined();
|
||||||
|
|
||||||
|
await resetBtn!.trigger('click');
|
||||||
|
expect(wrapper.emitted()).toHaveProperty('resetQuery');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -106,9 +106,9 @@
|
|||||||
<el-form-item v-if="projectForm.allowProjectOverBudget" label="超支阈值">
|
<el-form-item v-if="projectForm.allowProjectOverBudget" label="超支阈值">
|
||||||
<el-input-number v-model="projectForm.overBudgetThresholdRatio" :min="0" :max="1" :step="0.01" class="project-number-input" />
|
<el-input-number v-model="projectForm.overBudgetThresholdRatio" :min="0" :max="1" :step="0.01" class="project-number-input" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="发票信息">
|
<!-- <el-form-item label="发票信息">
|
||||||
<el-input v-model="projectForm.invoiceInfo" />
|
<el-input v-model="projectForm.invoiceInfo" />
|
||||||
</el-form-item>
|
</el-form-item> -->
|
||||||
<el-form-item label="主办单位负责人">
|
<el-form-item label="主办单位负责人">
|
||||||
<el-input :model-value="projectForm.hostOwnerUsers || '系统自动取TENANT_ADMIN'" disabled />
|
<el-input :model-value="projectForm.hostOwnerUsers || '系统自动取TENANT_ADMIN'" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
|
/// <reference types="vitest" />
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
test: {
|
||||||
|
environment: "happy-dom"
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user