generate users.

This commit is contained in:
2023-03-21 20:44:51 +08:00
parent 658c60998b
commit 1289a55f75
9 changed files with 219 additions and 36 deletions

View File

@@ -1,6 +1,10 @@
import http from "utils/http"
import { Problem, User } from "~/utils/types"
export function getBaseInfo() {
return http.get("admin/dashboard_info")
}
export async function getProblemList(
offset = 0,
limit = 10,
@@ -48,20 +52,28 @@ export function getContestProblem(id: number) {
return http.get("admin/contest/problem", { params: { id } })
}
// 用户列表
export function getUserList(offset = 0, limit = 10, keyword: string) {
return http.get("admin/user", {
params: { paging: true, offset, limit, keyword },
})
}
export function deleteUsers(userIDs: number[]) {
return http.delete("admin/user", { params: { id: userIDs.join(",") } })
}
// 编辑用户
export function editUser(user: User) {
return http.put("admin/user", user)
}
// 导入用户
export function importUsers(users: string[][]) {
return http.post("admin/user", { users })
}
// 批量删除用户
export function deleteUsers(userIDs: number[]) {
return http.delete("admin/user", { params: { id: userIDs.join(",") } })
}
export function getContestList(offset = 0, limit = 10, keyword: string) {
return http.get("admin/contest", {
params: { paging: true, offset, limit, keyword },

View File

@@ -1,7 +1,62 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { useUserStore } from "~/shared/store/user"
import { getBaseInfo } from "../api"
import party from "party-js"
const userCount = ref(0)
const submissionCount = ref(0)
const contestCount = ref(0)
const userStore = useUserStore()
party.resolvableShapes["fries"] = `<span style="font-size: 100px">🍟</span>`
party.resolvableShapes["joker"] = `<span style="font-size: 100px">🤡</span>`
function partyBegin1() {
party.sparkles(document.body, { shapes: ["fries"] })
}
function partyBegin2() {
party.sparkles(document.body, { shapes: ["joker"] })
}
onMounted(async () => {
const res = await getBaseInfo()
userCount.value = res.data.user_count
submissionCount.value = res.data.today_submission_count
contestCount.value = res.data.recent_contest_count
})
</script>
<template>
<div></div>
<n-space align="center">
<n-avatar round :size="60" :src="userStore.profile?.avatar"></n-avatar>
<h1 class="name">{{ userStore.user?.username }}</h1>
</n-space>
<h2>
<n-gradient-text type="info">总用户数{{ userCount }}</n-gradient-text>
</h2>
<h2>
<n-gradient-text type="error">
今日提交{{ submissionCount }}
</n-gradient-text>
</h2>
<h2>
<n-gradient-text type="warning">
近期比赛{{ contestCount }}
</n-gradient-text>
</h2>
<n-space align="center">
<span>我猜你要</span>
<n-button @click="$router.push('/admin/problem/create')">新题目</n-button>
<n-button @click="$router.push('/admin/contest/create')">新比赛</n-button>
<n-button @click="partyBegin1">来点薯条</n-button>
<n-button @click="partyBegin2">做回自己</n-button>
</n-space>
</template>
<style scoped></style>
<style scoped>
.name {
font-size: 48px;
margin: 0;
}
</style>

117
src/admin/user/generate.vue Normal file
View File

@@ -0,0 +1,117 @@
<script setup lang="ts">
import { DataTableColumn, NAlert } from "naive-ui"
import { importUsers } from "../api"
const possibleChars = "0123456789"
const message = useMessage()
const prefix = ref("")
const rawInput = ref("")
const [needKs] = useToggle(true)
const users = shallowRef<string[][]>([])
const columns: DataTableColumn[] = [
{ title: "用户名", key: "username" },
{ title: "密码", key: "password" },
{ title: "邮箱", key: "email" },
{ title: "真名", key: "realName" },
]
const usersToTable = computed(() => {
return users.value.map((u) => {
const username = u[0]
const password = u[1]
const email = u[2]
const realName = u[3]
return { username, password, realName, email }
})
})
function generateUsers() {
// 自动加上 ks 的开头
let myClass = ""
if (prefix.value) {
if (needKs.value && !prefix.value.startsWith("ks")) {
myClass = "ks" + prefix.value
} else {
myClass = prefix.value
}
}
if (!rawInput.value || !rawInput.value.trim()) return
rawInput.value = rawInput.value.trim()
const inputs = rawInput.value.split("\n")
users.value = inputs.map((u, i) => {
const username = myClass + u
let password = ""
for (let i = 0; i < 6; i++) {
password += possibleChars.charAt(
Math.floor(Math.random() * possibleChars.length)
)
}
const realName = u
const email = `${myClass}.${i + 1}@example.com`
return [username, password, email, realName]
})
}
async function uploadUsers() {
try {
await importUsers(users.value)
message.success("用户已上传成功")
const csv = users.value.map((u) => u.join(",")).join("\n")
const hiddenElement = document.createElement("a")
hiddenElement.href = "data:text/csv;charset=utf-8," + encodeURI(csv)
hiddenElement.target = "_blank"
hiddenElement.download = prefix.value + ".csv"
hiddenElement.click()
hiddenElement.remove()
} catch (err: any) {
message.error("上传失败:" + err.data)
}
}
function handleAll() {
generateUsers()
uploadUsers()
}
</script>
<template>
<n-space>
<n-space vertical>
<n-space align="center">
<n-switch v-model:value="needKs" />
<span>前面带上 ks</span>
</n-space>
<n-input v-model:value="prefix" placeholder="班级号" />
<n-input
type="textarea"
class="inputArea"
placeholder="每行一个用户名"
autofocus
v-model:value="rawInput"
/>
</n-space>
<n-scrollbar style="max-height: calc(100vh - 34px)">
<n-data-table
v-if="usersToTable.length"
:columns="columns"
:data="usersToTable"
/>
</n-scrollbar>
<n-space vertical>
<n-button @click="generateUsers">让我康康</n-button>
<n-button type="warning" :disabled="!users.length" @click="uploadUsers">
上传用户
</n-button>
<n-button type="info" @click="handleAll">一键三连</n-button>
</n-space>
</n-space>
</template>
<style scoped>
.inputArea {
width: 200px;
height: calc(100vh - 108px);
}
</style>

View File

@@ -1,7 +0,0 @@
<script setup lang="ts"></script>
<template>
<div>user import</div>
</template>
<style scoped></style>

View File

@@ -1,10 +1,5 @@
<script setup lang="ts">
import {
DataTableColumn,
DataTableRowKey,
messageDark,
SelectOption,
} from "naive-ui"
import { DataTableColumn, DataTableRowKey, SelectOption } from "naive-ui"
import Pagination from "~/shared/Pagination.vue"
import { parseTime } from "~/utils/functions"
import { User } from "~/utils/types"
@@ -134,7 +129,7 @@ watch(query, listUsers, { deep: true })
<template #trigger>
<n-button type="warning">删除</n-button>
</template>
确定删除这个用户吗删除后无法恢复
确定删除选中的用户吗删除后无法恢复
</n-popconfirm>
</n-form-item>
</n-form>

2
src/components.d.ts vendored
View File

@@ -32,6 +32,7 @@ declare module '@vue/runtime-core' {
NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi']
NGradientText: typeof import('naive-ui')['NGradientText']
NGrid: typeof import('naive-ui')['NGrid']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
@@ -52,6 +53,7 @@ declare module '@vue/runtime-core' {
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NTextarea: typeof import('naive-ui')['NTextarea']
NTooltip: typeof import('naive-ui')['NTooltip']
NUpload: typeof import('naive-ui')['NUpload']
RouterLink: typeof import('vue-router')['RouterLink']

View File

@@ -64,8 +64,9 @@ async function saveProfile() {
<n-button
@click="saveProfile"
:disabled="!userStore.profile.mood && !userStore.profile.real_name"
>更改信息</n-button
>
更改信息
</n-button>
</n-form>
</n-space>
</template>

View File

@@ -1,8 +1,5 @@
import { RouteRecordRaw } from "vue-router"
import { getProfile } from "./shared/api"
import { loadChart } from "./shared/composables/chart"
import { STORAGE_KEY, USER_TYPE } from "./utils/constants"
import storage from "./utils/storage"
export const routes: RouteRecordRaw[] = [
{
@@ -98,11 +95,6 @@ export const routes: RouteRecordRaw[] = [
{
path: "/admin",
component: () => import("~/shared/layout/admin.vue"),
beforeEnter: async () => {
if (!storage.get(STORAGE_KEY.AUTHED)) return "/"
const res = await getProfile()
if (res.data.user.admin_type === USER_TYPE.REGULAR_USER) return "/"
},
children: [
{
path: "",
@@ -125,9 +117,9 @@ export const routes: RouteRecordRaw[] = [
component: () => import("admin/user/list.vue"),
},
{
path: "user/importing",
name: "admin user importing",
component: () => import("~/admin/user/importing.vue"),
path: "user/generate",
name: "admin user generate",
component: () => import("~/admin/user/generate.vue"),
},
{
path: "problem/list",

View File

@@ -1,8 +1,13 @@
<script setup lang="ts">
import { MenuOption } from "naive-ui"
import { RouterLink } from "vue-router"
import { STORAGE_KEY } from "~/utils/constants"
import storage from "~/utils/storage"
import { useUserStore } from "../store/user"
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const options: MenuOption[] = [
{
label: () => h(RouterLink, { to: "/" }, { default: () => "返回 OJ" }),
@@ -45,10 +50,10 @@ const options: MenuOption[] = [
label: () =>
h(
RouterLink,
{ to: "/admin/user/importing" },
{ default: () => "导入用户" }
{ to: "/admin/user/generate" },
{ default: () => "批量生成" }
),
key: "admin user importing",
key: "admin user generate",
},
{ label: "比赛", key: "contest", disabled: true },
{
@@ -87,6 +92,17 @@ const options: MenuOption[] = [
]
const active = computed(() => (route.name as string) || "home")
onMounted(async () => {
if (!storage.get(STORAGE_KEY.AUTHED)) {
router.replace("/")
} else {
await userStore.getMyProfile()
if (!userStore.isAdminRole) {
router.replace("/")
}
}
})
</script>
<template>