| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- <script setup>
- import { UserRole } from '@/enums'
- import { createUserApi, listUsersApi, updateUserApi } from '@/services/api'
- import { Form } from '@primevue/forms'
- import { zodResolver } from '@primevue/forms/resolvers/zod'
- import { useDateFormat } from '@vueuse/core'
- import Button from 'primevue/button'
- import Column from 'primevue/column'
- import DataTable from 'primevue/datatable'
- import Dialog from 'primevue/dialog'
- import Dropdown from 'primevue/dropdown'
- import FloatLabel from 'primevue/floatlabel'
- import IconField from 'primevue/iconfield'
- import InputIcon from 'primevue/inputicon'
- import InputText from 'primevue/inputtext'
- import Message from 'primevue/message'
- import Password from 'primevue/password'
- import { useToast } from 'primevue/usetoast'
- import { computed, onMounted, ref } from 'vue'
- import { z } from 'zod'
- const toast = useToast()
- const tableData = ref({
- content: [],
- metadata: {
- page: 0,
- size: 20,
- total: 0
- }
- })
- const search = ref('')
- const fetchData = async () => {
- const response = await listUsersApi(tableData.value.metadata.page, tableData.value.metadata.size)
- tableData.value = response
- }
- const handlePageChange = (event) => {
- console.log('handlePageChange', event)
- tableData.value.metadata.page = event.page
- tableData.value.metadata.size = event.rows
- fetchData()
- }
- const formatDate = (date) => {
- return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
- }
- // 获取角色名称
- const getRoleName = (role) => {
- return UserRole[role] || role
- }
- // 用户角色选项
- const roleOptions = computed(() => {
- const allowedRoles = ['user', 'admin', 'channel', 'operator']
- return allowedRoles.map((role) => ({
- value: role,
- label: UserRole[role]
- }))
- })
- // 用户表单相关
- const userDialog = ref(false)
- const isEditMode = ref(false)
- const userForm = ref({
- id: null,
- name: '',
- password: '',
- confirmPassword: '',
- role: 'user'
- })
- const userFormLoading = ref(false)
- const userFormResolver = computed(() => {
- // 创建和更新时的验证规则不同
- const passwordRules = isEditMode.value
- ? z.string().optional() // 更新时密码可选
- : z.string().min(8, { message: '密码至少8位' })
- return zodResolver(
- z.object({
- name: z.string().min(1, { message: '用户名不能为空' }),
- password: passwordRules,
- confirmPassword: z
- .string()
- .refine((val) => !userForm.value.password || val === userForm.value.password, { message: '密码不一致' }),
- role: z.string().min(1, { message: '请选择角色' })
- })
- )
- })
- const openNewUserDialog = () => {
- userForm.value = {
- id: null,
- name: '',
- password: '',
- confirmPassword: '',
- role: 'user'
- }
- isEditMode.value = false
- userDialog.value = true
- }
- const openEditUserDialog = (user) => {
- userForm.value = {
- id: user.id,
- name: user.name,
- password: '',
- confirmPassword: '',
- role: user.role
- }
- isEditMode.value = true
- userDialog.value = true
- }
- const saveUser = async ({ valid, values }) => {
- if (!valid) return
- userFormLoading.value = true
- try {
- // 构建提交数据,过滤掉不需要的confirmPassword
- const submitData = {
- name: values.name,
- role: values.role
- }
- if (values.password) {
- submitData.password = values.password
- }
- if (isEditMode.value) {
- submitData.id = userForm.value.id
- await updateUserApi(submitData)
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: '用户更新成功',
- life: 3000
- })
- } else {
- // 新建用户必须有密码
- await createUserApi(submitData)
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: '用户创建成功',
- life: 3000
- })
- }
- userDialog.value = false
- fetchData() // 刷新列表
- } catch (error) {
- const errorMsg = error.message || (isEditMode.value ? '更新用户失败' : '创建用户失败')
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: errorMsg,
- life: 3000
- })
- } finally {
- userFormLoading.value = false
- }
- }
- onMounted(() => {
- fetchData()
- })
- </script>
- <template>
- <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
- <DataTable
- :value="tableData.content"
- :paginator="true"
- paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
- currentPageReportTemplate="{totalRecords} 条记录 "
- :rows="tableData.metadata.size"
- :rowsPerPageOptions="[10, 20, 50, 100]"
- :totalRecords="tableData.metadata.total"
- @page="handlePageChange"
- lazy
- scrollable
- >
- <template #header>
- <div class="flex flex-wrap items-center">
- <Button icon="pi pi-refresh" @click="fetchData" label="刷新" size="small" />
- <Button
- icon="pi pi-plus"
- @click="openNewUserDialog"
- label="新增用户"
- severity="success"
- size="small"
- class="ml-2"
- />
- <div class="flex-1"></div>
- <IconField>
- <InputIcon>
- <i class="pi pi-search" />
- </InputIcon>
- <InputText v-model="search" placeholder="搜素" />
- </IconField>
- </div>
- </template>
- <Column field="id" header="ID"></Column>
- <Column field="name" header="用户名"></Column>
- <Column field="role" header="角色">
- <template #body="slotProps">
- <span class="px-2 py-1 rounded-md text-sm">
- {{ getRoleName(slotProps.data.role) }}
- </span>
- </template>
- </Column>
- <Column field="createdAt" header="创建时间" style="min-width: 200px">
- <template #body="slotProps">
- {{ formatDate(slotProps.data.createdAt) }}
- </template>
- </Column>
- <Column header="操作" style="min-width: 150px">
- <template #body="slotProps">
- <Button
- icon="pi pi-pencil"
- severity="info"
- size="small"
- text
- rounded
- aria-label="编辑"
- @click="openEditUserDialog(slotProps.data)"
- />
- </template>
- </Column>
- </DataTable>
- <!-- 用户表单对话框 -->
- <Dialog
- v-model:visible="userDialog"
- :modal="true"
- :header="isEditMode ? '编辑用户' : '创建用户'"
- :style="{ width: '450px' }"
- position="center"
- >
- <Form v-slot="$form" :resolver="userFormResolver" :initialValues="userForm" @submit="saveUser" class="p-fluid">
- <div class="field mt-4">
- <FloatLabel variant="on">
- <IconField>
- <InputIcon class="pi pi-user" />
- <InputText id="name" name="name" v-model="userForm.name" autocomplete="off" fluid />
- </IconField>
- <label for="name">用户名</label>
- </FloatLabel>
- <Message v-if="$form.name?.invalid" severity="error" size="small" variant="simple">
- {{ $form.name.error?.message }}
- </Message>
- </div>
- <div class="field mt-4">
- <FloatLabel variant="on">
- <IconField>
- <InputIcon class="pi pi-lock" />
- <Password
- id="password"
- name="password"
- v-model="userForm.password"
- toggleMask
- :feedback="false"
- autocomplete="off"
- fluid
- />
- </IconField>
- <label for="password">{{ isEditMode ? '密码 (可选)' : '密码' }}</label>
- </FloatLabel>
- <Message v-if="$form.password?.invalid" severity="error" size="small" variant="simple">
- {{ $form.password.error?.message }}
- </Message>
- <div v-if="isEditMode" class="text-sm text-gray-500 mt-1 ml-1">* 留空则不修改</div>
- </div>
- <div class="field mt-4">
- <FloatLabel variant="on">
- <IconField>
- <InputIcon class="pi pi-lock" />
- <Password
- id="confirmPassword"
- name="confirmPassword"
- v-model="userForm.confirmPassword"
- toggleMask
- :feedback="false"
- fluid
- autocomplete="off"
- />
- </IconField>
- <label for="confirmPassword">确认密码</label>
- </FloatLabel>
- <Message v-if="$form.confirmPassword?.invalid" severity="error" size="small" variant="simple">
- {{ $form.confirmPassword.error?.message }}
- </Message>
- <div v-if="isEditMode" class="text-sm text-gray-500 mt-1 ml-1">* 留空则不修改</div>
- </div>
- <div class="field mt-4">
- <FloatLabel variant="on">
- <Dropdown
- id="role"
- name="role"
- v-model="userForm.role"
- :options="roleOptions"
- optionLabel="label"
- optionValue="value"
- fluid
- />
- <label for="role">角色</label>
- </FloatLabel>
- <Message v-if="$form.role?.invalid" severity="error" size="small" variant="simple">
- {{ $form.role.error?.message }}
- </Message>
- </div>
- <div class="flex justify-end gap-2 mt-4">
- <Button
- label="取消"
- severity="secondary"
- type="button"
- @click="userDialog = false"
- :disabled="userFormLoading"
- />
- <Button label="保存" type="submit" :loading="userFormLoading" />
- </div>
- </Form>
- </Dialog>
- </div>
- </template>
|