瀏覽代碼

feat: table和modal完成

xyh 3 年之前
父節點
當前提交
c831e5bd7e

+ 1 - 0
package.json

@@ -83,6 +83,7 @@
     "@vueuse/core": "^10.1.2",
     "@vueuse/integrations": "^10.1.2",
     "axios": "^1.4.0",
+    "classnames": "^2.3.2",
     "dayjs": "^1.11.7",
     "klona": "^2.0.6",
     "lodash-es": "^4.17.21",

+ 7 - 0
pnpm-lock.yaml

@@ -25,6 +25,9 @@ dependencies:
   axios:
     specifier: ^1.4.0
     version: 1.4.0
+  classnames:
+    specifier: ^2.3.2
+    version: 2.3.2
   dayjs:
     specifier: ^1.11.7
     version: 1.11.7
@@ -2569,6 +2572,10 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /classnames@2.3.2:
+    resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
+    dev: false
+
   /clean-stack@2.2.0:
     resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
     engines: {node: '>=6'}

+ 12 - 5
src/App.vue

@@ -12,6 +12,8 @@ import {
 } from 'naive-ui';
 import {computed} from 'vue';
 import {lightVariable} from '@utils';
+import {DndProvider} from 'vue3-dnd';
+import {HTML5Backend} from 'react-dnd-html5-backend';
 
 defineOptions({
   name: 'App',
@@ -57,14 +59,19 @@ const themeConfig: GlobalThemeOverrides = {
   Card: {
     borderRadius: '8px',
   },
+  Button: {
+    borderRadiusMedium: '6px',
+  },
 };
 
 </script>
 
 <template>
-  <NConfigProvider v-bind="uiLocale" :themeOverrides="themeConfig">
-    <NMessageProvider>
-      <RouterView />
-    </NMessageProvider>
-  </NConfigProvider>
+  <DndProvider :backend="HTML5Backend">
+    <NConfigProvider v-bind="uiLocale" :themeOverrides="themeConfig">
+      <NMessageProvider>
+        <RouterView />
+      </NMessageProvider>
+    </NConfigProvider>
+  </DndProvider>
 </template>

+ 12 - 0
src/apis/demo.ts

@@ -1,2 +1,14 @@
+import {BaseResult} from '@models';
 import {request} from './network';
 
+export function getDemoList(
+  data: Record<string, any>,
+  signal?: AbortSignal,
+): BaseResult<{id: number, name: string}> {
+  return request({
+    method: 'GET',
+    data,
+    signal,
+    url: '/users',
+  });
+}

+ 1 - 0
src/apis/index.ts

@@ -0,0 +1 @@
+export * from './demo';

+ 0 - 1
src/components/filter/select/index.tsx

@@ -66,7 +66,6 @@ export default defineComponent({
       <div class="ld-filter-field-wrapper">
         <label>{label}</label>
         <NSelect
-          clearFilterAfterSelect={false}
           clearable
           filterable
           remote

+ 2 - 0
src/components/index.ts

@@ -2,3 +2,5 @@ export * from './login-field';
 export * from './button';
 export * from './filter';
 export * from './modal';
+export * from './modal-field';
+export {default as LDTableTool} from './table-tool';

+ 36 - 0
src/components/modal-field/date/index.tsx

@@ -0,0 +1,36 @@
+import '../index.css';
+import {defineComponent} from 'vue';
+import {NDatePicker} from 'naive-ui';
+import {useField} from 'vee-validate';
+
+export default defineComponent({
+  name: 'LDModalInput',
+  props: {
+    name: {
+      type: String,
+      required: true,
+    },
+    label: {
+      type: String,
+      required: true,
+    },
+    optional: Boolean,
+  },
+  setup(props) {
+    const {value, setValue, errorMessage} = useField<number | null>(props.name);
+
+    return () => (
+      <div class="ld-modal-field-wrapper">
+        <div class="ld-modal-field-group">
+          <label class={props?.optional ? 'optional' : ''}>{props.label}</label>
+          <NDatePicker
+            value={value.value}
+            onUpdateValue={setValue}
+            bordered={false}
+          />
+        </div>
+        <p class="ld-modal-filed-error">{errorMessage.value}</p>
+      </div>
+    );
+  },
+});

+ 48 - 0
src/components/modal-field/index.css

@@ -0,0 +1,48 @@
+.ld-modal-field-wrapper {
+  width: 100%;
+  max-width: 400px;
+  margin: 0 auto;
+}
+
+.ld-modal-field-group {
+  display: flex;
+  gap: 20px;
+  align-items: center;
+
+  & label {
+    width: 5em;
+    font-size: var(--content-font-size);
+
+    &::before {
+      margin-right: 4px;
+      color: red;
+      content: '*';
+    }
+  }
+
+  & label.optional {
+    &::before {
+      display: none;
+    }
+  }
+
+  .n-input,
+  .n-select {
+    flex: 1;
+    border: none;
+    border-bottom: 1px solid #eee;
+  }
+
+  .n-input-number,
+  .n-date-picker {
+    flex: 1;
+  }
+}
+
+.ld-modal-filed-error {
+  height: calc(var(--tip-font-size) * 2);
+  padding-left: calc(5em + 42px);
+  margin-top: 6px;
+  font-size: var(--tip-font-size);
+  color: red;
+}

+ 4 - 0
src/components/modal-field/index.ts

@@ -0,0 +1,4 @@
+export {default as LDModalInput} from './input';
+export {default as LDModalNumberInput} from './number-input';
+export {default as LDModalDate} from './date';
+export {default as LDModalSelect} from './select';

+ 36 - 0
src/components/modal-field/input/index.tsx

@@ -0,0 +1,36 @@
+import '../index.css';
+import {defineComponent} from 'vue';
+import {NInput} from 'naive-ui';
+import {useField} from 'vee-validate';
+
+export default defineComponent({
+  name: 'LDModalInput',
+  props: {
+    name: {
+      type: String,
+      required: true,
+    },
+    label: {
+      type: String,
+      required: true,
+    },
+    optional: Boolean,
+  },
+  setup(props) {
+    const {value, setValue, errorMessage} = useField<string>(props.name);
+
+    return () => (
+      <div class="ld-modal-field-wrapper">
+        <div class="ld-modal-field-group">
+          <label class={props?.optional ? 'optional' : ''}>{props.label}</label>
+          <NInput
+            bordered={false}
+            value={value.value}
+            onUpdateValue={setValue}
+          />
+        </div>
+        <p class="ld-modal-filed-error">{errorMessage.value}</p>
+      </div>
+    );
+  },
+});

+ 36 - 0
src/components/modal-field/number-input/index.tsx

@@ -0,0 +1,36 @@
+import '../index.css';
+import {useField} from 'vee-validate';
+import {defineComponent} from 'vue';
+import {NInputNumber} from 'naive-ui';
+
+export default defineComponent({
+  name: 'LDModalNumberInput',
+  props: {
+    name: {
+      type: String,
+      required: true,
+    },
+    label: {
+      type: String,
+      required: true,
+    },
+    optional: Boolean,
+  },
+  setup(props) {
+    const {value, setValue, errorMessage} = useField<number | null>(props.name);
+
+    return () => (
+      <div class="ld-modal-field-wrapper">
+        <div class="ld-modal-field-group">
+          <label class={props?.optional ? 'optional' : ''}>{props.label}</label>
+          <NInputNumber
+            bordered={false}
+            value={value.value}
+            onUpdateValue={setValue}
+          />
+        </div>
+        <p class="ld-modal-filed-error">{errorMessage.value}</p>
+      </div>
+    );
+  },
+});

+ 81 - 0
src/components/modal-field/select/index.tsx

@@ -0,0 +1,81 @@
+import '../index.css';
+import {computed, defineComponent, ref} from 'vue';
+import {selectProps, NSelect} from 'naive-ui';
+import {useField} from 'vee-validate';
+import {debounce} from 'lodash-es';
+
+export default defineComponent({
+  name: 'LDModalInput',
+  props: {
+    name: {
+      type: String,
+      required: true,
+    },
+    label: {
+      type: String,
+      required: true,
+    },
+    optional: Boolean,
+    loading: selectProps.loading,
+    options: selectProps.options,
+    onSearch: selectProps.onSearch,
+  },
+  setup(props) {
+    const {value, setValue, errorMessage} = useField<string>(props.name);
+
+    const filterValue = ref<string>('');
+    const filterOptions = computed(function() {
+      const {options} = props;
+
+      if (!filterValue.value) return options;
+
+      return options.filter(function(val) {
+        return (val?.label as string ?? '').includes(filterValue.value)
+         || (val?.value as string ?? '').includes(filterValue.value);
+      });
+    });
+    const onDefaultSearch = debounce(
+      function(value: string) {
+        filterValue.value = value;
+      },
+      0,
+      {
+        trailing: true,
+        leading: false,
+      },
+    );
+    // 失去焦点之后判断是否有搜索结果
+    // 如果没有搜索结果将搜索值清空
+    // 防止没搜索到之后失去焦点再返回会显示空选项
+    function onBlur() {
+      filterOptions.value.length === 0
+      && filterValue.value.length > 0
+      && (filterValue.value = '');
+    }
+
+    return () => (
+      <div class="ld-modal-field-wrapper">
+        <div class="ld-modal-field-group">
+          <label class={props?.optional ? 'optional' : ''}>{props.label}</label>
+          <NSelect
+            bordered={false}
+            filterable
+            remote
+            loading={props.loading}
+            onClear={() => {
+              setValue('');
+              onDefaultSearch('');
+            }}
+            options={filterOptions.value}
+            // 防止空字符串没有placeholder
+            value={value.value || null}
+            onUpdateValue={setValue}
+            onSearch={props.onSearch ?? onDefaultSearch}
+            onBlur={onBlur}
+          />
+        </div>
+        <p class="ld-modal-filed-error">{errorMessage.value}</p>
+      </div>
+    );
+  },
+});

+ 5 - 1
src/components/modal/normal/index.tsx

@@ -22,6 +22,7 @@ export default defineComponent(
       isAdd: Boolean,
       icon: String,
       onSubmit: Function as PropType<(e: Event) => void>,
+      isLoading: Boolean,
     },
     emits: {
       'update:modelValue': Boolean,
@@ -66,7 +67,10 @@ export default defineComponent(
                   <div class="ld-normal-modal-form-content">
                     {slots.default?.()}
                   </div>
-                  <LDButton class="ld-normal-modal-button" attrType="submit">
+                  <LDButton
+                    loading={props.isLoading}
+                    class="ld-normal-modal-button"
+                    attrType="submit">
                     {t('common.confirm')}
                   </LDButton>
                 </form>

+ 10 - 0
src/components/table-tool/index.css

@@ -0,0 +1,10 @@
+.ld-table-tool {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  & h2 {
+    font-size: 20px;
+    font-weight: normal;
+  }
+}

+ 56 - 0
src/components/table-tool/index.tsx

@@ -0,0 +1,56 @@
+import {NButton, NSpace} from 'naive-ui';
+import './index.css';
+import {PropType, defineComponent, h, SlotsType} from 'vue';
+import {useI18n} from 'vue-i18n';
+import {
+  Download,
+  FileAdditionOne,
+  FileExcel,
+  Refresh,
+  Upload,
+} from '@icon-park/vue-next';
+
+// TODO: 控制对应的事件有显示对应的按钮
+export default defineComponent({
+  name: 'LDTableTool',
+  props: {
+    title: String,
+    isRefresh: Boolean,
+    isExporting: Boolean,
+    onAdd: Function as PropType<() => void>,
+    onRefresh: Function as PropType<() => void>,
+    onExport: Function as PropType<() => void>,
+    onModalExport: Function as PropType<() => void>,
+    onModalImport: Function as PropType<() => void>,
+  },
+  slots: Object as SlotsType<{
+    prefix?: string | undefined;
+  }>,
+  setup(props, {slots}) {
+    const {t} = useI18n();
+
+    return () => (
+      <div class="ld-table-tool">
+        <h2>{props.title}</h2>
+        <NSpace>
+          {slots.prefix?.()}
+          <NButton type="primary" renderIcon={() => h(FileAdditionOne)}>
+            {t('common.tableTool.add')}
+          </NButton>
+          <NButton type="success" renderIcon={() => h(Refresh)}>
+            {t('common.tableTool.refresh')}
+          </NButton>
+          <NButton type="info" renderIcon={() => h(FileExcel)}>
+            {t('common.tableTool.export')}
+          </NButton>
+          <NButton type="default" renderIcon={() => h(Download)}>
+            {t('common.tableTool.modalExport')}
+          </NButton>
+          <NButton type="default" renderIcon={() => h(Upload)}>
+            {t('common.tableTool.modalImport')}
+          </NButton>
+        </NSpace>
+      </div>
+    );
+  },
+});

+ 7 - 0
src/locales/common.ts

@@ -7,6 +7,13 @@ export default {
       filter: '筛选',
     },
     confirm: '确定',
+    tableTool: {
+      add: '新增',
+      refresh: '刷新',
+      export: '导出',
+      modalExport: '模板导出',
+      modalImport: '模板导入',
+    },
   },
   ko: {title: '供应商管理系统'},
 };

+ 2 - 0
src/pages/audit/index.vue

@@ -1,11 +1,13 @@
 <script setup lang='ts'>
 import {PATH_NAME} from './state';
 import Filter from './filter/index.vue';
+import Table from './table/index.vue';
 
 defineOptions({name: PATH_NAME});
 </script>
 
 <template>
   <Filter />
+  <Table />
 </template>
 

+ 80 - 0
src/pages/audit/table/index.vue

@@ -0,0 +1,80 @@
+<script setup lang='ts'>
+import {PATH_NAME} from '../state';
+import {
+  NCard,
+  NDataTable,
+  type DataTableColumns,
+  NSpace,
+  NButton,
+} from 'naive-ui';
+import {LDTableTool} from '@components';
+import {h, ref} from 'vue';
+import Modal from './modal/index.vue';
+
+defineOptions({name: PATH_NAME + 'table'});
+
+const visible = ref(false);
+
+const columns: DataTableColumns<{id: number, name: string, userName: string}>
+= [
+  {
+    key: 'no',
+    title: '序号',
+    width: 64,
+    render(_, idx) {
+      return idx;
+    },
+  },
+  {key: 'name', title: '姓名'},
+  {key: 'userName', title: '用户名'},
+  {
+    key: 'id',
+    title: '操作',
+    render() {
+      return h(
+        NSpace,
+        null,
+        [
+          h(
+            NButton,
+            {
+              type: 'primary',
+              quaternary: true,
+              focusable: false,
+              onClick() {
+                visible.value = true;
+              },
+            },
+            '修改',
+          ),
+          h(NButton, {type: 'error', quaternary: true, focusable: false}, '删除'),
+        ],
+      );
+    },
+  },
+];
+
+const data = [
+  {id: 1, name: 'simon', userName: 'simon'},
+  {id: 2, name: 'simon2', userName: 'simon2'},
+  {id: 3, name: 'simon3', userName: 'simon3'},
+  {id: 4, name: 'simon4', userName: 'simon4'},
+];
+
+</script>
+
+<template>
+  <NCard class="table-wrapper">
+    <LDTableTool title="Demo" />
+
+    <NDataTable
+      class="table-content"
+      :columns="columns"
+      :data="data"
+      :bordered="false"
+    />
+  </NCard>
+
+  <Modal v-model="visible" />
+</template>
+

+ 59 - 0
src/pages/audit/table/modal/index.vue

@@ -0,0 +1,59 @@
+<script setup lang='ts'>
+import {
+  LDNormalModal,
+  LDModalInput,
+  LDModalNumberInput,
+  LDModalDate,
+  LDModalSelect,
+} from '@components';
+import {useForm} from 'vee-validate';
+import {object, string, number} from 'zod';
+import {toTypedSchema} from '@vee-validate/zod';
+import {formatValidateError} from '@utils';
+import {useVModel} from '@vueuse/core';
+
+defineOptions({name: 'DemoModal'});
+
+const props = defineProps<{modelValue: boolean}>();
+const emits = defineEmits({'udpate:modelvalue': Boolean});
+const visible = useVModel(props, 'modelValue', emits);
+
+const {handleSubmit} = useForm({
+  initialValues: {
+    name: '',
+    age: void 0,
+    date: void 0,
+    type: '',
+  },
+  validationSchema: toTypedSchema(object({
+    name: string(formatValidateError('请输入姓名'))
+      .min(1, '请输入姓名'),
+    age: number(formatValidateError('请输入年龄')),
+    date: number(formatValidateError('请选择时间')),
+    type: string(formatValidateError('请选择类型'))
+      .min(1, '请选择类型'),
+  })),
+});
+
+const onSubmit = handleSubmit(function(e) {
+  console.log(e);
+});
+</script>
+
+<template>
+  <LDNormalModal
+    title="测试新增"
+    v-model="visible"
+    @submit="onSubmit"
+  >
+    <LDModalInput name="name" label="名称" />
+    <LDModalNumberInput name="age" label="年龄" />
+    <LDModalDate name="date" label="发货日期" />
+    <LDModalSelect
+      name="type"
+      label="类型"
+      :options="[{label: '1', value: '1'},{label: '2', value: '2'}]"
+    />
+  </LDNormalModal>
+</template>
+

+ 5 - 0
src/styles/global.css

@@ -32,3 +32,8 @@ img {
 [hidden] {
   display: none;
 }
+
+.table-wrapper,
+.table-content {
+  margin-top: var(--content-padding);
+}