lrf 3 роки тому
батько
коміт
3c33d90d69

+ 3 - 0
.env

@@ -0,0 +1,3 @@
+VUE_APP_BASE_URL = 'test'
+VUE_APP_AXIOS_BASE_URL = ''
+VUE_APP_STORAGE = 'local'

+ 126 - 26
package-lock.json

@@ -9,9 +9,11 @@
       "version": "0.1.0",
       "dependencies": {
         "@element-plus/icons-vue": "^2.0.6",
+        "axios": "^0.27.2",
         "core-js": "^3.6.5",
         "element-plus": "^2.2.9",
         "lodash": "^4.17.21",
+        "pinia": "^2.0.16",
         "vue": "^3.0.0",
         "vue-router": "^4.0.13"
       },
@@ -32,7 +34,7 @@
         "prettier": "^2.2.1",
         "sass": "^1.26.5",
         "sass-loader": "^8.0.2",
-        "typescript": "~4.1.5"
+        "typescript": "^4.7.4"
       }
     },
     "node_modules/@achrinza/node-ipc": {
@@ -3968,8 +3970,7 @@
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-      "dev": true
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
     },
     "node_modules/at-least-node": {
       "version": "1.0.0",
@@ -4030,6 +4031,28 @@
       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
       "dev": true
     },
+    "node_modules/axios": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+      "dependencies": {
+        "follow-redirects": "^1.14.9",
+        "form-data": "^4.0.0"
+      }
+    },
+    "node_modules/axios/node_modules/form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/babel-code-frame": {
       "version": "6.26.0",
       "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@@ -5256,7 +5279,6 @@
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
       "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "dev": true,
       "dependencies": {
         "delayed-stream": "~1.0.0"
       },
@@ -6421,7 +6443,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
       "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-      "dev": true,
       "engines": {
         "node": ">=0.4.0"
       }
@@ -8020,7 +8041,6 @@
       "version": "1.15.1",
       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
       "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
-      "dev": true,
       "funding": [
         {
           "type": "individual",
@@ -10736,7 +10756,6 @@
       "version": "1.52.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
       "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -10745,7 +10764,6 @@
       "version": "2.1.35",
       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
       "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "dev": true,
       "dependencies": {
         "mime-db": "1.52.0"
       },
@@ -11933,6 +11951,56 @@
         "node": ">=6"
       }
     },
+    "node_modules/pinia": {
+      "version": "2.0.16",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.16.tgz",
+      "integrity": "sha512-9/LMVO+/epny1NBfC77vnps4g3JRezxhhoF1xLUk8mZkUIxVnwfEAIRiAX8mYBTD/KCwZqnDMqXc8w3eU0FQGg==",
+      "dependencies": {
+        "@vue/devtools-api": "^6.1.4",
+        "vue-demi": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.4.0",
+        "typescript": ">=4.4.4",
+        "vue": "^2.6.14 || ^3.2.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pinia/node_modules/vue-demi": {
+      "version": "0.13.5",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz",
+      "integrity": "sha512-tO3K2bML3AwiHmVHeKCq6HLef2st4zBXIV5aEkoJl6HZ+gJWxWv2O8wLH8qrA3SX3lDoTDHNghLX1xZg83MXvw==",
+      "hasInstallScript": true,
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/pinkie": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
@@ -15476,10 +15544,10 @@
       "dev": true
     },
     "node_modules/typescript": {
-      "version": "4.1.6",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz",
-      "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==",
-      "dev": true,
+      "version": "4.7.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
+      "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
+      "devOptional": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -20471,8 +20539,7 @@
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-      "dev": true
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
     },
     "at-least-node": {
       "version": "1.0.0",
@@ -20514,6 +20581,27 @@
       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
       "dev": true
     },
+    "axios": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+      "requires": {
+        "follow-redirects": "^1.14.9",
+        "form-data": "^4.0.0"
+      },
+      "dependencies": {
+        "form-data": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+          "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+          "requires": {
+            "asynckit": "^0.4.0",
+            "combined-stream": "^1.0.8",
+            "mime-types": "^2.1.12"
+          }
+        }
+      }
+    },
     "babel-code-frame": {
       "version": "6.26.0",
       "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@@ -21504,7 +21592,6 @@
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
       "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "dev": true,
       "requires": {
         "delayed-stream": "~1.0.0"
       }
@@ -22419,8 +22506,7 @@
     "delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-      "dev": true
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
     },
     "depd": {
       "version": "2.0.0",
@@ -23682,8 +23768,7 @@
     "follow-redirects": {
       "version": "1.15.1",
       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
-      "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
-      "dev": true
+      "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
     },
     "for-in": {
       "version": "1.0.2",
@@ -25773,14 +25858,12 @@
     "mime-db": {
       "version": "1.52.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "dev": true
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
     },
     "mime-types": {
       "version": "2.1.35",
       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
       "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "dev": true,
       "requires": {
         "mime-db": "1.52.0"
       }
@@ -26737,6 +26820,23 @@
       "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
       "dev": true
     },
+    "pinia": {
+      "version": "2.0.16",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.16.tgz",
+      "integrity": "sha512-9/LMVO+/epny1NBfC77vnps4g3JRezxhhoF1xLUk8mZkUIxVnwfEAIRiAX8mYBTD/KCwZqnDMqXc8w3eU0FQGg==",
+      "requires": {
+        "@vue/devtools-api": "^6.1.4",
+        "vue-demi": "*"
+      },
+      "dependencies": {
+        "vue-demi": {
+          "version": "0.13.5",
+          "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz",
+          "integrity": "sha512-tO3K2bML3AwiHmVHeKCq6HLef2st4zBXIV5aEkoJl6HZ+gJWxWv2O8wLH8qrA3SX3lDoTDHNghLX1xZg83MXvw==",
+          "requires": {}
+        }
+      }
+    },
     "pinkie": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
@@ -29634,10 +29734,10 @@
       "dev": true
     },
     "typescript": {
-      "version": "4.1.6",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz",
-      "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==",
-      "dev": true
+      "version": "4.7.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
+      "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
+      "devOptional": true
     },
     "uglify-js": {
       "version": "3.4.10",

+ 3 - 1
package.json

@@ -9,9 +9,11 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.0.6",
+    "axios": "^0.27.2",
     "core-js": "^3.6.5",
     "element-plus": "^2.2.9",
     "lodash": "^4.17.21",
+    "pinia": "^2.0.16",
     "vue": "^3.0.0",
     "vue-router": "^4.0.13"
   },
@@ -32,6 +34,6 @@
     "prettier": "^2.2.1",
     "sass": "^1.26.5",
     "sass-loader": "^8.0.2",
-    "typescript": "~4.1.5"
+    "typescript": "^4.7.4"
   }
 }

