根据 props 更新 Vue3 组件中 div 中的 CSS 类不起作用(缺少反应性?)

发布于 2025-01-20 22:58:48 字数 5512 浏览 0 评论 0原文

我在 Github 上发现了一个好看的 vue 底部导航栏(https://github.com/imanmalekian31 /vue-bottom-navigation)。

遗憾的是它不适用于 Vue3、Typescript 和脚本设置语法。

我重写了该组件并让它运行并显示在我的应用程序中。

问题是,按钮目前没有反应。当 click 上的方法运行时,元素上的 css 类不会更新。

我最好的猜测是,localOptions 和按钮需要

<div v-for="(button, index) in localOptions" :key="`grow-button-${index}`"></div>

具有反应性,但由于脚本设置的更改,它们可能不会发生反应。

任何有关如何修复此代码的帮助将不胜感激。

更新: 我确实用 let localOptions = Reactor(props.options.slice()) 替换了 let localOptions: any[] = Reactor([]) ,这似乎已经完成了诡计。按钮工作和更新都很好。

现在出现的问题是,计算出的Property似乎并没有在每次组件重新渲染时运行。因此按钮不会改变宽度或颜色。

组件文件:

<template>
  <div class="gr-btn-container-foreground" :style="cssVariables">
    <div v-for="(button, index) in localOptions" :key="`grow-button-${index}`" :class="[
      'gr-btn-container',
      { 'gr-btn-container-active': button.selected },
    ]" @click="handleButtonClick(button, index)">
      <div :class="['gr-btn-item', { 'gr-btn-item-active': button.selected }]">
        <div :class="['gr-btn-icon', { 'gr-btn-icon-active': button.selected }]">
          <slot name="icon" :props="button">
            <i :class="`${button.icon}`" />
          </slot>
        </div>
        <div class="gr-btn-title">
          <span class="gr-hidden-title">
            <slot name="title" :props="button">{{ button.title }}</slot>
          </span>
          <span :class="[
            'gr-animated-title',
            { 'gr-animated-title-active': button.selected },
          ]">
            <slot name="title" :props="button">{{ button.title }}</slot>
          </span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, reactive } from "vue";
import { useRoute, useRouter } from 'vue-router'

const router = useRouter()
const route = useRoute()
const button = reactive({})


const model = {
  prop: "value",
  event: "update",
}
const props = defineProps({
  value: {
    default: null,
  },
  options: {
    type: Array,
    default: () => [],
  },
  color: {
    type: String,
    default: "#74cbbb",
  },
  replaceRoute: {
    type: Boolean,
    default: false,
  },
})

const emit = defineEmits<{
  (e: 'update', value: string): void
}>()

let prevSelected = null
let currSelected = null
let localOptions: any[] = reactive([])

const cssVariables = computed(() => {

  const activeTitle = (localOptions[currSelected] || {}).title;
  let activeWidth = 95;
  if (activeTitle && activeTitle.length * 15 > 110) {
    activeWidth = 95 + (activeTitle.length * 15 - 110) / 2;
  }

  const mainColor =
    (localOptions[currSelected] || {}).color || props.color;

  const styles = {
    "--color": mainColor,
    "--color-background": mainColor + "30",
    "--active-width": `${activeWidth}px`,
  };

  return styles;
})

localOptions = props.options.slice();

let index = localOptions.findIndex(
  (item) =>
    item.id == props.value ||
    (item.path || {}).name == (route || {}).name
);

if (index > -1) {
  currSelected = index;
  prevSelected = index;

  localOptions[index] = { ...localOptions[index], selected: true }
}


function handleButtonClick(button: any, index: number) {
  console.log("Button: " + button + " Index: " + index);
  currSelected = index;

  if (prevSelected !== null) {
    localOptions[prevSelected].selected = false;
  }

  localOptions[index] = { ...localOptions[index], selected: true }

  prevSelected = currSelected;
  updateValue(button);
}

function updateValue(button) {
  console.log("Update value: " + button.id + " with Path: " + button.path)

  emit("update", button.id);

  if (button.path && Object.keys(button.path).length) {
    router[!props.replaceRoute ? "push" : "replace"](
      button.path
    ).catch(() => { console.log("Error updating path") });
  }
}

</script>

<style scoped>
.gr-btn-container-foreground {
  position: fixed;
  direction: ltr;
  display: flex;
  align-items: center;
  bottom: 0;
  width: 100%;
  z-index: 2147483647;
  height: 64px;
  background: #fff;
  box-shadow: 0 0 5px 0 #eee;
}

.gr-btn-container {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 45px;
  flex-grow: 1;
  transition: all 0.3s;
}

@media (min-width: 576px) {
  .gr-btn-container {
    cursor: pointer;
  }
}

.gr-btn-container-active {
  background-color: var(--color-background);
  border-radius: 100px;
}

.gr-btn-item {
  display: flex;
  position: relative;
  overflow: hidden;
  width: 24px;
  transition: all 0.3s ease;
  color: #0000008a;
}

.gr-btn-item-active {
  width: var(--active-width);
}

.gr-btn-icon {
  font-size: 20px;
  transition: all 0.3s ease;
  margin: 0px !important;
}

.gr-btn-icon-active {
  color: var(--color);
}

