Pārlūkot izejas kodu

feat: 字典 + api

zhanghaijun 1 gadu atpakaļ
vecāks
revīzija
50df67d858

+ 1 - 0
.vscode/settings.json

@@ -3,6 +3,7 @@
   "i18n-ally.localesPaths": ["src/locales", "src/locales/langs"],
   "i18n-ally.localesPaths": ["src/locales", "src/locales/langs"],
   "i18n-ally.sourceLanguage": "zh",
   "i18n-ally.sourceLanguage": "zh",
   "i18n-ally.displayLanguage": "zh",
   "i18n-ally.displayLanguage": "zh",
+  "i18n-ally.keystyle": "nested",
 
 
   // Disable the default formatter, use eslint instead
   // Disable the default formatter, use eslint instead
   "prettier.enable": false,
   "prettier.enable": false,

+ 5 - 20
src/api/sys/dict.ts

@@ -3,41 +3,26 @@ import request from '@/utils/http'
 export class DictService {
 export class DictService {
   // 分页查询
   // 分页查询
   static page(params: any) {
   static page(params: any) {
-    return request.post({
-      url: '/sys/dict/page',
-      params
-    })
+    return request.post({ url: '/sys/dict/page', params })
   }
   }
 
 
   // 添加
   // 添加
   static save(params: any) {
   static save(params: any) {
-    return request.post({
-      url: '/sys/dict/save',
-      params
-    })
+    return request.post({ url: '/sys/dict/save', params })
   }
   }
 
 
   // 修改
   // 修改
   static update(params: any) {
   static update(params: any) {
-    return request.post({
-      url: '/sys/dict/update',
-      params
-    })
+    return request.post({ url: '/sys/dict/update', params })
   }
   }
 
 
   // 删除
   // 删除
   static remove(params: any) {
   static remove(params: any) {
-    return request.post({
-      url: '/sys/dict/remove',
-      params
-    })
+    return request.post({ url: '/sys/dict/remove', params })
   }
   }
 
 
   // 详情
   // 详情
   static detail(params: any) {
   static detail(params: any) {
-    return request.post({
-      url: '/sys/dict/detail',
-      params
-    })
+    return request.post({ url: '/sys/dict/detail', params })
   }
   }
 }
 }

+ 3 - 15
src/components/core/banners/art-basic-banner/index.vue

@@ -1,11 +1,6 @@
 <!-- 基础横幅组件 -->
 <!-- 基础横幅组件 -->
 <template>
 <template>
-  <div
-    class="basic-banner art-custom-card"
-    :class="{ 'has-decoration': decoration }"
-    :style="{ backgroundColor: backgroundColor, height: height }"
-    @click="emit('click')"
-  >
+  <div class="basic-banner art-custom-card" :class="{ 'has-decoration': decoration }" :style="{ backgroundColor: backgroundColor, height: height }" @click="emit('click')">
     <!-- 流星效果 -->
     <!-- 流星效果 -->
     <div v-if="meteorConfig?.enabled && isDark" class="basic-banner__meteors">
     <div v-if="meteorConfig?.enabled && isDark" class="basic-banner__meteors">
       <span
       <span
@@ -29,9 +24,7 @@
 
 
       <!-- subtitle slot -->
       <!-- subtitle slot -->
       <slot name="subtitle">
       <slot name="subtitle">
-        <p v-if="subtitle" class="basic-banner__subtitle" :style="{ color: subtitleColor }">{{
-          subtitle
-        }}</p>
+        <p v-if="subtitle" class="basic-banner__subtitle" :style="{ color: subtitleColor }">{{ subtitle }}</p>
       </slot>
       </slot>
 
 
       <!-- button slot -->
       <!-- button slot -->
@@ -284,12 +277,7 @@
         position: absolute;
         position: absolute;
         width: 2px;
         width: 2px;
         height: 60px;
         height: 60px;
-        background: linear-gradient(
-          to top,
-          rgb(255 255 255 / 40%),
-          rgb(255 255 255 / 10%),
-          transparent
-        );
+        background: linear-gradient(to top, rgb(255 255 255 / 40%), rgb(255 255 255 / 10%), transparent);
         opacity: 0;
         opacity: 0;
         transform-origin: top left;
         transform-origin: top left;
         animation-name: meteor-fall;
         animation-name: meteor-fall;

+ 7 - 28
src/utils/http/index.ts

@@ -1,11 +1,10 @@
 import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
 import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
 import { useUserStore } from '@/store/modules/user'
 import { useUserStore } from '@/store/modules/user'
 import { ApiStatus } from './status'
 import { ApiStatus } from './status'
-import { HttpError, handleError, showError, showSuccess } from './error'
+import { HttpError, handleError, showError, showSuccess } from './server.error'
 import { $t } from '@/locales'
 import { $t } from '@/locales'
 
 
 /** 请求配置常量 */
 /** 请求配置常量 */
-const REQUEST_TIMEOUT = 15000
 const LOGOUT_DELAY = 500
 const LOGOUT_DELAY = 500
 const MAX_RETRIES = 0
 const MAX_RETRIES = 0
 const RETRY_DELAY = 1000
 const RETRY_DELAY = 1000
