diff --git a/src/App.vue b/src/App.vue index 9f4f7a8..ec9f4b2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,13 +6,14 @@ </a-config-provider> </template> <script> - import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN' + // import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN' + import en_GB from 'ant-design-vue/lib/locale-provider/en_GB' import enquireScreen from '@/utils/device' export default { data () { return { - locale: zhCN, + locale: en_GB, } }, created () { diff --git a/src/assets/images/global/search-blue.png b/src/assets/images/global/search-blue.png new file mode 100644 index 0000000..2a96e76 Binary files /dev/null and b/src/assets/images/global/search-blue.png differ diff --git a/src/components/CustomPopoverSearch/index.vue b/src/components/CustomPopoverSearch/index.vue new file mode 100644 index 0000000..d40471a --- /dev/null +++ b/src/components/CustomPopoverSearch/index.vue @@ -0,0 +1,95 @@ +<template> + <div class="popover-search"> + <a-popover v-model="visible" trigger="click" placement="bottom" :overlayStyle="{ width: width + 'px' }"> + <a-input ref="inputRef" v-model="keyword" @click.stop=""> + <template slot="suffix"> + <img class="popover-search-btn" src="@/assets/images/global/search-blue.png" @click.stop="onSearch" /> + </template> + </a-input> + <template slot="content"> + <template v-if="isLoading"> + <a-spin :spinning="isLoading"></a-spin> + </template> + <template v-else> + <div + class="popover-search-item" + v-for="option in innerOptions" + :key="option.value" + @click="option.checked = !option.checked" + > + <a-checkbox v-model="option.checked"></a-checkbox> + <span class="popover-search-item-inner">{{ option.label }}</span> + </div> + </template> + </template> + </a-popover> + </div> +</template> +<script> +import { cloneDeep } from 'lodash' +export default { + props: { + remoteMethod: { + type: Function, + required: true + }, + options: { + type: Array, + required: true + }, + value: { + type: Array + } + }, + data() { + return { + keyword: '', + visible: false, + width: 0, + isLoading: true, + innerOptions: [] + } + }, + methods: { + async onSearch() { + const style = window.getComputedStyle(this.$refs.inputRef.$el) + this.width = parseInt(style.width) + this.visible = true + if (this.remoteMethod && typeof this.remoteMethod == 'function') { + this.isLoading = true + await this.remoteMethod() + this.isLoading = false + } + } + }, + watch: { + options: { + immediate: true, + handler() { + const options = cloneDeep(this.options) + options.forEach(item => (item.checked = false)) + this.innerOptions = options + } + } + } +} +</script> +<style lang="less" scoped> +.popover-search { + &-btn { + cursor: pointer; + } + &-item { + height: 30px; + cursor: pointer; + padding: 4px 14px; + user-select: none; + &:hover { + background-color: #055565; + } + &-inner { + margin-left: 5px; + } + } +} +</style> diff --git a/src/components/CustomSelect/index.vue b/src/components/CustomSelect/index.vue index b73a36c..39efd14 100644 --- a/src/components/CustomSelect/index.vue +++ b/src/components/CustomSelect/index.vue @@ -28,6 +28,7 @@ export default { </script> <style lang="less" scoped> .ant-select { + width: 100%; .ant-select-arrow-icon { transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); } diff --git a/src/style.less b/src/style.less index ba1c7fb..c1c0951 100644 --- a/src/style.less +++ b/src/style.less @@ -194,7 +194,7 @@ body { &-content { margin-top: 10px; height: 98px; - background-color: rgba(140, 255, 229, .1); + background-color: rgba(140, 255, 229, 0.1); &::-webkit-scrollbar { width: 4px !important; } @@ -216,7 +216,7 @@ body { &-last-month-cell, &-next-month-btn-day { .ant-fullcalendar-value { - color: rgba(173, 230, 238, .1) !important; + color: rgba(173, 230, 238, 0.1) !important; } } @@ -439,13 +439,34 @@ body { // 树形结构 .ant-tree { &-checkbox-inner { - background-color: #03353f; - border-color: #0a544e; + background-color: #03353f !important; + border-color: #0a544e !important; border-radius: 0; } &-node-content-wrapper { background-color: transparent !important; } + + &-treenode-disabled { + .ant-tree-checkbox-inner { + border-color: #0a544e !important; + } + .ant-tree-checkbox-disabled { + .ant-tree-checkbox-inner { + &::after { + border-color: rgba(255, 255, 255, .2) !important; + } + } + } + .ant-tree-title { + color: #fff !important; + } + } + &-node-selected { + .ant-tree-title { + color: #0cebc9; + } + } } // 按钮 @@ -613,7 +634,9 @@ body { .ant-message { &-notice { &-content { + border: 1px solid @formInputBorderColor; background-color: @modalBg; + border-radius: 0; } } } @@ -656,7 +679,14 @@ body { .ant-popover { &-inner { - background-color: @modalBg; + background-color: #03353f; + &-content { + padding: 0; + } + } + &-arrow { + border-left-color: #03353f !important; + border-top-color: #03353f !important; } } @@ -677,6 +707,9 @@ body { border-radius: 0; border-bottom-color: #224852; } + &-body { + overflow: auto; + } } &-operation { .ant-btn { @@ -692,6 +725,9 @@ body { } } +.ant-divider { + background-color: @formInputBorderColor; +} #userLayout { .container { @@ -721,4 +757,4 @@ body { ::-webkit-scrollbar-corner { background-color: #15494c; -} \ No newline at end of file +} diff --git a/src/views/system/Scheduling.vue b/src/views/system/Scheduling.vue index 87739a7..5566ff3 100644 --- a/src/views/system/Scheduling.vue +++ b/src/views/system/Scheduling.vue @@ -48,7 +48,7 @@ <a-col :span="6"> <a-form-model class="search-form-form"> <a-form-model-item label="Month" style="marign-bottom: 0"> - <a-month-picker v-model="currentMonth"></a-month-picker> + <a-month-picker v-model="currentMonth" :allow-clear="false"></a-month-picker> </a-form-model-item> </a-form-model> </a-col> @@ -89,46 +89,79 @@ <!-- 增加/编辑排班弹窗 --> <custom-modal :title="isAdd ? 'Add' : 'Edit'" :width="845" v-model="visible" :okHandler="submit"> - <a-transfer - :data-source="[{ key: '1', title: '标题', description: '描述' }]" - :render="item => item.title" - :operations="['Assign', 'Remove']" - :titles="['Particulate Station', 'Roster personnel']" - :show-select-all="false" - > - <template slot="children" slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect } }"> - <a-tree - v-if="direction === 'left'" - blockNode - checkable - checkStrictly - defaultExpandAll - :checkedKeys="[...selectedKeys, ...targetKeys]" - :treeData="treeData" - @check=" - (_, props) => { - onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect) - } - " - @select=" - (_, props) => { - onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect) - } - " + <div class="account-assign"> + <a-transfer + :data-source="stationList" + :target-keys="targetKeys" + :render="item => item.title" + :operations="['Assign', 'Remove']" + :titles="['Particulate Station', 'Roster personnel']" + :show-select-all="false" + @change="onChange" + > + <template slot="children" slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect } }"> + <!-- 左侧穿梭框中的树 --> + <a-tree + v-if="direction === 'left'" + blockNode + checkable + checkStrictly + defaultExpandAll + :checkedKeys="[...selectedKeys, ...targetKeys]" + :treeData="treeData" + @check=" + (_, props) => { + onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect) + } + " + @select=" + (_, props) => { + onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect) + } + " + /> + <!-- 右侧穿梭框中的树 --> + <a-tree + v-if="direction === 'right'" + blockNode + checkStrictly + defaultExpandAll + :treeData="accountTreeData" + :selectedKeys.sync="rightAccountChildSelectedKeys" + @select=" + (_, props) => { + onSelectAccount(_, props, itemSelect) + } + " + /> + </template> + </a-transfer> + + <!-- 穿梭框右上方搜索 --> + <div class="account-search"> + <label>User Name</label> + <!-- <a-select + :options="accountList" + mode="multiple" + show-search + :filter-option="filterOption" + style="width: 190px" + v-model="selectedAccount" > - <a-icon slot="switcherIcon" type="down" /> - </a-tree> - <a-tree - v-if="direction === 'right'" - blockNode - checkable - checkStrictly - defaultExpandAll - :checkedKeys="[...selectedKeys, ...targetKeys]" - :treeData="treeData" - /> - </template> - </a-transfer> + <div slot="dropdownRender" slot-scope="menu"> + <v-nodes :vnodes="menu" /> + <div class="account-add" @click="onAddToList()"> + <a>Add</a> + </div> + </div> + <a-select-option v-for="(account, index) of accountList" :key="index" :value="index"> + {{ account.title }} + </a-select-option> + </a-select> --> + <custom-popover-search :options="accountList" :remote-method="getAccountList"></custom-popover-search> + </div> + <!-- 穿梭框右上方搜索结束 --> + </div> </custom-modal> <!-- 增加/编辑排班弹窗结束 --> </div> @@ -136,34 +169,97 @@ <script> import FormMixin from '@/mixins/FormMixin' import moment from 'moment' +import { cloneDeep } from 'lodash' +import { getAction } from '@/api/manage' +import CustomPopoverSearch from '@/components/CustomPopoverSearch' export default { mixins: [FormMixin], + components: { + VNodes: { + functional: true, + render: (_, ctx) => ctx.props.vnodes + }, + CustomPopoverSearch + }, data() { return { currentMonth: moment(), form: {}, visible: true, - treeData: [ - { key: '0-0', title: '0-0' }, - { - key: '0-1', - title: '0-1', - children: [ - { key: '0-1-0', title: '0-1-0' }, - { key: '0-1-1', title: '0-1-1' } - ] - }, - { key: '0-2', title: '0-3' } - ], - selectedKeys: [], - targetKeys: [] + originalTreeData: [], + targetKeys: [], + + isGettingStationList: false, + isGettingAccountList: false, + stationList: [], + accountList: [], + selectedAccount: [], // 右上方User Name选中的账号 + accountTreeData: [], + checkedAccount: '', // 右侧穿梭框选中的账号 + rightAccountChildSelectedKeys: [] // 右侧穿梭框选中的值 } }, + created() { + this.getStationList() + this.getAccountList() + }, methods: { // 获取该月日程列表 async getList() {}, + async getStationList() { + try { + this.isGettingStationList = true + const { success, result, message } = await getAction('/gardsStations/findPage?pageIndex=1&pageSize=1000') + if (success) { + const records = result.records + const set = new Set(records.map(item => item.countryCode)) + this.originalTreeData = Array.from(set).map((countryCode, index) => { + return { + title: countryCode, + key: index.toString(), + disabled: true, + children: records + .filter(item => item.countryCode == countryCode) + .map(item => ({ title: item.stationCode, key: item.stationId.toString(), children: [] })) + } + }) + + this.stationList = records.map(item => ({ + title: item.stationCode, + key: item.stationId.toString() + })) + } else { + this.$message.error(message) + } + } catch (error) { + console.error(error) + } finally { + this.isGettingStationList = false + } + }, + + async getAccountList() { + try { + this.isGettingAccountList = true + const { success, result, message } = await getAction('/sys/user/list?pageIndex=1&pageSize=1000') + if (success) { + const records = result.records + this.accountList = records.map(item => ({ + label: item.realname, + value: item.id + })) + } else { + this.$message.error(message) + } + } catch (error) { + console.error(error) + } finally { + this.isGettingAccountList = false + } + }, + // 左侧删除某一天的安排 onDel(item) { console.log('%c [ 删除 ]-51', 'font-size:13px; background:pink; color:#bf2c9f;', item) @@ -187,18 +283,118 @@ export default { console.log('%c [ 新增 ]-88', 'font-size:13px; background:pink; color:#bf2c9f;') }, + /** + * 以下是对穿梭框的处理 + */ + isChecked(selectedKeys, eventKey) { return selectedKeys.indexOf(eventKey) !== -1 }, + onChecked(_, e, checkedKeys, itemSelect) { const { eventKey } = e.node itemSelect(eventKey, !this.isChecked(checkedKeys, eventKey)) + }, + + // 处理station列表 + handleTreeData(data, targetKeys = [], level) { + data.forEach(item => { + if (level !== 0) { + item.disabled = targetKeys.includes(item.key) + } + if (item.children) { + this.handleTreeData(item.children, targetKeys) + } + }) + return data + }, + + filterOption(input, option) { + return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }, + + // 将选中的账号加入到右侧穿梭框 + onAddToList() { + this.accountTreeData = this.selectedAccount.map(id => { + const find = this.accountList.find(account => account.value == id) + return { + title: find.label, + key: id, + children: [] + } + }) + this.checkedAccount = '' + }, + + // 选中了穿梭框右侧的树节点 + onSelectAccount(_, e, itemSelect) { + const { eventKey, isLeaf, selected } = e.node + // selected 是前一个状态,也就是selected 为false时,其实是选中了 + if (isLeaf) { + // 选中了子节点,也就是站点 + if (selected) { + // 取消选中 + itemSelect(eventKey, false) + } else { + this.accountTreeData.forEach(account => { + // 将所有右侧的穿梭状态重置 + account.children.forEach(child => { + itemSelect(child.key, false) + }) + }) + itemSelect(eventKey, true) // 选中该栏 + } + } else { + this.checkedAccount = selected ? '' : eventKey + } + }, + + // 穿梭框变化 + onChange(targetKeys, direction, moveKeys) { + if (direction == 'right') { + if (!this.checkedAccount) { + this.$message.warning('Please Select A Person To Assign') + return + } + const findAccount = this.accountTreeData.find(account => account.key == this.checkedAccount) + if (findAccount) { + const children = moveKeys.map(key => { + const findStation = this.stationList.find(station => station.key == key) + return { + isLeaf: true, + ...cloneDeep(findStation) + } + }) + findAccount.children.push(...children) + } + } else { + const moveKey = moveKeys[0] + let parentIndex = -1, + childIndex = -1 + for (const pIndex in this.accountTreeData) { + // 找到要移除的Station在右侧列表中的位置 + const account = this.accountTreeData[pIndex] + const cIndex = account.children.findIndex(child => child.key == moveKey) + if (-1 !== cIndex) { + parentIndex = pIndex + childIndex = cIndex + break + } + } + this.accountTreeData[parentIndex].children.splice(childIndex, 1) + } + this.targetKeys = targetKeys } }, watch: { currentMonth() { this.getList() } + }, + computed: { + treeData() { + return this.handleTreeData(cloneDeep(this.originalTreeData), this.targetKeys, 0) + } } } </script> @@ -378,71 +574,112 @@ export default { } } -::v-deep { +.account-assign { + position: relative; + width: 672px; + margin: 0 auto; .ant-transfer { margin-bottom: 10px; + ::v-deep { + .ant-transfer-list { + width: 282px; + height: 411px; + &-header { + height: 37px; + &-selected { + span:first-child { + display: none; + } + } + &-title { + left: 16px; + } + } + &-content { + &-item { + &:hover { + background-color: transparent; + } + } + } - &-list { - width: 282px; - height: 374px; - &-header { - height: 37px; - &-selected { - span:first-child { + &:last-child { + height: 364px; + position: relative; + top: 47px; + } + } + + .ant-transfer-operation { + .ant-btn { + width: 92px; + height: 26px; + padding: 0; + .anticon { display: none; } - } - &-title { - left: 16px; - } - } - &-content { - &-item { - &:hover { - background-color: transparent; + span { + margin-left: 0; } - } - } - } - - &-operation { - .ant-btn { - width: 92px; - height: 26px; - padding: 0; - .anticon { - display: none; - } - span { - margin-left: 0; - } - &:first-child { - margin-bottom: 52px; - &::after { - display: inline-block; - margin-left: 13px; - content: ''; - width: 18px; - height: 10px; - background: url(~@/assets/images/system/transfer-right.png) no-repeat; - background-size: contain; + &:first-child { + margin-bottom: 52px; + &::after { + display: inline-block; + margin-left: 13px; + content: ''; + width: 18px; + height: 10px; + background: url(~@/assets/images/system/transfer-right.png) no-repeat; + background-size: contain; + } } - } - &:nth-child(2) { - &::before { - display: inline-block; - margin-right: 6px; - content: ''; - width: 18px; - height: 10px; - background: url(~@/assets/images/system/transfer-left.png) no-repeat; - background-size: contain; - position: static; - opacity: initial; + &:nth-child(2) { + &::before { + display: inline-block; + margin-right: 6px; + content: ''; + width: 18px; + height: 10px; + background: url(~@/assets/images/system/transfer-left.png) no-repeat; + background-size: contain; + position: static; + opacity: initial; + } } } } } } + .account-search { + position: absolute; + top: 0; + right: 0; + width: 282px; + display: flex; + align-items: center; + label { + color: #5b9cba; + font-size: 16px; + flex-shrink: 0; + margin-right: 10px; + } + } +} +.ant-select-dropdown-content::before { + top: 2px; +} +.account-add { + padding: 0; + padding-top: 10px; + text-align: center; + cursor: pointer; + background: #03353f; + a { + display: inline-block; + width: 100%; + padding: 5px; + border-top: 1px solid #0da397; + color: #0cebc9; + } } </style>