Browse Source

feat(文本处理): 优化HTML实体处理和字符长度计算

refactor(音频查询): 使用内容哈希码替代原始内容查询音频
style(输入限制): 添加字符长度提示和中文加权计算
fix(状态查询): 修正音频缓存状态查询条件
master
前端-胡立永 3 days ago
parent
commit
5a84da023b
6 changed files with 127 additions and 25 deletions
  1. +1
    -1
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java
  2. +1
    -1
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiIndexServiceImpl.java
  3. +4
    -7
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java
  4. +7
    -11
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/util/HtmlUtils.java
  5. +26
    -1
      jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue
  6. +88
    -4
      jeecgboot-vue3/src/views/applet/course-page/components/WordTable.vue

+ 1
- 1
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java View File

@ -64,7 +64,7 @@ public interface AppletApiTTService {
*/ */
boolean handleLongTextTtsCallback(String taskId, String resultUrl, Long status, String statusStr, String subtitlesJson); boolean handleLongTextTtsCallback(String taskId, String resultUrl, Long status, String statusStr, String subtitlesJson);
Map<Integer, Object> selectMapByHtml(String content);
Map<Integer, Object> selectMapByHtml(String textHash);
/** /**
* 根据内容哈希码获取音频构建状态 * 根据内容哈希码获取音频构建状态


+ 1
- 1
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiIndexServiceImpl.java View File

@ -118,7 +118,7 @@ public class AppletApiIndexServiceImpl implements AppletApiIndexService {
} }
//查询音频 //查询音频
Map<Integer, Object> map = appletApiTTService.selectMapByHtml(byId.getContent());
Map<Integer, Object> map = appletApiTTService.selectMapByHtml(byId.getContentHashcode());
byId.setAudios(map); byId.setAudios(map);
return byId; return byId;


+ 4
- 7
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java View File

@ -654,11 +654,8 @@ public class AppletApiTTServiceImpl implements AppletApiTTService {
} }
@Override @Override
public Map<Integer, Object> selectMapByHtml(String content) {
public Map<Integer, Object> selectMapByHtml(String textHash) {
HashMap<Integer, Object> map = new HashMap<>(); HashMap<Integer, Object> map = new HashMap<>();
String textHash = HtmlUtils.calculateStringHash(HtmlUtils.stripHtml(content));
List<AppletTtsTimbre> list = appletTtsTimbreService List<AppletTtsTimbre> list = appletTtsTimbreService
.lambdaQuery() .lambdaQuery()
.select(AppletTtsTimbre::getVoiceType) .select(AppletTtsTimbre::getVoiceType)
@ -668,10 +665,10 @@ public class AppletApiTTServiceImpl implements AppletApiTTService {
AppletTtsCache one = appletTtsCacheService.lambdaQuery() AppletTtsCache one = appletTtsCacheService.lambdaQuery()
.eq(AppletTtsCache::getText, textHash) .eq(AppletTtsCache::getText, textHash)
.eq(AppletTtsCache::getVoiceType, timbre.getVoiceType()) .eq(AppletTtsCache::getVoiceType, timbre.getVoiceType())
.select(AppletTtsCache::getAudioId)
.select(AppletTtsCache::getAudioId, AppletTtsCache::getId)
.one(); .one();
map.put(timbre.getVoiceType(), one != null ? one.getAudioId() : null);
map.put(timbre.getVoiceType(), one);
} }
return map; return map;
@ -866,7 +863,7 @@ public class AppletApiTTServiceImpl implements AppletApiTTService {
.lambdaQuery() .lambdaQuery()
.eq(AppletTtsCache::getText, contentHashcode) .eq(AppletTtsCache::getText, contentHashcode)
// .eq(AppletTtsCache::getSuccess, "Y") // .eq(AppletTtsCache::getSuccess, "Y")
// .eq(AppletTtsCache::getState, 1)
.eq(AppletTtsCache::getState, 1)
.count(); .count();
result.put("total", total); result.put("total", total);


+ 7
- 11
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/util/HtmlUtils.java View File

@ -44,6 +44,7 @@ public class HtmlUtils {
HTML_ENTITIES.put("&ndash;", "–"); HTML_ENTITIES.put("&ndash;", "–");
HTML_ENTITIES.put("&lsquo;", "'"); HTML_ENTITIES.put("&lsquo;", "'");
HTML_ENTITIES.put("&rsquo;", "'"); HTML_ENTITIES.put("&rsquo;", "'");
HTML_ENTITIES.put("&middot;", "'");
} }
/** /**
@ -230,17 +231,12 @@ public class HtmlUtils {
// 移除HTML标签 // 移除HTML标签
String noTag = html.replaceAll("<[^>]*>", ""); String noTag = html.replaceAll("<[^>]*>", "");
// 处理常见的HTML实体 // 处理常见的HTML实体
noTag = noTag.replace("&rdquo;", " ");
noTag = noTag.replace("&mdash;", " ");
noTag = noTag.replace("&ldquo;", " ");
noTag = noTag.replace("&middot;", " ");
noTag = noTag.replace("&nbsp;", " ");
noTag = noTag.replace("&amp;", "&");
noTag = noTag.replace("&lt;", "<");
noTag = noTag.replace("&gt;", ">");
noTag = noTag.replace("&quot;", "\"");
// 清理多余空白
return noTag.replaceAll("\\s+", " ").trim();
for (String key : HTML_ENTITIES.keySet()) {
noTag = noTag.replace(key, HTML_ENTITIES.get(key));
}
return noTag;
} catch (Exception e) { } catch (Exception e) {
return html; return html;
} }


+ 26
- 1
jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue View File

@ -82,7 +82,18 @@
<a-switch :checked="!!component.isLead" @change="(checked)=>setLead(component, checked)" /> <a-switch :checked="!!component.isLead" @change="(checked)=>setLead(component, checked)" />
</div> </div>
</div> </div>
<a-textarea v-model:value="component.content" :rows="4" placeholder="请输入文本内容" @change="emitUpdate" />
<a-textarea
v-model:value="component.content"
:rows="4"
placeholder="请输入文本内容(建议不超过290字符,中文算2)"
@change="emitUpdate"
/>
<div
class="field-count"
:class="{ exceed: getWeightedLength(component.content) > 290 }"
>
{{ getWeightedLength(component.content) }}/290
</div>
</div> </div>
<!-- 图片组件 --> <!-- 图片组件 -->
@ -168,6 +179,10 @@ const { createMessage } = useMessage();
const componentsRef = ref<any[]>([]); const componentsRef = ref<any[]>([]);
// 21
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( watch(
() => props.components, () => props.components,
(val) => { (val) => {
@ -483,4 +498,14 @@ function setLead(component: any, val: boolean) {
margin-bottom: 16px; margin-bottom: 16px;
color: #d9d9d9; color: #d9d9d9;
} }
.field-count {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 4px;
}
.field-count.exceed {
color: #faad14; /* 轻提示色,不做强制校验 */
}
</style> </style>