@@ -25,7 +24,7 @@ const { VITE_API_URL, VITE_WITH_CREDENTIALS } = import.meta.env
 
 
 /** Axios实例 */
 /** Axios实例 */
 const axiosInstance = axios.create({
 const axiosInstance = axios.create({
-  timeout: REQUEST_TIMEOUT,
+  timeout: 5000,
   baseURL: VITE_API_URL,
   baseURL: VITE_API_URL,
   withCredentials: VITE_WITH_CREDENTIALS === 'true',
   withCredentials: VITE_WITH_CREDENTIALS === 'true',
   validateStatus: (status) => status >= 200 && status < 300,
   validateStatus: (status) => status >= 200 && status < 300,
@@ -115,20 +114,11 @@ function logOut() {
 
 
 /** 是否需要重试 */
 /** 是否需要重试 */
 function shouldRetry(statusCode: number) {
 function shouldRetry(statusCode: number) {
-  return [
-    ApiStatus.requestTimeout,
-    ApiStatus.internalServerError,
-    ApiStatus.badGateway,
-    ApiStatus.serviceUnavailable,
-    ApiStatus.gatewayTimeout
-  ].includes(statusCode)
+  return [ApiStatus.requestTimeout, ApiStatus.internalServerError, ApiStatus.badGateway, ApiStatus.serviceUnavailable, ApiStatus.gatewayTimeout].includes(statusCode)
 }
 }
 
 
 /** 请求重试逻辑 */
 /** 请求重试逻辑 */
-async function retryRequest<T>(
-  config: ExtendedAxiosRequestConfig,
-  retries: number = MAX_RETRIES
-): Promise<T> {
+async function retryRequest<T>(config: ExtendedAxiosRequestConfig, retries: number = MAX_RETRIES): Promise<T> {
   try {
   try {
     return await request<T>(config)
     return await request<T>(config)
   } catch (error) {
   } catch (error) {
@@ -148,11 +138,7 @@ function delay(ms: number) {
 /** 请求函数 */
 /** 请求函数 */
 async function request<T = any>(config: ExtendedAxiosRequestConfig): Promise<T> {
 async function request<T = any>(config: ExtendedAxiosRequestConfig): Promise<T> {
   // POST | PUT 参数自动填充
   // POST | PUT 参数自动填充
-  if (
-    ['POST', 'PUT'].includes(config.method?.toUpperCase() || '') &&
-    config.params &&
-    !config.data
-  ) {
+  if (['POST', 'PUT'].includes(config.method?.toUpperCase() || '') && config.params && !config.data) {
     config.data = config.params
     config.data = config.params
     config.params = undefined
     config.params = undefined
   }
   }
@@ -192,11 +178,7 @@ const api = {
   request<T>(config: ExtendedAxiosRequestConfig): Promise<T> {
   request<T>(config: ExtendedAxiosRequestConfig): Promise<T> {
     return retryRequest<T>({ ...config })
     return retryRequest<T>({ ...config })
   },
   },
-  upload<T = any>(
-    url: string,
-    data: Record<string, any> & { file: Blob | File },
-    config?: ExtendedAxiosRequestConfig
-  ): Promise<T> {
+  upload<T = any>(url: string, data: Record<string, any> & { file: Blob | File }, config?: ExtendedAxiosRequestConfig): Promise<T> {
     const formData = new FormData()
     const formData = new FormData()
 
 
     Object.entries(data).forEach(([key, value]) => {
     Object.entries(data).forEach(([key, value]) => {
@@ -211,10 +193,7 @@ const api = {
 
 
     const finalConfig: ExtendedAxiosRequestConfig = {
     const finalConfig: ExtendedAxiosRequestConfig = {
       ...config,
       ...config,
-      headers: {
-        'Content-Type': 'multipart/form-data',
-        ...config?.headers
-      }
+      headers: { 'Content-Type': 'multipart/form-data', ...config?.headers }
     }
     }
 
 
     return retryRequest({
     return retryRequest({

+ 109 - 0
src/utils/http/server.axios.ts

@@ -0,0 +1,109 @@
+import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
+import { useUserStore } from '@/store/modules/user'
+import { $t } from '@/locales'
+
+export enum ApiStatus {
+  success = 0, // 成功
+  error = 400, // 错误
+  unauthorized = 401, // 未授权
+  forbidden = 403, // 禁止访问
+  notFound = 404, // 未找到
+  methodNotAllowed = 405, // 方法不允许
+  requestTimeout = 408, // 请求超时
+  internalServerError = 500, // 服务器错误
+  notImplemented = 501, // 未实现
+  badGateway = 502, // 网关错误
+  serviceUnavailable = 503, // 服务不可用
+  gatewayTimeout = 504, // 网关超时
+  httpVersionNotSupported = 505 // HTTP版本不支持
+}
+export interface ApiResponse<T = unknown> {
+  code: number /* 状态码 */
+  msg: string /* 消息 */
+  data?: T /* 数据 */
+}
+export interface ApiResponsePage<T = unknown> {
+  pageNum: number /* 当前页码。从 1 开始计数,表示当前是第几页。 */
+  pageSize: number /* 每页大小。表示每页包含多少条数据。 */
+  totalCount: number /* 总条数。表示数据总共有多少条。 */
+  list: T[] /* 数据列表 */
+}
+
+export class RequestHttp {
+  service: AxiosInstance
+
+  public constructor(config: AxiosRequestConfig) {
+    this.service = axios.create(config)
+
+    /**
+     * @description 请求拦截器
+     * 客户端发送请求 -> [请求拦截器] -> 服务器
+     */
+    this.service.interceptors.request.use(
+      (request: InternalAxiosRequestConfig) => {
+        const { accessToken } = useUserStore()
+        if (accessToken) request.headers.set('Authorization', accessToken)
+
+        if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) {
+          request.headers.set('Content-Type', 'application/json')
+          request.data = JSON.stringify(request.data)
+        }
+        return request
+      },
+      (error: AxiosError) => {
+        // showError(createHttpError($t('httpMsg.requestConfigError'), ApiStatus.error))
+        return Promise.reject(error)
+      }
+    )
+    /**
+     * @description 响应拦截器
+     *  服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
+     */
+    this.service.interceptors.response.use(
+      (response: AxiosResponse<Http.BaseResponse>) => {
+        const { code, msg } = response.data
+        if (code === ApiStatus.success) return response
+        if (code === ApiStatus.unauthorized) {
+          setTimeout(() => useUserStore().logOut(), 500)
+        }
+        throw new Error(msg || $t('httpMsg.requestFailed'))
+      },
+      (error) => {
+        if (error.response?.status === ApiStatus.unauthorized) {
+          setTimeout(() => useUserStore().logOut(), 500)
+        }
+        return Promise.reject(error)
+      }
+    )
+  }
+
+  public request<T = any>(config: AxiosRequestConfig): Promise<T> {
+    return this.service.request(config)
+  }
+  /**
+   * @description 常用请求方法封装
+   */
+  public get<T>(url: string, params?: object, _object: AxiosRequestConfig = {}): Promise<ApiResponse<T>> {
+    return this.service.get(url, { params, ..._object })
+  }
+
+  public post<T>(url: string, data?: object | string, _object: AxiosRequestConfig = {}): Promise<ApiResponse<T>> {
+    return this.service.post(url, data, _object)
+  }
+
+  public postPage<T>(url: string, data?: object | string, _object: AxiosRequestConfig = {}): Promise<ApiResponse<ApiResponsePage<T>>> {
+    return this.post<ApiResponsePage<T>>(url, data, { ..._object, params: { pageNum: 1, pageSize: 10 } })
+  }
+
+  public put<T>(url: string, data?: object, _object: AxiosRequestConfig = {}): Promise<ApiResponse<T>> {
+    return this.service.put(url, data, _object)
+  }
+
+  public delete<T>(url: string, params?: object, _object: AxiosRequestConfig = {}): Promise<ApiResponse<T>> {
+    return this.service.delete(url, { params, ..._object })
+  }
+
+  public download(url: string, params?: object, _object: AxiosRequestConfig = {}): Promise<BlobPart> {
+    return this.service.post(url, params, { ..._object, responseType: 'blob' })
+  }
+}

+ 1 - 3
src/utils/http/error.ts

@@ -106,9 +106,7 @@ export function handleError(error: AxiosError<ErrorResponse>): never {
   }
   }
 
 
   // 处理 HTTP 状态码错误
   // 处理 HTTP 状态码错误
-  const message = statusCode
-    ? getErrorMessage(statusCode)
-    : errorMessage || $t('httpMsg.requestFailed')
+  const message = statusCode ? getErrorMessage(statusCode) : errorMessage || $t('httpMsg.requestFailed')
   throw new HttpError(message, statusCode || ApiStatus.error, {
   throw new HttpError(message, statusCode || ApiStatus.error, {
     data: error.response.data,
     data: error.response.data,
     url: requestConfig?.url,
     url: requestConfig?.url,

+ 6 - 0
src/utils/http/server.help.ts

@@ -0,0 +1,6 @@
+import { HttpError } from './server.error'
+
+/** 统一创建HttpError */
+export function createHttpError(message: string, code: number) {
+  return new HttpError(message, code)
+}

+ 1 - 1
src/views/auth/login/index.vue

@@ -100,7 +100,7 @@
   import { languageOptions } from '@/locales'
   import { languageOptions } from '@/locales'
   import { LanguageEnum, SystemThemeEnum } from '@/enums/appEnum'
   import { LanguageEnum, SystemThemeEnum } from '@/enums/appEnum'
   import { useI18n } from 'vue-i18n'
   import { useI18n } from 'vue-i18n'
-  import { HttpError } from '@/utils/http/error'
+  import { HttpError } from '@/utils/http/server.error'
   defineOptions({ name: 'Login' })
   defineOptions({ name: 'Login' })
   const dictStore = useDictStore()
   const dictStore = useDictStore()
 
 

+ 1 - 1
src/views/dashboard/console/index.vue

@@ -4,7 +4,7 @@
       <!-- 教员培训 -->
       <!-- 教员培训 -->
       <div class="col-span-2 row-span-2 flex items-center justify-center rounded-2xl bg-green-500 text-xl font-bold text-white shadow-md">
       <div class="col-span-2 row-span-2 flex items-center justify-center rounded-2xl bg-green-500 text-xl font-bold text-white shadow-md">
         <div class="text-2xl font-extrabold">教员培训</div>
         <div class="text-2xl font-extrabold">教员培训</div>
-        <div class="text-lg font-semibold">二级标题</div>
+        <div class="text-lg font-semibold">Subway Teacher Center</div>
         <el-button class="rounded-2xl" type="primary" size="large" @click="() => router.push('/trainer/course/list')">
         <el-button class="rounded-2xl" type="primary" size="large" @click="() => router.push('/trainer/course/list')">
           <span>开始学习课程</span>
           <span>开始学习课程</span>
           <Icon :icon="'&#xe792;'" />
           <Icon :icon="'&#xe792;'" />

+ 1 - 3
src/views/sys/dict/dict-item-wrap.vue

@@ -20,9 +20,7 @@
     show({ data, getCrudApi }: { data: any; getCrudApi: () => CrudMethods }) {
     show({ data, getCrudApi }: { data: any; getCrudApi: () => CrudMethods }) {
       record.value = data
       record.value = data
       crudApi.value = getCrudApi()
       crudApi.value = getCrudApi()
-      dialogApi.setState({
-        title: `【${data.name}】字典项管理`
-      })
+      dialogApi.setState({ title: `【${data.name}】字典项管理` })
       dialogApi.open()
       dialogApi.open()
     }
     }
   })
   })

+ 10 - 59
src/views/system/role/index.vue

@@ -1,23 +1,9 @@
 <template>
 <template>
   <div class="role-page art-full-height">
   <div class="role-page art-full-height">
-    <RoleSearch
-      v-show="showSearchBar"
-      v-model="searchForm"
-      @search="handleSearch"
-      @reset="resetSearchParams"
-    ></RoleSearch>
-
-    <ElCard
-      class="art-table-card"
-      shadow="never"
-      :style="{ 'margin-top': showSearchBar ? '12px' : '0' }"
-    >
-      <ArtTableHeader
-        v-model:columns="columnChecks"
-        v-model:showSearchBar="showSearchBar"
-        :loading="loading"
-        @refresh="refreshData"
-      >
+    <RoleSearch v-show="showSearchBar" v-model="searchForm" @search="handleSearch" @reset="resetSearchParams"></RoleSearch>
+
+    <ElCard class="art-table-card" shadow="never" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
+      <ArtTableHeader v-model:columns="columnChecks" v-model:showSearchBar="showSearchBar" :loading="loading" @refresh="refreshData">
         <template #left>
         <template #left>
           <ElSpace wrap>
           <ElSpace wrap>
             <ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton>
             <ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton>
@@ -26,31 +12,14 @@
       </ArtTableHeader>
       </ArtTableHeader>
 
 
       <!-- 表格 -->
       <!-- 表格 -->
-      <ArtTable
-        :loading="loading"
-        :data="data"
-        :columns="columns"
-        :pagination="pagination"
-        @pagination:size-change="handleSizeChange"
-        @pagination:current-change="handleCurrentChange"
-      >
-      </ArtTable>
+      <ArtTable :loading="loading" :data="data" :columns="columns" :pagination="pagination" @pagination:size-change="handleSizeChange" @pagination:current-change="handleCurrentChange"> </ArtTable>
     </ElCard>
     </ElCard>
 
 
     <!-- 角色编辑弹窗 -->
     <!-- 角色编辑弹窗 -->
-    <RoleEditDialog
-      v-model="dialogVisible"
-      :dialog-type="dialogType"
-      :role-data="currentRoleData"
-      @success="refreshData"
-    />
+    <RoleEditDialog v-model="dialogVisible" :dialog-type="dialogType" :role-data="currentRoleData" @success="refreshData" />
 
 
     <!-- 菜单权限弹窗 -->
     <!-- 菜单权限弹窗 -->
-    <RolePermissionDialog
-      v-model="permissionDialog"
-      :role-data="currentRoleData"
-      @success="refreshData"
-    />
+    <RolePermissionDialog v-model="permissionDialog" :role-data="currentRoleData" @success="refreshData" />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -84,19 +53,7 @@
   const permissionDialog = ref(false)
   const permissionDialog = ref(false)
   const currentRoleData = ref<RoleListItem | undefined>(undefined)
   const currentRoleData = ref<RoleListItem | undefined>(undefined)
 
 
-  const {
-    columns,
-    columnChecks,
-    data,
-    loading,
-    pagination,
-    getData,
-    searchParams,
-    resetSearchParams,
-    handleSizeChange,
-    handleCurrentChange,
-    refreshData
-  } = useTable({
+  const { columns, columnChecks, data, loading, pagination, getData, searchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData } = useTable({
     // 核心配置
     // 核心配置
     core: {
     core: {
       apiFn: fetchGetRoleList,
       apiFn: fetchGetRoleList,
@@ -133,14 +90,8 @@
           label: '角色状态',
           label: '角色状态',
           width: 100,
           width: 100,
           formatter: (row) => {
           formatter: (row) => {
-            const statusConfig = row.enabled
-              ? { type: 'success', text: '启用' }
-              : { type: 'warning', text: '禁用' }
-            return h(
-              ElTag,
-              { type: statusConfig.type as 'success' | 'warning' },
-              () => statusConfig.text
-            )
+            const statusConfig = row.enabled ? { type: 'success', text: '启用' } : { type: 'warning', text: '禁用' }
+            return h(ElTag, { type: statusConfig.type as 'success' | 'warning' }, () => statusConfig.text)
           }
           }
         },
         },
         {
         {

+ 1 - 7
src/views/system/role/modules/role-edit-dialog.vue

@@ -1,11 +1,5 @@
 <template>
 <template>
-  <ElDialog
-    v-model="visible"
-    :title="dialogType === 'add' ? '新增角色' : '编辑角色'"
-    width="30%"
-    align-center
-    @close="handleClose"
-  >
+  <ElDialog v-model="visible" :title="dialogType === 'add' ? '新增角色' : '编辑角色'" width="30%" align-center @close="handleClose">
     <ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
     <ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
       <ElFormItem label="角色名称" prop="roleName">
       <ElFormItem label="角色名称" prop="roleName">
         <ElInput v-model="form.roleName" />
         <ElInput v-model="form.roleName" />

+ 3 - 21
src/views/system/role/modules/role-permission-dialog.vue

@@ -1,23 +1,7 @@
 <template>
 <template>
-  <ElDialog
-    v-model="visible"
-    title="菜单权限"
-    width="520px"
-    align-center
-    class="el-dialog-border"
-    @close="handleClose"
-  >
+  <ElDialog v-model="visible" title="菜单权限" width="520px" align-center class="el-dialog-border" @close="handleClose">
     <ElScrollbar height="70vh">
     <ElScrollbar height="70vh">
-      <ElTree
-        ref="treeRef"
-        :data="processedMenuList"
-        show-checkbox
-        node-key="name"
-        :default-expand-all="isExpandAll"
-        :default-checked-keys="[1, 2, 3]"
-        :props="defaultProps"
-        @check="handleTreeCheck"
-      >
+      <ElTree ref="treeRef" :data="processedMenuList" show-checkbox node-key="name" :default-expand-all="isExpandAll" :default-checked-keys="[1, 2, 3]" :props="defaultProps" @check="handleTreeCheck">
         <template #default="{ data }">
         <template #default="{ data }">
           <div style="display: flex; align-items: center">
           <div style="display: flex; align-items: center">
             <span v-if="data.isAuth">
             <span v-if="data.isAuth">
@@ -33,9 +17,7 @@
         <ElButton @click="outputSelectedData" style="margin-left: 8px">获取选中数据</ElButton>
         <ElButton @click="outputSelectedData" style="margin-left: 8px">获取选中数据</ElButton>
 
 
         <ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
         <ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
-        <ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
-          isSelectAll ? '取消全选' : '全部选择'
-        }}</ElButton>
+        <ElButton @click="toggleSelectAll" style="margin-left: 8px">{{ isSelectAll ? '取消全选' : '全部选择' }}</ElButton>
         <ElButton type="primary" @click="savePermission">保存</ElButton>
         <ElButton type="primary" @click="savePermission">保存</ElButton>
       </div>
       </div>
     </template>
     </template>

+ 1 - 9
src/views/system/role/modules/role-search.vue

@@ -1,13 +1,5 @@
 <template>
 <template>
-  <ArtSearchBar
-    ref="searchBarRef"
-    v-model="formData"
-    :items="formItems"
-    :rules="rules"
-    @reset="handleReset"
-    @search="handleSearch"
-  >
-  </ArtSearchBar>
+  <ArtSearchBar ref="searchBarRef" v-model="formData" :items="formItems" :rules="rules" @reset="handleReset" @search="handleSearch"> </ArtSearchBar>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">

+ 0 - 9
src/views/system/user-center/index.vue

@@ -36,15 +36,6 @@
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
-
-        <!-- <el-carousel class="gallery" height="160px"
-          :interval="5000"
-          indicator-position="none"
-        >
-          <el-carousel-item class="item" v-for="item in galleryList" :key="item">
-            <img :src="item"/>
-          </el-carousel-item>
-        </el-carousel> -->
       </div>
       </div>
       <div class="right-wrap">
       <div class="right-wrap">
         <div class="info box-style">
         <div class="info box-style">

+ 3 - 23
src/views/system/user/index.vue

@@ -30,12 +30,7 @@
       </ArtTable>
       </ArtTable>
 
 
       <!-- 用户弹窗 -->
       <!-- 用户弹窗 -->
-      <UserDialog
-        v-model:visible="dialogVisible"
-        :type="dialogType"
-        :user-data="currentUserData"
-        @submit="handleDialogSubmit"
-      />
+      <UserDialog v-model:visible="dialogVisible" :type="dialogType" :user-data="currentUserData" @submit="handleDialogSubmit" />
     </ElCard>
     </ElCard>
   </div>
   </div>
 </template>
 </template>
@@ -90,19 +85,7 @@
     )
     )
   }
   }
 
 
-  const {
-    columns,
-    columnChecks,
-    data,
-    loading,
-    pagination,
-    getData,
-    searchParams,
-    resetSearchParams,
-    handleSizeChange,
-    handleCurrentChange,
-    refreshData
-  } = useTable({
+  const { columns, columnChecks, data, loading, pagination, getData, searchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData } = useTable({
     // 核心配置
     // 核心配置
     core: {
     core: {
       apiFn: fetchGetUserList,
       apiFn: fetchGetUserList,
@@ -129,10 +112,7 @@
                 // 图片预览是否插入至 body 元素上,用于解决表格内部图片预览样式异常
                 // 图片预览是否插入至 body 元素上,用于解决表格内部图片预览样式异常
                 previewTeleported: true
                 previewTeleported: true
               }),
               }),
-              h('div', {}, [
-                h('p', { class: 'user-name' }, row.userName),
-                h('p', { class: 'email' }, row.userEmail)
-              ])
+              h('div', {}, [h('p', { class: 'user-name' }, row.userName), h('p', { class: 'email' }, row.userEmail)])
             ])
             ])
           }
           }
         },
         },

+ 2 - 12
src/views/system/user/modules/user-dialog.vue

@@ -1,10 +1,5 @@
 <template>
 <template>
-  <ElDialog
-    v-model="dialogVisible"
-    :title="dialogType === 'add' ? '添加用户' : '编辑用户'"
-    width="30%"
-    align-center
-  >
+  <ElDialog v-model="dialogVisible" :title="dialogType === 'add' ? '添加用户' : '编辑用户'" width="30%" align-center>
     <ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
     <ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
       <ElFormItem label="用户名" prop="username">
       <ElFormItem label="用户名" prop="username">
         <ElInput v-model="formData.username" />
         <ElInput v-model="formData.username" />
@@ -20,12 +15,7 @@
       </ElFormItem>
       </ElFormItem>
       <ElFormItem label="角色" prop="role">
       <ElFormItem label="角色" prop="role">
         <ElSelect v-model="formData.role" multiple>
         <ElSelect v-model="formData.role" multiple>
-          <ElOption
-            v-for="role in roleList"
-            :key="role.roleCode"
-            :value="role.roleCode"
-            :label="role.roleName"
-          />
+          <ElOption v-for="role in roleList" :key="role.roleCode" :value="role.roleCode" :label="role.roleName" />
         </ElSelect>
         </ElSelect>
       </ElFormItem>
       </ElFormItem>
     </ElForm>
     </ElForm>

+ 1 - 9
src/views/system/user/modules/user-search.vue

@@ -1,13 +1,5 @@
 <template>
 <template>
-  <ArtSearchBar
-    ref="searchBarRef"
-    v-model="formData"
-    :items="formItems"
-    :rules="rules"
-    @reset="handleReset"
-    @search="handleSearch"
-  >
-  </ArtSearchBar>
+  <ArtSearchBar ref="searchBarRef" v-model="formData" :items="formItems" :rules="rules" @reset="handleReset" @search="handleSearch"> </ArtSearchBar>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">

+ 34 - 0
src/views/train/system/dictionary/dict-item-add.vue

@@ -0,0 +1,34 @@
+<template>
+  <div class="sys-dict-item-page art-full-height">
+    <BasicCrud />
+  </div>
+</template>
+<script setup lang="tsx">
+  import { useCrud } from '@/components/crud'
+  import { dictItemSchema } from './dict.schemas'
+  const props = defineProps<{
+    dictId?: string
+  }>()
+  const [BasicCrud] = useCrud({
+    ...dictItemSchema,
+    // 查询参数处理
+    beforeFetch(params: any) {
+      return {
+        ...params,
+        m_EQ_dictId: props.dictId
+      }
+    },
+    // 表单提交前处理
+    transformFormValues(values: any) {
+      return {
+        ...values,
+        dictId: props.dictId
+      }
+    },
+    toolbarActions: [],
+    dropDownToolbarActions: [],
+    tableActions: [],
+    dropDownActions: []
+  })
+</script>
+<style lang="scss" scoped></style>

+ 28 - 0
src/views/train/system/dictionary/dict-item-wrap.vue

@@ -0,0 +1,28 @@
+<template>
+  <Dialog> <DictItemIndex :dict-id="record.id" /> </Dialog>
+</template>
+<script setup lang="tsx">
+  import { useDialog } from '@/components/dialog'
+  import { CrudMethods } from '@/components/crud'
+  import DictItemIndex from './dict-item-add.vue'
+  const record = ref<any>({})
+  const crudApi = ref<CrudMethods>()
+  const [Dialog, dialogApi] = useDialog({
+    title: '我是标题',
+    width: '50%',
+    dialogType: 'drawer',
+    footer: false,
+    modal: true,
+    closeOnClickModal: true,
+    destroyOnClose: true
+  })
+  defineExpose({
+    show({ data, getCrudApi }: { data: any; getCrudApi: () => CrudMethods }) {
+      record.value = data
+      crudApi.value = getCrudApi()
+      dialogApi.setState({ title: `【${data.name}】字典项管理` })
+      dialogApi.open()
+    }
+  })
+</script>
+<style lang="scss" scoped></style>

+ 79 - 0
src/views/train/system/dictionary/dict.api.ts

@@ -0,0 +1,79 @@
+import request from '@/utils/http'
+
+export interface Dict {
+  /** 唯一编码 */
+  code: string
+  /** 字典名称 */
+  name: string
+  /** 备注 */
+  remark: string
+  /** 排序 */
+  sort: number
+}
+
+export interface DictItem {
+  /** 字典ID */
+  dictId: string
+  /** 唯一编码,唯一编码或值 */
+  code: string
+  /** 字典项名称 */
+  name: string
+  /** 备注 */
+  remark: string
+  /** 排序 */
+  sort: number
+}
+
+export class DictService {
+  // 分页查询
+  static page(params: any) {
+    return request.post({ url: '/sys/dict/page', params })
+  }
+
+  // 添加
+  static save(params: any) {
+    return request.post({ url: '/sys/dict/save', params })
+  }
+
+  // 修改
+  static update(params: any) {
+    return request.post({ url: '/sys/dict/update', params })
+  }
+
+  // 删除
+  static remove(params: any) {
+    return request.post({ url: '/sys/dict/remove', params })
+  }
+
+  // 详情
+  static detail(params: any) {
+    return request.post({ url: '/sys/dict/detail', params })
+  }
+}
+
+export class DictItemService {
+  // 分页查询
+  static page(params: any) {
+    return request.post({ url: '/sys/dictItem/page', params })
+  }
+
+  // 添加
+  static save(params: any) {
+    return request.post({ url: '/sys/dictItem/save', params })
+  }
+
+  // 修改
+  static update(params: any) {
+    return request.post({ url: '/sys/dictItem/update', params })
+  }
+
+  // 删除
+  static remove(params: any) {
+    return request.post({ url: '/sys/dictItem/remove', params })
+  }
+
+  // 详情
+  static detail(params: any) {
+    return request.post({ url: '/sys/dictItem/detail', params })
+  }
+}

+ 403 - 0
src/views/train/system/dictionary/dict.schemas.tsx

@@ -0,0 +1,403 @@
+import { CrudSchema } from '@/components/crud'
+import { DictService, DictItemService } from './dict.api'
+
+// 字典配置
+export const dictSchema: CrudSchema = {
+  // 接口地址
+  api: DictService.page,
+  dialogTitle: '字典',
+  crudApi: {
+    // 新增接口
+    add: DictService.save,
+    // 修改接口
+    edit: DictService.update,
+    // 删除接口
+    remove: DictService.remove,
+    // 详情接口
+    view: DictService.detail
+  },
+  // 使用crud
+  useCrud: true,
+  // 是否有新增
+  hasAdd: true,
+  // 是否有删除
+  hasRemove: true,
+  // 是否有修改
+  hasEdit: true,
+  // 是否有详情
+  hasView: true,
+  // 新增权限码
+  addAuth: 'sys:dict:save',
+  // 删除权限码
+  removeAuth: 'sys:dict:remove',
+  // 修改权限码
+  editAuth: 'sys:dict:update',
+  // 详情权限码
+  viewAuth: 'sys:dict:detail',
+  // 表格列定义
+  columns: [
+    {
+      type: 'selection'
+    },
+    {
+      prop: 'groupCode',
+      label: '分组编码',
+      component: 'ApiDict',
+      minWidth: 200,
+      componentProps: {
+        code: 'sys_dict_group_code'
+      }
+    },
+    {
+      prop: 'name',
+      label: '字典名称',
+      minWidth: 200
+    },
+    {
+      prop: 'code',
+      label: '唯一编码',
+      minWidth: 200
+    },
+    {
+      prop: 'dataType',
+      label: '数据类型',
+      component: 'ApiDict',
+      componentProps: {
+        code: 'sys_dict_data_type'
+      },
+      minWidth: 100
+    },
+    {
+      prop: 'sort',
+      label: '排序',
+      minWidth: 100
+    },
+    {
+      prop: 'enabled',
+      label: '是否启用',
+      component: 'ApiDict',
+      componentProps: {
+        code: 'yes_no'
+      },
+      minWidth: 100
+    }
+  ],
+  // 搜索表单定义
+  searchFormSchema: {
+    schema: [
+      {
+        label: '所属分组',
+        prop: 'm_IN_groupCode',
+        component: 'ApiDict',
+        componentProps: {
+          code: 'sys_dict_group_code',
+          clearable: true,
+          placeholder: '请选择所属分组',
+          multiple: true
+        },
+        colSpan: 6
+      },
+      {
+        prop: 'm_LIKE_name',
+        label: '字典名称',
+        component: 'Input',
+        componentProps: {
+          clearable: true,
+          placeholder: '请输入字典名称'
+        },
+        colSpan: 6
+      },
+      {
+        prop: 'm_LIKE_code',
+        label: '唯一编码',
+        component: 'Input',
+        componentProps: {
+          clearable: true,
+          placeholder: '请输入唯一编码'
+        },
+        colSpan: 6
+      },
+      {
+        label: '是否启用',
+        prop: 'm_IN_enabled',
+        component: 'ApiDict',
+        componentProps: {
+          code: 'yes_no',
+          clearable: true,
+          placeholder: '请选择是否启用'
+        },
+        colSpan: 6
+      }
+    ]
+  },
+  // 内置弹窗配置
+  dialogProps: {
+    width: '40%'
+  },
+  // crud表单配置
+  formSchema: {
+    showDefaultActions: false,
+    labelWidth: '100px',
+    schema: [
+      {
+        label: '字典ID',
+        prop: 'id',
+        component: 'Input',
+        colSpan: 24,
+        show: false,
+        ifDetail: false
+      },
+      {
+        label: '所属分组',
+        prop: 'groupCode',
+        component: 'ApiDict',
+        componentProps: {
+          code: 'sys_dict_group_code'
+        },
+        rules: [
+          {
+            required: true,
+            message: '请选择所属分组'
+          }
+        ],
+        colSpan: 24
+      },
+      {
+        label: '字典名称',
+        prop: 'name',
+        component: 'Input',
+        rules: [
+          {
+            required: true,
+            message: '请输入字典名称'
+          }
+        ],
+        componentProps: {
+          placeholder: '请输入字典名称'
+        },
+        colSpan: 24
+      },
+      {
+        label: '唯一编码',
+        prop: 'code',
+        component: 'Input',
+        rules: [
+          {
+            required: true,
+            message: '请输入唯一编码'
+          }
+        ],
+        componentProps: {
+          placeholder: '请输入唯一编码'
+        },
+        colSpan: 24
+      },
+
+      {
+        label: '数据类型',
+        prop: 'dataType',
+        component: 'ApiDict',
+        defaultValue: 1,
+        componentProps: {
+          code: 'sys_dict_data_type',
+          renderType: 'RadioGroup',
+          isBtn: true,
+          placeholder: '请选择数据类型'
+        },
+        rules: [
+          {
+            required: true,
+            message: '请选择数据类型'
+          }
+        ],
+        colSpan: 24
+      },
+      {
+        label: '是否启用',
+        prop: 'enabled',
+        component: 'ApiDict',
+        defaultValue: 1,
+        componentProps: {
+          code: 'yes_no',
+          renderType: 'RadioGroup',
+          isBtn: true
+        },
+        rules: [
+          {
+            required: true,
+            message: '请选择是否启用'
+          }
+        ],
+        colSpan: 24
+      },
+      {
+        label: '排序',
+        prop: 'sort',
+        component: 'InputNumber',
+        componentProps: {},
+        colSpan: 24
+      },
+      {
+        label: '备注',
+        prop: 'remark',
+        component: 'Input',
+        componentProps: {
+          type: 'textarea',
+          rows: 4,
+          placeholder: '请输入备注'
+        },
+        colSpan: 24
+      }
+    ]
+  }
+}
+
+// 字典项配置
+export const dictItemSchema: CrudSchema = {
+  api: DictItemService.page,
+  dialogTitle: '字典项管理',
+  crudApi: {
+    add: DictItemService.save,
+    edit: DictItemService.update,
+    remove: DictItemService.remove,
+    view: DictItemService.detail
+  },
+  useCrud: true,
+  hasAdd: true,
+  hasRemove: true,
+  hasEdit: true,
+  hasView: true,
+  addAuth: 'sys:dict-item:save',
+  removeAuth: 'sys:dict-item:remove',
+  editAuth: 'sys:dict-item:update',
+  viewAuth: 'sys:dict-item:detail',
+  columns: [
+    {
+      type: 'selection'
+    },
+    {
+      prop: 'name',
+      label: '字典项名称'
+    },
+    {
+      prop: 'code',
+      label: '字典值'
+    },
+    {
+      prop: 'sort',
+      label: '排序'
+    },
+    {
+      prop: 'enabled',
+      label: '是否启用',
+      component: 'ApiDict',
+      componentProps: {
+        code: 'yes_no',
+        renderType: 'RadioGroup'
+      }
+    }
+  ],
+  searchFormSchema: {
+    schema: [
+      {
+        prop: 'm_LIKE_name',
+        labelWidth: '90px',
+        label: '字典项名称',
+        component: 'Input',
+        componentProps: {
+          clearable: true,
+          placeholder: '请输入字典项名称'
+        },
+        colSpan: 8
+      },
+      {
+        prop: 'm_LIKE_code',
+        label: '字典值',
+        component: 'Input',
+        componentProps: {
+          clearable: true,
+          placeholder: '请输入字典值'
+        },
+        colSpan: 8
+      }
+    ]
+  },
+  dialogProps: {
+    width: '50%'
+  },
+  formSchema: {
+    showDefaultActions: false,
+    labelWidth: '100px',
+    schema: [
+      {
+        label: '字典项ID',
+        prop: 'id',
+        component: 'Input',
+        colSpan: 24,
+        show: false,
+        ifDetail: false
+      },
+      {
+        label: '字典项名称',
+        prop: 'name',
+        component: 'Input',
+        rules: [
+          {
+            required: true,
+            message: '请输入字典项名称'
+          }
+        ],
+        colSpan: 24
+      },
+      {
+        label: '字典值',
+        prop: 'code',
+        component: 'Input',
+        rules: [
+          {
+            required: true,
+            message: '请输入唯一编码'
+          }
+        ],
+        colSpan: 24
+      },
+      {
+        label: '是否启用',
+        prop: 'enabled',
+        component: 'ApiDict',
+        defaultValue: 1,
+        componentProps: {
+          code: 'yes_no',
+          renderType: 'RadioGroup',
+          isBtn: true
+        },
+        rules: [
+          {
+            required: true,
+            message: '请选择是否启用'
+          }
+        ],
+        colSpan: 24
+      },
+      {
+        label: '排序',
+        prop: 'sort',
+        component: 'InputNumber',
+        componentProps: {
+          min: 0
+        },
+        colSpan: 24
+      },
+      {
+        label: '备注',
+        prop: 'remark',
+        component: 'Input',
+        componentProps: {
+          type: 'textarea',
+          rows: 3
+        },
+        colSpan: 24
+      }
+    ]
+  }
+}

+ 48 - 4
src/views/train/system/dictionary/index.vue

@@ -1,8 +1,52 @@
 <template>
 <template>
-  <div>
-    <h2>字典管理</h2>
-    <div>正在开发中,敬请期待</div>
+  <div class="sys-dict-page art-full-height">
+    <BasicCrud />
+    <DictItemWrap ref="dictItemWrapRef" />
   </div>
   </div>
 </template>
 </template>
-<script lang="ts" setup></script>
+<script setup lang="tsx">
+  import { useCrud } from '@/components/crud'
+  import { dictSchema } from './dict.schemas'
+  import DictItemWrap from './dict-item-wrap.vue'
+  const dictItemWrapRef = ref()
+  const [BasicCrud, crudApi] = useCrud({
+    ...dictSchema,
+    hasEdit: false,
+    // 左侧工具栏
+    toolbarActions: [],
+    // 左侧栏工具-下拉菜单
+    dropDownToolbarActions: [],
+    // 表格行操作
+    tableActions: [
+      {
+        label: '字典项',
+        icon: 'ant-design:setting-outlined',
+        type: 'primary',
+        link: true,
+        dialogRef: dictItemWrapRef,
+        dialogParams() {
+          return {
+            getCrudApi() {
+              return crudApi
+            }
+          }
+        }
+      }
+    ],
+    // 表格行操作-下拉菜单
+    dropDownActions: [
+      {
+        label: dictSchema.editButtonText || '编辑',
+        type: 'primary',
+        sort: 10,
+        link: true,
+        icon: 'ant-design:edit-outlined',
+        auth: dictSchema.editAuth,
+        onClick() {
+          crudApi.openEditDialog()
+        }
+      }
+    ]
+  })
+</script>
 <style lang="scss" scoped></style>
 <style lang="scss" scoped></style>