diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java index 25d8181..5a5033e 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java @@ -64,7 +64,7 @@ public interface AppletApiTTService { */ boolean handleLongTextTtsCallback(String taskId, String resultUrl, Long status, String statusStr, String subtitlesJson); - Map selectMapByHtml(String content); + Map selectMapByHtml(String textHash); /** * 根据内容哈希码获取音频构建状态 diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiIndexServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiIndexServiceImpl.java index 6833e09..5ed79bb 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiIndexServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiIndexServiceImpl.java @@ -118,7 +118,7 @@ public class AppletApiIndexServiceImpl implements AppletApiIndexService { } //查询音频 - Map map = appletApiTTService.selectMapByHtml(byId.getContent()); + Map map = appletApiTTService.selectMapByHtml(byId.getContentHashcode()); byId.setAudios(map); return byId; diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java index dbf31a3..c66d478 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java @@ -654,11 +654,8 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { } @Override - public Map selectMapByHtml(String content) { + public Map selectMapByHtml(String textHash) { HashMap map = new HashMap<>(); - - String textHash = HtmlUtils.calculateStringHash(HtmlUtils.stripHtml(content)); - List list = appletTtsTimbreService .lambdaQuery() .select(AppletTtsTimbre::getVoiceType) @@ -668,10 +665,10 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { AppletTtsCache one = appletTtsCacheService.lambdaQuery() .eq(AppletTtsCache::getText, textHash) .eq(AppletTtsCache::getVoiceType, timbre.getVoiceType()) - .select(AppletTtsCache::getAudioId) + .select(AppletTtsCache::getAudioId, AppletTtsCache::getId) .one(); - map.put(timbre.getVoiceType(), one != null ? one.getAudioId() : null); + map.put(timbre.getVoiceType(), one); } return map; @@ -866,7 +863,7 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { .lambdaQuery() .eq(AppletTtsCache::getText, contentHashcode) // .eq(AppletTtsCache::getSuccess, "Y") -// .eq(AppletTtsCache::getState, 1) + .eq(AppletTtsCache::getState, 1) .count(); result.put("total", total); diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/util/HtmlUtils.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/util/HtmlUtils.java index a12de8d..9d6616b 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/util/HtmlUtils.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/util/HtmlUtils.java @@ -44,6 +44,7 @@ public class HtmlUtils { HTML_ENTITIES.put("–", "–"); HTML_ENTITIES.put("‘", "'"); HTML_ENTITIES.put("’", "'"); + HTML_ENTITIES.put("·", "'"); } /** @@ -230,17 +231,12 @@ public class HtmlUtils { // 移除HTML标签 String noTag = html.replaceAll("<[^>]*>", ""); // 处理常见的HTML实体 - noTag = noTag.replace("”", " "); - noTag = noTag.replace("—", " "); - noTag = noTag.replace("“", " "); - noTag = noTag.replace("·", " "); - noTag = noTag.replace(" ", " "); - noTag = noTag.replace("&", "&"); - noTag = noTag.replace("<", "<"); - noTag = noTag.replace(">", ">"); - noTag = noTag.replace(""", "\""); - // 清理多余空白 - return noTag.replaceAll("\\s+", " ").trim(); + + for (String key : HTML_ENTITIES.keySet()) { + noTag = noTag.replace(key, HTML_ENTITIES.get(key)); + } + + return noTag; } catch (Exception e) { return html; } diff --git a/jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue b/jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue index 70d0629..e8ddae6 100644 --- a/jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue +++ b/jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue @@ -82,7 +82,18 @@ - + +
+ {{ getWeightedLength(component.content) }}/290 +
@@ -168,6 +179,10 @@ const { createMessage } = useMessage(); const componentsRef = ref([]); +// 中文判断与加权长度(中文算2,其它算1) +const isChinese = (ch: string) => /[\u4e00-\u9fa5]/.test(ch); +const getWeightedLength = (str: string = '') => Array.from(str).reduce((sum, ch) => sum + (isChinese(ch) ? 2 : 1), 0); + watch( () => props.components, (val) => { @@ -483,4 +498,14 @@ function setLead(component: any, val: boolean) { margin-bottom: 16px; color: #d9d9d9; } + +.field-count { + text-align: right; + font-size: 12px; + color: #999; + margin-top: 4px; +} +.field-count.exceed { + color: #faad14; /* 轻提示色,不做强制校验 */ +} \ No newline at end of file diff --git a/jeecgboot-vue3/src/views/applet/course-page/components/WordTable.vue b/jeecgboot-vue3/src/views/applet/course-page/components/WordTable.vue index 171b1ed..a0261fe 100644 --- a/jeecgboot-vue3/src/views/applet/course-page/components/WordTable.vue +++ b/jeecgboot-vue3/src/views/applet/course-page/components/WordTable.vue @@ -81,7 +81,10 @@ :wrapper-col="{ span: 18 }" > - + +
+ {{ wordLen }}/30 +
@@ -98,9 +101,12 @@ +
+ {{ paraphraseLen }}/240 +
@@ -126,6 +132,45 @@ import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vu import { JImageUpload } from '/@/components/Form' import { list, saveOrUpdate, deleteOne, batchDelete } from './coursePageWord' +// 更稳妥的中文判断(常用区) +const isChinese = (ch: string) => /[\u4e00-\u9fa5]/.test(ch) + +// 计算加权长度:中文按2字符计算,其它按1 +const weightedLength = (str: string = ''): number => { + return Array.from(str).reduce((sum, ch) => sum + (isChinese(ch) ? 2 : 1), 0) +} + +// 截断到指定加权长度 +const truncateByWeightedLen = (str: string = '', max: number): string => { +// let acc = 0 +// let out = '' +// for (const ch of Array.from(str)) { +// const w = isChinese(ch) ? 2 : 1 +// if (acc + w > max) break +// acc += w +// out += ch +// } +// return out + return str +} + +// 自定义表单校验:限制加权长度(中文算2) +const createWeightedMaxRule = (max: number) => ({ + validator: async (_: any, value: string) => { + if (!value) return Promise.resolve() + const len = Array.from(value).reduce((sum, ch) => sum + (isChinese(ch) ? 2 : 1), 0) + if (len > max) { + return Promise.reject(`长度不能超过${max}字符(中文算2字符)`) + } + return Promise.resolve() + }, + trigger: ['change', 'blur'], +}) + +// 加权长度计数(中文算2) +const wordLen = computed(() => weightedLength(formData.word || '')) +const paraphraseLen = computed(() => weightedLength(formData.paraphrase || '')) + interface WordRecord { id?: string word: string @@ -209,11 +254,11 @@ const formData = reactive({ const formRules = { word: [ { required: true, message: '请输入单词', trigger: 'blur' }, - { max: 50, message: '单词长度不能超过50个字符', trigger: 'blur' }, + createWeightedMaxRule(30), ], paraphrase: [ { required: true, message: '请输入释义', trigger: 'blur' }, - { max: 500, message: '释义长度不能超过500个字符', trigger: 'blur' }, + createWeightedMaxRule(240), ], soundmark: [ { max: 100, message: '音标长度不能超过100个字符', trigger: 'blur' }, @@ -312,6 +357,17 @@ const handleBatchDelete = async () => { const handleModalOk = async () => { try { await formRef.value.validate() + // 二次校验:加权长度超限则阻止保存 + const wWord = weightedLength(formData.word || '') + const wPara = weightedLength(formData.paraphrase || '') + if (wWord > 30) { + message.error('单词长度不能超过30字符(中文算2字符)') + return + } + if (wPara > 240) { + message.error('释义长度不能超过240字符(中文算2字符)') + return + } confirmLoading.value = true const params = { @@ -377,6 +433,27 @@ watch(() => props.coursePageId, (newId) => { } }, { immediate: true }) +// 实时限制加权长度(中文算2):标题30,释义240 +watch(() => formData.word, (val) => { + const max = 30 + if (!val) return + const trimmed = truncateByWeightedLen(val, max) + if (trimmed !== val) { + formData.word = trimmed + message.warning(`标题最多${max}字符(中文算2字符)`) + } +}) + +watch(() => formData.paraphrase, (val) => { + const max = 240 + if (!val) return + const trimmed = truncateByWeightedLen(val, max) + if (trimmed !== val) { + formData.paraphrase = trimmed + message.warning(`释义最多${max}字符(中文算2字符)`) + } +}) + // 图片加载错误处理 const handleImageError = (event: Event) => { const img = event.target as HTMLImageElement @@ -458,4 +535,11 @@ defineExpose({ :deep(.ant-form-item-label > label) { font-weight: 500; } + +.field-count { + text-align: right; + font-size: 12px; + color: #999; + margin-top: 4px; +} \ No newline at end of file