+ 3 - 0
src/App.vue

@@ -1,5 +1,8 @@
 <template>
+  <!-- 根组件添加异步 -->
+  <!-- <suspense> -->
   <router-view />
+  <!-- </suspense> -->
 </template>
 
 <style lang="scss">

+ 177 - 0
src/components/FForm/index.vue

@@ -0,0 +1,177 @@
+<template>
+  <el-form :model="form" ref="FForm" :label-width="labelWidth" :rules="rules">
+    <template v-for="f in fields" :key="f.model">
+      <template v-if="!f.custom">
+        <el-form-item :label="f.label" :prop="f.model" :required="f.required">
+          <template v-if="f.type === 'checkbox'">
+            <el-checkbox-group v-model="form[f.model]">
+              <slot :name="f.model" />
+            </el-checkbox-group>
+          </template>
+          <template v-else-if="f.type === 'select'">
+            <el-select v-model="form[f.model]" style="width: 100%" :placeholder="getPlaceholder(f)">
+              <slot :name="f.model" />
+            </el-select>
+          </template>
+          <template v-else-if="f.type === 'number'">
+            <el-input-number v-model="form[f.model]" :placeholder="getPlaceholder(f)" style="width: 100%" />
+          </template>
+          <template v-else-if="f.type === 'radio'">
+            <el-radio-group v-model="form[f.model]">
+              <slot :name="f.model" />
+            </el-radio-group>
+          </template>
+          <template v-else-if="f.type === 'textarea'">
+            <el-input v-model="form[f.model]" :placeholder="getPlaceholder(f)" :autosize="{ minRows: 2, maxRows: 4 }" type="textarea" />
+          </template>
+          <template v-else-if="f.type === 'time' || f.type === 'timerange'">
+            <el-time-picker
+              v-model="form[f.model]"
+              range-separator="至"
+              :is-range="f.type === 'timerange'"
+              :placeholder="getPlaceholder(f)"
+              value-format="HH:mm:ss"
+              arrow-control
+            />
+          </template>
+          <template v-else-if="isDateType(f.type)">
+            <el-date-picker
+              v-model="form[f.model]"
+              range-separator="至"
+              :type="getDateType(f.type)"
+              :format="isDateType(f.type)"
+              :value-format="isDateType(f.type)"
+              :placeholder="getPlaceholder(f)"
+            />
+          </template>
+          <template v-else>
+            <el-input v-model="form[f.model]" :placeholder="getPlaceholder(f)" />
+          </template>
+        </el-form-item>
+      </template>
+    </template>
+    <el-row style="text-align: center" v-if="needSave">
+      <el-col :span="24">
+        <el-button type="primary" @click="submit()" :disabled="isSubmiting">提交</el-button>
+      </el-col>
+    </el-row>
+    <template v-else>
+      <slot name="sumbit"></slot>
+    </template>
+  </el-form>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+export default defineComponent({
+  name: 'FForm',
+  components: {},
+});
+</script>
+
+<script setup lang="ts">
+import { ref, defineProps, defineEmits, withDefaults, watch, defineExpose, nextTick } from 'vue';
+import { IField, formType } from '@/types/FForm';
+import type { FormInstance } from 'element-plus';
+import _ from 'lodash';
+interface IProps {
+  /**字段设置 */
+  fields: Array<IField>;
+  /**验证设置 */
+  rules?: Record<string, any>;
+  /**数据 */
+  modelValue: Record<string, any>;
+  /**文案标签宽度 */
+  labelWidth?: string | number;
+  /**是否需要保存按钮栏 */
+  needSave?: boolean;
+}
+const props = withDefaults(defineProps<IProps>(), {
+  labelWidth: '120px',
+  needSave: true,
+});
+let form = ref(_.cloneDeep(props.modelValue));
+const emits = defineEmits<{
+  (e: 'update:modelValue', p: any): void;
+  (e: 'save', p: any): void;
+  (e: 'save'): void;
+  (e: string, p: any): void;
+  (e: string): void;
+}>();
+
+let isSubmiting = ref(false);
+const FForm = ref<FormInstance>();
+let submit = () => {
+  if (!FForm.value) return;
+  FForm.value.validate((valid) => {
+    if (valid) {
+      let data = _.cloneDeep(form);
+      emits('save');
+      isSubmiting.value = true;
+    } else {
+      console.log('error submit!');
+      return false;
+    }
+  });
+};
+let canSubmit = () => {
+  console.log('line 115 in function:');
+  nextTick(() => {
+    isSubmiting.value = false;
+  });
+};
+
+// #region model处理
+watch(
+  () => form,
+  (val) => {
+    emits('update:modelValue', val);
+  },
+  { deep: true }
+);
+// #endregion
+// #region placeholder输出
+let placeholderWord = (type: string | undefined, label: string): string => {
+  let word = '';
+  switch (type) {
+    case undefined:
+    case 'input':
+    case 'number':
+    case 'textarea':
+      word = `请输入${label}`;
+      break;
+    default:
+      word = `请选择${label}`;
+      break;
+  }
+  return word;
+};
+let getPlaceholder = (field: IField): string => {
+  let { type, label } = field;
+  let word = placeholderWord(type, label);
+  return word;
+};
+
+// #endregion
+// #region 时间相关
+enum dateType {
+  date = 'YYYY-MM-DD',
+  year = 'YYYY',
+  month = 'YYYY-MM',
+  dates = 'YYYY-MM-DD',
+  datetime = 'YYYY-MM-DD HH:mm:ss',
+  datetimerange = 'YYYY-MM-DD HH:mm:ss',
+  daterange = 'YYYY-MM-DD',
+  monthrange = 'YYYY-MM',
+}
+let isDateType = (type: formType) => {
+  if (!type) return;
+  const t = _.get(dateType, type);
+  return t;
+};
+let getDateType = (type: any) => type;
+// #endregion
+defineExpose({ canSubmit });
+</script>
+
+<style lang="scss" scoped></style>

