Foro: selector para fijar el personaje del autor

- Modal "Cambiar personaje" (diseño de la web) con filtro, avatar de raza,
  nombre en color de clase y subtítulo nivel/raza/clase.
- Barra "Publicas como: X · Cambiar personaje" sobre el formulario de respuesta
  (solo con sesión iniciada).
- Preferencia persistente en acore_web.forum_character (por cuenta), validada.
- resolvePosters usa el personaje fijado si existe; si no, el de más nivel.
- API /api/forum/character (GET lista + actual, POST fijar), gated por sesión.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 08:14:54 +00:00
parent bf9931e042
commit 0f6e0d514f
7 changed files with 311 additions and 4 deletions
+79 -1
View File
@@ -413,8 +413,21 @@ export async function resolvePosters(ids: number[]): Promise<Map<number, PosterI
if (!byAcc.has(acc)) byAcc.set(acc, [])
byAcc.get(acc)!.push(c)
}
// Personaje fijado por el usuario (si lo hay y sigue siendo suyo); si no, el de más nivel.
const pickByAcc = new Map<number, number>()
try {
const [picks] = await db(DB.web).query<RowDataPacket[]>(
`SELECT account, guid FROM forum_character WHERE account IN (${placeholders})`,
unique,
)
for (const p of picks) pickByAcc.set(Number(p.account), Number(p.guid))
} catch {
/* sin tabla de preferencias */
}
for (const [acc, list] of byAcc) {
const c = list.reduce((best, cur) => (Number(cur.level) > Number(best.level) ? cur : best), list[0])
const pg = pickByAcc.get(acc)
const picked = pg ? list.find((x) => Number(x.guid) === pg) : undefined
const c = picked || list.reduce((best, cur) => (Number(cur.level) > Number(best.level) ? cur : best), list[0])
charByAccount.set(acc, {
guid: Number(c.guid), name: String(c.name), race: Number(c.race),
gender: Number(c.gender), class: Number(c.class), level: Number(c.level),
@@ -500,3 +513,68 @@ export async function resolvePosters(ids: number[]): Promise<Map<number, PosterI
}
return map
}
export interface ForumCharOption {
guid: number
name: string
race: number
gender: number
class: number
level: number
}
/** Personajes de la cuenta para el selector del foro. */
export async function getAccountCharactersForForum(accountId: number): Promise<ForumCharOption[]> {
if (!accountId) return []
try {
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT guid, name, race, gender, class, level FROM characters WHERE account = ? ORDER BY level DESC, name ASC',
[accountId],
)
return rows.map((r) => ({
guid: Number(r.guid), name: String(r.name), race: Number(r.race),
gender: Number(r.gender), class: Number(r.class), level: Number(r.level),
}))
} catch {
return []
}
}
/** GUID del personaje fijado por la cuenta para el foro (validado), o null. */
export async function getForumCharacterPick(accountId: number): Promise<number | null> {
if (!accountId) return null
try {
const [rows] = await db(DB.web).query<RowDataPacket[]>(
'SELECT guid FROM forum_character WHERE account = ?',
[accountId],
)
const guid = rows[0] ? Number(rows[0].guid) : 0
if (!guid) return null
const [ok] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT 1 FROM characters WHERE guid = ? AND account = ? LIMIT 1',
[guid, accountId],
)
return ok.length ? guid : null
} catch {
return null
}
}
/** Fija el personaje del foro para la cuenta (valida que le pertenece). */
export async function setForumCharacterPick(accountId: number, guid: number): Promise<boolean> {
if (!accountId || !guid) return false
try {
const [ok] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT 1 FROM characters WHERE guid = ? AND account = ? LIMIT 1',
[guid, accountId],
)
if (!ok.length) return false
await db(DB.web).query(
'INSERT INTO forum_character (account, guid) VALUES (?, ?) ON DUPLICATE KEY UPDATE guid = VALUES(guid)',
[accountId, guid],
)
return true
} catch {
return false
}
}