| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- <template>
- <uni-popup
- ref="popupTransferRef"
- background-color="#fff"
- border-radius="10px 10px 0 0"
- type="bottom"
- >
- <view class="popup-transfer-container">
- <!-- 标题 -->
- <uni-row class="popup-header align-center justify-center">
- <uni-col :span="4" class="align-center justify-start">
- <view class="confirm-btn" @click="handleConfirm">
- {{ $t("operation.confirm") }}
- </view>
- </uni-col>
- <uni-col :span="18" class="align-center justify-center">
- {{ $t("operation.select") }}{{ $t("ruiDu.constructionEquipment") }}
- </uni-col>
- <uni-col :span="4" class="align-center justify-end">
- <uni-icons
- type="close"
- color="#666"
- size="24"
- @click="close"
- ></uni-icons>
- </uni-col>
- </uni-row>
- <scroll-view scroll-y="true" class="popup-transfer-content">
- <!-- 可选设备 -->
- <uni-card
- :spacing="'0'"
- :margin="'10px'"
- :is-shadow="false"
- :title="$t('ruiDu.optionalEquipment')"
- :extra="`${unSelectedChecked.length}/${localDataUncheckedOriginal.length}`"
- >
- <!-- 搜索框 -->
- <uni-easyinput
- v-model="searchValueUnchecked"
- :placeholder="$t('operation.searchText')"
- ></uni-easyinput>
- <!-- 可选设备列表 -->
- <scroll-view scroll-y="true" class="scroll-card">
- <template v-if="localDataUnchecked.length > 0">
- <uni-data-checkbox
- mode="list"
- :multiple="true"
- :wrap="true"
- v-model="unSelectedChecked"
- :localdata="localDataUnchecked"
- ></uni-data-checkbox>
- </template>
- <template v-else>
- <view class="empty-state align-center justify-center">
- <uni-icons type="none" size="48" color="#ccc"></uni-icons>
- <text class="empty-text">{{ $t("common.noData") }}</text>
- </view>
- </template>
- </scroll-view>
- </uni-card>
- <!-- 操作按钮 -->
- <uni-row class="transfer-btn-row flex-row align-center justify-center">
- <uni-col :span="8" class="align-center justify-center">
- <button
- class="mini-btn align-center justify-center"
- size="mini"
- type="primary"
- :disabled="localDataUncheckedOriginal.length === 0"
- @click="handleTransferToSelected(unSelectedChecked)"
- >
- <uni-icons type="down" color="#fff" size="20"></uni-icons>
- </button>
- <button
- class="mini-btn align-center justify-center"
- size="mini"
- type="primary"
- :disabled="localDataCheckedOriginal.length === 0"
- @click="handleTransferToUnSelected(selectedChecked)"
- >
- <uni-icons type="up" color="#fff" size="20"></uni-icons>
- </button>
- </uni-col>
- </uni-row>
- <!-- 已选设备 -->
- <uni-card
- :spacing="'0'"
- :margin="'10px'"
- :is-shadow="false"
- :title="$t('ruiDu.selectedEquipment')"
- :extra="`${selectedChecked.length}/${localDataCheckedOriginal.length}`"
- >
- <!-- 搜索框 -->
- <uni-easyinput
- v-model="searchValueChecked"
- :placeholder="$t('operation.searchText')"
- ></uni-easyinput>
- <!-- 已选设备列表 -->
- <scroll-view scroll-y="true" class="scroll-card">
- <template v-if="localDataChecked.length > 0">
- <uni-data-checkbox
- mode="list"
- :multiple="true"
- :wrap="true"
- v-model="selectedChecked"
- :localdata="localDataChecked"
- ></uni-data-checkbox>
- </template>
- <template v-else>
- <view class="empty-state align-center justify-center">
- <uni-icons type="none" size="48" color="#ccc"></uni-icons>
- <text class="empty-text">{{ $t("common.noData") }}</text>
- </view>
- </template>
- </scroll-view>
- </uni-card>
- </scroll-view>
- </view>
- </uni-popup>
- </template>
- <script setup>
- import {
- ref,
- reactive,
- computed,
- getCurrentInstance,
- onMounted,
- nextTick,
- watch
- } from "vue";
- // -------------------------- 全局变量与国际化 -------------------------------
- const { appContext } = getCurrentInstance();
- const t = appContext.config.globalProperties.$t;
- // -------------------------- 父组件通信 -------------------------------
- const emit = defineEmits(["confirm"]);
- const props = defineProps({
- allList: { type: Array, default: () => [] },
- selected: { type: Array, default: () => [] },
- });
- // -------------------------- 组件内部变量 -------------------------------
- const popupTransferRef = ref(null);
- const selectedChecked = ref([]); // 已选列表的选中项(用于转移)
- const unSelectedChecked = ref([]); // 未选列表的选中项(用于转移)
- const searchValueChecked = ref(""); // 已选搜索框值
- const searchValueUnchecked = ref(""); // 未选搜索框值
- // 1. 原始数据(格式化后:过滤无效+去重)
- const localDataOriginal = ref([]);
- // 2. 已选/未选「原始列表」(未过滤,初始化时从原始数据拆分,后续转移时增删项)
- const localDataCheckedOriginal = ref([]); // 已选完整列表
- const localDataUncheckedOriginal = ref([]); // 未选完整列表
- // 3. 已选/未选「过滤后列表」(基于原始列表+搜索条件,转移时直接操作此列表)
- const localDataChecked = ref([]); // 已选搜索过滤后列表
- const localDataUnchecked = ref([]); // 未选搜索过滤后列表
- // -------------------------- 搜索过滤逻辑(基于原始列表,保留搜索状态) -------------------------------
- // 已选列表过滤:实时基于「已选原始列表」+ 搜索值
- const filterCheckedList = computed(() => {
- const searchVal = searchValueChecked.value.trim().toLowerCase();
- return localDataCheckedOriginal.value.filter((item) =>
- item.text.toLowerCase().includes(searchVal)
- );
- });
- // 未选列表过滤:实时基于「未选原始列表」+ 搜索值
- const filterUncheckedList = computed(() => {
- const searchVal = searchValueUnchecked.value.trim().toLowerCase();
- return localDataUncheckedOriginal.value.filter((item) =>
- item.text.toLowerCase().includes(searchVal)
- );
- });
- // 监听搜索值变化,更新过滤后列表(保留搜索状态)
- watch(
- [searchValueChecked, searchValueUnchecked],
- () => {
- localDataChecked.value = [...filterCheckedList.value];
- localDataUnchecked.value = [...filterUncheckedList.value];
- },
- { immediate: true }
- );
- // -------------------------- 核心优化:基于当前列表更新(而非重建) -------------------------------
- // 初始化:从原始数据拆分「已选/未选原始列表」(仅初始化时调用)
- const initCheckedUncheckedList = () => {
- const { allList = [], selected = [] } = props;
- const selectedSet = new Set(selected.map((val) => String(val)));
- // 格式化原始数据(过滤无效+去重)
- localDataOriginal.value = allList
- .filter(
- (item) => item && item.id != null && item.deviceCode && item.deviceName
- )
- .map((item) => ({
- text: `${item.deviceCode} - ${item.deviceName}`,
- value: item.id,
- }))
- .filter(
- (item, idx, self) =>
- self.findIndex((i) => String(i.value) === String(item.value)) === idx
- );
- // 初始化已选/未选原始列表
- localDataCheckedOriginal.value = localDataOriginal.value.filter((item) =>
- selectedSet.has(String(item.value))
- );
- localDataUncheckedOriginal.value = localDataOriginal.value.filter(
- (item) => !selectedSet.has(String(item.value))
- );
- // 初始化过滤后列表(应用初始搜索值)
- localDataChecked.value = [...filterCheckedList.value];
- localDataUnchecked.value = [...filterUncheckedList.value];
- };
- // 转移:未选 → 已选(在当前列表基础上更新)
- const handleTransferToSelected = () => {
- if (!unSelectedChecked.value.length) return;
- // 1. 从「未选过滤列表」中找到被选中的项(要转移的项)
- const transferItems = localDataUnchecked.value.filter((item) =>
- unSelectedChecked.value.includes(item.value)
- );
- if (!transferItems.length) return;
- // 2. 去重:避免已选列表中已有该项目
- const uniqueTransferItems = transferItems.filter(
- (item) =>
- !localDataCheckedOriginal.value.some(
- (i) => String(i.value) === String(item.value)
- )
- );
- // 3. 更新「已选列表」:添加转移项(原始+过滤后)
- localDataCheckedOriginal.value.push(...uniqueTransferItems);
- // 过滤后列表直接添加(保留当前搜索状态)
- localDataChecked.value.push(...uniqueTransferItems);
- // 4. 更新「未选列表」:移除转移项(原始+过滤后)
- // 原始列表:过滤掉转移项
- localDataUncheckedOriginal.value = localDataUncheckedOriginal.value.filter(
- (item) =>
- !uniqueTransferItems.some((t) => String(t.value) === String(item.value))
- );
- // 过滤后列表:过滤掉转移项(保留搜索结果)
- localDataUnchecked.value = localDataUnchecked.value.filter(
- (item) =>
- !uniqueTransferItems.some((t) => String(t.value) === String(item.value))
- );
- // 5. 清空未选列表的选中状态
- unSelectedChecked.value = [];
- };
- // 转移:已选 → 未选(在当前列表基础上更新)
- const handleTransferToUnSelected = () => {
- if (!selectedChecked.value.length) return;
- // 1. 从「已选过滤列表」中找到被选中的项(要转移的项)
- const transferItems = localDataChecked.value.filter((item) =>
- selectedChecked.value.includes(item.value)
- );
- if (!transferItems.length) return;
- // 2. 去重:避免未选列表中已有该项目
- const uniqueTransferItems = transferItems.filter(
- (item) =>
- !localDataUncheckedOriginal.value.some(
- (i) => String(i.value) === String(item.value)
- )
- );
- // 3. 更新「未选列表」:添加转移项(原始+过滤后)
- localDataUncheckedOriginal.value.push(...uniqueTransferItems);
- // 过滤后列表直接添加(保留当前搜索状态)
- localDataUnchecked.value.push(...uniqueTransferItems);
- // 4. 更新「已选列表」:移除转移项(原始+过滤后)
- // 原始列表:过滤掉转移项
- localDataCheckedOriginal.value = localDataCheckedOriginal.value.filter(
- (item) =>
- !uniqueTransferItems.some((t) => String(t.value) === String(item.value))
- );
- // 过滤后列表:过滤掉转移项(保留搜索结果)
- localDataChecked.value = localDataChecked.value.filter(
- (item) =>
- !uniqueTransferItems.some((t) => String(t.value) === String(item.value))
- );
- // 5. 清空已选列表的选中状态
- selectedChecked.value = [];
- };
- // -------------------------- 其他方法 -------------------------------
- const open = () => {
- initCheckedUncheckedList(); // 仅打开时初始化一次
- popupTransferRef.value.open();
- };
- const close = () => {
- clear();
- popupTransferRef.value.close();
- };
- const clear = () => {
- selectedChecked.value = [];
- unSelectedChecked.value = [];
- searchValueChecked.value = "";
- searchValueUnchecked.value = "";
- // 不清空已选/未选列表,仅清空选中和搜索
- };
- const handleConfirm = () => {
- // 最终已选结果:取已选原始列表的value
- const finalSelected = localDataCheckedOriginal.value.map(
- (item) => item.value
- );
- emit("confirm", [...new Set(finalSelected)]);
- close();
- };
- // -------------------------- 生命周期与监听 -------------------------------
- onMounted(() => {
- console.log("device-transfer mounted", props);
- });
- // 监听父组件selected变化(仅初始化时用,后续转移不依赖)
- watch(
- () => props.selected,
- () => {
- if (popupTransferRef.value?.isOpen) {
- initCheckedUncheckedList();
- }
- },
- { deep: true }
- );
- // --------------------------组件暴露的方法--------------------------------
- defineExpose({
- open,
- close,
- });
- </script>
- <style lang="scss" scoped>
- .popup-transfer-container {
- height: calc(100vh - 160rpx);
- }
- .popup-header {
- padding: 10px 10px 0 10px;
- box-sizing: border-box;
- height: 80rpx;
- }
- .confirm-btn {
- font-weight: bold;
- color: #004098;
- }
- .popup-transfer-content {
- box-sizing: border-box;
- height: calc(100% - 80rpx - 20rpx);
- }
- :deep(.uni-card) {
- box-sizing: border-box;
- padding: 0;
- min-height: 570rpx;
- height: calc(50% - 30rpx - 40rpx);
- }
- :deep(.uni-card__header) {
- background: #f5f7fa;
- }
- :deep(.uni-card__content) {
- box-sizing: border-box;
- height: calc(100% - 160rpx);
- }
- /* 空状态样式 */
- .empty-state {
- color: #999;
- padding: 40rpx 0;
- }
- .empty-text {
- margin-top: 16rpx;
- font-size: 28rpx;
- }
- .scroll-card {
- box-sizing: border-box;
- height: 100%;
- }
- .transfer-btn-row {
- height: 60rpx;
- }
- .mini-btn {
- display: flex;
- width: 100rpx;
- height: 60rpx;
- padding: 0;
- line-height: 1.5;
- }
- </style>
|