chore: 更新素材和组件优化

- 更新展品图片(鲁迅/毛泽东/孙中山/黄炎培/聂耳/张乐平故居)
- 新增点击查看图标、报纸素材
- 优化视频组件、拼图组件、展品组件
- 更新陈云音频资源
- 新增输入验证文档和工具函数
- 组件布局和动画优化

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhongyidianandClaude Opus 4.8 committed 2026-06-21 21:50:40 +08:00
1 parent 73d02eb62d
commit 73f6df92ce
130 files changed
+2345 -759

No files matched your search

+101 -212
View File
@@ -1,16 +1,10 @@
import { useUserStore } from '@/store/userStore'
//超时时间
const TIMEOUT_MS = 1000 * 10
const TIMEOUT_MS = 1000 * 6
// 声明外部依赖(由运行环境提供)
declare const $: any
declare const iscallyb: boolean
declare const emykey: string
declare function videoAjax(): VideoAjax
declare function getFutureDa(days: number): string
declare function getCurrentDa(str: string): void
declare function formatDateFromMilliseconds(date: Date): string
interface UserDefineParam {
lessonId: string
@@ -54,113 +48,57 @@ interface VideoAjax {
finishCourse(param: { order: number; co: string; process: number }): void
}
// 扩展 jQuery,获取 URL 参数
$.extend({
getUrlVars(): string[] & Record<string, string | undefined> {
const vars: any[] & Record<string, string | undefined> = [] as any
let hash: string[]
const queryIndex = window.location.href.indexOf('?')
if (queryIndex === -1) return vars
const hashes = window.location.href
.slice(queryIndex + 1)
.split('&')
.filter(Boolean)
for (let i = 0; i < hashes.length; i++) {
const item = hashes[i]
if (!item) continue
hash = item.split('=')
const keyRaw = hash[0] ?? ''
const valueRaw = hash[1] ?? ''
const key = decodeURIComponent(keyRaw)
const value = decodeURIComponent(valueRaw)
vars.push(key)
vars[key] = value
}
return vars
},
getUrlVar(name: string): string | undefined {
return ($ as any).getUrlVars()[name]
},
})
const LESSON_PREFIX = 'hsgrl'
export type StorageBackend = 'auto' | 'yiban' | 'session'
export interface StorageOptions {
backend?: StorageBackend
}
const getStorageKey = (lessonId: string | number, key: string | number) =>
`${LESSON_PREFIX}_${lessonId}_${key}`
const resolveStorageBackend = (backend: StorageBackend = 'auto'): 'yiban' | 'session' => {
if (backend === 'yiban' || backend === 'session') return backend
return useUserStore.getState().yibanConnectionStatus === 'connected' ? 'yiban' : 'session'
}
const videoajax: VideoAjax = videoAjax()
videoajax.setHost('/')
// 设置我的自定义数据(所有的自定义数据都设置在第一个课时ID上,以key来区分不同的类型)
function setLtDefine(key: string | number, value: string): void {
if (iscallyb) {
setUserDefine(1, key, value)
} else {
sessionStorage.setItem(`${LESSON_PREFIX}_${key}`, value)
}
}
let mycstr9 = ''
// 获取定义数据(由于底层是异步接口,这里返回 Promise)
async function getLtDefine(key: string | number): Promise<string> {
if (iscallyb) {
if (mycstr9 === '') {
const all = await getUsersDefine(1, emykey)
if (all === undefined || all === null || all === 'null' || all === '') {
mycstr9 = getFutureDa(10)
setLtDefine(emykey, mycstr9)
} else {
mycstr9 = all as string // 这里是 emykey,对应上面 getUsersDefine 的 string 分支
}
}
const str = mycstr9 === undefined || mycstr9 === null || mycstr9 === 'null' ? '' : mycstr9
getCurrentDa(str)
const objitem = await getUserDefine(1, key)
return objitem === undefined || objitem === null || objitem === 'null' ? '' : objitem
} else {
const itemvalue = sessionStorage.getItem(`${LESSON_PREFIX}_${key}`)
return itemvalue === undefined || itemvalue === null || itemvalue === 'null' ? '' : itemvalue
}
}
// 设置课程笔记10002
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function setCourseNotepad(collect: string[]): void {
// 保持和原逻辑一致:序列化为 JSON 存储
setLtDefine(10002, $.toJSON(collect))
}
// 获取课程笔记10002(异步)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function getCourseNotepad(): Promise<string[]> {
const raw = await getLtDefine(10002)
if (raw === undefined || raw === null || raw === '') {
// 原逻辑:返回长度为 9 的空字符串数组
return ['', '', '', '', '', '', '', '', '']
}
return $.parseJSON(raw) as string[]
}
// lessonId(课时唯一ID)
export function setUserDefine(
lessonId: string | number,
key: string | number,
value: string
value: string,
options: StorageOptions = {}
): void {
const backend = resolveStorageBackend(options.backend)
if (backend === 'session') {
sessionStorage.setItem(getStorageKey(lessonId, key), value)
return
}
const param: UserDefineParam = {
lessonId: `${LESSON_PREFIX}_${lessonId}`,
key,
content: value,
}
console.log(param)
videoajax.addUserDefine(param)
}
// 内部:将回调形式封装为 Promise
function getUserDefineResultAsyncInternal(
lessonId: string | number,
key: string | number
key: string | number,
options: StorageOptions = {}
): Promise<UserDefineFetchResult> {
if (!iscallyb) {
const itemValue = sessionStorage.getItem(`${LESSON_PREFIX}_${lessonId}_${key}`)
const backend = resolveStorageBackend(options.backend)
if (backend === 'session') {
const itemValue = sessionStorage.getItem(getStorageKey(lessonId, key))
return Promise.resolve(
itemValue === undefined || itemValue === null || itemValue === 'null' || itemValue === ''
? { status: 'empty', content: '' }
@@ -175,149 +113,100 @@ function getUserDefineResultAsyncInternal(
return new Promise((resolve) => {
let settled = false
const done = (result: UserDefineFetchResult) => {
if (settled) return
settled = true
resolve(result)
}
const timeout = setTimeout(() => {
console.warn(`getUserDefine timeout: lessonId=${lessonId}, key=${key}`)
done({ status: 'error', content: '', message: '学习进度获取超时' })
}, TIMEOUT_MS)
videoajax.getUserDefineInfoByUser(param, (res) => {
const done = (result: UserDefineFetchResult) => {
if (settled) return
settled = true
clearTimeout(timeout)
console.log(res)
resolve(result)
}
if (res === undefined || res === null || res === ('null' as unknown)) {
done({ status: 'empty', content: '' })
return
}
try {
videoajax.getUserDefineInfoByUser(param, (res) => {
try {
console.log(res)
if (res.code === 202) {
done({ status: 'unauthenticated', content: '', code: res.code, message: res.msg })
return
}
if (res === undefined || res === null || res === ('null' as unknown)) {
done({ status: 'empty', content: '' })
return
}
if (res.code !== 200) {
done({
status: 'error',
content: '',
code: res.code,
message: res.msg || '学习进度获取失败',
})
return
}
if (res.code === 202) {
done({ status: 'unauthenticated', content: '', code: res.code, message: res.msg })
return
}
const value = res.data?.value
if (value === undefined || value === null || value === 'null' || value === '') {
done({ status: 'empty', content: '' })
return
}
if (res.code !== 200) {
done({
status: 'error',
content: '',
code: res.code,
message: res.msg || '学习进度获取失败',
})
return
}
const json = Array.isArray(value) ? value : []
const content = json[0]?.content || ''
done(
content === '' || content === 'null'
? { status: 'empty', content: '' }
: { status: 'success', content }
)
})
const value = res.data?.value
if (value === undefined || value === null || value === 'null' || value === '') {
done({ status: 'empty', content: '' })
return
}
const json = Array.isArray(value) ? value : []
const content = json[0]?.content || ''
done(
content === '' || content === 'null'
? { status: 'empty', content: '' }
: { status: 'success', content }
)
} catch (error) {
console.error('Failed to handle getUserDefine response', error)
done({ status: 'error', content: '', message: '学习进度解析失败' })
}
})
} catch (error) {
console.error('Failed to call getUserDefine', error)
done({ status: 'error', content: '', message: '学习进度获取失败' })
}
})
}
function getUserDefineAsyncInternal(
lessonId: string | number,
key: string | number
): Promise<string> {
return getUserDefineResultAsyncInternal(lessonId, key).then((result) =>
result.status === 'success' ? result.content : ''
)
}
// Promise 版本:获取用户自定义数据
export async function getUserDefineResult(
lessonId: string | number,
key: string | number
key: string | number,
options: StorageOptions = {}
): Promise<UserDefineFetchResult> {
return getUserDefineResultAsyncInternal(lessonId, key)
}
export async function getUserDefine(
lessonId: string | number,
key: string | number
): Promise<string> {
return getUserDefineAsyncInternal(lessonId, key)
}
// 内部:获取【所有】用户自定义数据(Promise 封装)
function getUsersDefineAsyncInternal(
lessonId: string | number,
key: string | number
): Promise<string | VideoAjaxResponseItem[]> {
const param: UserDefineParam = {
lessonId: `${LESSON_PREFIX}_${lessonId}`,
order: key,
}
return new Promise((resolve) => {
let settled = false
const done = (content: string | VideoAjaxResponseItem[]) => {
if (settled) return
settled = true
resolve(content)
}
const timeout = setTimeout(() => {
console.warn(`getUsersDefine timeout: lessonId=${lessonId}, key=${key}`)
done('')
}, TIMEOUT_MS)
videoajax.getUserDefineInfo(param, (data) => {
clearTimeout(timeout)
let content: string | VideoAjaxResponseItem[] = ''
if (data && data.code === 200) {
const json = (data.data.value || []) as VideoAjaxResponseItem[]
if (json.length > 1) {
if (key === emykey) {
const mdate = new Date(
Math.max.apply(
null,
json.map((item) => new Date(item.content || '').getTime())
)
)
content = formatDateFromMilliseconds(mdate)
} else {
content = json
}
} else {
content = json[0]?.content || ''
}
}
done(content)
})
})
}
// Promise 版本:获取【所有】用户自定义数据
export async function getUsersDefine(
lessonId: string | number,
key: string | number
): Promise<string | VideoAjaxResponseItem[]> {
return getUsersDefineAsyncInternal(lessonId, key)
return getUserDefineResultAsyncInternal(lessonId, key, options)
}
// 根据课时顺序号(所处课程的第几课时)更新对应易班平台上的每个环节进度(异步)
export async function setCourseTimeProgress(order: number): Promise<void> {
export async function setCourseTimeProgress(order: number): Promise<boolean> {
if (useUserStore.getState().yibanConnectionStatus !== 'connected') {
console.info(`[yiban] skip finishCourse: disconnected, order=${order}`)
return false
}
const courseid = useUserStore.getState().courseId
// var courseid = getCookie(lesson+"_course_id");//获取url链接的课程ID
// const courseid = await getLtDefine('10000')
console.info(`courseid:${courseid} order:${order}`)
if (iscallyb && courseid !== undefined && courseid !== null && courseid !== '') {
if (order > 0) {
const param2 = { order, co: courseid, process: 100 }
videoajax.finishCourse(param2)
}
if (courseid === undefined || courseid === null || courseid === '') {
console.info(`[yiban] skip finishCourse: missing courseId, order=${order}`)
return false
}
if (order <= 0) {
console.info(`[yiban] skip finishCourse: invalid order=${order}`)
return false
}
const param2 = { order, co: courseid, process: 100 }
console.info('[yiban] finishCourse', param2)
videoajax.finishCourse(param2)
return true
}
+105 -5
View File
@@ -5,20 +5,56 @@
// 敏感词库(可根据实际需求扩展)
const SENSITIVE_WORDS = [
// 基础不文明用语
'垃圾',
'脏话',
'骂人',
'辱骂',
'仇恨',
'傻逼',
'煞笔',
'傻b',
'傻比',
'sb',
'SB',
'草泥马',
'操',
'妈的',
'他妈',
'你妈',
'去死',
'找死',
'该死',
'白痴',
'智障',
'弱智',
'脑残',
'废物',
'贱',
'滚',
// 暴力相关
'暴力',
'恐怖',
'杀人',
'杀害',
'血腥',
'虐待',
// 违法违规
'色情',
'赌博',
'诈骗',
'毒品',
'邪教',
'反动',
'辱骂',
'仇恨',
'sb',
'法轮功',
'传销',
'洗钱',
'贩毒',
'走私',
// 歧视性用语
'歧视',
'种族',
'民族歧视',
]
type FileSaveHandle = {
@@ -78,6 +114,28 @@ export async function downloadDataUrlImage(dataUrl: string, filename: string): P
triggerAnchorDownload(blob, filename)
}
/**
* 检查文本中是否包含英文字母
*/
export function containsEnglish(text: string): boolean {
return /[a-zA-Z]/.test(text)
}
/**
* 检查文本中是否包含数字
*/
export function containsNumber(text: string): boolean {
return /\d/.test(text)
}
/**
* 检查文本中是否包含特殊符号或标点符号
*/
export function containsSpecialChars(text: string): boolean {
// 检测所有标点符号和特殊符号(包括中文标点)
return /[\p{P}\p{S}]/u.test(text)
}
/**
* 检查文本中是否包含敏感词
* @param text 待检查的文本
@@ -117,8 +175,50 @@ export function filterSensitiveWords(text: string): string {
export function filterInput(input: string): string {
if (!input) return input
// 先进行敏感词过滤
const filtered = filterSensitiveWords(input)
let filtered = input
// 过滤英文字母
filtered = filtered.replace(/[a-zA-Z]/g, '')
// 过滤数字
filtered = filtered.replace(/\d/g, '')
// 过滤所有标点符号和特殊符号
// 保留:中文字符、空格
// 过滤:所有标点符号(包括中文标点和英文标点)、特殊符号
filtered = filtered.replace(/[\p{P}\p{S}]/gu, '')
// 进行敏感词过滤(替换为星号,不直接删除)
filtered = filterSensitiveWords(filtered)
return filtered
}
/**
* 验证输入内容的完整性
* @param input 用户输入的内容
* @returns 验证结果 { valid: boolean, message: string }
*/
export function validateInput(input: string): { valid: boolean; message: string } {
if (!input || !input.trim()) {
return { valid: false, message: '内容不能为空' }
}
if (containsEnglish(input)) {
return { valid: false, message: '不能包含英文字母' }
}
if (containsNumber(input)) {
return { valid: false, message: '不能包含数字' }
}
if (containsSpecialChars(input)) {
return { valid: false, message: '不能包含特殊符号' }
}
if (containsSensitiveWord(input)) {
return { valid: false, message: '内容包含敏感词,请修改后重试' }
}
return { valid: true, message: '' }
}
+40
View File
@@ -0,0 +1,40 @@
import { useChenyunStore, saveChenyunStore } from '@/store/chenyunStore'
import { useHuangyanpeiStore, saveHuangyanpeiStore } from '@/store/huangyanpeiStore'
import { useSunzhongshanStore, saveSunzhongshanStore } from '@/store/sunzhongshanStore'
import { useLuxunStore, saveLuxunStore } from '@/store/luxunStore'
import { useMaozedongStore, saveMaozedongStore } from '@/store/maozedongStore'
import { useNieerStore, saveNieerStore } from '@/store/nieerStore'
import { useRongdeshengStore, saveRongdeshengStore } from '@/store/rongdeshengStore'
import { useZhanglepingStore, saveZhanglepingStore } from '@/store/zhanglepingStore'
const STORE_MAP: Record<string, { store: any; save: () => void }> = {
chenyun: { store: useChenyunStore, save: saveChenyunStore },
huangyanpei: { store: useHuangyanpeiStore, save: saveHuangyanpeiStore },
sunzhongshan: { store: useSunzhongshanStore, save: saveSunzhongshanStore },
luxun: { store: useLuxunStore, save: saveLuxunStore },
maozedong: { store: useMaozedongStore, save: saveMaozedongStore },
nieer: { store: useNieerStore, save: saveNieerStore },
rongdesheng: { store: useRongdeshengStore, save: saveRongdeshengStore },
zhangleping: { store: useZhanglepingStore, save: saveZhanglepingStore },
}
export function markVideoCompletedByResidence(residence: string, videoKey: string) {
const entry = STORE_MAP[residence]
if (!entry) return
entry.store.getState().markVideoCompleted(videoKey)
entry.save()
}
export function isVideoCompletedByResidence(residence: string, videoKey: string): boolean {
const entry = STORE_MAP[residence]
if (!entry) return false
return !!entry.store.getState().completedVideos?.[videoKey]
}
export function getVideoCompletionProps(residence: string, videoKey: string) {
return {
skipKey: videoKey,
initiallyCompleted: isVideoCompletedByResidence(residence, videoKey),
onVideoCompleted: () => markVideoCompletedByResidence(residence, videoKey),
}
}