mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	feat(ui):用户
This commit is contained in:
		@@ -84,7 +84,7 @@ const optionsEvent = {
 | 
			
		||||
        </AFormItem>
 | 
			
		||||
      </AGridItem>
 | 
			
		||||
      <AGridItem suffix>
 | 
			
		||||
        <ASpace>
 | 
			
		||||
        <ASpace class="flex-end">
 | 
			
		||||
          <slot name="search-options" :option="optionsEvent">
 | 
			
		||||
            <AButton
 | 
			
		||||
              type="primary"
 | 
			
		||||
@@ -114,4 +114,8 @@ const optionsEvent = {
 | 
			
		||||
.search-form-conteiner {
 | 
			
		||||
  padding: 16px 0;
 | 
			
		||||
}
 | 
			
		||||
.flex-end {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: end;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -33,8 +33,9 @@ const handleSearch = async (tips?: boolean) => {
 | 
			
		||||
onActivated(handleSearch);
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="search-table">
 | 
			
		||||
    <div ref="tableContainerRef" class="search-table-container">
 | 
			
		||||
  <slot name="header" v-bind="{ reload: handleSearch }" />
 | 
			
		||||
  <div class="simple-table">
 | 
			
		||||
    <div ref="tableContainerRef" class="simple-table-container">
 | 
			
		||||
      <ATable
 | 
			
		||||
        v-bind="{
 | 
			
		||||
          ...$attrs,
 | 
			
		||||
@@ -52,15 +53,15 @@ onActivated(handleSearch);
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<style scoped>
 | 
			
		||||
.search-table {
 | 
			
		||||
.simple-table {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
.search-table-container {
 | 
			
		||||
.simple-table-container {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
}
 | 
			
		||||
.search-table-header {
 | 
			
		||||
.simple-table-header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,115 @@
 | 
			
		||||
<script lang="ts" setup></script>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import SearchTable from "@/components/SearchTable/SearchTable.vue";
 | 
			
		||||
import type { SearchTableColumns } from "@/components/SearchTable/type";
 | 
			
		||||
import { getList, save as saveApi, deletApi, resetPassword } from "./api";
 | 
			
		||||
import UserForm from "./UserForm.vue";
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import { Message } from "@arco-design/web-vue";
 | 
			
		||||
import { dateFormat } from "@gpt-vue/packages/utils";
 | 
			
		||||
import usePopup from "@/composables/usePopup";
 | 
			
		||||
import UserPassword from "./UserPassword.vue";
 | 
			
		||||
const columns: SearchTableColumns[] = [
 | 
			
		||||
  {
 | 
			
		||||
    title: "账号",
 | 
			
		||||
    dataIndex: "username",
 | 
			
		||||
    search: {
 | 
			
		||||
      valueType: "input",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: "剩余对话次数",
 | 
			
		||||
    dataIndex: "calls",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: "剩余绘图次数",
 | 
			
		||||
    dataIndex: "img_calls",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: "累计消耗tokens",
 | 
			
		||||
    dataIndex: "total_tokens",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: "状态",
 | 
			
		||||
    dataIndex: "status",
 | 
			
		||||
    render: ({ record }) => {
 | 
			
		||||
      return record.status ? "正常" : "停用";
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: "过期时间",
 | 
			
		||||
    dataIndex: "expired_time",
 | 
			
		||||
    render: ({ record }) => {
 | 
			
		||||
      return dateFormat(record.expired_time);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: "注册时间",
 | 
			
		||||
    dataIndex: "created_at",
 | 
			
		||||
    render: ({ record }) => {
 | 
			
		||||
      return dateFormat(record.created_at);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: "操作",
 | 
			
		||||
    slotName: "actions",
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
//弹窗
 | 
			
		||||
 | 
			
		||||
const popup = (node, api) => {
 | 
			
		||||
  const nodeProps = (arg) => {
 | 
			
		||||
    return {
 | 
			
		||||
      data: arg[0].record,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const popupProps = (arg, getExposed) => {
 | 
			
		||||
    return {
 | 
			
		||||
      width: 700,
 | 
			
		||||
      onBeforeOk: async () => {
 | 
			
		||||
        const exposed = getExposed();
 | 
			
		||||
        const validateRes = await exposed?.formRef.value.validate();
 | 
			
		||||
        if (validateRes) {
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
        const res = await api(exposed?.form.value);
 | 
			
		||||
        if (res.code === 0) {
 | 
			
		||||
          Message.success("操作成功");
 | 
			
		||||
        }
 | 
			
		||||
        arg[0].reload();
 | 
			
		||||
        return res.code === 0;
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return usePopup(node, { nodeProps, popupProps });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const editModal = popup(UserForm, saveApi);
 | 
			
		||||
const password = popup(UserPassword, resetPassword);
 | 
			
		||||
 | 
			
		||||
const handleDelete = async ({ id }: { id: string }, reload) => {
 | 
			
		||||
  const res = await deletApi(id);
 | 
			
		||||
  if (res.code === 0) {
 | 
			
		||||
    Message.success("操作成功");
 | 
			
		||||
    reload();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div></div>
 | 
			
		||||
  <SearchTable :request="getList" :columns="columns">
 | 
			
		||||
    <template #actions="{ record, reload }">
 | 
			
		||||
      <a-link @click="editModal({ record, reload })">编辑</a-link>
 | 
			
		||||
      <a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)">
 | 
			
		||||
        <a-link>删除</a-link>
 | 
			
		||||
      </a-popconfirm>
 | 
			
		||||
      <a-link @click="password({ record, reload })">重置密码</a-link>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #search-extra="{ reload }">
 | 
			
		||||
      <a-button @click="editModal({ reload })" status="success" size="small"
 | 
			
		||||
        ><icon-plus />新增用户</a-button
 | 
			
		||||
      >
 | 
			
		||||
    </template>
 | 
			
		||||
  </SearchTable>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										117
									
								
								gpt-vue/projects/vue-admin/src/views/User/UserForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								gpt-vue/projects/vue-admin/src/views/User/UserForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
 | 
			
		||||
    <a-form-item
 | 
			
		||||
      field="username"
 | 
			
		||||
      label="账号"
 | 
			
		||||
      :rules="[{ required: true, message: 'name is required' }]"
 | 
			
		||||
      :validate-trigger="['change', 'input']"
 | 
			
		||||
    >
 | 
			
		||||
      <a-input v-model="form.username" placeholder="请输入账号" />
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
    <a-form-item
 | 
			
		||||
      v-if="!props.data.id"
 | 
			
		||||
      field="password"
 | 
			
		||||
      label="密码"
 | 
			
		||||
      :rules="[{ required: true, message: 'password is required' }]"
 | 
			
		||||
      :validate-trigger="['change', 'input']"
 | 
			
		||||
      showable
 | 
			
		||||
    >
 | 
			
		||||
      <a-input v-model="form.password" placeholder="请输入密码" />
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
    <a-form-item
 | 
			
		||||
      field="calls"
 | 
			
		||||
      label="对话次数"
 | 
			
		||||
      :rules="[
 | 
			
		||||
        { required: true, message: 'count is required' },
 | 
			
		||||
        { type: 'number', message: 'age is max than 200' },
 | 
			
		||||
      ]"
 | 
			
		||||
    >
 | 
			
		||||
      <a-input-number v-model="form.calls" placeholder="请输入对话次数" />
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
    <a-form-item
 | 
			
		||||
      field="img_calls"
 | 
			
		||||
      label="绘图次数"
 | 
			
		||||
      :rules="[
 | 
			
		||||
        { required: true, message: 'count is required' },
 | 
			
		||||
        { type: 'number', message: 'age is max than 200' },
 | 
			
		||||
      ]"
 | 
			
		||||
    >
 | 
			
		||||
      <a-input-number v-model="form.img_calls" placeholder="请输入绘图次数" />
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
    <a-form-item field="expired_time" label="有效期">
 | 
			
		||||
      <a-date-picker v-model="form.expired_time" placeholder="请选择有效期" />
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
    <a-form-item field="chat_roles" label="聊天角色">
 | 
			
		||||
      <a-select
 | 
			
		||||
        :field-names="{ value: 'key', label: 'name' }"
 | 
			
		||||
        v-model="form.chat_roles"
 | 
			
		||||
        placeholder="请选择聊天角色"
 | 
			
		||||
        multiple
 | 
			
		||||
        :options="roleOption"
 | 
			
		||||
      >
 | 
			
		||||
      </a-select>
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
    <a-form-item field="chat_models" label="模型角色">
 | 
			
		||||
      <a-select
 | 
			
		||||
        :field-names="{ value: 'value', label: 'name' }"
 | 
			
		||||
        v-model="form.chat_models"
 | 
			
		||||
        placeholder="请选择模型角色"
 | 
			
		||||
        multiple
 | 
			
		||||
        :options="modalOption"
 | 
			
		||||
      >
 | 
			
		||||
      </a-select>
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
    <a-form-item field="status" label="启用状态">
 | 
			
		||||
      <a-switch v-model="form.status" />
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
    <a-form-item field="vip" label="开通VIP">
 | 
			
		||||
      <a-switch v-model="form.vip" />
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
  </a-form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref, defineExpose, defineProps } from "vue";
 | 
			
		||||
import { getModel, getRole } from "./api";
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  data: {},
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const formRef = ref();
 | 
			
		||||
const form = ref({
 | 
			
		||||
  id: "",
 | 
			
		||||
  username: "",
 | 
			
		||||
  password: "",
 | 
			
		||||
  calls: "",
 | 
			
		||||
  img_calls: "",
 | 
			
		||||
  expired_time: "",
 | 
			
		||||
  chat_roles: [],
 | 
			
		||||
  chat_models: [],
 | 
			
		||||
  status: false,
 | 
			
		||||
  vip: false,
 | 
			
		||||
});
 | 
			
		||||
if (props.data?.id) {
 | 
			
		||||
  form.value = Object.assign({}, props.data);
 | 
			
		||||
  if (form.value.expired_time === 0) {
 | 
			
		||||
    form.value.expired_time = "";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//拿选项
 | 
			
		||||
const modalOption = ref([]);
 | 
			
		||||
const roleOption = ref([]);
 | 
			
		||||
const getOption = (api, container) => {
 | 
			
		||||
  api().then(({ code, data }) => {
 | 
			
		||||
    if (code === 0) {
 | 
			
		||||
      container.value = data;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
getOption(getModel, modalOption);
 | 
			
		||||
getOption(getRole, roleOption);
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  formRef,
 | 
			
		||||
  form,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										44
									
								
								gpt-vue/projects/vue-admin/src/views/User/UserPassword.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								gpt-vue/projects/vue-admin/src/views/User/UserPassword.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
 | 
			
		||||
    <a-form-item
 | 
			
		||||
      field="username"
 | 
			
		||||
      label="账号"
 | 
			
		||||
      :rules="[{ required: true, message: 'name is required' }]"
 | 
			
		||||
      :validate-trigger="['change', 'input']"
 | 
			
		||||
      :disabled="true"
 | 
			
		||||
    >
 | 
			
		||||
      <a-input v-model="form.username" placeholder="请输入账号" />
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
    <a-form-item
 | 
			
		||||
      field="password"
 | 
			
		||||
      label="新密码"
 | 
			
		||||
      :rules="[{ required: true, message: 'password is required' }]"
 | 
			
		||||
      :validate-trigger="['change', 'input']"
 | 
			
		||||
      showable
 | 
			
		||||
    >
 | 
			
		||||
      <a-input v-model="form.password" placeholder="请输入密码" />
 | 
			
		||||
    </a-form-item>
 | 
			
		||||
  </a-form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref, defineExpose, defineProps } from "vue";
 | 
			
		||||
import { getModel, getRole } from "./api";
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  data: {},
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const formRef = ref();
 | 
			
		||||
const form = ref({
 | 
			
		||||
  id: "",
 | 
			
		||||
  username: "",
 | 
			
		||||
  password: "",
 | 
			
		||||
});
 | 
			
		||||
form.value.id = props.data.id;
 | 
			
		||||
form.value.username = props.data.username;
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  formRef,
 | 
			
		||||
  form,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										46
									
								
								gpt-vue/projects/vue-admin/src/views/User/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								gpt-vue/projects/vue-admin/src/views/User/api.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
import http from "@/http/config";
 | 
			
		||||
 | 
			
		||||
export const getList = (params?: Record<string, unknown>) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/user/list",
 | 
			
		||||
    method: "get",
 | 
			
		||||
    params,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const save = (data?: Record<string, unknown>) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/user/save",
 | 
			
		||||
    method: "post",
 | 
			
		||||
    data,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const deletApi = (id: string | number) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: `/api/admin/user/remove?id=${id}`,
 | 
			
		||||
    method: "get",
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getRole = () => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: `/api/admin/role/list`,
 | 
			
		||||
    method: "get",
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getModel = () => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: `/api/admin/model/list`,
 | 
			
		||||
    method: "get",
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const resetPassword = (data) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: `/api/admin/user/resetPass`,
 | 
			
		||||
    method: "post",
 | 
			
		||||
    data,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user