Implement student roster login gating
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
||||
DashboardStats,
|
||||
KnowledgeItem,
|
||||
ModelItem,
|
||||
UserImportResult,
|
||||
} from "./types/api";
|
||||
|
||||
const admin = ref<AdminProfile | null>(null);
|
||||
@@ -40,6 +41,17 @@ const agentPreviewMessages = ref<{ role: "user" | "assistant" | "system"; conten
|
||||
{ role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" },
|
||||
]);
|
||||
|
||||
const studentForm = reactive({
|
||||
phone: "",
|
||||
name: "",
|
||||
nickname: "",
|
||||
status: 1,
|
||||
dailyChatLimit: 100,
|
||||
expiredAt: "",
|
||||
});
|
||||
const studentImportText = ref("手机号,姓名,昵称\n13800000001,张三,三三\n13800000002,李四,");
|
||||
const studentImportResult = ref<UserImportResult | null>(null);
|
||||
|
||||
const knowledgeForm = reactive({
|
||||
name: "",
|
||||
feishuSpaceId: "",
|
||||
@@ -241,6 +253,59 @@ async function saveUser(row: AdminUser) {
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function createStudent() {
|
||||
if (!studentForm.phone || !studentForm.name) {
|
||||
ElMessage.error("请填写手机号和姓名");
|
||||
return;
|
||||
}
|
||||
await api.createUser({
|
||||
phone: studentForm.phone,
|
||||
name: studentForm.name,
|
||||
nickname: studentForm.nickname || null,
|
||||
status: studentForm.status,
|
||||
dailyChatLimit: studentForm.dailyChatLimit,
|
||||
expiredAt: studentForm.expiredAt || null,
|
||||
});
|
||||
ElMessage.success("学员已添加");
|
||||
resetStudentForm();
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function importStudents() {
|
||||
const students = parseStudentImportText(studentImportText.value);
|
||||
if (!students.length) {
|
||||
ElMessage.error("请粘贴学员数据");
|
||||
return;
|
||||
}
|
||||
studentImportResult.value = await api.importUsers(students);
|
||||
ElMessage.success(
|
||||
`导入完成:新增 ${studentImportResult.value.created},更新 ${studentImportResult.value.updated},失败 ${studentImportResult.value.failed}`,
|
||||
);
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
function parseStudentImportText(text: string) {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.filter((line, index) => !(index === 0 && line.includes("手机号")))
|
||||
.map((line) => {
|
||||
const [phone = "", name = "", nickname = ""] = line.split(/,|\t/).map((item) => item.trim());
|
||||
return {
|
||||
phone,
|
||||
name,
|
||||
nickname: nickname || null,
|
||||
status: 1,
|
||||
dailyChatLimit: 100,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function resetStudentForm() {
|
||||
Object.assign(studentForm, { phone: "", name: "", nickname: "", status: 1, dailyChatLimit: 100, expiredAt: "" });
|
||||
}
|
||||
|
||||
async function deleteUser(row: AdminUser) {
|
||||
if (!window.confirm(`确认删除用户 ${row.phone} 吗?删除后前台将无法继续使用该账号。`)) return;
|
||||
await api.deleteUser(row.id);
|
||||
@@ -659,9 +724,55 @@ function buildChatQuery() {
|
||||
|
||||
<template v-if="activeMenu === 'users'">
|
||||
<div class="page-head inline">
|
||||
<div><h2>用户管理</h2><p>查询用户、启用禁用和调整额度。</p></div>
|
||||
<div><h2>用户管理</h2><p>维护学员名单;只有名单内启用学员可以登录用户端。</p></div>
|
||||
<el-input v-model="userKeyword" placeholder="手机号或姓名" clearable @change="loadCurrentMenu" />
|
||||
</div>
|
||||
<section class="student-tools">
|
||||
<div class="student-tool-panel">
|
||||
<h3>手动添加学员</h3>
|
||||
<div class="student-form-grid">
|
||||
<el-input v-model="studentForm.phone" placeholder="手机号" />
|
||||
<el-input v-model="studentForm.name" placeholder="姓名" />
|
||||
<el-input v-model="studentForm.nickname" placeholder="昵称(可选)" />
|
||||
<el-input-number v-model="studentForm.dailyChatLimit" :min="0" />
|
||||
<el-date-picker
|
||||
v-model="studentForm.expiredAt"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
placeholder="有效期(可选)"
|
||||
/>
|
||||
<div class="form-switch-row">
|
||||
<span>启用</span>
|
||||
<el-switch v-model="studentForm.status" :active-value="1" :inactive-value="0" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions compact">
|
||||
<el-button type="primary" @click="createStudent">添加学员</el-button>
|
||||
<el-button @click="resetStudentForm">清空</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="student-tool-panel">
|
||||
<h3>批量导入学员</h3>
|
||||
<el-input
|
||||
v-model="studentImportText"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
resize="none"
|
||||
placeholder="每行一个学员:手机号,姓名,昵称"
|
||||
/>
|
||||
<div class="actions compact">
|
||||
<el-button type="primary" @click="importStudents">导入学员</el-button>
|
||||
<span v-if="studentImportResult" class="import-result">
|
||||
新增 {{ studentImportResult.created }},更新 {{ studentImportResult.updated }},失败 {{ studentImportResult.failed }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="studentImportResult?.failures.length" class="import-failures">
|
||||
<span v-for="failure in studentImportResult.failures.slice(0, 5)" :key="`${failure.row}-${failure.phone}`">
|
||||
第 {{ failure.row }} 行 {{ failure.phone || '-' }}:{{ failure.reason }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<el-table :data="users" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="phone" label="手机号" width="150" />
|
||||
|
||||
Reference in New Issue
Block a user