xyhxx 2 gadi atpakaļ
revīzija
23915a733a
16 mainītis faili ar 1836 papildinājumiem un 0 dzēšanām
  1. 24 0
      .gitignore
  2. 3 0
      .vscode/extensions.json
  3. 18 0
      README.md
  4. 13 0
      index.html
  5. 27 0
      package.json
  6. 1377 0
      pnpm-lock.yaml
  7. 1 0
      public/vite.svg
  8. 78 0
      src/App.vue
  9. 1 0
      src/assets/vue.svg
  10. 178 0
      src/components/CustomSelect.tsx
  11. 15 0
      src/main.ts
  12. 56 0
      src/style.css
  13. 1 0
      src/vite-env.d.ts
  14. 26 0
      tsconfig.json
  15. 10 0
      tsconfig.node.json
  16. 8 0
      vite.config.ts

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}

+ 18 - 0
README.md

@@ -0,0 +1,18 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+## Recommended IDE Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Type Support For `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
+
+If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
+
+1. Disable the built-in TypeScript Extension
+   1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
+   2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
+2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite + Vue + TS</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 27 - 0
package.json

@@ -0,0 +1,27 @@
+{
+  "name": "customer-demo",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@tanstack/vue-query": "^4.29.11",
+    "lodash-es": "^4.17.21",
+    "naive-ui": "^2.34.4",
+    "vee-validate": "^4.9.6",
+    "vue": "^3.3.4",
+    "vue-i18n": "^9.2.2"
+  },
+  "devDependencies": {
+    "@types/lodash-es": "^4.17.7",
+    "@vitejs/plugin-vue": "^4.2.3",
+    "@vitejs/plugin-vue-jsx": "^3.0.1",
+    "typescript": "^5.1.3",
+    "vite": "^4.3.9",
+    "vue-tsc": "^1.6.5"
+  }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1377 - 0
pnpm-lock.yaml


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
public/vite.svg


+ 78 - 0
src/App.vue

@@ -0,0 +1,78 @@
+<script setup lang="ts">
+import {NMessageProvider} from 'naive-ui';
+import { useForm } from 'vee-validate';
+import Select from './components/CustomSelect';
+
+useForm({
+  initialValues: {
+    name: '',
+  }
+})
+
+
+</script>
+
+<template>
+  <NMessageProvider>
+    <Select  label="名称" name="name" />
+  </NMessageProvider>
+</template>
+
+<style>
+.ld-modal-field-wrapper {
+  width: 100%;
+  max-width: 400px;
+  margin: 0 auto;
+}
+
+.ld-modal-field-group {
+  display: flex;
+  gap: 20px;
+  align-items: flex-start;
+
+  & label {
+    min-width: 5em;
+    height: 34px;
+    font-size: var(--content-font-size);
+    line-height: 34px;
+
+    &::before {
+      margin-right: 4px;
+      color: red;
+      content: '*';
+    }
+  }
+
+  & label.optional {
+    &::before {
+      visibility: hidden;
+    }
+  }
+
+  & .n-input,
+  .n-select {
+    flex: 1;
+    border: none;
+    border-bottom: 1px solid #eee;
+  }
+
+  & .n-input-number,
+  .n-date-picker {
+    flex: 1;
+  }
+
+  /* stylelint-disable-next-line selector-class-pattern */
+  & .n-input.n-input--textarea {
+    border: 1px solid #eee;
+    border-radius: 4px;
+  }
+}
+
+.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;
+}
+</style>

+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 178 - 0
src/components/CustomSelect.tsx

