feat(admin): 账号表新增「编辑」入口与选手信息编辑弹窗(R108)
· 账号表新增操作列(fixed=right)+ 编辑按钮
· 编辑弹窗:可改 name/phone/id_number;group 只读并说明原因(改组别会令
已作答答卷与新组别抽题/发布开关错配);口径说明 alert + warnings 区
· api/admin.js 新增 updateContestant(silent: true,调用方负责提示)
· 修正一处死代码:初版按 e.response.status 判 409/404 —— 实测证伪:
helper.error() 不改 HTTP 状态,业务错 = HTTP 200 + body.code != 0,
拦截器以 new Error(res.message) reject(该 Error 无 response 属性)
=> 改用 e.message
This commit is contained in:
@@ -512,4 +512,32 @@ export function getContestantScore(group, contestantId) {
|
|||||||
return request.get(`/admin/review/score/${group}/${contestantId}`)
|
return request.get(`/admin/review/score/${group}/${contestantId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 选手信息编辑(R108 新增) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑已入库选手的身份字段(姓名 / 手机号 / 身份证号)
|
||||||
|
*
|
||||||
|
* 背景:此前选手信息只有「Excel 导入」一个写入口,录错了既改不了也删不掉。
|
||||||
|
*
|
||||||
|
* silent: true —— 抑制响应拦截器的通用错误提示,由调用方按业务码分支提示。
|
||||||
|
* 本接口的失败形态是**业务性**的(400 入参非法 / 404 选手不存在 / 409 证号被占用),
|
||||||
|
* 后端 message 已含可读原因(如「身份证号 X 已被占用:张三 / id=123 / 组别=zhongzhi」),
|
||||||
|
* 拦截器再弹一次「请求失败」即为同族双弹(见 frontend-error-notify.md 口径)。
|
||||||
|
*
|
||||||
|
* @param {number|string} id - contestants.id
|
||||||
|
* @param {{name?: string, phone?: string, id_number?: string}} payload
|
||||||
|
* 仅这三项可改;传其他字段后端会显式 400 拒绝(调用方切勿透传整行对象,
|
||||||
|
* 否则 group / score 等只读字段会把请求打成 400)。
|
||||||
|
* @returns {Promise<{code:number,message:string,data:{
|
||||||
|
* ok:boolean, changed:string[], warnings:string[],
|
||||||
|
* contestant:{id:number,name:string,phone:string,id_number:string,group:string,
|
||||||
|
* team_id:number|null,activated:boolean,test_flag:number}
|
||||||
|
* }}>}
|
||||||
|
* `changed` 为空数组 = 提交值与库内一致,未落库(后端 message 为「未检测到需要修改的内容」)。
|
||||||
|
* `warnings` 非空须展示(典型:改号后口令不重置,需走「忘记密码」)。
|
||||||
|
*/
|
||||||
|
export function updateContestant(id, payload) {
|
||||||
|
return request.put(`/admin/contestants/${id}`, payload, { silent: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,13 @@
|
|||||||
<el-table-column prop="createdAt" label="注册时间" min-width="170" align="center">
|
<el-table-column prop="createdAt" label="注册时间" min-width="170" align="center">
|
||||||
<template #default="{ row }">{{ fmtDate(row.createdAt) }}</template>
|
<template #default="{ row }">{{ fmtDate(row.createdAt) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<!-- R108 新增:编辑入口。此前 contestants 只有「Excel 导入」一个写入口,
|
||||||
|
身份证号/姓名/手机号录错即无退路(改不了也删不掉)⇒ 只能改库。 -->
|
||||||
|
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button text type="primary" size="small" @click="openEditContestant(row)">编辑</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="pagination-wrap" v-if="accountsTotal > 0">
|
<div class="pagination-wrap" v-if="accountsTotal > 0">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
@@ -1656,6 +1663,106 @@
|
|||||||
<el-button type="primary" :loading="carouselSaving" v-guard="() => saveCarousel()">保存</el-button>
|
<el-button type="primary" :loading="carouselSaving" v-guard="() => saveCarousel()">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- R107(2026-10-12)导入结果明细:
|
||||||
|
旧实现仅在「0 条成功」时提示前 3 条原因,其余一律只进控制台 ⇒
|
||||||
|
成功但有跳过的场景下,管理员根本看不到是哪几行、是谁
|
||||||
|
(生产事故:3 名选手身份证号位数异常被静默跳过,只能人工逐行比对才发现)。 -->
|
||||||
|
<el-dialog v-model="importResultVisible" title="导入结果" width="780px" top="8vh">
|
||||||
|
<template v-if="importResult">
|
||||||
|
<div class="import-result-summary">
|
||||||
|
<span class="ir-ok">成功 {{ importResult.imported || 0 }} 条</span>
|
||||||
|
<span v-if="importResult.skipped" class="ir-skip">跳过 {{ importResult.skipped }} 条</span>
|
||||||
|
<span v-if="importResult.ignored" class="ir-ignore">
|
||||||
|
忽略非参赛选手 {{ importResult.ignored }} 条
|
||||||
|
</span>
|
||||||
|
<span v-if="importResult.warnings && importResult.warnings.length" class="ir-warn">
|
||||||
|
需人工复核 {{ importResult.warnings.length }} 条
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="importResult.errors && importResult.errors.length">
|
||||||
|
<div class="import-result-subtitle ir-error-title">
|
||||||
|
以下 {{ importResult.errors.length }} 行<strong>未导入</strong>,请修正后重新导入:
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="importResult.errors"
|
||||||
|
size="small"
|
||||||
|
max-height="240"
|
||||||
|
border
|
||||||
|
class="import-result-table"
|
||||||
|
>
|
||||||
|
<el-table-column prop="row" label="Excel 行" width="88" align="center" />
|
||||||
|
<el-table-column prop="name" label="姓名" width="110" />
|
||||||
|
<el-table-column prop="message" label="原因" min-width="400" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="importResult.warnings && importResult.warnings.length">
|
||||||
|
<div class="import-result-subtitle ir-warn-title">
|
||||||
|
以下 {{ importResult.warnings.length }} 行<strong>已导入</strong>,但需人工核对:
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="importResult.warnings"
|
||||||
|
size="small"
|
||||||
|
max-height="200"
|
||||||
|
border
|
||||||
|
class="import-result-table"
|
||||||
|
>
|
||||||
|
<el-table-column prop="row" label="Excel 行" width="88" align="center" />
|
||||||
|
<el-table-column prop="name" label="姓名" width="110" />
|
||||||
|
<el-table-column prop="message" label="提示" min-width="400" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="isImportResultClean" class="import-result-clean">
|
||||||
|
全部行均已成功导入,无异常。
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="importResultVisible = false">知道了</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- R108 新增:编辑选手信息(姓名 / 手机号 / 身份证号)
|
||||||
|
可改字段的完整口径与理由见 backend/app/service/admin.js#updateContestant:
|
||||||
|
组别只读(改组别会使「已作答的答卷」与「新组别的抽题 + 成绩发布开关」错配);
|
||||||
|
成绩字段禁止(唯一写入口是成绩发布,本处若可写即为越权改分通道)。 -->
|
||||||
|
<el-dialog v-model="editVisible" title="编辑选手信息" width="580px" :close-on-click-modal="false">
|
||||||
|
<template v-if="editForm">
|
||||||
|
<el-form label-width="92px">
|
||||||
|
<el-form-item label="当前组别">
|
||||||
|
<el-tag size="small">{{ groupMap[editForm.group] || editForm.group }}</el-tag>
|
||||||
|
<span class="edit-readonly-hint">组别不可修改</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="姓名" required>
|
||||||
|
<el-input v-model="editForm.name" maxlength="50" show-word-limit placeholder="请输入姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号">
|
||||||
|
<el-input v-model="editForm.phone" maxlength="11" placeholder="11 位手机号,可留空" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="身份证号" required>
|
||||||
|
<el-input v-model="editForm.id_number" maxlength="18" placeholder="18 位身份证号(同时是登录账号)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-alert type="warning" :closable="false" show-icon class="edit-alert">
|
||||||
|
<div class="edit-alert-title">身份证号同时是<strong>登录账号</strong>,修改后的连带影响:</div>
|
||||||
|
<div>· 选手<strong>未激活</strong>且口令仍为「证号后 6 位」派生值 → 口令<strong>自动同步重算</strong>;</div>
|
||||||
|
<div>· 选手<strong>已激活</strong>(口令由本人设置)→ 口令<strong>保持不变</strong>,需让选手走「忘记密码」;</div>
|
||||||
|
<div>· 若新证号与库内他人重复 → 保存会被拒绝(身份证号全局唯一)。</div>
|
||||||
|
<div class="edit-alert-title">本弹窗<strong>不可</strong>修改:组别、成绩、测试数据标记、激活状态、小组归属。</div>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<div v-if="editWarnings.length" class="edit-warnings">
|
||||||
|
<div v-for="(w, i) in editWarnings" :key="i" class="edit-warning-item">⚠️ {{ w }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="editVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="editSaving" @click="submitEditContestant">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1798,6 +1905,14 @@ const groupStats = ref([])
|
|||||||
const statsOnlyProduction = ref(false)
|
const statsOnlyProduction = ref(false)
|
||||||
// Excel 导入时是否标记为测试数据(默认勾选,便于一键重置、隔离生产作用域)
|
// Excel 导入时是否标记为测试数据(默认勾选,便于一键重置、隔离生产作用域)
|
||||||
const importAsTest = ref(true)
|
const importAsTest = ref(true)
|
||||||
|
// R107:导入结果明细(成功/跳过/忽略/需复核 四类),供「导入结果」对话框展示
|
||||||
|
const importResultVisible = ref(false)
|
||||||
|
const importResult = ref(null)
|
||||||
|
const isImportResultClean = computed(() => {
|
||||||
|
const r = importResult.value
|
||||||
|
if (!r) return false
|
||||||
|
return !(r.errors && r.errors.length) && !(r.warnings && r.warnings.length)
|
||||||
|
})
|
||||||
// 默认口令提示(仅用于前端文案展示,真实值由后端 TEST_DEFAULT_PASSWORD 控制)
|
// 默认口令提示(仅用于前端文案展示,真实值由后端 TEST_DEFAULT_PASSWORD 控制)
|
||||||
const testDefaultPasswordHint = ref(import.meta.env.VITE_TEST_DEFAULT_PASSWORD || '123456')
|
const testDefaultPasswordHint = ref(import.meta.env.VITE_TEST_DEFAULT_PASSWORD || '123456')
|
||||||
|
|
||||||
@@ -1844,6 +1959,88 @@ async function handleToggleTestFlag(row, val) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 选手信息编辑(R108 新增) ====================
|
||||||
|
// 背景:此前 contestants 只有「Excel 导入」一个写入口,身份证号/姓名/手机号录错即无退路
|
||||||
|
// (改不了也删不掉)。本组函数提供事后纠正通道。
|
||||||
|
// 口径来源:backend/app/service/admin.js#updateContestant(可改字段白名单 / 口令同步规则)。
|
||||||
|
const editVisible = ref(false)
|
||||||
|
const editSaving = ref(false)
|
||||||
|
const editWarnings = ref([])
|
||||||
|
const editRow = ref(null)
|
||||||
|
// ⚠️ 只放「可改字段 + 只读展示字段」。切勿整行透传 —— 后端对白名单外字段一律 400
|
||||||
|
// (group / score / test_flag 等为只读),整行透传会把请求直接打成失败。
|
||||||
|
const editForm = ref(null)
|
||||||
|
|
||||||
|
function openEditContestant(row) {
|
||||||
|
editRow.value = row
|
||||||
|
editForm.value = {
|
||||||
|
name: row.name || '',
|
||||||
|
phone: row.phone || '',
|
||||||
|
id_number: row.idNumber || row.id_number || '',
|
||||||
|
group: row.group || '',
|
||||||
|
}
|
||||||
|
editWarnings.value = []
|
||||||
|
editVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitEditContestant() {
|
||||||
|
const form = editForm.value
|
||||||
|
const row = editRow.value
|
||||||
|
if (!form || !row) return
|
||||||
|
|
||||||
|
// 客户端只做「后端必然也会拒」的空值预检,避免无谓往返。
|
||||||
|
// 长度 / 校验位 / 唯一性等判据的**唯一真相源在后端**(lib/idCard.js + UNIQUE 约束),此处不复制口径。
|
||||||
|
const name = String(form.name || '').trim()
|
||||||
|
const phone = String(form.phone || '').trim()
|
||||||
|
const idNumber = String(form.id_number || '').trim()
|
||||||
|
if (!name) { ElMessage.warning('姓名不能为空'); return }
|
||||||
|
if (!idNumber) { ElMessage.warning('身份证号不能为空'); return }
|
||||||
|
if (phone && !/^1\d{10}$/.test(phone)) { ElMessage.warning('手机号应为 1 开头的 11 位数字(可留空)'); return }
|
||||||
|
|
||||||
|
// 只提交「与列表行不同」的字段:减少无谓写库,也让后端返回的 changed 语义更干净。
|
||||||
|
const payload = {}
|
||||||
|
if (name !== String(row.name || '')) payload.name = name
|
||||||
|
if (phone !== String(row.phone || '')) payload.phone = phone
|
||||||
|
if (idNumber !== String(row.idNumber || row.id_number || '')) payload.id_number = idNumber
|
||||||
|
if (Object.keys(payload).length === 0) {
|
||||||
|
ElMessage.info('未做任何修改')
|
||||||
|
editVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editSaving.value = true
|
||||||
|
try {
|
||||||
|
const data = await adminApi.updateContestant(row.uid, payload)
|
||||||
|
const warnings = (data && data.warnings) || []
|
||||||
|
const changed = (data && data.changed) || []
|
||||||
|
editWarnings.value = warnings
|
||||||
|
if (warnings.length) {
|
||||||
|
// 有需人工知悉的连带影响(典型:已激活者改号后口令不重置)⇒ 留在弹窗内展示,
|
||||||
|
// 不关窗、不吞掉 —— 这类提示一旦被 toast 带过就等于没提示。
|
||||||
|
editVisible.value = true
|
||||||
|
ElMessage.warning('已保存,但有需注意的提示,请查看弹窗内说明')
|
||||||
|
} else if (changed.length === 0) {
|
||||||
|
ElMessage.info('未检测到需要修改的内容')
|
||||||
|
editVisible.value = false
|
||||||
|
} else {
|
||||||
|
ElMessage.success('选手信息已更新')
|
||||||
|
editVisible.value = false
|
||||||
|
}
|
||||||
|
fetchAccounts()
|
||||||
|
} catch (e) {
|
||||||
|
// ⚠️ 此处**不能**读 `e.response.status`:后端 `ctx.helper.error(code, msg)` **不改 HTTP 状态**
|
||||||
|
// (extend/helper.js:164-169)⇒ 业务错 = HTTP 200 + body.code≠0 ⇒ 响应拦截器
|
||||||
|
// (api/request.js:178-183)以 `Promise.reject(new Error(res.message))` 抛出,
|
||||||
|
// 该 Error **没有 response 属性**。后端的 message 本身已自带上下文
|
||||||
|
// (如「身份证号 X 已被占用:李雨桐(测试数据) / id=2 / 组别=zhongzhi」),
|
||||||
|
// 故直接透传即可,不再自行拼接状态码前缀。
|
||||||
|
// silent: true 已抑制拦截器提示 ⇒ 这里只弹一次,无同族双弹。
|
||||||
|
ElMessage.error((e && e.message) || '编辑失败')
|
||||||
|
} finally {
|
||||||
|
editSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleResetTestData() {
|
async function handleResetTestData() {
|
||||||
resetLoading.value = true
|
resetLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -1972,21 +2169,29 @@ async function onExcelImport(file) {
|
|||||||
// 导致「标记为测试数据」勾选失效、测试数据被当成生产数据导入(test_flag=0)。
|
// 导致「标记为测试数据」勾选失效、测试数据被当成生产数据导入(test_flag=0)。
|
||||||
formData.append('testFlag', importAsTest.value ? '1' : '0')
|
formData.append('testFlag', importAsTest.value ? '1' : '0')
|
||||||
formData.append('file', file.raw)
|
formData.append('file', file.raw)
|
||||||
// 响应拦截器已解包,这里拿到的是后端返回的导入结果 {imported, skipped, errors}
|
// 响应拦截器已解包,这里拿到的是后端返回的导入结果
|
||||||
|
// { imported, skipped, ignored, ignoredBy, errors:[{row,name,message}], warnings:[{row,name,message}] }
|
||||||
const result = await adminApi.importExcel(formData)
|
const result = await adminApi.importExcel(formData)
|
||||||
const imported = (result && result.imported) || 0
|
const imported = (result && result.imported) || 0
|
||||||
const skipped = (result && result.skipped) || 0
|
const skipped = (result && result.skipped) || 0
|
||||||
// 非参赛选手行(报名人/指导教师/领队)按业务规则主动忽略,与「跳过」分开计数并展示
|
// 非参赛选手行(报名人/指导教师/领队)按业务规则主动忽略,与「跳过」分开计数并展示
|
||||||
const ignored = (result && result.ignored) || 0
|
const ignored = (result && result.ignored) || 0
|
||||||
|
const errors = (result && result.errors) || []
|
||||||
|
const warnings = (result && result.warnings) || []
|
||||||
if (ignored && result.ignoredBy) {
|
if (ignored && result.ignoredBy) {
|
||||||
console.info('按身份忽略明细:', result.ignoredBy)
|
console.info('按身份忽略明细:', result.ignoredBy)
|
||||||
}
|
}
|
||||||
|
// R107:明细不再只打控制台 —— 统一填进「导入结果」对话框,便于定位到具体行与人
|
||||||
|
importResult.value = { imported, skipped, ignored, errors, warnings }
|
||||||
if (imported === 0 && skipped > 0) {
|
if (imported === 0 && skipped > 0) {
|
||||||
const detail = ((result && result.errors) || []).slice(0, 3)
|
ElMessage.warning(`导入完成但 0 条成功、跳过 ${skipped} 条,详见「导入结果」`)
|
||||||
.map(e => `第${e.row}行:${e.message}`).join(';')
|
|
||||||
ElMessage.warning(`导入完成但 0 条成功、跳过 ${skipped} 条。原因:${detail || '见控制台'}`)
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.success(`Excel 导入成功:成功 ${imported} 条${importAsTest.value ? '(已标记为测试数据)' : ''}${skipped ? `,跳过 ${skipped} 条` : ''}${ignored ? `,忽略非参赛选手 ${ignored} 条` : ''}`)
|
const warnTip = warnings.length ? `,${warnings.length} 行需人工复核` : ''
|
||||||
|
ElMessage.success(`Excel 导入成功:成功 ${imported} 条${importAsTest.value ? '(已标记为测试数据)' : ''}${skipped ? `,跳过 ${skipped} 条` : ''}${ignored ? `,忽略非参赛选手 ${ignored} 条` : ''}${warnTip}`)
|
||||||
|
}
|
||||||
|
// 有跳过或有需复核项时自动弹出明细;全部正常则只留 toast,不打扰
|
||||||
|
if (skipped > 0 || errors.length || warnings.length) {
|
||||||
|
importResultVisible.value = true
|
||||||
}
|
}
|
||||||
accountsPage.value = 1
|
accountsPage.value = 1
|
||||||
fetchAccounts()
|
fetchAccounts()
|
||||||
@@ -3790,6 +3995,33 @@ onMounted(() => {
|
|||||||
.top-left { display: flex; align-items: center; gap: 10px; }
|
.top-left { display: flex; align-items: center; gap: 10px; }
|
||||||
.admin-logo { font-size: 22px; color: #c9a84c; }
|
.admin-logo { font-size: 22px; color: #c9a84c; }
|
||||||
.admin-title { font-size: 16px; font-weight: 600; }
|
.admin-title { font-size: 16px; font-weight: 600; }
|
||||||
|
|
||||||
|
/* R107(2026-10-12)导入结果明细对话框 */
|
||||||
|
.import-result-summary { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 6px; font-size: 13px; }
|
||||||
|
.import-result-summary span { padding: 2px 10px; border-radius: 12px; line-height: 20px; }
|
||||||
|
.import-result-summary .ir-ok { background: #f0f9eb; color: #67c23a; }
|
||||||
|
.import-result-summary .ir-skip { background: #fdf6ec; color: #e6a23c; }
|
||||||
|
.import-result-summary .ir-ignore { background: #f4f4f5; color: #909399; }
|
||||||
|
.import-result-summary .ir-warn { background: #fef0f0; color: #f56c6c; }
|
||||||
|
.import-result-subtitle { margin: 14px 0 8px; font-size: 13px; color: #303133; }
|
||||||
|
.import-result-subtitle.ir-error-title { color: #f56c6c; }
|
||||||
|
.import-result-subtitle.ir-warn-title { color: #e6a23c; }
|
||||||
|
.import-result-table { margin-bottom: 4px; }
|
||||||
|
.import-result-clean { padding: 18px 0; text-align: center; color: #67c23a; font-size: 14px; }
|
||||||
|
|
||||||
|
/* R108 选手信息编辑弹窗 */
|
||||||
|
.edit-readonly-hint { margin-left: 10px; color: #909399; font-size: 12px; }
|
||||||
|
.edit-alert { margin-top: 4px; }
|
||||||
|
.edit-alert .edit-alert-title { margin-bottom: 2px; }
|
||||||
|
.edit-alert .edit-alert-title:not(:first-child) { margin-top: 8px; }
|
||||||
|
.edit-warnings {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #f3d19e;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fdf6ec;
|
||||||
|
}
|
||||||
|
.edit-warning-item { color: #b88230; font-size: 12px; line-height: 20px; }
|
||||||
.top-right { display: flex; gap: 8px; }
|
.top-right { display: flex; gap: 8px; }
|
||||||
|
|
||||||
/* ==================== 主体 ==================== */
|
/* ==================== 主体 ==================== */
|
||||||
|
|||||||
Reference in New Issue
Block a user