.gr-btn-title {
  position: relative;
  color: var(--color);
  padding: 0px 5px;
  margin-left: 10px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.gr-hidden-title {
  visibility: hidden;
}

.gr-animated-title {
  color: var(--color);
  position: absolute;
  left: 5px;
  bottom: -15px;
  transition: bottom 0.4s ease 0.1s;
}

.gr-animated-title-active {
  bottom: 2px;
}
</style>

I found a good looking bottom navigation bar for vue on Github (https://github.com/imanmalekian31/vue-bottom-navigation).

Sadly it did not work with Vue3, Typescript and the script setup syntax.

I rewrote the component and got it to run and show up in my app.

The problem is, the buttons are not reactive at the moment. While the methods on click get run, the css classes on the elements do not get updated.

My best guess here is, that localOptions and button in

<div v-for="(button, index) in localOptions" :key="`grow-button-${index}`"></div>

need to be reactive, which they might not due to the change to script setup.

Any help on how to fix this code would be greatly appreciated.

Update:
I did replace let localOptions: any[] = reactive([]) with let localOptions = reactive(props.options.slice()) and that seems to have done the trick. The buttons are working and updating just fine.

The problem that appeared now is, that the computed Property doesn't seem to be run every time the component re-renders. Therefore the buttons are not changing width or color.

Component file:

<template>
  <div class="gr-btn-container-foreground" :style="cssVariables">
    <div v-for="(button, index) in localOptions" :key="`grow-button-${index}`" :class="[
      'gr-btn-container',
      { 'gr-btn-container-active': button.selected },
    ]" @click="handleButtonClick(button, index)">
      <div :class="['gr-btn-item', { 'gr-btn-item-active': button.selected }]">
        <div :class="['gr-btn-icon', { 'gr-btn-icon-active': button.selected }]">
          <slot name="icon" :props="button">
            <i :class="`${button.icon}`" />
          </slot>
        </div>
        <div class="gr-btn-title">
          <span class="gr-hidden-title">
            <slot name="title" :props="button">{{ button.title }}</slot>
          </span>
          <span :class="[
            'gr-animated-title',
            { 'gr-animated-title-active': button.selected },
          ]">
            <slot name="title" :props="button">{{ button.title }}</slot>
          </span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, reactive } from "vue";
import { useRoute, useRouter } from 'vue-router'

const router = useRouter()
const route = useRoute()
const button = reactive({})


const model = {
  prop: "value",
  event: "update",
}
const props = defineProps({
  value: {
    default: null,
  },
  options: {
    type: Array,
    default: () => [],
  },
  color: {
    type: String,
    default: "#74cbbb",
  },
  replaceRoute: {
    type: Boolean,
    default: false,
  },
})

const emit = defineEmits<{
  (e: 'update', value: string): void
}>()

let prevSelected = null
let currSelected = null
let localOptions: any[] = reactive([])

const cssVariables = computed(() => {

  const activeTitle = (localOptions[currSelected] || {}).title;
  let activeWidth = 95;
  if (activeTitle && activeTitle.length * 15 > 110) {
    activeWidth = 95 + (activeTitle.length * 15 - 110) / 2;
  }

  const mainColor =
    (localOptions[currSelected] || {}).color || props.color;

  const styles = {
    "--color": mainColor,
    "--color-background": mainColor + "30",
    "--active-width": `${activeWidth}px`,
  };

  return styles;
})

localOptions = props.options.slice();

let index = localOptions.findIndex(
  (item) =>
    item.id == props.value ||
    (item.path || {}).name == (route || {}).name
);

if (index > -1) {
  currSelected = index;
  prevSelected = index;

  localOptions[index] = { ...localOptions[index], selected: true }
}


function handleButtonClick(button: any, index: number) {
  console.log("Button: " + button + " Index: " + index);
  currSelected = index;

  if (prevSelected !== null) {
    localOptions[prevSelected].selected = false;
  }

  localOptions[index] = { ...localOptions[index], selected: true }

  prevSelected = currSelected;
  updateValue(button);
}

function updateValue(button) {
  console.log("Update value: " + button.id + " with Path: " + button.path)

  emit("update", button.id);

  if (button.path && Object.keys(button.path).length) {
    router[!props.replaceRoute ? "push" : "replace"](
      button.path
    ).catch(() => { console.log("Error updating path") });
  }
}

</script>

<style scoped>
.gr-btn-container-foreground {
  position: fixed;
  direction: ltr;
  display: flex;
  align-items: center;
  bottom: 0;
  width: 100%;
  z-index: 2147483647;
  height: 64px;
  background: #fff;
  box-shadow: 0 0 5px 0 #eee;
}

.gr-btn-container {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 45px;
  flex-grow: 1;
  transition: all 0.3s;
}

@media (min-width: 576px) {
  .gr-btn-container {
    cursor: pointer;
  }
}

.gr-btn-container-active {
  background-color: var(--color-background);
  border-radius: 100px;
}

.gr-btn-item {
  display: flex;
  position: relative;
  overflow: hidden;
  width: 24px;
  transition: all 0.3s ease;
  color: #0000008a;
}

.gr-btn-item-active {
  width: var(--active-width);
}

.gr-btn-icon {
  font-size: 20px;
  transition: all 0.3s ease;
  margin: 0px !important;
}

.gr-btn-icon-active {
  color: var(--color);
}

.gr-btn-title {
  position: relative;
  color: var(--color);
  padding: 0px 5px;
  margin-left: 10px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.gr-hidden-title {
  visibility: hidden;
}

.gr-animated-title {
  color: var(--color);
  position: absolute;
  left: 5px;
  bottom: -15px;
  transition: bottom 0.4s ease 0.1s;
}

.gr-animated-title-active {
  bottom: 2px;
}
</style>

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

寂寞笑我太脆弱 2025-01-27 22:58:48

currSelected 不是反应变量 (ref)。据我很快发现,这就是您的 cssVariables 计算不重新计算的原因 - 它错过了 currSelected 的反应依赖

currSelected is not a reactive variable (ref). As far as I quickly see this is the reason why your cssVariables computed does not recalculate - it misses reactive dependence of currSelected

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文