删除过时的批处理脚本和部署Python文件

- 删除了macOS虚拟机的安装和迁移批处理脚本,因为它们已不再需要。
- 删除了macOS虚拟机的导出脚本,以简化项目流程。
- 删除了soul-admin项目的部署Python脚本,以简化代码库。
- 更新了小程序,以反映环境配置的变化并提升用户体验。
This commit is contained in:
Alex-larget
2026-03-16 16:10:30 +08:00
parent e75092eaad
commit 219ae3b843
19 changed files with 708 additions and 322 deletions

View File

@@ -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()
}}>