+ 56 - 0
src/components/FTable/example.vue

@@ -0,0 +1,56 @@
+<template>
+  <div id="example">
+    <f-table v-model:selected="selected" :data="list" :fields="fields" :total="total" :opera="opera" @query="search" :sumcol="sumcol"></f-table>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+export default defineComponent({
+  name: 'example',
+  components: {},
+});
+</script>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue';
+// #region table
+import FTable from '@/components/FTable/index.vue';
+import { IOpera, IField } from '@/types/FTable';
+interface IUser {
+  _id: string;
+  name: string;
+  age: number;
+  address: string;
+}
+let list = ref([] as IUser[]);
+let search = ({ skip = 0, limit = 10 } = {}) => {
+  let midArr: Array<IUser> = [];
+  for (let i = 0; i < limit; i++) {
+    const _id = `${i + skip}`;
+    const name = `n-${_id}`;
+    const age = i + skip;
+    const address = `addr-${_id}`;
+    midArr.push({ _id, name, age, address });
+  }
+  list.value = midArr;
+};
+search();
+let total = ref(300);
+
+let fields: Array<IField> = [
+  { label: '名称', model: 'name' },
+  { label: '年龄', model: 'age' },
+  { label: '地址', model: 'address' },
+];
+let opera: Array<IOpera> = [
+  { label: '修改', method: 'edit' },
+  { label: '删除', type: 'danger', method: 'delete', confirm: true },
+];
+
+let sumcol = ['age'];
+let selected = ref([]);
+// #endregion
+</script>
+
+<style lang="scss" scoped></style>

+ 251 - 0
src/components/FTable/index.vue

