generate users.
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
import http from "utils/http"
|
import http from "utils/http"
|
||||||
import { Problem, User } from "~/utils/types"
|
import { Problem, User } from "~/utils/types"
|
||||||
|
|
||||||
|
export function getBaseInfo() {
|
||||||
|
return http.get("admin/dashboard_info")
|
||||||
|
}
|
||||||
|
|
||||||
export async function getProblemList(
|
export async function getProblemList(
|
||||||
offset = 0,
|
offset = 0,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
@@ -48,20 +52,28 @@ export function getContestProblem(id: number) {
|
|||||||
return http.get("admin/contest/problem", { params: { id } })
|
return http.get("admin/contest/problem", { params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户列表
|
||||||
export function getUserList(offset = 0, limit = 10, keyword: string) {
|
export function getUserList(offset = 0, limit = 10, keyword: string) {
|
||||||
return http.get("admin/user", {
|
return http.get("admin/user", {
|
||||||
params: { paging: true, offset, limit, keyword },
|
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) {
|
export function editUser(user: User) {
|
||||||
return http.put("admin/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) {
|
export function getContestList(offset = 0, limit = 10, keyword: string) {
|
||||||
return http.get("admin/contest", {
|
return http.get("admin/contest", {
|
||||||
params: { paging: true, offset, limit, keyword },
|
params: { paging: true, offset, limit, keyword },
|
||||||
|
|||||||
@@ -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>
|
<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>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.name {
|
||||||
|
font-size: 48px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
117
src/admin/user/generate.vue
Normal file
117
src/admin/user/generate.vue
Normal 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>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>user import</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { DataTableColumn, DataTableRowKey, SelectOption } from "naive-ui"
|
||||||
DataTableColumn,
|
|
||||||
DataTableRowKey,
|
|
||||||
messageDark,
|
|
||||||
SelectOption,
|
|
||||||
} from "naive-ui"
|
|
||||||
import Pagination from "~/shared/Pagination.vue"
|
import Pagination from "~/shared/Pagination.vue"
|
||||||
import { parseTime } from "~/utils/functions"
|
import { parseTime } from "~/utils/functions"
|
||||||
import { User } from "~/utils/types"
|
import { User } from "~/utils/types"
|
||||||
@@ -134,7 +129,7 @@ watch(query, listUsers, { deep: true })
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button type="warning">删除</n-button>
|
<n-button type="warning">删除</n-button>
|
||||||
</template>
|
</template>
|
||||||
确定删除这个用户吗?删除后无法恢复!
|
确定删除选中的用户吗?删除后无法恢复!
|
||||||
</n-popconfirm>
|
</n-popconfirm>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
|||||||
2
src/components.d.ts
vendored
2
src/components.d.ts
vendored
@@ -32,6 +32,7 @@ declare module '@vue/runtime-core' {
|
|||||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||||
NGi: typeof import('naive-ui')['NGi']
|
NGi: typeof import('naive-ui')['NGi']
|
||||||
|
NGradientText: typeof import('naive-ui')['NGradientText']
|
||||||
NGrid: typeof import('naive-ui')['NGrid']
|
NGrid: typeof import('naive-ui')['NGrid']
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
@@ -52,6 +53,7 @@ declare module '@vue/runtime-core' {
|
|||||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||||
NTabs: typeof import('naive-ui')['NTabs']
|
NTabs: typeof import('naive-ui')['NTabs']
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
|
NTextarea: typeof import('naive-ui')['NTextarea']
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
NUpload: typeof import('naive-ui')['NUpload']
|
NUpload: typeof import('naive-ui')['NUpload']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ async function saveProfile() {
|
|||||||
<n-button
|
<n-button
|
||||||
@click="saveProfile"
|
@click="saveProfile"
|
||||||
:disabled="!userStore.profile.mood && !userStore.profile.real_name"
|
:disabled="!userStore.profile.mood && !userStore.profile.real_name"
|
||||||
>更改信息</n-button
|
|
||||||
>
|
>
|
||||||
|
更改信息
|
||||||
|
</n-button>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { RouteRecordRaw } from "vue-router"
|
import { RouteRecordRaw } from "vue-router"
|
||||||
import { getProfile } from "./shared/api"
|
|
||||||
import { loadChart } from "./shared/composables/chart"
|
import { loadChart } from "./shared/composables/chart"
|
||||||
import { STORAGE_KEY, USER_TYPE } from "./utils/constants"
|
|
||||||
import storage from "./utils/storage"
|
|
||||||
|
|
||||||
export const routes: RouteRecordRaw[] = [
|
export const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@@ -98,11 +95,6 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
component: () => import("~/shared/layout/admin.vue"),
|
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: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
@@ -125,9 +117,9 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import("admin/user/list.vue"),
|
component: () => import("admin/user/list.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "user/importing",
|
path: "user/generate",
|
||||||
name: "admin user importing",
|
name: "admin user generate",
|
||||||
component: () => import("~/admin/user/importing.vue"),
|
component: () => import("~/admin/user/generate.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "problem/list",
|
path: "problem/list",
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MenuOption } from "naive-ui"
|
import { MenuOption } from "naive-ui"
|
||||||
import { RouterLink } from "vue-router"
|
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 route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
const options: MenuOption[] = [
|
const options: MenuOption[] = [
|
||||||
{
|
{
|
||||||
label: () => h(RouterLink, { to: "/" }, { default: () => "返回 OJ" }),
|
label: () => h(RouterLink, { to: "/" }, { default: () => "返回 OJ" }),
|
||||||
@@ -45,10 +50,10 @@ const options: MenuOption[] = [
|
|||||||
label: () =>
|
label: () =>
|
||||||
h(
|
h(
|
||||||
RouterLink,
|
RouterLink,
|
||||||
{ to: "/admin/user/importing" },
|
{ to: "/admin/user/generate" },
|
||||||
{ default: () => "导入用户" }
|
{ default: () => "批量生成" }
|
||||||
),
|
),
|
||||||
key: "admin user importing",
|
key: "admin user generate",
|
||||||
},
|
},
|
||||||
{ label: "比赛", key: "contest", disabled: true },
|
{ label: "比赛", key: "contest", disabled: true },
|
||||||
{
|
{
|
||||||
@@ -87,6 +92,17 @@ const options: MenuOption[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const active = computed(() => (route.name as string) || "home")
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Reference in New Issue
Block a user