@@ -0,0 +1,178 @@
+import '../style.css';
+
+import {computed, defineComponent, ref} from 'vue';
+import {selectProps, NSelect, NEmpty, NButton, useMessage} from 'naive-ui';
+import {useField} from 'vee-validate';
+import {debounce} from 'lodash-es';
+import { useI18n } from 'vue-i18n';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
+
+async function mockGetList() {
+  return await new Promise<{msg: '200', data: {id: string, name:string}[]}>(function(res){
+    setTimeout(function() {
+      res({
+        msg: '200',
+        data: [{id: '1', name: 'simon'}, {id: '2', name: 'david'}]
+      });
+    }, 2000);
+  })
+}
+
+async function mockAdd(name: string){
+  return await new Promise<{msg: '200', data: string}>(function(res){
+    setTimeout(function() {
+      res({
+        msg: '200',
+        data: '12',
+      });
+    }, 2000);
+  })
+}
+
+export default defineComponent({
+  name: 'LDModalInput',
+  props: {
+    name: {
+      type: String,
+      required: true,
+    },
+    label: {
+      type: String,
+      required: true,
+    },
+    optional: Boolean,
+    multiple: selectProps.multiple,
+  },
+  setup(props) {
+    const {value, setValue, errorMessage} = useField<string | string[]>(
+      props.name,
+      void 0,
+      {validateOnMount: false, validateOnValueUpdate: false},
+    );
+
+    const showOptions = ref(false);
+
+    const {isFetching, data} = useQuery({
+      queryKey: [mockGetList.name],
+      async queryFn () {
+        const data = await mockGetList();
+        
+        if(data.msg === '200') return data.data.map(function({id, name}) {
+          return {label: name, value: id};
+        });
+
+        return [];
+      },
+      initialData: [],
+    })
+    
+    const client = useQueryClient();
+    const {isLoading, mutate} = useMutation({
+      mutationFn: mockAdd,
+      onSuccess(data, variables){
+        if(data.msg === '200'){
+          const id = data.data;
+          const name = variables;
+          
+          client.setQueryData<{label: string, value: string}[]>(
+            [mockGetList.name],
+            function(data) {
+              return [...(data ?? []), {label: name, value: id}];
+            }
+          );
+
+          setValue(id);
+          showOptions.value = false;
+        }
+      }
+    });
+
+    const message = useMessage();
+    function onAdd() {
+      if(!filterValue.value) return message.warning('请输入客户名称');
+
+      mutate(filterValue.value);
+    }
+    const filterValue = ref<string>('');
+    const filterOptions = computed(function() {
+      const options = data.value;
+
+      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
+      && !isLoading.value
+      && (filterValue.value = '');
+    }
+
+    const {t} = useI18n();
+
+    function onUpdateShow(state: boolean) {
+      if(isLoading.value) return;
+
+      showOptions.value = state;
+    }
+
+
+    return () => (
+      <div class="ld-modal-field-wrapper">
+        <div class="ld-modal-field-group">
+          <label class={props?.optional ? 'optional' : ''}>{props.label}</label>
+          <NSelect
+            show={showOptions.value}
+            multiple={props.multiple}
+            bordered={false}
+            filterable
+            remote
+            loading={isFetching.value}
+            onClear={() => {
+              setValue('');
+              onDefaultSearch('');
+            }}
+            options={filterOptions.value}
+            // 防止空字符串没有placeholder
+            value={value.value || null}
+            onUpdateValue={setValue}
+            onSearch={onDefaultSearch}
+            onBlur={onBlur}
+            on-update:show={onUpdateShow}
+          >
+            {{empty() {
+              return <NEmpty description="未查到客户,点击新增按钮新增客户信息">
+                {{extra() {
+                  return <NButton 
+                    disabled={isLoading.value} 
+                    loading={isLoading.value}
+                    onClick={onAdd}
+                  >
+                    新增
+                  </NButton>
+                }}}
+              </NEmpty>
+            }}}
+          </NSelect>
+        </div>
+        <p class="ld-modal-filed-error">{errorMessage.value && t(errorMessage.value)}</p>
+      </div>
+    );
+  },
+});

+ 15 - 0
src/main.ts

@@ -0,0 +1,15 @@
+import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+import {VueQueryPlugin} from '@tanstack/vue-query';
+import {createI18n} from 'vue-i18n';
+
+const i18n = createI18n({
+  locale: 'zh',
+  messages: {},
+  legacy: false,
+  fallbackLocale: 'zh',
+  missingWarn: true,
+});
+
+createApp(App).use(i18n).use(VueQueryPlugin).mount('#app')

+ 56 - 0
src/style.css

@@ -0,0 +1,56 @@
+.ld-modal-field-wrapper {
+  width: 100%;
+  max-width: 400px;
+  margin: 0 auto;
+}
+
+.ld-modal-field-group {
+  display: flex;
+  gap: 20px;
+  align-items: flex-start;
+
+  & label {
+    min-width: 5em;
+    height: 34px;
+    font-size: var(--content-font-size);
+    line-height: 34px;
+
+    &::before {
+      margin-right: 4px;
+      color: red;
+      content: '*';
+    }
+  }
+
+  & label.optional {
+    &::before {
+      visibility: hidden;
+    }
+  }
+
+  & .n-input,
+  .n-select {
+    flex: 1;
+    border: none;
+    border-bottom: 1px solid #eee;
+  }
+
+  & .n-input-number,
+  .n-date-picker {
+    flex: 1;
+  }
+
+  /* stylelint-disable-next-line selector-class-pattern */
+  & .n-input.n-input--textarea {
+    border: 1px solid #eee;
+    border-radius: 4px;
+  }
+}
+
+.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;
+}

+ 1 - 0
src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 26 - 0
tsconfig.json

@@ -0,0 +1,26 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+    "noImplicitAny": false,
+    "allowJs": true,
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "preserve",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 10 - 0
tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 8 - 0
vite.config.ts

@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import jsxPlugin from '@vitejs/plugin-vue-jsx';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue(), jsxPlugin()],
+})