@@ -0,0 +1,251 @@
+<template>
+  <el-table
+    ref="ftable"
+    :show-summary="sumcol && sumcol.length > 0"
+    :summary-method="sumComputed"
+    :row-key="rowKey"
+    :data="data"
+    border
+    stripe
+    :max-height="height"
+    @select="selectChange"
+    @select-all="selectAll"
+  >
+    <el-table-column type="selection" align="center" width="60" v-if="useSelect" />
+    <template v-for="i in fields" :key="i.model">
+      <template v-if="!i.custom">
+        <el-table-column sortable align="center" :label="i.label" :prop="i.model" :formatter="toFormatter"></el-table-column>
+      </template>
+      <slot :name="i.model"></slot>
+    </template>
+    <el-table-column align="center" label="操作" v-if="opera">
+      <template #default="{ row, $index }">
+        <template v-for="i in opera" :key="i.method">
+          <el-link :type="i.type ? i.type : 'primary'" :underline="false" @click="toOpera(row, $index, i)" class="opera__btn">{{ i.label }}</el-link>
+        </template>
+      </template>
+    </el-table-column>
+  </el-table>
+  <el-row class="page__row" v-if="total">
+    <el-col :span="24">
+      <el-pagination small :page-size="limit" background layout="->, total, prev, pager, next " :total="total" @current-change="pageChange" />
+    </el-col>
+  </el-row>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+export default defineComponent({
+  name: 'FTable',
+});
+</script>
+
+<script setup lang="ts">
+import _ from 'lodash';
+import { ElMessageBox } from 'element-plus';
+import type { Action } from 'element-plus';
+import { ElTable } from 'element-plus';
+import type { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults';
+import { IOpera, IField, OperaType, rowKeyType } from '@/types/FTable';
+import { ref, defineProps, defineEmits, withDefaults, watch, nextTick } from 'vue';
+interface IProps {
+  /** 数据 */
+  data: Array<any>;
+  /** 显示设置 */
+  fields: Array<IField>;
+  /** 操作设置 */
+  opera?: Array<IOpera>;
+  // limit: number;
+  /** 总数 */
+  total?: number;
+  /** 最大高度 */
+  height?: number;
+  /**显示格式化函数 */
+  toFormat?: (obj: any) => string;
+  /**行数据的key */
+  rowKey?: rowKeyType | string;
+  /**合计字段 */
+  sumcol?: Array<string>;
+  /**使用多选 */
+  useSelect?: boolean;
+  /**选择的数据 */
+  selected?: Array<any>;
+}
+const props = withDefaults(defineProps<IProps>(), {
+  rowKey: '_id',
+  useSelect: true,
+});
+let limit = 10;
+const emits = defineEmits<{
+  (e: 'query', p: any): void;
+  (e: 'update:selected', p: any): void;
+  (e: string, p: any): void;
+}>();
+/**
+ * 运行操作
+ * @prop {Record<string,any>} row 行数据
+ * @prop {number} index 行数据
+ * @prop {Record<string,any>} opera 行数据
+ */
+let toOpera = (row: Record<string, any>, index: number, opera: IOpera) => {
+  const { confirm, method, methodZh } = opera;
+  if (!confirm) {
+    emits(method, { row, index });
+  } else {
+    let word;
+    if (methodZh) word = methodZh;
+    else word = _.get(OperaType, method);
+    ElMessageBox.confirm(`您确定要${word}该数据吗?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+      callback: (res: Action) => {
+        if (res === 'confirm') {
+          // 执行函数
+          emits(method, { row, index });
+        } else {
+          // 取消
+          console.log('cancel');
+        }
+      },
+    });
+  }
+};
+
+/**
+ * 表格显示格式化
+ * @prop {Record<string,any>} row 行数据
+ * @prop {Record<string,any>} column 列设置数据
+ * @prop {any} cellValue 表格数据
+ * @prop {number} index 索引
+ * @return string
+ */
+let toFormatter = (row: Record<string, any>, column: Record<string, any>, cellValue: any, index: number): string => {
+  let this_fields = props.fields.filter((fil) => fil.model === column.property);
+  if (this_fields.length > 0) {
+    let format = _.get(this_fields[0], `format`, false);
+    if (format) {
+      let res;
+      if (_.isFunction(format)) {
+        res = format(cellValue);
+      } else if (_.isFunction(props.toFormat)) {
+        res = props.toFormat({
+          model: this_fields[0].model,
+          value: cellValue,
+          row,
+        });
+      }
+      return res;
+    } else return cellValue;
+  }
+  return cellValue;
+};
+
+// #region 计算部分
+interface SummaryMethodProps<T> {
+  columns: TableColumnCtx<T>[];
+  data: T[];
+}
+/**
+ * 计算函数
+ * @prop {Array} columns 列数组
+ * @prop {Array} data 表格中所有的数据
+ * @return Array
+ * */
+let sumComputed = (param: SummaryMethodProps<any>) => {
+  const { columns, data } = param;
+  let result: string[] = [];
+  if (columns.length <= 0 || data.length <= 0) return [];
+  const reg = new RegExp(/^\d+$/);
+  for (const column of columns) {
+    const prop = _.get(column, 'property');
+    if (!prop) {
+      result.push('');
+      continue;
+    }
+    // 判断是否需要计算
+    if (!props.sumcol) return result;
+    const inlist = props.sumcol.find((f) => f == prop);
+    if (!inlist) {
+      result.push('');
+      continue;
+    }
+    let res;
+    // 整理出要计算的属性(只取出数字或者可以为数字的值)
+    const resetList = data.map((i) => {
+      const d = _.get(i, prop);
+      const res = reg.test(d);
+      if (res) return d * 1;
+      else return 0;
+    });
+    res = `${resetList.reduce((p, n) => p + n, 0)}`;
+    result.push(res);
+  }
+  result[0] = '合计';
+  return result;
+};
+// #endregion
+
+// #region 选择部分
+const ftable = ref<InstanceType<typeof ElTable>>();
+/**选择/反选 */
+let selectChange = (selection: any, row: any) => {
+  if (!props.selected) return;
+  let selected = _.cloneDeep(props.selected);
+  let has = props.selected.find((i) => i[props.rowKey] === row[props.rowKey]);
+  if (has) {
+    selected = props.selected.filter((f) => f[props.rowKey] !== row[props.rowKey]);
+  } else {
+    selected.push(row);
+  }
+  emits('update:selected', selected);
+};
+/**全选/反选 */
+let selectAll = (selection: any) => {
+  if (!props.selected) return;
+  let selected = _.cloneDeep(props.selected);
+  let res = [];
+  if (selection.length > 0) {
+    res = _.uniqBy(selected.concat(selection), props.rowKey);
+  } else {
+    res = _.differenceBy(selected, props.data, props.rowKey);
+  }
+  emits('update:selected', res);
+};
+/**渲染选项 */
+let initSelection = async () => {
+  // 清除已选择的选项样式
+  if (!props.selected) return;
+  await nextTick();
+  let selected = props.selected;
+  ftable.value?.clearSelection();
+  selected.forEach((info) => {
+    let d = props.data.filter((p) => p[props.rowKey] === info[props.rowKey]);
+    if (d.length > 0) ftable.value?.toggleRowSelection(d[0], true);
+  });
+};
+watch(
+  () => props.data,
+  (val) => initSelection(),
+  { deep: true, immediate: true }
+);
+// #endregion
+
+/**
+ * 换页
+ * @prop {number} page 当前页
+ */
+let pageChange = (page: number) => {
+  const query = { skip: (page - 1) * limit, limit };
+  emits('query', query);
+};
+</script>
+
+<style lang="scss" scoped>
+.opera__btn {
+  padding-right: 10px;
+}
+.page__row {
+  padding: 15px 0;
+}
+</style>

+ 0 - 132
src/components/HelloWorld.vue

@@ -1,132 +0,0 @@
-<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <p>
-      For a guide and recipes on how to configure / customize this project,<br />
-      check out the
-      <a href="https://cli.vuejs.org" target="_blank" rel="noopener"
-        >vue-cli documentation</a
-      >.
-    </p>
-    <h3>Installed CLI Plugins</h3>
-    <ul>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
-          target="_blank"
-          rel="noopener"
-          >babel</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
-          target="_blank"
-          rel="noopener"
-          >router</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
-          target="_blank"
-          rel="noopener"
-          >eslint</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
-          target="_blank"
-          rel="noopener"
-          >typescript</a
-        >
-      </li>
-    </ul>
-    <h3>Essential Links</h3>
-    <ul>
-      <li>
-        <a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
-      </li>
-      <li>
-        <a href="https://forum.vuejs.org" target="_blank" rel="noopener"
-          >Forum</a
-        >
-      </li>
-      <li>
-        <a href="https://chat.vuejs.org" target="_blank" rel="noopener"
-          >Community Chat</a
-        >
-      </li>
-      <li>
-        <a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
-          >Twitter</a
-        >
-      </li>
-      <li>
-        <a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
-      </li>
-    </ul>
-    <h3>Ecosystem</h3>
-    <ul>
-      <li>
-        <a href="https://router.vuejs.org" target="_blank" rel="noopener"
-          >vue-router</a
-        >
-      </li>
-      <li>
-        <a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-devtools#vue-devtools"
-          target="_blank"
-          rel="noopener"
-          >vue-devtools</a
-        >
-      </li>
-      <li>
-        <a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
-          >vue-loader</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/awesome-vue"
-          target="_blank"
-          rel="noopener"
-          >awesome-vue</a
-        >
-      </li>
-    </ul>
-  </div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from "vue";
-
-export default defineComponent({
-  name: "HelloWorld",
-  props: {
-    msg: String,
-  },
-});
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped lang="scss">
-h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-a {
-  color: #42b983;
-}
-</style>

+ 36 - 0
src/layout/FBreadcrumb.vue

@@ -0,0 +1,36 @@
+<template>
+  <div id="FBreadcrumb">
+    <el-row>
+      <el-col :span="24" class="crumbs">
+        <el-breadcrumb separator="/">
+          <el-breadcrumb-item>
+            <div class="text">
+              <el-icon><Grid /></el-icon>{{ title }}
+            </div>
+          </el-breadcrumb-item>
+        </el-breadcrumb>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+export default defineComponent({
+  name: 'FBreadcrumb',
+  components: {},
+});
+</script>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue';
+import { useRoute } from 'vue-router';
+let route = useRoute();
+let title = computed(() => route.meta.title);
+</script>
+
+<style lang="scss" scoped>
+.text {
+  display: flex;
+}
+</style>

+ 29 - 14
src/layout/FTags.vue

@@ -18,13 +18,15 @@
             <!-- #endregion -->
           </el-col>
           <el-col :span="1" class="right">
-            <!-- <el-dropdown @command="handleTags">
-              <el-button size="mini" type="primary"> 标签选项<i class="el-icon-arrow-down el-icon--right"></i> </el-button>
-              <el-dropdown-menu size="small" slot="dropdown">
-                <el-dropdown-item command="other">关闭其他</el-dropdown-item>
-                <el-dropdown-item command="all">关闭所有</el-dropdown-item>
-              </el-dropdown-menu>
-            </el-dropdown> -->
+            <el-dropdown @command="handleTags">
+              <el-button size="small" type="primary"> 标签选项<i class="el-icon-arrow-down el-icon--right"></i> </el-button>
+              <template #dropdown>
+                <el-dropdown-menu size="small">
+                  <el-dropdown-item command="other">关闭其他</el-dropdown-item>
+                  <el-dropdown-item command="all">关闭所有</el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
           </el-col>
         </el-col>
       </el-col>
@@ -57,25 +59,38 @@ interface tag {
   path: string;
 }
 // 定义标签列表
-let tagsList = reactive([] as tag[]);
+let tagsList = ref([] as tag[]);
 // 设置标签
 let setTags = (route: Record<string, any>) => {
   const { fullPath: path, meta } = route;
   const { title } = meta;
   const tag: tag = { title, path };
-  const r = tagsList.some((f) => _.isEqual(tag, f));
-  if (!r) tagsList.push(tag);
+  const r = tagsList.value.some((f) => _.isEqual(tag, f));
+  if (!r) tagsList.value.push(tag);
 };
 // 监听路由变化,设置标签
 watch(route, (val) => setTags(val), { deep: true, immediate: true });
 // 关闭标签
 let closeTags = (index: number) => {
-  tagsList.splice(index, 1);
-  if (tagsList.length === 0) router.push({ name: 'Index' });
+  tagsList.value.splice(index, 1);
+  if (tagsList.value.length === 0) router.push({ name: 'Index' });
+};
+// 关闭所有标签
+let closeAll = () => {
+  tagsList.value = [];
+  router.push({ name: 'Index' });
+};
+// 关闭其他标签
+let closeOther = () => {
+  const thisTag = tagsList.value.find((f) => isActive(f.path));
+  tagsList.value = [thisTag as tag];
 };
-// #endregion
 
-console.log(123);
+// 打开标签
+let handleTags = (command: string) => {
+  command === 'other' ? closeOther() : closeAll();
+};
+// #endregion
 </script>
 
 <style lang="scss" scoped>

+ 2 - 10
src/layout/home.vue

@@ -14,7 +14,7 @@
             <el-col :span="24" class="content">
               <transition name="move" mode="out-in">
                 <el-row>
-                  <!-- <breadcrumb :breadcrumbTitle="this.$route.meta.title"></breadcrumb> -->
+                  <f-breadcrumb />
                   <el-col :span="24" class="container" style="padding: 15px"><router-view :key="viewKey"></router-view></el-col>
                 </el-row>
               </transition>
@@ -31,19 +31,11 @@
 import { defineComponent } from 'vue';
 export default defineComponent({
   name: 'home',
-  components: {},
-  props: {},
-  data() {
-    return {
-      // collapse: false,
-      // viewKey: new Date().getTime(),
-    };
-  },
-  methods: {},
 });
 </script>
 
 <script setup lang="ts">
+import FBreadcrumb from './FBreadcrumb.vue';
 import FHeader from './FHead.vue';
 import FSidebar from './FSidebar.vue';
 import FTags from './FTags.vue';

+ 4 - 2
src/main.ts

@@ -1,13 +1,15 @@
 import { createApp } from 'vue';
 import App from './App.vue';
 import router from './router';
+import { createPinia } from 'pinia';
 import ElementPlus from 'element-plus';
 import 'element-plus/dist/index.css';
 import * as ElementPlusIconsVue from '@element-plus/icons-vue';
-
+import locale from 'element-plus/lib/locale/lang/zh-cn'; //中文
 const app = createApp(App);
 app.use(router);
-app.use(ElementPlus);
+app.use(createPinia());
+app.use(ElementPlus, { locale });
 for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
   app.component(key, component);
 }

+ 117 - 0
src/plugins/axios.ts

@@ -0,0 +1,117 @@
+import Axios from 'axios';
+import _ from 'lodash';
+
+interface IQuery {
+  params: Record<string, any>;
+  [k: string]: any;
+}
+/**请求次数计数器 */
+let currentRequests = 0;
+export default class AxiosWrapper {
+  baseUrl: string;
+  unwrap: boolean;
+  constructor({ baseUrl = '', unwrap = true } = {}) {
+    this.baseUrl = baseUrl;
+    this.unwrap = unwrap;
+  }
+  // 替换uri中的参数变量
+  static merge = (uri: string, query: IQuery): string => {
+    if (!uri.includes(':')) {
+      return uri;
+    }
+    const keys = [];
+    const regexp = /\/:([a-z0-9_]+)/gi;
+    let res;
+    // eslint-disable-next-line no-cond-assign
+    while ((res = regexp.exec(uri)) != null) {
+      keys.push(res[1]);
+    }
+    keys.forEach((key) => {
+      if (!query[key]) {
+        uri = uri.replace(`:${key}`, query[key]);
+      }
+    });
+    return uri;
+  };
+  $get = (uri: string, query?: IQuery, options?: Record<string, any>): Record<string, any> => {
+    return this.$request(uri, undefined, query, options);
+  };
+  $post = (uri: string, data: Record<string, any> = {}, query: IQuery, options: Record<string, any> = {}): Record<string, any> => {
+    return this.$request(uri, data, query, options);
+  };
+  $delete = (uri: string, data: Record<string, any> = {}, query: IQuery, options: Record<string, any> = {}): Record<string, any> => {
+    options = { ...options, method: 'delete' };
+    return this.$request(uri, data, query, options);
+  };
+  $request = async (uri: string, data?: Record<string, any> | undefined, query?: IQuery, options?: Record<string, any>): Promise<Record<string, any>> => {
+    // #region 合并query和options
+    if (_.isObject(query) && _.isObject(options)) {
+      options = { ...options, params: query, method: 'get' };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    if (!options) options = {};
+    // #endregion
+    const url = AxiosWrapper.merge(uri, options.params);
+    currentRequests += 1;
+    try {
+      const axios = Axios.create({
+        baseURL: this.baseUrl,
+      });
+      // #region token
+      let token: string | null;
+      if (process.env.VUE_APP_STORAGE === 'local') {
+        token = localStorage.getItem('token');
+      } else if (process.env.VUE_APP_STORAGE === 'session') {
+        token = sessionStorage.getItem('token');
+      } else {
+        token = null;
+      }
+      if (token) axios.defaults.headers.common.Authorization = token;
+      // #endregion
+      const res = await axios.request({
+        method: data ? 'get' : 'post',
+        url,
+        data,
+        responseType: 'json',
+        ...options,
+      });
+      if (!res) return {};
+      /**最后返回的结果 */
+      let rResult: Record<string, any> = res.data;
+      const { errcode, errmsg, details } = rResult;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return rResult;
+      }
+      // unwrap data
+      if (this.unwrap) {
+        rResult = _.omit(rResult, ['errmsg', 'details']);
+        const keys = Object.keys(rResult);
+        if (keys.length === 1 && keys.includes('data')) {
+          rResult = rResult.data;
+        }
+      }
+      return rResult;
+    } catch (err: any) {
+      let errmsg = '接口请求失败,请稍后重试';
+      if (err.response) {
+        const { status } = err.response;
+        if (status === 401) errmsg = '用户认证失败,请重新登录';
+        if (status === 403) errmsg = '当前用户不允许执行该操作';
+      }
+      console.error(
+        `[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - 
+        ${err.message}`
+      );
+      return { errcode: err?.response?.status, errmsg, details: err.message };
+    } finally {
+      currentRequests -= 1;
+      if (currentRequests <= 0) {
+        currentRequests = 0;
+      }
+    }
+  };
+}

+ 1 - 2
src/router/index.ts

@@ -1,5 +1,4 @@
 import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
-import Home from '../views/Home.vue';
 
 const routes: Array<RouteRecordRaw> = [
   {
@@ -30,7 +29,7 @@ const routes: Array<RouteRecordRaw> = [
 ];
 
 const router = createRouter({
-  history: createWebHistory(process.env.BASE_URL),
+  history: createWebHistory(process.env.VUE_APP_BASE_URL),
   routes,
 });
 

+ 15 - 0
src/store/role.ts

@@ -0,0 +1,15 @@
+import { defineStore } from 'pinia';
+import AxiosWarpper from '@/plugins/axios';
+const axios = new AxiosWarpper();
+const api = '/freeAdmin/api/user/role';
+export const roleStore = defineStore('role', {
+  state: () => ({
+    user: { name: 'none' },
+  }),
+  actions: {
+    async query() {
+      const res = await axios.$get(api);
+      return res;
+    },
+  },
+});

+ 31 - 0
src/types/FForm.ts

@@ -0,0 +1,31 @@
+type formType =
+  | 'input'
+  | 'number'
+  | 'select'
+  | 'radio'
+  | 'checkbox'
+  | 'textarea'
+  | 'date'
+  | 'year'
+  | 'month'
+  | 'datetime'
+  | 'dates'
+  | 'datetimerange'
+  | 'monthrange'
+  | 'daterange'
+  | 'time'
+  | 'timerange'
+  | undefined;
+interface IField {
+  /**字段中文 */
+  label: string;
+  /**字段名 */
+  model: string;
+  /**是否自定义 */
+  custom?: boolean;
+  /**类型 */
+  type?: formType;
+  [k: string]: any;
+}
+
+export { IField, formType };

+ 49 - 0
src/types/FTable.ts

@@ -0,0 +1,49 @@
+/**
+ * @property {string} 按钮类型
+ */
+type btnType = 'primary' | 'danger';
+
+/**
+ * 操作设置
+ */
+declare interface IOpera {
+  /**
+   * 按钮文案
+   * @type {string}
+   */
+  label: string;
+  /**
+   * 返回到的函数
+   * @type {string}
+   */
+  method: string;
+  /**
+   * 按钮类型
+   * @type {btnType}
+   */
+  type?: btnType | undefined;
+  /**
+   * 自定义属性
+   * @type {any}
+   */
+  [k: string]: any;
+}
+
+enum OperaType {
+  create = '创建',
+  edit = '修改',
+  delete = '删除',
+}
+
+/**
+ * 展示设置
+ */
+declare interface IField {
+  label: string;
+  model: string;
+  [k: string]: any;
+}
+/**表格rowKey的默认 */
+type rowKeyType = '_id' | 'id';
+
+export { IOpera, IField, OperaType, rowKeyType };

+ 0 - 5
src/views/About.vue

@@ -1,5 +0,0 @@
-<template>
-  <div class="about">
-    <h1>This is an about page</h1>
-  </div>
-</template>

+ 0 - 18
src/views/Home.vue

@@ -1,18 +0,0 @@
-<template>
-  <div class="home">
-    <img alt="Vue logo" src="../assets/logo.png" />
-    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
-  </div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
-export default defineComponent({
-  name: 'Home',
-  components: {
-    HelloWorld,
-  },
-  methods: {},
-});
-</script>

+ 38 - 3
src/views/admin/role/index.vue

@@ -1,6 +1,16 @@
 <template>
   <div id="index">
-    <p>role</p>
+    <!-- <f-table v-model:selected="selected" :data="list" :fields="fields" :total="total" :opera="opera" @query="search" :sumcol="sumcol"></f-table> -->
+    <f-form ref="fform" v-model="form" :fields="fields" :rules="rules" @save="toSave">
+      <template #age>
+        <!-- <el-option label="12" :value="12"></el-option>
+        <el-option label="22" :value="22"></el-option> -->
+        <!-- <el-radio :label="32">32</el-radio>
+        <el-radio :label="42">42</el-radio> -->
+        <el-checkbox label="a">a</el-checkbox>
+        <el-checkbox label="b">b</el-checkbox>
+      </template>
+    </f-form>
   </div>
 </template>
 
@@ -8,12 +18,37 @@
 import { defineComponent } from 'vue';
 export default defineComponent({
   name: 'index',
-  components: {},
 });
 </script>
 
 <script setup lang="ts">
-import { ref, reactive } from 'vue';
+import { ref, onMounted } from 'vue';
+import FForm from '@/components/FForm/index.vue';
+import { IField } from '@/types/FForm';
+import { roleStore } from '@/store/role';
+const r = roleStore();
+
+let fields: Array<IField> = [
+  { label: '名称', model: 'name' },
+  { label: '年龄', model: 'age', type: 'checkbox' },
+  { label: '地址', model: 'address', type: 'textarea' },
+  { label: '日期', model: 'date', type: 'date' },
+];
+let form = ref({ name: 'bcs' });
+let rules = {
+  name: [{ required: true, message: '请填写名字', trigger: 'blur' }],
+};
+let fform = ref();
+let toSave = () => {
+  fform.value.canSubmit();
+};
+let search = async () => {
+  const res = await r.query();
+  console.log(res);
+};
+onMounted(async () => {
+  await search();
+});
 </script>
 
 <style lang="scss" scoped></style>

+ 16 - 0
vue.config.js

@@ -0,0 +1,16 @@
+module.exports = {
+  publicPath: `/${process.env.VUE_APP_BASE_URL}`,
+  devServer: {
+    port: '8001',
+    proxy: {
+      '/files': {
+        target: 'http://broadcast.waityou24.cn',
+      },
+      '/freeAdmin/api/user': {
+        target: 'http://127.0.0.1:13001',
+        changeOrigin: true,
+        ws: false,
+      },
+    },
+  },
+};