Commit 036d3f05664afa75c2a5b44853de38c6a061e879

Authored by 刘汉宸
1 parent cc185744

feat: 新增选择器

examples/router/routes.js
@@ -22,6 +22,17 @@ const _guides = [ @@ -22,6 +22,17 @@ const _guides = [
22 22
23 const _components = [ 23 const _components = [
24 { 24 {
  25 + group: '基础组件',
  26 + children: [
  27 + {
  28 + path: 'select',
  29 + name: 'select',
  30 + meta: { title: 'Select 选择器' },
  31 + component: () => import('@/views/docs/component/select.md'),
  32 + },
  33 + ],
  34 + },
  35 + {
25 group: '业务组件', 36 group: '业务组件',
26 children: [ 37 children: [
27 { 38 {
examples/views/docs/component/select.md 0 → 100644
@@ -0,0 +1,461 @@ @@ -0,0 +1,461 @@
  1 +# Select 选择器
  2 +
  3 +可指定`url`或者`options`,生成远程加载或静态选择的选择器
  4 +
  5 +## 基础用法
  6 +
  7 +设置一个静态选择器
  8 +
  9 +<div class="code-snippet-box">
  10 +
  11 +::: snippet `options`设置选择器选项
  12 +
  13 +```html
  14 +<template>
  15 + <div>
  16 + <pre class="demo-model" v-if="model">{{ model }}</pre>
  17 + <z-select v-model="model" :options="options"></z-select>
  18 + </div>
  19 +</template>
  20 +
  21 +<script>
  22 +export default {
  23 + data() {
  24 + return {
  25 + model: '',
  26 + options: [
  27 + { name: '张三', code: 'zs' },
  28 + { name: '李四', code: 'ls' }
  29 + ],
  30 + }
  31 + }
  32 +}
  33 +</script>
  34 +```
  35 +
  36 +:::
  37 +
  38 +::: snippet 自定义选项
  39 +
  40 +```html
  41 +<template>
  42 + <div>
  43 + <pre class="demo-model" v-if="model">{{ model }}</pre>
  44 + <z-select v-model="model" :options="options" :labelFormat="labelFormat">
  45 + <template #default="{ item, value }">
  46 + <div class="custom-option" :class="{ checked: item.code === value }">
  47 + <span class="name">{{ item.name }}</span>
  48 + <el-tag class="code" type="success" size="mini">{{ item.code }}</el-tag>
  49 + </div>
  50 + </template>
  51 + <template #empty>
  52 + <el-alert title="无匹配数据" type="error" :closable="false" />
  53 + </template>
  54 + <template #prefix>
  55 + <i class="el-icon-search"></i>
  56 + </template>
  57 + </z-select>
  58 + </div>
  59 +</template>
  60 +
  61 +<script>
  62 +export default {
  63 + data() {
  64 + return {
  65 + model: '',
  66 + options: [
  67 + { name: '张三', code: 'zs' },
  68 + { name: '李四', code: 'ls' }
  69 + ],
  70 + }
  71 + },
  72 + methods: {
  73 + labelFormat(item) {
  74 + return `[${item.name}]`;
  75 + }
  76 + }
  77 +}
  78 +</script>
  79 +
  80 +<style>
  81 +.custom-option {
  82 + display: flex;
  83 + align-items: center;
  84 + justify-content: space-between;
  85 +}
  86 +.custom-option.checked {
  87 + color: deepskyblue;
  88 +}
  89 +.name {
  90 + font-weight: bold;
  91 +}
  92 +</style>
  93 +```
  94 +
  95 +:::
  96 +
  97 +</div>
  98 +
  99 +## 设置多选
  100 +
  101 +支持多选
  102 +
  103 +::: snippet `multiple`设置多选
  104 +
  105 +```html
  106 +<template>
  107 + <div>
  108 + <pre class="demo-model" v-if="model.length > 0">{{ model }}</pre>
  109 + <z-select v-model="model" :options="options" multiple @change="onChange"></z-select>
  110 + </div>
  111 +</template>
  112 +
  113 +<script>
  114 +export default {
  115 + data() {
  116 + return {
  117 + model: [],
  118 + options: [
  119 + { name: '张三', code: 'zs' },
  120 + { name: '李四', code: 'ls' },
  121 + { name: '王五', code: 'ww', disabled: true }
  122 + ],
  123 + }
  124 + },
  125 + methods: {
  126 + onChange(value, selected) {
  127 + console.log(JSON.parse(JSON.stringify(value)), JSON.parse(JSON.stringify(selected)));
  128 + }
  129 + }
  130 +}
  131 +</script>
  132 +```
  133 +
  134 +:::
  135 +
  136 +## 绑定对象
  137 +
  138 +可以直接以对象作为绑定值
  139 +
  140 +<div class="code-snippet-box">
  141 +
  142 +::: snippet `raw`设置为原值模式,默认以`valueKey`为主键
  143 +
  144 +```html
  145 +<template>
  146 + <div>
  147 + <pre class="demo-model" v-if="model">{{ model }}</pre>
  148 + <z-select v-model="model" :options="options" raw></z-select>
  149 + </div>
  150 +</template>
  151 +
  152 +<script>
  153 +export default {
  154 + data() {
  155 + return {
  156 + model: [],
  157 + options: [
  158 + { name: '张三', code: 'zs' },
  159 + { name: '李四', code: 'ls' },
  160 + ],
  161 + }
  162 + }
  163 +}
  164 +</script>
  165 +```
  166 +
  167 +:::
  168 +
  169 +::: snippet 支持多选
  170 +
  171 +```html
  172 +<template>
  173 + <div>
  174 + <pre class="demo-model" v-if="model.length > 0">{{ model }}</pre>
  175 + <z-select v-model="model" :options="options" multiple raw></z-select>
  176 + </div>
  177 +</template>
  178 +
  179 +<script>
  180 +export default {
  181 + data() {
  182 + return {
  183 + model: [],
  184 + options: [
  185 + { name: '张三', code: 'zs' },
  186 + { name: '李四', code: 'ls' },
  187 + ],
  188 + }
  189 + }
  190 +}
  191 +</script>
  192 +```
  193 +
  194 +:::
  195 +
  196 +</div>
  197 +
  198 +## 远程加载
  199 +
  200 +可以通过设置`queryApi`自定义查询接口,也可以设置`url`及`http`进行内置格式的查询,其中`http`可在组件库全局配置
  201 +
  202 +<div class="code-snippet-box">
  203 +
  204 +::: snippet `url`设置加载地址,`http`设置请求库
  205 +
  206 +```html
  207 +<template>
  208 + <div>
  209 + <pre class="demo-model" v-if="model">{{ model }}</pre>
  210 + <z-select v-model="model" url="/user/select" :http="$http"></z-select>
  211 + </div>
  212 +</template>
  213 +
  214 +<script>
  215 +export default {
  216 + data() {
  217 + return {
  218 + model: '',
  219 + }
  220 + },
  221 +}
  222 +</script>
  223 +```
  224 +
  225 +:::
  226 +
  227 +::: snippet `queryApi`自定义查询接口
  228 +
  229 +```html
  230 +<template>
  231 + <div>
  232 + <pre class="demo-model" v-if="model">{{ model }}</pre>
  233 + <z-select v-model="model" :queryApi="queryAPI"></z-select>
  234 + </div>
  235 +</template>
  236 +
  237 +<script>
  238 +export default {
  239 + data() {
  240 + return {
  241 + model: '',
  242 + }
  243 + },
  244 + methods: {
  245 + queryAPI(val) {
  246 + console.log(val);
  247 + return new Promise(resolve => {
  248 + setTimeout(() => {
  249 + resolve([
  250 + { name: '王五', code: 'ww' },
  251 + { name: '赵六', code: 'zl' },
  252 + ]);
  253 + }, 1000);
  254 + });
  255 + },
  256 + }
  257 +}
  258 +</script>
  259 +```
  260 +
  261 +:::
  262 +
  263 +</div>
  264 +
  265 +<div class="code-snippet-box">
  266 +
  267 +::: snippet `auto`设置自动加载一次数据
  268 +
  269 +```html
  270 +<template>
  271 + <div>
  272 + <pre class="demo-model" v-if="model.length">{{ model }}</pre>
  273 + <z-select v-model="model" :queryApi="queryAPI" :options="options" multiple auto></z-select>
  274 + </div>
  275 +</template>
  276 +
  277 +<script>
  278 +export default {
  279 + data() {
  280 + return {
  281 + model: ['wq', 'cs'],
  282 + options: [
  283 + { name: '王七', code: 'wq' },
  284 + { name: '陈拾', code: 'cs' },
  285 + ]
  286 + }
  287 + },
  288 + methods: {
  289 + queryAPI(val) {
  290 + return new Promise(resolve => {
  291 + setTimeout(() => {
  292 + resolve([
  293 + { name: '王五', code: 'ww' },
  294 + { name: '赵六', code: 'zl' },
  295 + ]);
  296 + }, 1000);
  297 + });
  298 + },
  299 + }
  300 +}
  301 +</script>
  302 +```
  303 +
  304 +:::
  305 +
  306 +::: snippet `update`设置每次打开下拉框都保持最新数据
  307 +
  308 +```html
  309 +<template>
  310 + <div>
  311 + <pre class="demo-model" v-if="model.length">{{ model }}</pre>
  312 + <z-select v-model="model" :queryApi="queryAPI" :options="options" multiple update></z-select>
  313 + </div>
  314 +</template>
  315 +
  316 +<script>
  317 +export default {
  318 + data() {
  319 + return {
  320 + model: ['wq', 'cs'],
  321 + options: [
  322 + { name: '王七', code: 'wq' },
  323 + { name: '陈拾', code: 'cs' },
  324 + ]
  325 + }
  326 + },
  327 + methods: {
  328 + queryAPI(val) {
  329 + return new Promise(resolve => {
  330 + setTimeout(() => {
  331 + resolve([
  332 + { name: '王五', code: 'ww' },
  333 + { name: '赵六', code: 'zl' },
  334 + ]);
  335 + }, 1000);
  336 + });
  337 + },
  338 + }
  339 +}
  340 +</script>
  341 +```
  342 +
  343 +:::
  344 +
  345 +</div>
  346 +
  347 +<div class="code-snippet-box">
  348 +
  349 +::: snippet 单选时,当前选中项不包含在远程数据源中,可以通过`options`设置默认选项
  350 +
  351 +```html
  352 +<template>
  353 + <div>
  354 + <pre class="demo-model" v-if="model">{{ model }}</pre>
  355 + <z-select v-model="model" :queryApi="queryAPI" :options="options" auto></z-select>
  356 + </div>
  357 +</template>
  358 +
  359 +<script>
  360 +export default {
  361 + data() {
  362 + return {
  363 + model: 'wq',
  364 + options: [
  365 + { name: '王七', code: 'wq' }
  366 + ]
  367 + }
  368 + },
  369 + methods: {
  370 + queryAPI(val) {
  371 + return new Promise(resolve => {
  372 + this.label = '';
  373 + setTimeout(() => {
  374 + resolve([
  375 + { name: '王五', code: 'ww' },
  376 + { name: '赵六', code: 'zl' },
  377 + ]);
  378 + }, 1000);
  379 + });
  380 + },
  381 + }
  382 +}
  383 +</script>
  384 +```
  385 +
  386 +:::
  387 +
  388 +::: snippet 多选时,当前选中项不包含在远程数据源中,可以通过`options`设置默认选项
  389 +
  390 +```html
  391 +<template>
  392 + <div>
  393 + <pre class="demo-model" v-if="model.length">{{ model }}</pre>
  394 + <z-select v-model="model" :queryApi="queryAPI" :options="options" multiple auto></z-select>
  395 + </div>
  396 +</template>
  397 +
  398 +<script>
  399 +export default {
  400 + data() {
  401 + return {
  402 + model: ['wq', 'cs'],
  403 + options: [
  404 + { name: '王七', code: 'wq' },
  405 + { name: '陈拾', code: 'cs' },
  406 + ]
  407 + }
  408 + },
  409 + methods: {
  410 + queryAPI(val) {
  411 + return new Promise(resolve => {
  412 + setTimeout(() => {
  413 + resolve([
  414 + { name: '王五', code: 'ww' },
  415 + { name: '赵六', code: 'zl' },
  416 + ]);
  417 + }, 1000);
  418 + });
  419 + },
  420 + }
  421 +}
  422 +</script>
  423 +```
  424 +
  425 +:::
  426 +
  427 +</div>
  428 +
  429 +## API
  430 +
  431 +## Attribute 属性
  432 +
  433 +参数|说明|类型|可选值|默认值
  434 +-|-|-|-|-
  435 +value | 值 | String, Number, Boolean, Object, Array | - | -
  436 +placeholder | 占位符 | String | - | 请选择
  437 +options | 选项列表 | Array | - | []
  438 +labelFormat | 标签格式化 | Function | - | -
  439 +labelKey | 标签字段名 | String | - | name
  440 +valueKey | 值字段名 | String | - | code
  441 +searchKey | 搜索字段名 | String | - | query
  442 +size | 大小 | String | mini、small、large | mini
  443 +multiple | 多选 | Boolean | - | false
  444 +disabled | 禁用 | Boolean | - | false
  445 +clearable | 可清除 | Boolean | - | true
  446 +filterable | 可搜索 | Boolean | - | true
  447 +reserveKeyword | 保留当前的搜索关键词 | Boolean | - | true
  448 +selectProps | Element Select组件参数 | Object | - | -
  449 +raw | 是否绑定原始对象 | Boolean | - | false
  450 +url | 远程搜索URL | String | - | -
  451 +http | HTTP请求库 | Function | - | -
  452 +queryApi | 自定义接口 | Function | - | -
  453 +triggerSize | 触发远程搜索的字段长度 | Number | - | 0
  454 +auto | 初始化时自动查询数据 | Boolean | - | false
  455 +update | 点开下拉框时更新数据 | Boolean | - | false
  456 +
  457 +## Events 事件
  458 +
  459 +事件名称|说明|回调参数
  460 +-|-|-
  461 +change | 改变选中 | 值,选中项数据
0 \ No newline at end of file 462 \ No newline at end of file
packages/scheme/index.vue
@@ -216,7 +216,7 @@ export default { @@ -216,7 +216,7 @@ export default {
216 auto: Boolean, 216 auto: Boolean,
217 realSelection: Boolean, 217 realSelection: Boolean,
218 url: String, // 请求地址 218 url: String, // 请求地址
219 - http: [Function, Promise], // http库 219 + http: Function, // http库
220 alias: Object, // 别名配置 220 alias: Object, // 别名配置
221 }, 221 },
222 data() { 222 data() {
packages/select/index.vue 0 → 100644
@@ -0,0 +1,269 @@ @@ -0,0 +1,269 @@
  1 +<template>
  2 + <el-select
  3 + class="zee__select"
  4 + :disabled="disabled"
  5 + :value-key="valueKey"
  6 + :filterable="filterable"
  7 + :remote="remote"
  8 + :reserve-keyword="reserveKeyword"
  9 + :clearable="clearable"
  10 + :placeholder="placeholder"
  11 + :remote-method="remoteMethod"
  12 + :loading="loading"
  13 + :size="size"
  14 + :multiple="multiple"
  15 + v-model="model"
  16 + v-on="bindEvents"
  17 + v-bind="selectProps"
  18 + >
  19 + <el-option v-for="item in optionsCurrent" :key="item.id" :label="labelFormat ? labelFormat(item) : item[labelKey]" :value="raw ? item : item[valueKey]" :disabled="item.disabled">
  20 + <slot :item="item" :value="model"></slot>
  21 + </el-option>
  22 + <slot name="empty" slot="empty"></slot>
  23 + <template slot="prefix">
  24 + <i v-if="initing" class="el-icon-loading"></i>
  25 + <slot v-else name="prefix"></slot>
  26 + </template>
  27 + </el-select>
  28 +</template>
  29 +
  30 +<script>
  31 +export default {
  32 + name: 'Select',
  33 + props: {
  34 + value: [String, Number, Boolean, Object, Array],
  35 + // 占位符
  36 + placeholder: {
  37 + type: String,
  38 + default: '请选择',
  39 + },
  40 + // 选项数组
  41 + options: {
  42 + type: Array,
  43 + default: () => [],
  44 + },
  45 + // 选中Label格式化方法
  46 + labelFormat: Function,
  47 + labelKey: {
  48 + type: String,
  49 + default: 'name',
  50 + },
  51 + valueKey: {
  52 + type: String,
  53 + default: 'code',
  54 + },
  55 + searchKey: {
  56 + type: String,
  57 + default: 'query',
  58 + },
  59 + size: {
  60 + type: String,
  61 + default: 'mini',
  62 + },
  63 + multiple: Boolean,
  64 + disabled: Boolean,
  65 + clearable: {
  66 + type: Boolean,
  67 + default: true,
  68 + },
  69 + filterable: {
  70 + type: Boolean,
  71 + default: true,
  72 + },
  73 + reserveKeyword: {
  74 + type: Boolean,
  75 + default: true,
  76 + },
  77 + selectProps: Object,
  78 + // 保持原值,即绑定值为对象
  79 + raw: Boolean,
  80 + // 远程搜索URL
  81 + url: String,
  82 + // HTTP请求库
  83 + http: Function,
  84 + // 自定义接口
  85 + queryApi: Function,
  86 + // 触发远程搜索的字段长度
  87 + triggerSize: {
  88 + type: Number,
  89 + default: 0,
  90 + },
  91 + auto: Boolean,
  92 + update: Boolean,
  93 + },
  94 + data() {
  95 + return {
  96 + model: this.value,
  97 + optionsDataSource: this.fixOptions(this.options),
  98 + optionsCurrent: this.fixOptions(this.options),
  99 + loading: false,
  100 + initing: false,
  101 + };
  102 + },
  103 + created() {
  104 + if (this.remote && this.auto) {
  105 + this.initing = true;
  106 + this.remoteMethod();
  107 + }
  108 + },
  109 + watch: {
  110 + value(val) {
  111 + this.model = val;
  112 + },
  113 + },
  114 + computed: {
  115 + request() {
  116 + return this.http || this.zHttp;
  117 + },
  118 + remote() {
  119 + return Boolean(this.queryApi || (this.url && (this.http || this.zHttp)));
  120 + },
  121 + bindEvents() {
  122 + let _events = {};
  123 + Object.keys(this.$listeners || {}).forEach(key => {
  124 + // 非绑定对象的情况下,通过change事件向上emit出当前选中项
  125 + if (key === 'change' && !this.raw) {
  126 + _events[key] = value => {
  127 + this.$emit(
  128 + key,
  129 + value,
  130 + this.optionsCurrent.reduce((result, item) => {
  131 + if (value.includes(item[this.valueKey])) {
  132 + result.push(item);
  133 + }
  134 + return result;
  135 + }, []),
  136 + );
  137 + };
  138 + } else {
  139 + _events[key] = e => {
  140 + this.$emit(key, e);
  141 + };
  142 + }
  143 + });
  144 + return {
  145 + ..._events,
  146 + 'visible-change': show => {
  147 + if (this.remote && this.update && show) {
  148 + this.remoteMethod();
  149 + }
  150 + _events['visible-change'] && _events['visible-change'](show);
  151 + },
  152 + };
  153 + },
  154 + },
  155 + methods: {
  156 + // 修复当前数据源包含当前选中项
  157 + fixOptions(list) {
  158 + if (!this.value || (this.value instanceof Array && this.value.length === 0) || (this.value instanceof Object && Object.keys(this.value).length === 0)) {
  159 + return list;
  160 + }
  161 + if (this.raw) {
  162 + // 绑定为对象值时修复默认项
  163 + let fixOptions = [];
  164 + let defaultOptions = [];
  165 + if (this.multiple) {
  166 + fixOptions = [...this.value];
  167 + this.options.forEach(item => {
  168 + if (this.value.find(i => i[this.valueKey] === item[this.valueKey])) {
  169 + defaultOptions.push(item);
  170 + }
  171 + });
  172 + } else {
  173 + fixOptions = [this.value];
  174 + this.options.forEach(item => {
  175 + if (item[this.valueKey] === this.value[this.valueKey]) {
  176 + defaultOptions.push(item);
  177 + }
  178 + });
  179 + }
  180 + let hash = {};
  181 + const options = [...fixOptions, ...defaultOptions, ...list].reduce((result, item) => {
  182 + if (!hash[item[this.valueKey]]) {
  183 + // 如果当前元素的key值没有在hash对象里,则可放入最终结果数组
  184 + hash[item[this.valueKey]] = true; // 把当前元素key值添加到hash对象
  185 + result.push(item); // 把当前元素放入结果数组
  186 + }
  187 + return result; // 返回结果数组
  188 + }, []);
  189 + return options;
  190 + } else {
  191 + // 绑定为非对象值时修复默认项
  192 + let defaultOptions = [];
  193 + if (this.multiple) {
  194 + this.options.forEach(item => {
  195 + if (this.value.find(i => i === item[this.valueKey])) {
  196 + defaultOptions.push(item);
  197 + }
  198 + });
  199 + } else {
  200 + const targetOption = this.options.find(item => item[this.valueKey] === this.value);
  201 + if (targetOption) {
  202 + defaultOptions = [targetOption];
  203 + }
  204 + }
  205 + let hash = {};
  206 + const options = [...defaultOptions, ...list].reduce((result, item) => {
  207 + if (!hash[item[this.valueKey]]) {
  208 + // 如果当前元素的key值没有在hash对象里,则可放入最终结果数组
  209 + hash[item[this.valueKey]] = true; // 把当前元素key值添加到hash对象
  210 + result.push(item); // 把当前元素放入结果数组
  211 + }
  212 + return result; // 返回结果数组
  213 + }, []);
  214 + return options;
  215 + }
  216 + },
  217 + // 远程加载
  218 + remoteMethod(val = '') {
  219 + const searchText = val.trim();
  220 + if (searchText.length >= this.triggerSize) {
  221 + this.loading = true;
  222 + let requestPrimise;
  223 + if (this.queryApi) {
  224 + requestPrimise = this.queryApi(searchText);
  225 + } else {
  226 + requestPrimise = this.request({
  227 + url: this.url,
  228 + method: 'get',
  229 + params: searchText ? { [this.searchKey]: searchText } : {},
  230 + });
  231 + }
  232 + requestPrimise
  233 + .then(list => {
  234 + const options = this.fixOptions(list);
  235 + this.optionsDataSource = options;
  236 + this.optionsCurrent = options;
  237 + // 获取到选项数据源时刷新绑定显示值
  238 + this.model = undefined;
  239 + this.$nextTick(() => {
  240 + this.model = this.value;
  241 + });
  242 + })
  243 + .finally(() => {
  244 + this.loading = false;
  245 + this.initing = false;
  246 + });
  247 + } else {
  248 + this.optionsCurrent = this.optionsDataSource.filter(item => {
  249 + return item[this.labelKey].includes(val);
  250 + });
  251 + }
  252 + },
  253 + },
  254 +};
  255 +</script>
  256 +
  257 +<style lang="scss">
  258 +.zee__select {
  259 + .el-input__prefix {
  260 + height: 100%;
  261 + display: flex;
  262 + align-items: center;
  263 + justify-content: center;
  264 + .el-icon-loading {
  265 + font-size: 16px;
  266 + }
  267 + }
  268 +}
  269 +</style>