Prechádzať zdrojové kódy

Merge branch 'main' of http://git.cc-lotus.info/util/web-template-vue3-js into main

zs 1 rok pred
rodič
commit
85d62f28b6

+ 8 - 3
.eslintrc.cjs

@@ -3,7 +3,12 @@ require('@rushstack/eslint-patch/modern-module-resolution')
 
 module.exports = {
   root: true,
-  extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-prettier/skip-formatting', './.eslintrc-auto-import.json'],
+  extends: [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/eslint-config-prettier/skip-formatting',
+    './.eslintrc-auto-import.json'
+  ],
   parserOptions: {
     ecmaVersion: 'latest'
   },
@@ -12,7 +17,7 @@ module.exports = {
     'max-len': [
       'warn',
       {
-        code: 150
+        code: 200
       }
     ],
     'prettier/prettier': [
@@ -21,7 +26,7 @@ module.exports = {
         singleQuote: true,
         bracketSpacing: true,
         jsxBracketSameLine: true,
-        printWidth: 150
+        printWidth: 200
       }
     ]
   }

+ 40 - 0
src/components/custom/custom-button-bar.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-row style="padding: 5px">
+    <el-col :span="24" style="text-align: right">
+      <template v-for="b in fields" :key="b.method">
+        <el-button v-method="b.method" :type="getType(b)" @click="toClick(b)" :disabled="getDisabled(b)">{{ b.label }}</el-button>
+      </template>
+    </el-col>
+  </el-row>
+</template>
+
+<script setup>
+import { get, isFunction } from 'lodash-es'
+const { t } = useI18n()
+const props = defineProps({
+  fields: { type: Array, default: () => [] }
+})
+const emits = defineEmits([])
+const getType = (field) => {
+  return get(field, 'type', 'primary')
+}
+const toClick = (field) => {
+  const method = get(field, 'method')
+  if (!method) {
+    ElMessage({ type: 'error', message: t('common.no_method') })
+    return
+  }
+  emits(method)
+}
+const getDisabled = (field) => {
+  const disabled = get(field, 'disabled')
+  if (!disabled) return false
+  if (isFunction(disabled)) return disabled(field)
+  return disabled
+}
+</script>
+<style scoped>
+.el-button {
+  margin-top: 5px;
+}
+</style>

+ 193 - 0
src/components/custom/custom-form.vue

@@ -0,0 +1,193 @@
+<template>
+  <div id="custom-form">
+    <el-form ref="formRef" :model="form" :rules="rules" :label-width="labelWidth" class="form" @submit.prevent :disabled="disabled">
+      <el-col :span="span" v-for="(item, index) in fields" :key="index">
+        <el-form-item v-if="display(item)" :key="`form-field-${item.model}`" :label="getField('label', item)" :prop="item.model" :required="item.required">
+          <template v-if="item.custom">
+            <slot :name="item.model" v-bind="{ item }"></slot>
+          </template>
+          <template v-else>
+            <template v-if="item.type === 'textarea'">
+              <el-input
+                clearable
+                v-model="form[item.model]"
+                :type="item.type"
+                :placeholder="getField('placeholder', item)"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+                show-word-limit
+              ></el-input>
+            </template>
+            <template v-else-if="item.type === 'numbers'">
+              <el-input-number v-model="form[item.model]" :placeholder="getField('placeholder', item)" @change="dataChange(item.model)" style="width: 100%" />
+            </template>
+            <template v-else-if="item.type === 'radio'">
+              <el-radio-group v-model="form[item.model]" :type="item.type" v-bind="item.options" @change="dataChange(item.model)">
+                <slot :name="item.model" v-bind="{ item }"></slot>
+              </el-radio-group>
+            </template>
+            <template v-else-if="item.type === 'checkbox'">
+              <el-checkbox-group v-model="form[item.model]" :type="item.type" v-bind="item.options">
+                <slot :name="item.model" v-bind="{ item }"></slot>
+              </el-checkbox-group>
+            </template>
+            <template v-else-if="item.type === 'select'">
+              <el-select
+                clearable
+                filterable
+                allow-create
+                default-first-option
+                v-model="form[item.model]"
+                :type="item.type"
+                :placeholder="getField('selectplaceholder', item)"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+                style="width: 100%"
+              >
+                <slot :name="item.model" v-bind="{ item }"></slot>
+              </el-select>
+            </template>
+            <template v-else-if="item.type === 'selectMany'">
+              <el-select
+                filterable
+                clearable
+                multiple
+                collapse-tags
+                v-model="form[item.model]"
+                :type="item.type"
+                :placeholder="getField('selectplaceholder', item)"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+                style="width: 100%"
+              >
+                <slot :name="item.model" v-bind="{ item }"></slot>
+              </el-select>
+            </template>
+            <template v-else-if="item.type === `year` || item.type == 'month' || item.type == 'date' || item.type == 'daterange' || item.type == 'datetime' || item.type == 'datetimerange'">
+              <el-date-picker
+                v-model="form[item.model]"
+                :type="item.type"
+                :placeholder="getField('selectplaceholder', item)"
+                :format="getDateFormat(item.type)"
+                :value-format="getDateFormat(item.type)"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+                range-separator="至"
+                style="width: 100%"
+              >
+              </el-date-picker>
+            </template>
+            <template v-else-if="item.type === `time`">
+              <el-time-picker
+                v-model="form[item.model]"
+                :placeholder="getField('selectplaceholder', item)"
+                :format="getDateFormat(item.type)"
+                :value-format="getDateFormat(item.type)"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+                style="width: 100%"
+              >
+              </el-time-picker>
+            </template>
+            <template v-else-if="item.type === `inputnumber`">
+              <el-input-number v-model="form[item.model]" :placeholder="getField('placeholder', item)" v-bind="item.options" @change="dataChange(item.model)" style="width: 100%"></el-input-number>
+            </template>
+            <template v-else>
+              <el-input
+                clearable
+                v-model="form[item.model]"
+                :type="getField('type', item)"
+                :placeholder="getField('placeholder', item)"
+                :show-password="getField('type', item) === 'password'"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+              ></el-input>
+            </template>
+          </template>
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" label="" class="btn" v-if="useSave">
+        <slot name="submit">
+          <el-button type="primary" @click="save(formRef)">{{ submitText || submitTextDefault }}</el-button>
+        </slot>
+      </el-col>
+    </el-form>
+  </div>
+</template>
+
+<script setup>
+import { get, isFunction } from 'lodash-es'
+const { t } = useI18n()
+const submitTextDefault = t('common.save')
+const props = defineProps({
+  modelValue: { type: Object },
+  rules: { type: Array, default: () => [] },
+  labelWidth: { type: String, default: 'auto' },
+  disabled: { type: Boolean, default: false },
+  fields: { type: Array, default: () => [] },
+  submitText: { type: String },
+  useSave: { type: Boolean, default: true },
+  span: { type: Number, default: 24 } // 限制两侧的距离,24就是整行全用
+})
+const emits = defineEmits(['update:modelValue', 'dataChange', 'save'])
+const formRef = ref()
+const form = computed({
+  get() {
+    return props.modelValue
+  },
+  set(value) {
+    console.log(value)
+    emits('update:modelValue', value)
+  }
+})
+const save = async (formEl) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      emits('save', form.value)
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+const getField = (item, data) => {
+  let res = get(data, item, null)
+  if (item === 'type') res = res === null ? `text` : res
+  if (item === 'placeholder') res = res === null ? `请输入${data.label}` : res
+  if (item === `selectplaceholder`) res = res === null ? `请选择${data.label}` : res
+  if (item === 'required') res = res === null ? false : res
+  if (item === `error`) res = res === null ? `${data.label}错误` : res
+  return res
+}
+const dataChange = (model) => {
+  const value = form.value[model]
+  emits('dataChange', { model, value })
+}
+const display = (field) => {
+  let dis = get(field, `display`)
+  if (!isFunction(dis)) return true
+  else {
+    return dis(field, form)
+  }
+}
+const getDateFormat = (e) => {
+  if (e === 'year') return 'YYYY'
+  if (e === 'month') return 'MM'
+  if (e === 'date') return 'YYYY-MM-DD'
+  if (e === 'daterange') return 'YYYY-MM-DD'
+  if (e === 'datetime') return 'YYYY-MM-DD HH:mm:ss'
+  if (e === 'datetimerange') return 'YYYY-MM-DD HH:mm:ss'
+  if (e === 'time') return 'HH:mm:ss'
+}
+</script>
+<style scoped>
+.btn {
+  text-align: center;
+}
+
+.form {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+</style>

+ 146 - 0
src/components/custom/custom-search-bar.vue

@@ -0,0 +1,146 @@
+<template>
+  <div id="custom-search-bar">
+    <el-form ref="formRef" :model="form" :label-width="labelWidth" class="form" @submit.prevent :inline="true">
+      <el-form-item v-for="item in fields" :key="`form-field-${item.model}`" :label="getField('label', item)" :prop="item.model" :required="item.required">
+        <template v-if="item.custom">
+          <slot :name="item.model" v-bind="{ item }"></slot>
+        </template>
+        <template v-else>
+          <template v-if="item.type === 'numbers'">
+            <el-input-number v-model="form[item.model]" :placeholder="getField('placeholder', item)" @change="dataChange(item.model)" style="width: 100%" />
+          </template>
+          <template v-else-if="item.type === 'radio'">
+            <el-radio-group v-model="form[item.model]" :type="item.type" v-bind="item.options" @change="dataChange(item.model)">
+              <slot :name="item.model" v-bind="{ item }"></slot>
+            </el-radio-group>
+          </template>
+          <template v-else-if="item.type === 'checkbox'">
+            <el-checkbox-group v-model="form[item.model]" :type="item.type" v-bind="item.options">
+              <slot :name="item.model" v-bind="{ item }"></slot>
+            </el-checkbox-group>
+          </template>
+          <template v-else-if="item.type === 'select'">
+            <el-select
+              clearable
+              filterable
+              allow-create
+              default-first-option
+              v-model="form[item.model]"
+              :type="item.type"
+              :placeholder="getField('selectplaceholder', item)"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+              style="width: 100%; min-width: 200px"
+            >
+              <slot :name="item.model" v-bind="{ item }"></slot>
+            </el-select>
+          </template>
+          <template v-else-if="item.type === 'selectMany'">
+            <el-select
+              filterable
+              clearable
+              multiple
+              collapse-tags
+              v-model="form[item.model]"
+              :type="item.type"
+              :placeholder="getField('selectplaceholder', item)"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+              style="width: 100%"
+            >
+              <slot :name="item.model" v-bind="{ item }"></slot>
+            </el-select>
+          </template>
+          <template v-else-if="item.type === `year` || item.type == 'month' || item.type == 'date' || item.type == 'daterange' || item.type == 'datetime' || item.type == 'datetimerange'">
+            <el-date-picker
+              v-model="form[item.model]"
+              :type="item.type"
+              :placeholder="getField('selectplaceholder', item)"
+              :format="getDateFormat(item.type)"
+              :value-format="getDateFormat(item.type)"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+              range-separator="至"
+              style="width: 100%"
+            >
+            </el-date-picker>
+          </template>
+          <template v-else-if="item.type === `time`">
+            <el-time-picker
+              v-model="form[item.model]"
+              :placeholder="getField('selectplaceholder', item)"
+              :format="getDateFormat(item.type)"
+              :value-format="getDateFormat(item.type)"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+              style="width: 100%"
+            >
+            </el-time-picker>
+          </template>
+          <template v-else-if="item.type === `inputnumber`">
+            <el-input-number v-model="form[item.model]" :placeholder="getField('placeholder', item)" v-bind="item.options" @change="dataChange(item.model)" style="width: 100%"></el-input-number>
+          </template>
+          <template v-else>
+            <el-input
+              clearable
+              v-model="form[item.model]"
+              :type="getField('type', item)"
+              :placeholder="getField('placeholder', item)"
+              :show-password="getField('type', item) === 'password'"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+            ></el-input>
+          </template>
+        </template>
+      </el-form-item>
+      <el-form-item v-if="fields.length > 0">
+        <el-button type="primary" @click="toClick">{{ $t('common.search') }}</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup>
+import { get } from 'lodash-es'
+const props = defineProps({
+  modelValue: { type: Object },
+  fields: { type: Array, default: () => [] }
+})
+const emits = defineEmits(['update:modelValue', 'dataChange', 'search'])
+const formRef = ref()
+const form = computed({
+  get() {
+    return props.modelValue
+  },
+  set(value) {
+    console.log(value)
+    emits('update:modelValue', value)
+  }
+})
+const toClick = () => {
+  emits('search')
+}
+const getField = (item, data) => {
+  let res = get(data, item, null)
+  if (item === 'type') res = res === null ? `text` : res
+  if (item === 'placeholder') res = res === null ? `请输入${data.label}` : res
+  if (item === `selectplaceholder`) res = res === null ? `请选择${data.label}` : res
+  if (item === 'required') res = res === null ? false : res
+  if (item === `error`) res = res === null ? `${data.label}错误` : res
+  return res
+}
+const dataChange = (model) => {
+  const value = form.value[model]
+  emits('dataChange', { model, value })
+}
+const getDateFormat = (e) => {
+  if (e === 'year') return 'YYYY'
+  if (e === 'month') return 'MM'
+  if (e === 'date') return 'YYYY-MM-DD'
+  if (e === 'daterange') return 'YYYY-MM-DD'
+  if (e === 'datetime') return 'YYYY-MM-DD HH:mm:ss'
+  if (e === 'datetimerange') return 'YYYY-MM-DD HH:mm:ss'
+  if (e === 'time') return 'HH:mm:ss'
+}
+</script>
+<style scoped></style>

+ 121 - 0
src/components/custom/custom-table.vue

@@ -0,0 +1,121 @@
+<template>
+  <el-row>
+    <el-col>
+      <el-table :data="data" border :height="height" @selection-change="toSelect">
+        <el-table-column type="selection" width="55" v-if="select"> </el-table-column>
+        <template v-for="f in fields" :key="f.model">
+          <el-table-column v-if="f.custom" :label="f.label" :prop="f.model" align="center" v-bind="f.options">
+            <template v-slot="{ row }">
+              <slot :name="f.model" v-bind="{ f, row }"></slot>
+            </template>
+          </el-table-column>
+          <el-table-column v-else :label="f.label" :prop="f.model" align="center" :formatter="toFormatter"></el-table-column>
+        </template>
+        <el-table-column :label="$t('common.opera')" align="center" v-if="opera.length > 0">
+          <template v-slot="{ row, $index }">
+            <slot v-bind="{ row }">
+              <template v-for="f in opera">
+                <template v-if="display(f, row)">
+                  <el-link :key="f.method" :type="f.type || 'primary'" size="small" :underline="false" class="link" v-opera="f.method" @click="handleOpera(f, row, $index)">
+                    {{ f.label }}
+                  </el-link>
+                </template>
+              </template>
+            </slot>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-col>
+  </el-row>
+  <el-row justify="end">
+    <el-pagination
+      background
+      layout="total, prev, pager, next"
+      :page-sizes="[10, 20, 50, 100, 200]"
+      :total="total"
+      :page-size="limit"
+      v-model:current-page="currentPage"
+      @current-change="changePage"
+      @size-change="sizeChange"
+    >
+    </el-pagination>
+  </el-row>
+</template>
+<script setup>
+import { isFunction, get, isString, cloneDeep } from 'lodash-es'
+const props = defineProps({
+  data: { type: Array, default: () => [] },
+  height: { type: String, default: '60vh' },
+  fields: { type: Array, default: () => [] },
+  opera: { type: Array, default: () => [] },
+  total: { type: Number, default: 0 },
+  limit: { type: Number, default: 10 },
+  select: { type: Boolean, default: false }
+})
+const emit = defineEmits(['query', 'toSelect'])
+const toSelect = (val) => {
+  emit(`toSelect`, val)
+}
+
+const handleOpera = (field, data, index) => {
+  let { method, confirm = false, methodZh, label, confirmWord } = cloneDeep(field)
+  if (isFunction(methodZh)) methodZh = methodZh(data)
+  else if (isString(methodZh)) {
+    methodZh = label
+  }
+  if (confirm) {
+    let word = methodZh ? methodZh : `您确认${label}该数据?`
+    if (confirmWord) word = confirmWord
+    ElMessageBox.confirm(word, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
+      .then(() => {
+        emit(method, cloneDeep(data), index)
+      })
+      .catch(() => {})
+  } else emit(method, cloneDeep(data), index)
+}
+
+/**
+ * 根据field中的 format函数 格式化该单元格数据
+ * @param {Object} row 本行数据
+ * @param {Object} column 本列实例
+ * @param {any} cellValue 该单元格原数据
+ */
+const toFormatter = (row, column, cellValue) => {
+  // 先找到field
+  const fields = get(props, 'fields')
+  if (!fields) return cellValue
+  let this_field = fields.find((fil) => fil.model === column.property)
+  if (!this_field) return cellValue
+  // 再找field中format函数
+  let format = get(this_field, `format`, false)
+  if (!format) return cellValue
+  if (isFunction(format)) {
+    const formatResult = format(cellValue, row, this_field)
+    return formatResult
+  }
+}
+const display = (field, row) => {
+  let display = get(field, `display`, true)
+  if (display === true) return true
+  else {
+    let res = display(row)
+    return res
+  }
+}
+const currentPage = ref(1)
+// 分页
+const changePage = (page = currentPage.value) => {
+  emit('search', { skip: (page - 1) * props.limit, limit: props.limit })
+}
+</script>
+<style scoped>
+.el-row {
+  padding-top: 20px;
+}
+.link {
+  padding: 0 5px 0 0;
+}
+.page {
+  margin: 10px 0 0 0;
+}
+</style>

+ 8 - 3
src/lang/package/zh-cn/common.js

@@ -3,12 +3,17 @@ export default {
   add: '添加',
   update: '修改',
   delete: '删除',
+  delete_confirm: '您确定删除该数据?',
   search: '查询',
   view: '查看',
   save: '保存',
   submit: '提交',
-  is_use_use: '启用',
-  is_use_notUse: '禁用',
+  is_use_abled: '启用',
+  is_use_disabled: '禁用',
   yes: '是',
-  no: '否'
+  no: '否',
+  no_method: '功能暂未开放',
+  warning: '注意',
+  confirm: '确定',
+  cancel: '取消'
 }

+ 3 - 1
src/lang/package/zh-cn/menus.js

@@ -6,5 +6,7 @@ export default {
   system_role: '角色设置',
   system_parameter: '系统参数',
   system_dict: '字典管理',
-  system_dict_data: '字典数据'
+  system_dict_data: '字典数据',
+  user: '用户管理',
+  user_admin: '管理员用户'
 }

+ 16 - 1
src/lang/package/zh-cn/pages.js

@@ -25,7 +25,8 @@ export default {
     baseInfo: '基本信息',
     configInfo: '功能列表',
     config_zh: '功能说明',
-    config_code: '功能编码'
+    config_code: '功能编码',
+    add_config: '添加功能'
   },
   role: {
     dialogTitle: '角色信息',
@@ -36,5 +37,19 @@ export default {
     brief: '简介',
     menu: '权限配置',
     is_use: '是否启用'
+  },
+  admin: {
+    dialogTitle: '用户信息',
+    account: '账号',
+    nick_name: '名称',
+    role: '角色',
+    is_super: '是否是超级管理员',
+    is_use: '是否启用',
+    bind: '绑定用户',
+    rp: '重置密码',
+    rpConfirm: '您确定要重置密码?',
+    changeToAbled: '您确定要启用该用户?',
+    changeToDisabled: '您确定要禁用该用户?',
+    password: '密码'
   }
 }

+ 3 - 0
src/main.js

@@ -15,6 +15,8 @@ import { InitCheckResult } from './utils/checkResult'
 import { InitVariable } from './utils/variable'
 // 组件
 import globalComponents from '@/components'
+// 指令
+import { InitDirective } from './utils/directives'
 const app = createApp(App)
 globalComponents(app)
 setupStore(app)
@@ -24,3 +26,4 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
 app.use(i18n).use(router).mount('#app')
 InitCheckResult(app)
 InitVariable(app)
+InitDirective(app)

+ 22 - 3
src/router/guard.js

@@ -1,9 +1,10 @@
 import { AxiosWrapper } from '@/utils/axios-wrapper'
 import { checkResult } from '@/utils/checkResult'
 import { UserStore } from '@/store/user'
-import { cloneDeep, omit } from 'lodash-es'
+import { cloneDeep, isArray, omit } from 'lodash-es'
 import NProgress from 'nprogress'
 import 'nprogress/nprogress.css'
+const whiteList = ['/redirect', '/login', '/401', '/404']
 
 NProgress.configure({ showSpinner: false }) // 进度条
 // 检查路由是否存在
@@ -70,6 +71,9 @@ const routesRegister = (menus, defaultRoutes, router) => {
   const loadComponent = import.meta.glob('../views/**/*.vue')
   for (const route of menus) {
     const { type, route_name, path, component, parent_id } = route
+    if (!path) {
+      console.log(route)
+    }
     // 检查路由是否已存在,存在跳过
     const hasRoute = defaultRoutes.find((f) => f.path === path)
     if (hasRoute) continue
@@ -97,6 +101,11 @@ const routesRegister = (menus, defaultRoutes, router) => {
         if (!parent) continue
         const pos = parent.route_name
         router.addRoute(pos, route)
+        // try {
+        // } catch (error) {
+        //   console.log(pos, route)
+        //   console.error(error)
+        // }
       } else {
         router.addRoute(__def, route)
       }
@@ -105,10 +114,11 @@ const routesRegister = (menus, defaultRoutes, router) => {
 }
 
 const resetMenus = (menus) => {
+  if (!isArray(menus) || menus.length <= 0) return []
   const cMenus = cloneDeep(menus)
   const result = []
   for (const m of cMenus) {
-    const mid = omit(m, ['is_use', 'order_num', 'in_admin_frame', 'route_name'])
+    const mid = omit(m, ['is_use', 'order_num', 'in_admin_frame'])
     const { children } = mid
     if (children) mid.children = resetMenus(children)
     result.push(mid)
@@ -121,21 +131,30 @@ export const registerBeforeRouter = async (router) => {
   router.beforeEach(async (to, from, next) => {
     NProgress.start() //开启进度条
     const token = localStorage.getItem('token')
-    if (to.path === '/login') {
+    NProgress.inc()
+    if (whiteList.includes(to.path)) {
       next()
       return
     }
     if (token) {
+      NProgress.inc()
       if (to.path === '/login') next()
+      NProgress.inc()
       const menus = await getUserMeta(token)
+      if (!menus) {
+        next('/401')
+        return
+      }
       // 检查目的地路由是否注册
       const hasRoute = hasNecessaryRoute(to, router)
+      NProgress.inc()
       if (hasRoute || to.meta.hidden) {
         // 注册了直接进入
         next()
       } else {
         // 没注册就先注册再重定向进入直到进入为止
         await addUserRoutes(menus, router)
+        NProgress.inc()
         next({ ...to, replace: true })
       }
     } else {

+ 40 - 0
src/store/api/system/dictData.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/dictData'
+const axios = new AxiosWrapper()
+
+export const DictDataStore = defineStore('dictData', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, '_id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/system/dictType.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/dictType'
+const axios = new AxiosWrapper()
+
+export const DictType = defineStore('dictType', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, '_id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/user/admin.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/admin'
+const axios = new AxiosWrapper()
+
+export const AdminStore = defineStore('admin', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, '_id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 45 - 0
src/utils/base-methods.js

@@ -0,0 +1,45 @@
+import { cloneDeep, get } from 'lodash-es'
+const InitBaseMethods = (store) => {
+  const $checkRes = inject('$checkRes')
+  let limit = inject('limit')
+  const data = ref([])
+  const total = ref(0)
+  const searchForm = ref({})
+  const form = ref({})
+  const b_search = async (query) => {
+    const info = { skip: query.skip, limit: query.limit, ...searchForm.value, is_del: '0' }
+    const res = await store.query(info)
+    if (res.errcode == '0') {
+      data.value = res.data
+      total.value = res.total
+    }
+  }
+  const b_delete = async (data) => {
+    const res = await store.del(data._id)
+    if ($checkRes(res, true)) {
+      b_search({ skip: 0, limit })
+    }
+  }
+  const b_save = async () => {
+    const data = cloneDeep(form.value)
+    let res
+    if (get(data, '_id')) res = await store.update(data)
+    else res = await store.create(data)
+    if ($checkRes(res, true)) {
+      b_search({ skip: 0, limit })
+    }
+  }
+  return {
+    $checkRes,
+    limit,
+    data,
+    total,
+    searchForm,
+    form,
+    b_search,
+    b_delete,
+    b_save
+  }
+}
+
+export default InitBaseMethods

+ 29 - 0
src/utils/directives.js

@@ -0,0 +1,29 @@
+import { UserStore } from '@/store/user'
+import { get, isArray } from 'lodash-es'
+import router from '@/router'
+const InitDirective = (app) => {
+  app.directive('method', {
+    mounted(el, binding) {
+      const { user } = UserStore()
+      const { value: code } = binding
+      const rUser = toRaw(user)
+      // 超级管理员不进行检查
+      if (get(rUser, 'role') === 'Admin' && get(rUser, 'is_super') === '0') return
+      const roleCode = get(rUser, 'role_code')
+      // 需要判断roleCode中是否有这个权限.但是权限又需要路由拼接起来,最少也得有上层级组合
+      const cr = router.currentRoute.value
+      // 拼接当前路由和层级的name
+      const rArr = cr.matched.filter((f) => f.name !== 'Layout').map((i) => i.name)
+      rArr.push(code)
+      const thisMethodCode = `${rArr.join('.')}`
+      if (!isArray(roleCode)) {
+        el.parentNode.removeChild(el)
+      }
+      if (!roleCode.includes(thisMethodCode)) {
+        el.parentNode.removeChild(el)
+      }
+    }
+  })
+}
+
+export { InitDirective }

+ 47 - 6
src/views/system/dict/index.vue

@@ -1,14 +1,55 @@
 <template>
-  <div id="index">
-    <el-row>
-      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
-        <el-col :span="24" class="one"> 字典表 </el-col>
-      </el-col>
-    </el-row>
+  <div class="main animate__animated animate__backInRight" v-loading="loading">
+    <custom-search-bar :fields="fields.filter((f) => f.filter)" v-model="searchForm">
+      <template #test3>
+        <el-option label="s3" value="s3"></el-option>
+      </template>
+      <template #test4>
+        <el-radio label="s4">s4</el-radio>
+        <el-radio label="s42">s42</el-radio>
+        <el-radio label="s43">s43</el-radio>
+        <el-radio label="s44">s44</el-radio>
+        <el-radio label="s45">s45</el-radio>
+      </template>
+    </custom-search-bar>
+    <!-- <custom-button-bar :fields="buttonFields"></custom-button-bar> -->
+    <custom-table :data="data" :fields="fields" @query="search" :total="total" :opera="opera" @edit="toEdit"></custom-table>
+    <!-- <custom-form v-model="data" :fields="fields" @save="toSave"></custom-form> -->
   </div>
 </template>
 
 <script setup>
+const data = ref([])
+const searchForm = ref({})
+const fields = [
+  { label: '测试', model: 'test', filter: true },
+  { label: '测试2', model: 'test2', filter: true },
+  { label: '测试3', model: 'test3', filter: true, type: 'select' },
+  { label: '测试4', model: 'test4', filter: true, type: 'radio' },
+  { label: '测试5', model: 'test5', filter: true, type: 'datetime' },
+  { label: '测试6', model: 'test6', filter: true, type: 'inputnumber' },
+  { label: '测试7', model: 'test7', filter: true },
+  { label: '测试8', model: 'test8', filter: true },
+  { label: '测试9', model: 'test9', filter: true },
+  { label: '测试10', model: 'test10', filter: true }
+]
+const opera = [{ label: '修改', method: 'edit', confirm: true }]
+const buttonFields = [
+  { label: '添加', method: 'pages.system.system_menus.add' },
+  { label: '导出', method: 'pages.system.system_menus.export', type: 'success' }
+]
+const total = ref(20)
+
+const toSave = (val) => {
+  console.log(val)
+  console.log(data)
+}
+const search = (query) => {
+  console.log(query)
+}
+const toEdit = (data) => {
+  console.log(data)
+}
 // 加载中
 const loading = ref(false)
 // 请求

+ 5 - 3
src/views/system/menus/index.vue

@@ -19,6 +19,8 @@ import menuTable from './parts/menu-table.vue'
 import menuInfo from './parts/menu-info.vue'
 import { cloneDeep, get, omit } from 'lodash-es'
 import { MenusStore } from '@/store/api/system/menus'
+import { useI18n } from 'vue-i18n'
+const { t } = useI18n
 const store = MenusStore()
 const $checkRes = inject('$checkRes')
 const dialog = ref(false)
@@ -53,9 +55,9 @@ const toSave = async () => {
   }
 }
 const toDelete = async (row) => {
-  ElMessageBox.confirm('您确定删除该数据?', '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
+  ElMessageBox.confirm(t('common.delete_confirm'), t('common.warning'), {
+    confirmButtonText: t('common.confirm'),
+    cancelButtonText: t('common.cancel'),
     type: 'warning'
   }).then(async () => {
     const res = await store.del(row._id)

+ 2 - 2
src/views/system/menus/parts/menu-table.vue

@@ -51,10 +51,10 @@ const getStatus = (row) => {
   let word = ''
   switch (row.is_use) {
     case '0':
-      word = t('common.is_use_use')
+      word = t('common.is_use_abled')
       break
     case '1':
-      word = t('common.is_use_notUse')
+      word = t('common.is_use_disabled')
       break
 
     default:

+ 1 - 1
src/views/system/menus/parts/parts/func.vue

@@ -1,7 +1,7 @@
 <template>
   <el-row>
     <el-col :span="24" style="text-align: right; margin: 10px 0">
-      <el-button size="mini" type="primary" @click="toAddConfig()">添加功能</el-button>
+      <el-button size="mini" type="primary" @click="toAddConfig()">{{ $t('pages.menus.add_config') }}</el-button>
     </el-col>
     <el-col :span="24">
       <el-table :data="form.config">

+ 2 - 2
src/views/system/menus/parts/parts/info.vue

@@ -47,8 +47,8 @@
     </el-form-item>
     <el-form-item :label="$t('pages.menus.is_use')">
       <el-radio-group v-model="form.is_use">
-        <el-radio label="0">{{ $t('common.is_use_use') }}</el-radio>
-        <el-radio label="1">{{ $t('common.is_use_notUse') }}</el-radio>
+        <el-radio label="0">{{ $t('common.is_use_abled') }}</el-radio>
+        <el-radio label="1">{{ $t('common.is_use_disabled') }}</el-radio>
       </el-radio-group>
     </el-form-item>
     <el-form-item :label="$t('pages.menus.remark')">

+ 1 - 1
src/views/system/role/index.vue

@@ -2,7 +2,7 @@
   <div class="main animate__animated animate__backInRight" v-loading="loading">
     <el-row style="height: 5vh; padding: 5px">
       <el-col :span="24" style="text-align: right">
-        <el-button type="primary" @click="toAdd">{{ $t('common.add') }}</el-button>
+        <el-button type="primary" @click="toAdd" v-method="'add'">{{ $t('common.add') }}</el-button>
       </el-col>
     </el-row>
     <el-row>

+ 2 - 2
src/views/system/role/parts/form.vue

@@ -8,8 +8,8 @@
     </el-form-item>
     <el-form-item :label="$t('pages.role.is_use')">
       <el-radio-group v-model="form.is_use">
-        <el-radio label="0">{{ $t('common.is_use_use') }}</el-radio>
-        <el-radio label="1">{{ $t('common.is_use_notUse') }}</el-radio>
+        <el-radio label="0">{{ $t('common.is_use_abled') }}</el-radio>
+        <el-radio label="1">{{ $t('common.is_use_disabled') }}</el-radio>
       </el-radio-group>
     </el-form-item>
     <el-form-item :label="$t('pages.role.brief')">

+ 9 - 8
src/views/system/role/parts/table.vue

@@ -7,14 +7,14 @@
     </el-table-column>
     <el-table-column align="center" :label="$t('common.opera')">
       <template #default="{ row }">
-        <el-link :underline="false" type="primary" size="mini" @click="toEdit(row)" style="margin-right: 10px">{{ $t('common.update') }}</el-link>
-        <el-link :underline="false" v-if="row.is_use === '1'" type="success" size="mini" @click="toEdit(row)" style="margin-right: 10px">
-          {{ $t('common.is_use_use') }}
+        <el-link v-method="`update`" :underline="false" type="primary" size="mini" @click="toEdit(row)" style="margin-right: 10px">{{ $t('common.update') }}</el-link>
+        <el-link v-method="`toAbled`" :underline="false" v-if="row.is_use === '1'" type="success" size="mini" @click="changeUse(row)" style="margin-right: 10px">
+          {{ $t('common.is_use_abled') }}
         </el-link>
-        <el-link :underline="false" v-if="row.is_use === '0'" type="warning" size="mini" @click="toEdit(row)" style="margin-right: 10px">
-          {{ $t('common.is_use_notUse') }}
+        <el-link v-method="`toDisabled`" :underline="false" v-if="row.is_use === '0'" type="warning" size="mini" @click="changeUse(row)" style="margin-right: 10px">
+          {{ $t('common.is_use_disabled') }}
         </el-link>
-        <el-link v-if="row.is_default !== '0'" :underline="false" type="danger" size="mini" @click="toDelete(row)">
+        <el-link v-method="`delete`" v-if="row.is_default !== '0'" :underline="false" type="danger" size="mini" @click="toDelete(row)">
           {{ $t('common.delete') }}
         </el-link>
       </template>
@@ -26,15 +26,16 @@
 const data = inject('data')
 const toEdit = inject('toEdit')
 const toDelete = inject('toDelete')
+const changeUse = inject('changeUse')
 const { t } = useI18n()
 const getStatus = (row) => {
   let word = ''
   switch (row.is_use) {
     case '0':
-      word = t('common.is_use_use')
+      word = t('common.is_use_abled')
       break
     case '1':
-      word = t('common.is_use_notUse')
+      word = t('common.is_use_disabled')
       break
 
     default:

+ 171 - 0
src/views/user/admin/index.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="main animate__animated animate__backInRight">
+    <custom-search-bar v-model="searchForm" :fields="fields.filter((f) => f.filter)" @search="search"></custom-search-bar>
+    <custom-button-bar :fields="buttonFields" @add="toAdd"></custom-button-bar>
+    <custom-table :data="data" :fields="fields" @search="search" :total="total" :opera="opera" @edit="toEdit" @changeUse="toChangeUse" @delete="toDelete" @rp="toResetPwd"></custom-table>
+    <el-dialog v-model="dialog" :title="$t('pages.menus.dialogTitle')" :destroy-on-close="false" @close="toClose">
+      <custom-form v-model="form" :fields="formFields" @save="toSave">
+        <template #is_use>
+          <el-radio v-for="i in isUseList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #role>
+          <el-option v-for="i in roleList" :key="i._id" :label="i.name" :value="i.code"></el-option>
+        </template>
+      </custom-form>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { AdminStore } from '@/store/api/user/admin'
+import { LoginStore } from '@/store/api/login'
+import { RoleStore } from '@/store/api/system/role'
+import { DictDataStore } from '@/store/api/system/dictData'
+import { cloneDeep, get } from 'lodash-es'
+const $checkRes = inject('$checkRes')
+const store = AdminStore()
+const loginStore = LoginStore()
+const dictDataStore = DictDataStore()
+const roleStore = RoleStore()
+const { t } = useI18n()
+const loading = ref(false)
+let skip = 0
+let limit = inject('limit')
+const data = ref([])
+const total = ref(0)
+onMounted(async () => {
+  loading.value = true
+  await searchOther()
+  await search({ skip, limit })
+  loading.value = false
+})
+
+const fields = [
+  { label: t('pages.admin.account'), model: 'account', filter: true },
+  { label: t('pages.admin.nick_name'), model: 'nick_name' },
+  { label: t('pages.admin.role'), model: 'role', format: (i) => getRole(i) },
+  { label: t('pages.admin.is_super'), model: 'is_super', format: (i) => (i === '0' ? t('common.yes') : t('common.no')) },
+  { label: t('pages.admin.is_use'), model: 'is_use', format: (i) => getDict(i) }
+]
+const opera = [
+  { label: t('common.update'), method: 'edit', display: (i) => i.is_super !== '0' },
+  // { label: t('pages.admin.bind'), method: 'bind' },
+  { label: t('pages.admin.rp'), method: 'rp', type: 'warning', confirm: true, confirmWord: t('pages.admin.rpConfirm') },
+  {
+    label: t('common.is_use_disabled'),
+    method: 'changeUse',
+    type: 'warning',
+    confirm: true,
+    confirmWord: t('pages.admin.changeToDisabled'),
+    display: (i) => i.is_use === '0' && i.is_super !== '0'
+  },
+  {
+    label: t('common.is_use_abled'),
+    method: 'changeUse',
+    type: 'success',
+    confirm: true,
+    confirmWord: t('pages.admin.changeToAbled'),
+    display: (i) => i.is_use === '1' && i.is_super !== '0'
+  },
+  { label: t('common.delete'), method: 'delete', confirm: true, type: 'danger', display: (i) => i.is_super !== '0' }
+]
+const buttonFields = [{ label: t('common.add'), method: 'add' }]
+const searchForm = ref({})
+const search = async (query = { skip: 0, limit }) => {
+  const info = { skip: query.skip, limit: query.limit, ...searchForm.value, is_del: '0' }
+  const res = await store.query(info)
+  if (res.errcode == '0') {
+    data.value = res.data
+    total.value = res.total
+  }
+}
+const isUseList = ref([])
+const roleList = ref([])
+const searchOther = async () => {
+  const result = await dictDataStore.query({ code: 'isUse', is_use: '0' })
+  if ($checkRes(result)) {
+    isUseList.value = result.data
+  }
+  const roleResult = await roleStore.query()
+  if ($checkRes(roleResult)) {
+    roleList.value = roleResult.data
+  }
+}
+
+const toDelete = async (data) => {
+  const res = await store.del(data._id)
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit })
+  }
+}
+const userType = 'Admin'
+const toResetPwd = async (data) => {
+  const res = await loginStore.rpNoNewPassword({ type: userType, _id: data._id })
+  if ($checkRes(res, true)) {
+    ElMessageBox.confirm(`新密码为:${res.data}`, '请确认', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+  }
+}
+const toChangeUse = async (data) => {
+  const udata = { _id: data._id, is_use: data.is_use === '0' ? '1' : '0' }
+  const res = await store.update(udata)
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit })
+  }
+}
+const dialog = ref(false)
+const form = ref({})
+const defaultForm = { is_use: '0' }
+
+const formFields = ref([])
+const formFieldsForCreate = [
+  { label: t('pages.admin.account'), model: 'account' },
+  { label: t('pages.admin.nick_name'), model: 'nick_name' },
+  { label: t('pages.admin.password'), model: 'password', type: 'password' },
+  { label: t('pages.admin.is_use'), model: 'is_use', type: 'radio' }
+]
+const formFieldsForUpdate = [
+  { label: t('pages.admin.account'), model: 'account' },
+  { label: t('pages.admin.nick_name'), model: 'nick_name' },
+  { label: t('pages.admin.is_use'), model: 'is_use', type: 'radio' }
+]
+const roleField = { label: t('pages.admin.role'), model: 'role', type: 'select' }
+const toAdd = () => {
+  formFields.value = formFieldsForCreate
+  formFields.value.push(roleField)
+  form.value = cloneDeep(defaultForm)
+  dialog.value = true
+}
+const toEdit = (data) => {
+  formFields.value = cloneDeep(formFieldsForUpdate)
+  form.value = data
+  if (data.is_super !== '0') formFields.value.push(roleField)
+  dialog.value = true
+}
+const toSave = async () => {
+  const data = cloneDeep(form.value)
+  let res
+  if (get(data, '_id')) res = await store.update(data)
+  else res = await store.create(data)
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit })
+    toClose()
+  }
+}
+const getRole = (data) => {
+  const res = roleList.value.find((f) => f.code === data)
+  return get(res, 'name')
+}
+const getDict = (data) => {
+  const res = isUseList.value.find((f) => f.value == data)
+  return get(res, 'label')
+}
+const toClose = () => {
+  form.value = {}
+  dialog.value = false
+}
+</script>
+<style scoped></style>

+ 8 - 0
src/views/user/user/index.vue

@@ -0,0 +1,8 @@
+<template>
+  <div id="index">
+    <p>index</p>
+  </div>
+</template>
+
+<script setup></script>
+<style scoped></style>