+ 88
- 4
jeecgboot-vue3/src/views/applet/course-page/components/WordTable.vue View File

@ -81,7 +81,10 @@
:wrapper-col="{ span: 18 }" :wrapper-col="{ span: 18 }"
> >
<a-form-item label="单词" name="word"> <a-form-item label="单词" name="word">
<a-input v-model:value="formData.word" placeholder="请输入单词" />
<a-input v-model:value="formData.word" placeholder="请输入单词(最多30字符,中文算2)" />
<div class="field-count">
{{ wordLen }}/30
</div>
</a-form-item> </a-form-item>
<a-form-item label="图片" name="image"> <a-form-item label="图片" name="image">
@ -98,9 +101,12 @@
<a-form-item label="释义" name="paraphrase"> <a-form-item label="释义" name="paraphrase">
<a-textarea <a-textarea
v-model:value="formData.paraphrase" v-model:value="formData.paraphrase"
placeholder="请输入释义"
placeholder="请输入释义(最多240字符,中文算2)"
:rows="3" :rows="3"
/> />
<div class="field-count">
{{ paraphraseLen }}/240
</div>
</a-form-item> </a-form-item>
<a-form-item label="音标" name="soundmark"> <a-form-item label="音标" name="soundmark">
@ -126,6 +132,45 @@ import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vu
import { JImageUpload } from '/@/components/Form' import { JImageUpload } from '/@/components/Form'
import { list, saveOrUpdate, deleteOne, batchDelete } from './coursePageWord' import { list, saveOrUpdate, deleteOne, batchDelete } from './coursePageWord'
//
const isChinese = (ch: string) => /[\u4e00-\u9fa5]/.test(ch)
// 21
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 { interface WordRecord {
id?: string id?: string
word: string word: string
@ -209,11 +254,11 @@ const formData = reactive<WordRecord>({
const formRules = { const formRules = {
word: [ word: [
{ required: true, message: '请输入单词', trigger: 'blur' }, { required: true, message: '请输入单词', trigger: 'blur' },
{ max: 50, message: '单词长度不能超过50个字符', trigger: 'blur' },
createWeightedMaxRule(30),
], ],
paraphrase: [ paraphrase: [
{ required: true, message: '请输入释义', trigger: 'blur' }, { required: true, message: '请输入释义', trigger: 'blur' },
{ max: 500, message: '释义长度不能超过500个字符', trigger: 'blur' },
createWeightedMaxRule(240),
], ],
soundmark: [ soundmark: [
{ max: 100, message: '音标长度不能超过100个字符', trigger: 'blur' }, { max: 100, message: '音标长度不能超过100个字符', trigger: 'blur' },
@ -312,6 +357,17 @@ const handleBatchDelete = async () => {
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {
await formRef.value.validate() 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 confirmLoading.value = true
const params = { const params = {
@ -377,6 +433,27 @@ watch(() => props.coursePageId, (newId) => {
} }
}, { immediate: true }) }, { immediate: true })
// 230240
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 handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement const img = event.target as HTMLImageElement
@ -458,4 +535,11 @@ defineExpose({
:deep(.ant-form-item-label > label) { :deep(.ant-form-item-label > label) {
font-weight: 500; font-weight: 500;
} }
.field-count {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 4px;
}
</style> </style>

Loading…
Cancel
Save