删除过时的批处理脚本和部署Python文件
- 删除了macOS虚拟机的安装和迁移批处理脚本,因为它们已不再需要。 - 删除了macOS虚拟机的导出脚本,以简化项目流程。 - 删除了soul-admin项目的部署Python脚本,以简化代码库。 - 更新了小程序,以反映环境配置的变化并提升用户体验。
This commit is contained in:
@@ -125,6 +125,11 @@ interface EditingSection {
|
||||
editionPremium?: boolean
|
||||
}
|
||||
|
||||
/** 去除名字中的括号及内容,名字不带符号(如 南风(管理) → 南风) */
|
||||
function sanitizeNameOrLabel(s: string): string {
|
||||
return s.replace(/\s*[((][^))]*(\)|))?/g, '').trim()
|
||||
}
|
||||
|
||||
// 在保存前自动把纯文本中的 @用户 / #标签 转成带 token / 配置的节点
|
||||
function autoLinkContent(html: string, persons: PersonItem[], linkTags: LinkTagItem[]): string {
|
||||
if (!html || (!html.includes('@') && !html.includes('#'))) return html
|
||||
@@ -134,10 +139,10 @@ function autoLinkContent(html: string, persons: PersonItem[], linkTags: LinkTagI
|
||||
container.innerHTML = html
|
||||
|
||||
const matchPerson = (name: string): PersonItem | undefined =>
|
||||
persons.find((p) => p.name === name)
|
||||
persons.find((p) => p.name === name || sanitizeNameOrLabel(p.name) === sanitizeNameOrLabel(name))
|
||||
|
||||
const matchTag = (label: string): LinkTagItem | undefined =>
|
||||
linkTags.find((t) => t.label === label)
|
||||
linkTags.find((t) => t.label === label || sanitizeNameOrLabel(t.label) === sanitizeNameOrLabel(label))
|
||||
|
||||
const processTextNode = (node: Text) => {
|
||||
const text = node.textContent || ''
|
||||
@@ -147,7 +152,8 @@ function autoLinkContent(html: string, persons: PersonItem[], linkTags: LinkTagI
|
||||
if (!parent) return
|
||||
|
||||
const frag = document.createDocumentFragment()
|
||||
const regex = /(@[^\s@#]+|#[^\s@#]+)/g
|
||||
// 排除 <> 避免把 HTML 标签带入 @/# 匹配(如 @远志</span> 只匹配 @远志)
|
||||
const regex = /(@[^\s@#<>]+|#[^\s@#<>]+)/g
|
||||
let lastIndex = 0
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
@@ -160,8 +166,8 @@ function autoLinkContent(html: string, persons: PersonItem[], linkTags: LinkTagI
|
||||
}
|
||||
|
||||
if (full.startsWith('@')) {
|
||||
const name = full.slice(1)
|
||||
const person = matchPerson(name)
|
||||
const rawName = full.slice(1)
|
||||
const person = matchPerson(rawName)
|
||||
if (person) {
|
||||
const span = document.createElement('span')
|
||||
span.setAttribute('data-type', 'mention')
|
||||
@@ -173,8 +179,8 @@ function autoLinkContent(html: string, persons: PersonItem[], linkTags: LinkTagI
|
||||
frag.appendChild(document.createTextNode(full))
|
||||
}
|
||||
} else if (full.startsWith('#')) {
|
||||
const label = full.slice(1)
|
||||
const tag = matchTag(label)
|
||||
const rawLabel = full.slice(1)
|
||||
const tag = matchTag(rawLabel)
|
||||
if (tag) {
|
||||
const span = document.createElement('span')
|
||||
span.setAttribute('data-type', 'linkTag')
|
||||
@@ -210,8 +216,17 @@ function autoLinkContent(html: string, persons: PersonItem[], linkTags: LinkTagI
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as HTMLElement
|
||||
const dataType = el.getAttribute('data-type')
|
||||
// 已经是 mention/linkTag 的节点不再解析
|
||||
if (dataType === 'mention' || dataType === 'linkTag') return
|
||||
// mention 节点:若 data-id 为空,按昵称匹配 persons 回填(修复 TipTap 插入时 id 丢失导致小程序 data-user-id 为空)
|
||||
if (dataType === 'mention') {
|
||||
const existingId = el.getAttribute('data-id')
|
||||
const nickname = (el.getAttribute('data-label') || el.textContent || '').replace(/^@/, '').trim()
|
||||
if ((!existingId || !existingId.trim()) && nickname) {
|
||||
const person = matchPerson(nickname)
|
||||
if (person?.id) el.setAttribute('data-id', person.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (dataType === 'linkTag') return
|
||||
node.childNodes.forEach((child) => walk(child))
|
||||
return
|
||||
}
|
||||
@@ -591,19 +606,20 @@ export function ContentPage() {
|
||||
/** 文章编辑时自动创建不存在的 @人物 和 #标签,返回合并后的列表供 autoLinkContent 使用 */
|
||||
const ensureMentionsAndTags = useCallback(
|
||||
async (content: string): Promise<{ persons: PersonItem[]; linkTags: LinkTagItem[] }> => {
|
||||
const regex = /(@[^\s@#]+|#[^\s@#]+)/g
|
||||
// 排除 <> 避免把 HTML 标签带入 @/# 匹配
|
||||
const regex = /(@[^\s@#<>]+|#[^\s@#<>]+)/g
|
||||
const names = new Set<string>()
|
||||
const labels = new Set<string>()
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = regex.exec(content)) !== null) {
|
||||
const full = m[0]
|
||||
if (full.startsWith('@')) names.add(full.slice(1).trim())
|
||||
else if (full.startsWith('#')) labels.add(full.slice(1).trim())
|
||||
if (full.startsWith('@')) names.add(sanitizeNameOrLabel(full.slice(1)))
|
||||
else if (full.startsWith('#')) labels.add(sanitizeNameOrLabel(full.slice(1)))
|
||||
}
|
||||
let personsCopy = [...persons]
|
||||
let linkTagsCopy = [...linkTags]
|
||||
for (const name of names) {
|
||||
if (!name || personsCopy.some((p) => p.name === name)) continue
|
||||
if (!name || personsCopy.some((p) => p.name === name || sanitizeNameOrLabel(p.name) === name)) continue
|
||||
try {
|
||||
const res = await post<{
|
||||
success?: boolean
|
||||
@@ -625,7 +641,7 @@ export function ContentPage() {
|
||||
}
|
||||
}
|
||||
for (const label of labels) {
|
||||
if (!label || linkTagsCopy.some((t) => t.label === label)) continue
|
||||
if (!label || linkTagsCopy.some((t) => t.label === label || sanitizeNameOrLabel(t.label) === label)) continue
|
||||
try {
|
||||
const res = await post<{
|
||||
success?: boolean
|
||||
@@ -2488,6 +2504,7 @@ export function ContentPage() {
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="text-red-400 hover:text-red-300 h-6 px-2" title="删除(同时删除存客宝对应获客计划)" onClick={async () => {
|
||||
if (!confirm(`确定删除「SOUL链接人与事-${p.name}」?将同时删除存客宝对应获客计划。`)) return
|
||||
if (!confirm(`二次确认:删除后无法恢复,文章中的 @${p.name} 将无法正常跳转。确定删除?`)) return
|
||||
await del(`/api/db/persons?personId=${p.personId}`)
|
||||
loadPersons()
|
||||
}}>
|
||||
|
||||
Reference in New Issue
Block a user