mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 20:29:03 +08:00
支持学生头像设置与展示(上传/清除)
This commit is contained in:
@@ -3,11 +3,14 @@ import { Card, Space, Button, Tag, Input, Select, Modal, message, InputNumber, D
|
|||||||
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"
|
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { match, pinyin } from "pinyin-pro"
|
import { match, pinyin } from "pinyin-pro"
|
||||||
|
import { getAvatarFromExtraJson } from "../utils/studentAvatar"
|
||||||
|
|
||||||
interface student {
|
interface student {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
score: number
|
score: number
|
||||||
|
extra_json?: string | null
|
||||||
|
avatarUrl?: string | null
|
||||||
pinyinName?: string
|
pinyinName?: string
|
||||||
pinyinFirst?: string
|
pinyinFirst?: string
|
||||||
}
|
}
|
||||||
@@ -67,6 +70,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
if (stuRes.success) {
|
if (stuRes.success) {
|
||||||
const enrichedStudents = (stuRes.data as student[]).map((s) => ({
|
const enrichedStudents = (stuRes.data as student[]).map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
|
avatarUrl: getAvatarFromExtraJson(s.extra_json),
|
||||||
pinyinName: pinyin(s.name, { toneType: "none" }).toLowerCase(),
|
pinyinName: pinyin(s.name, { toneType: "none" }).toLowerCase(),
|
||||||
pinyinFirst: getFirstLetter(s.name),
|
pinyinFirst: getFirstLetter(s.name),
|
||||||
}))
|
}))
|
||||||
@@ -313,6 +317,22 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||||||
|
{student.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={student.avatarUrl}
|
||||||
|
alt={student.name}
|
||||||
|
style={{
|
||||||
|
width: "44px",
|
||||||
|
height: "44px",
|
||||||
|
borderRadius: "12px",
|
||||||
|
objectFit: "cover",
|
||||||
|
flexShrink: 0,
|
||||||
|
boxShadow: `0 4px 10px ${avatarColor}40`,
|
||||||
|
border: "1px solid var(--ss-border-color)",
|
||||||
|
backgroundColor: "var(--ss-bg-color)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "44px",
|
width: "44px",
|
||||||
@@ -331,6 +351,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
>
|
>
|
||||||
{avatarText}
|
{avatarText}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div style={{ flex: 1, overflow: "hidden" }}>
|
<div style={{ flex: 1, overflow: "hidden" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -621,6 +642,19 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||||||
|
{selectedStudent.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={selectedStudent.avatarUrl}
|
||||||
|
alt={selectedStudent.name}
|
||||||
|
style={{
|
||||||
|
width: "32px",
|
||||||
|
height: "32px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover",
|
||||||
|
border: "1px solid var(--ss-border-color)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "32px",
|
width: "32px",
|
||||||
@@ -637,6 +671,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
>
|
>
|
||||||
{getDisplayText(selectedStudent.name)}
|
{getDisplayText(selectedStudent.name)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<span style={{ fontWeight: 600 }}>{selectedStudent.name}</span>
|
<span style={{ fontWeight: 600 }}>{selectedStudent.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState, useCallback } from "react"
|
import React, { useEffect, useMemo, useRef, useState, useCallback } from "react"
|
||||||
import { Table, Button, Space, message, Modal, Form, Input, Tag, Pagination } from "antd"
|
import { Table, Button, Space, message, Modal, Form, Input, Tag, Pagination } from "antd"
|
||||||
import type { ColumnsType } from "antd/es/table"
|
import type { ColumnsType } from "antd/es/table"
|
||||||
|
import { UploadOutlined } from "@ant-design/icons"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { TagEditorDialog } from "./TagEditorDialog"
|
import { TagEditorDialog } from "./TagEditorDialog"
|
||||||
|
import { getAvatarFromExtraJson, setAvatarInExtraJson } from "../utils/studentAvatar"
|
||||||
|
|
||||||
const createXlsxWorker = () => {
|
const createXlsxWorker = () => {
|
||||||
return new Worker(new URL("../workers/xlsxWorker.ts", import.meta.url), {
|
return new Worker(new URL("../workers/xlsxWorker.ts", import.meta.url), {
|
||||||
@@ -16,6 +18,8 @@ interface student {
|
|||||||
score: number
|
score: number
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
tagIds?: number[]
|
tagIds?: number[]
|
||||||
|
extra_json?: string | null
|
||||||
|
avatarUrl?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||||
@@ -29,6 +33,11 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
const [xlsxVisible, setXlsxVisible] = useState(false)
|
const [xlsxVisible, setXlsxVisible] = useState(false)
|
||||||
const [tagEditVisible, setTagEditVisible] = useState(false)
|
const [tagEditVisible, setTagEditVisible] = useState(false)
|
||||||
const [editingStudent, setEditingStudent] = useState<student | null>(null)
|
const [editingStudent, setEditingStudent] = useState<student | null>(null)
|
||||||
|
const [avatarVisible, setAvatarVisible] = useState(false)
|
||||||
|
const [avatarSaving, setAvatarSaving] = useState(false)
|
||||||
|
const [avatarStudent, setAvatarStudent] = useState<student | null>(null)
|
||||||
|
const [avatarValue, setAvatarValue] = useState<string | null>(null)
|
||||||
|
const avatarInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const [xlsxLoading, setXlsxLoading] = useState(false)
|
const [xlsxLoading, setXlsxLoading] = useState(false)
|
||||||
const [xlsxFileName, setXlsxFileName] = useState("")
|
const [xlsxFileName, setXlsxFileName] = useState("")
|
||||||
const [xlsxAoa, setXlsxAoa] = useState<any[][]>([])
|
const [xlsxAoa, setXlsxAoa] = useState<any[][]>([])
|
||||||
@@ -75,6 +84,8 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
id: s.id,
|
id: s.id,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
score: s.score,
|
score: s.score,
|
||||||
|
extra_json: s.extra_json ?? null,
|
||||||
|
avatarUrl: getAvatarFromExtraJson(s.extra_json),
|
||||||
tags,
|
tags,
|
||||||
tagIds,
|
tagIds,
|
||||||
}
|
}
|
||||||
@@ -175,6 +186,74 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
setTagEditVisible(true)
|
setTagEditVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenAvatarEditor = (student: student) => {
|
||||||
|
if (!canEdit) {
|
||||||
|
messageApi.error(t("common.readOnly"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setAvatarStudent(student)
|
||||||
|
setAvatarValue(student.avatarUrl || null)
|
||||||
|
setAvatarVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const readFileAsDataUrl = (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
const result = reader.result
|
||||||
|
if (typeof result === "string") resolve(result)
|
||||||
|
else reject(new Error("invalid-result"))
|
||||||
|
}
|
||||||
|
reader.onerror = () => reject(reader.error || new Error("read-failed"))
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAvatarFileChange = async (file?: File) => {
|
||||||
|
if (!file) return
|
||||||
|
if (!file.type.startsWith("image/")) {
|
||||||
|
messageApi.error(t("students.avatarInvalidFile"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const maxSize = 2 * 1024 * 1024
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
messageApi.error(t("students.avatarTooLarge"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const dataUrl = await readFileAsDataUrl(file)
|
||||||
|
setAvatarValue(dataUrl)
|
||||||
|
} catch {
|
||||||
|
messageApi.error(t("students.avatarReadFailed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveAvatar = async () => {
|
||||||
|
if (!(window as any).api || !avatarStudent) return
|
||||||
|
|
||||||
|
setAvatarSaving(true)
|
||||||
|
try {
|
||||||
|
const payload = setAvatarInExtraJson(avatarStudent.extra_json, avatarValue)
|
||||||
|
const res = await (window as any).api.updateStudent(avatarStudent.id, {
|
||||||
|
extra_json: payload,
|
||||||
|
})
|
||||||
|
if (res?.success) {
|
||||||
|
messageApi.success(t("students.avatarSaveSuccess"))
|
||||||
|
setAvatarVisible(false)
|
||||||
|
setAvatarStudent(null)
|
||||||
|
setAvatarValue(null)
|
||||||
|
fetchStudents()
|
||||||
|
emitDataUpdated("students")
|
||||||
|
} else {
|
||||||
|
messageApi.error(res?.message || t("students.avatarSaveFailed"))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
messageApi.error(t("students.avatarSaveFailed"))
|
||||||
|
} finally {
|
||||||
|
setAvatarSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSaveTags = async (tagIds: number[]) => {
|
const handleSaveTags = async (tagIds: number[]) => {
|
||||||
if (!editingStudent || !(window as any).api) return
|
if (!editingStudent || !(window as any).api) return
|
||||||
|
|
||||||
@@ -353,6 +432,44 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<student> = [
|
const columns: ColumnsType<student> = [
|
||||||
|
{
|
||||||
|
title: t("students.avatar"),
|
||||||
|
key: "avatar",
|
||||||
|
width: 88,
|
||||||
|
align: "center",
|
||||||
|
render: (_, row) =>
|
||||||
|
row.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={row.avatarUrl}
|
||||||
|
alt={row.name}
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover",
|
||||||
|
border: "1px solid var(--ss-border-color)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: "50%",
|
||||||
|
margin: "0 auto",
|
||||||
|
backgroundColor: "var(--ss-bg-color)",
|
||||||
|
border: "1px dashed var(--ss-border-color)",
|
||||||
|
color: "var(--ss-text-secondary)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{ title: t("students.name"), dataIndex: "name", key: "name", width: 100 },
|
{ title: t("students.name"), dataIndex: "name", key: "name", width: 100 },
|
||||||
{
|
{
|
||||||
title: t("students.currentScore"),
|
title: t("students.currentScore"),
|
||||||
@@ -399,12 +516,15 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
{
|
{
|
||||||
title: t("common.operation"),
|
title: t("common.operation"),
|
||||||
key: "operation",
|
key: "operation",
|
||||||
width: 150,
|
width: 220,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="link" disabled={!canEdit} onClick={() => handleOpenTagEditor(row)}>
|
<Button type="link" disabled={!canEdit} onClick={() => handleOpenTagEditor(row)}>
|
||||||
{t("students.editTags")}
|
{t("students.editTags")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="link" disabled={!canEdit} onClick={() => handleOpenAvatarEditor(row)}>
|
||||||
|
{t("students.editAvatar")}
|
||||||
|
</Button>
|
||||||
<Button type="link" danger disabled={!canEdit} onClick={() => handleDelete(row.id)}>
|
<Button type="link" danger disabled={!canEdit} onClick={() => handleDelete(row.id)}>
|
||||||
{t("common.delete")}
|
{t("common.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -545,6 +665,81 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
initialTagIds={editingStudent?.tagIds || []}
|
initialTagIds={editingStudent?.tagIds || []}
|
||||||
title={t("students.editTagTitle", { name: editingStudent?.name || "" })}
|
title={t("students.editTagTitle", { name: editingStudent?.name || "" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={t("students.editAvatarTitle", { name: avatarStudent?.name || "" })}
|
||||||
|
open={avatarVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setAvatarVisible(false)
|
||||||
|
setAvatarStudent(null)
|
||||||
|
setAvatarValue(null)
|
||||||
|
}}
|
||||||
|
onOk={handleSaveAvatar}
|
||||||
|
okButtonProps={{ loading: avatarSaving, disabled: !avatarStudent }}
|
||||||
|
okText={t("common.save")}
|
||||||
|
cancelText={t("common.cancel")}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", padding: "8px 0" }}>
|
||||||
|
{avatarValue ? (
|
||||||
|
<img
|
||||||
|
src={avatarValue}
|
||||||
|
alt={avatarStudent?.name || "avatar"}
|
||||||
|
style={{
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover",
|
||||||
|
border: "1px solid var(--ss-border-color)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: "1px dashed var(--ss-border-color)",
|
||||||
|
backgroundColor: "var(--ss-bg-color)",
|
||||||
|
color: "var(--ss-text-secondary)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("students.noAvatar")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
|
<Button
|
||||||
|
icon={<UploadOutlined />}
|
||||||
|
onClick={() => avatarInputRef.current?.click()}
|
||||||
|
disabled={!canEdit}
|
||||||
|
>
|
||||||
|
{t("students.avatarUpload")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setAvatarValue(null)} disabled={!canEdit}>
|
||||||
|
{t("students.avatarClear")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={avatarInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
handleAvatarFileChange(file)
|
||||||
|
if (avatarInputRef.current) avatarInputRef.current.value = ""
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
|
||||||
|
{t("students.avatarTip")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,10 +350,12 @@
|
|||||||
"importList": "Import List",
|
"importList": "Import List",
|
||||||
"addStudent": "Add Student",
|
"addStudent": "Add Student",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"avatar": "Avatar",
|
||||||
"currentScore": "Current Points",
|
"currentScore": "Current Points",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"noTags": "No tags",
|
"noTags": "No tags",
|
||||||
"editTags": "Edit Tags",
|
"editTags": "Edit Tags",
|
||||||
|
"editAvatar": "Set Avatar",
|
||||||
"deleteConfirm": "Confirm delete this student?",
|
"deleteConfirm": "Confirm delete this student?",
|
||||||
"addTitle": "Add Student",
|
"addTitle": "Add Student",
|
||||||
"addConfirm": "Add",
|
"addConfirm": "Add",
|
||||||
@@ -379,7 +381,17 @@
|
|||||||
"selectNameColFirst": "Please select \"Name Column\" first",
|
"selectNameColFirst": "Please select \"Name Column\" first",
|
||||||
"noNamesFound": "No importable names found in selected column",
|
"noNamesFound": "No importable names found in selected column",
|
||||||
"importFailed": "Import failed",
|
"importFailed": "Import failed",
|
||||||
"editTagTitle": "Edit Tags - {{name}}"
|
"editTagTitle": "Edit Tags - {{name}}",
|
||||||
|
"editAvatarTitle": "Set Avatar - {{name}}",
|
||||||
|
"avatarUpload": "Upload Image",
|
||||||
|
"avatarClear": "Clear Avatar",
|
||||||
|
"noAvatar": "No Avatar",
|
||||||
|
"avatarTip": "Supports common image formats, up to 2MB",
|
||||||
|
"avatarSaveSuccess": "Avatar saved",
|
||||||
|
"avatarSaveFailed": "Failed to save avatar",
|
||||||
|
"avatarInvalidFile": "Please choose an image file",
|
||||||
|
"avatarTooLarge": "Image must be smaller than 2MB",
|
||||||
|
"avatarReadFailed": "Failed to read image"
|
||||||
},
|
},
|
||||||
"score": {
|
"score": {
|
||||||
"title": "Points Management",
|
"title": "Points Management",
|
||||||
|
|||||||
@@ -350,10 +350,12 @@
|
|||||||
"importList": "导入名单",
|
"importList": "导入名单",
|
||||||
"addStudent": "添加学生",
|
"addStudent": "添加学生",
|
||||||
"name": "姓名",
|
"name": "姓名",
|
||||||
|
"avatar": "头像",
|
||||||
"currentScore": "当前积分",
|
"currentScore": "当前积分",
|
||||||
"tags": "标签",
|
"tags": "标签",
|
||||||
"noTags": "无标签",
|
"noTags": "无标签",
|
||||||
"editTags": "编辑标签",
|
"editTags": "编辑标签",
|
||||||
|
"editAvatar": "设置头像",
|
||||||
"deleteConfirm": "确认删除该学生?",
|
"deleteConfirm": "确认删除该学生?",
|
||||||
"addTitle": "添加学生",
|
"addTitle": "添加学生",
|
||||||
"addConfirm": "添加",
|
"addConfirm": "添加",
|
||||||
@@ -379,7 +381,17 @@
|
|||||||
"selectNameColFirst": "请先点击选择\"姓名列\"",
|
"selectNameColFirst": "请先点击选择\"姓名列\"",
|
||||||
"noNamesFound": "所选列未解析到可导入的姓名",
|
"noNamesFound": "所选列未解析到可导入的姓名",
|
||||||
"importFailed": "导入失败",
|
"importFailed": "导入失败",
|
||||||
"editTagTitle": "编辑标签 - {{name}}"
|
"editTagTitle": "编辑标签 - {{name}}",
|
||||||
|
"editAvatarTitle": "设置头像 - {{name}}",
|
||||||
|
"avatarUpload": "上传图片",
|
||||||
|
"avatarClear": "清除头像",
|
||||||
|
"noAvatar": "无头像",
|
||||||
|
"avatarTip": "支持常见图片格式,文件大小不超过 2MB",
|
||||||
|
"avatarSaveSuccess": "头像保存成功",
|
||||||
|
"avatarSaveFailed": "头像保存失败",
|
||||||
|
"avatarInvalidFile": "请选择图片文件",
|
||||||
|
"avatarTooLarge": "图片不能超过 2MB",
|
||||||
|
"avatarReadFailed": "读取图片失败"
|
||||||
},
|
},
|
||||||
"score": {
|
"score": {
|
||||||
"title": "积分管理",
|
"title": "积分管理",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
export const getAvatarFromExtraJson = (extraJson?: string | null): string | null => {
|
||||||
|
if (!extraJson) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(extraJson)
|
||||||
|
if (!parsed || typeof parsed !== "object") return null
|
||||||
|
|
||||||
|
const avatar = (parsed as Record<string, unknown>).avatar
|
||||||
|
if (typeof avatar !== "string") return null
|
||||||
|
|
||||||
|
const trimmed = avatar.trim()
|
||||||
|
if (!trimmed) return null
|
||||||
|
|
||||||
|
if (trimmed.startsWith("data:image/")) return trimmed
|
||||||
|
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setAvatarInExtraJson = (
|
||||||
|
extraJson: string | null | undefined,
|
||||||
|
avatar: string | null
|
||||||
|
): string => {
|
||||||
|
let base: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
if (extraJson) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(extraJson)
|
||||||
|
if (parsed && typeof parsed === "object") {
|
||||||
|
base = parsed as Record<string, unknown>
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
base = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatar && avatar.trim()) {
|
||||||
|
base.avatar = avatar.trim()
|
||||||
|
} else {
|
||||||
|
delete base.avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(base)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user