feat(user-apps): appearance settings (dark mode + reduced motion), product morph, filter sheet

Applies to both user-app and user-app-v2. Logic and structure are shared;
each app keeps its own look.

Appearance settings
- New "تنظیمات ظاهری" page (/settings/appearance), opened from the
  dashboard and available without login. It offers a light/dark/system
  theme and a "fast & light" motion switch.
- appearanceStore saves the choice to localStorage. A small inline boot
  script in index.html applies it before first paint so there is no
  white flash. The "system" option follows OS changes live.
- New ToggleSwitch component.

Dark mode
- Theme LESS tokens now compile to CSS custom properties. Light/dark
  values come from a generated virtual:palette.css (themeConfig/
  appearance.ts, with colors.json + new colors.dark.json).
- New LESS alpha() plugin replaces fade() on theme tokens.
- New semantic tokens: page, onInk, heroSurface, hairlineStrong,
  photoMat, photoStage.
- The dark palette is layered (page < card < panel < border) so
  surfaces separate without shadows.
- Hard-coded colors were replaced by tokens according to their role.
- antd inputs/text get dark overrides; vant uses its own
  .van-theme-dark.
- Light mode looks the same as before.

Reduced motion
- With html.motion-reduced, transitions are capped at 120ms, keyframe
  loops stop, and backdrop blurs and permanent will-change are dropped.
- Spinners and the toast progress bar (which closes the toast) are
  exempt via .keep-motion.
- Carousel autoplay, the v1 Lottie and the product morph honor the
  setting.

Product detail
- Opening a product is now a container-transform morph from the tapped
  card (ProductMorph). It replaces the bottom Drawer and uses composited
  transform/opacity only.
- The detail view is seeded with the card's data, so it no longer
  opens blank and jumps.

Filters and sheets
- Filters are now a full-width bottom sheet. Selections stay a draft
  until applied, so the result list no longer re-renders while
  dragging the slider.
- DropDown collapse animates grid-template-rows instead of a capped
  max-height.
- v2 IslandSheet is restyled to the v2 design language (API unchanged).

Fixes
- filtersStore.reset() shallow-copied shared arrays, so clearing the
  filters never cleared the selected brands.
This commit is contained in:
s.esmaeili
2026-10-04 16:18:56 +03:30
parent c05352d6fd
commit f441ca8289
102 changed files with 4602 additions and 1053 deletions
+5 -5
View File
@@ -121,8 +121,8 @@
</template>
<template v-slot:title>آیا مطمئن به ثبت سفارش هستید ؟</template>
<template v-slot:button>
<div class="text-center text-[#23493c]" @click="submitOrder">بله</div>
<div class="text-center text-[#6b7280]" @click="cartData.notify = false">بی خیال</div>
<div class="text-center text-[color:var(--c-primary)]" @click="submitOrder">بله</div>
<div class="text-center text-[color:var(--c-inkMuted)]" @click="cartData.notify = false">بی خیال</div>
</template>
</DialogVant>
@@ -134,7 +134,7 @@
<div class="p-8 text-justify">برای ثبت سفارش ابتدا باید وارد حساب کاربری خود شوید.</div>
</template>
<template v-slot:button>
<div class="text-center text-[#23493c]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center text-[color:var(--c-primary)]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center text-danger" @click="loginNotify = false">بیخیال</div>
</template>
</DialogVant>
@@ -240,7 +240,7 @@ onMounted(async () => {
<style scoped lang="less">
.cart {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -347,7 +347,7 @@ onMounted(async () => {
left: 0;
right: 0;
z-index: 5;
background: rgba(255, 255, 255, 0.92);
background: alpha(@canvas, 92%);
backdrop-filter: blur(14px);
border-top: 1px solid @hairline;
padding-bottom: 66px;
+7 -20
View File
@@ -113,27 +113,15 @@
</div>
</container>
</PullRefresh>
<Drawer
v-model:visible="productDrawer"
placement="bottom"
:closable="false"
class="rtl"
height="100vh"
:bodyStyle="{ padding: 0, position: 'relative' }"
>
<SingleProduct v-if="activeProductId" :id="activeProductId" @closeDrawer="productDrawer = false" />
</Drawer>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Drawer } from 'ant-design-vue'
import { Loading, PullRefresh } from 'vant'
import Icon from '../components/icon/Icon.vue'
import SingleProduct from '../components/product/SingleProduct.vue'
import { openProduct as openProductMorph } from '../composables/useProductMorph'
import RateStrip from '../components/home/RateStrip.vue'
import ShowcaseSlider from '../components/home/ShowcaseSlider.vue'
import type { ShowcaseSlide } from '../components/home/ShowcaseSlider.vue'
@@ -162,8 +150,6 @@ const goldPrice = ref<GoldPriceQuote | null>(null)
// knows whether to wait or fall back to the category-derived hero.
const adminSlides = ref<AdminSlide[] | null>(null)
const productDrawer = ref(false)
const activeProductId = ref<string | null>(null)
// First-paint entrance: played once, after the first data load, so the
// sections settle into place instead of popping in piecemeal. Pull-to-refresh
@@ -219,9 +205,10 @@ function productImageForCategory(categoryId: string): string | undefined {
return match?.images?.[0]?.url
}
// Opens as a morph out of the tapped card (ProductMorph.vue, mounted in
// App.vue) — the card is found from the tap, so the emit chain stays as is.
function openProduct(product: ProductSummary) {
activeProductId.value = product.id
productDrawer.value = true
openProductMorph(product)
}
function onCategory(cat: HomeCategory) {
@@ -290,7 +277,7 @@ onMounted(async () => {
<style scoped lang="less">
.home {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -300,7 +287,7 @@ onMounted(async () => {
left: 0;
right: 0;
z-index: 20;
background: rgba(255, 255, 255, 0.88);
background: alpha(@canvas, 88%);
backdrop-filter: blur(14px);
border-bottom: 1px solid @hairline;
}
@@ -346,7 +333,7 @@ onMounted(async () => {
font-weight: 700;
color: #fff;
background: @wine;
border: 2px solid #fff;
border: 2px solid @canvas;
}
.home__body {
+1 -1
View File
@@ -73,7 +73,7 @@ const pull = async () => {
<style scoped lang="less">
.newest {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -308,7 +308,7 @@ onBeforeUnmount(() => observer?.disconnect())
font-size: 12px;
font-weight: 600;
color: #06b6d4;
background: #ecfeff;
background: fade(#06b6d4, 12%);
white-space: nowrap;
}
@@ -245,13 +245,13 @@ onMounted(load)
}
.cch__state.is-on {
color: #16a34a;
background: #dcfce7;
color: @green;
background: alpha(@green, 14%);
}
.cch__state.is-off {
color: #dc2626;
background: #fee2e2;
color: @red;
background: alpha(@red, 12%);
}
.cch__facts {
@@ -179,10 +179,10 @@ function rial(value: string): string {
}
.is-in {
color: #16a34a;
color: @green;
}
.is-out {
color: #dc2626;
color: @red;
}
</style>
@@ -360,11 +360,11 @@ onBeforeUnmount(() => observer?.disconnect())
}
.is-in {
color: #16a34a;
color: @green;
}
.is-out {
color: #dc2626;
color: @red;
}
.rep__more,
@@ -67,7 +67,7 @@ onMounted(async () => {
<style scoped lang="less">
.coll {
background: @canvas;
background: @page;
min-height: 100vh;
}
+13 -51
View File
@@ -14,12 +14,12 @@
-->
<template>
<div class="search">
<PageBar :title="showFilter ? 'فیلترها' : 'جستجو'" :back="showFilter" @click.capture="onBarClick">
<PageBar title="جستجو">
<template #below>
<div class="search__barExtra">
<SearchInput @search="search" place-holder="جستجوی دسته بندی، محصولات و ..." />
<div v-if="!showFilter" class="search__chips no-scrollbar">
<div class="search__chips no-scrollbar">
<button
type="button"
class="search__chip search__chip--filter"
@@ -61,15 +61,7 @@
indicator, over the results already on screen.
-->
<div>
<Filters
v-model:filterKeys="filterKeys"
v-show="showFilter"
v-model="filters.query"
:products="allProducts"
@change="hideFilters"
/>
<PullRefresh v-show="!showFilter" v-model="refreshing" @refresh="onRefresh">
<PullRefresh v-model="refreshing" @refresh="onRefresh">
<template #pulling><Loading size="18" /></template>
<template #loosing><Loading size="18" /></template>
<template #loading><Loading size="18" /></template>
@@ -133,12 +125,20 @@
</container>
</PullRefresh>
</div>
<!-- A bottom sheet over a see-through backdrop (teleported to <body>);
see Filters.vue. The result list stays in place underneath. -->
<Filters
v-model:show="showFilter"
v-model:filterKeys="filterKeys"
v-model="filters.query"
:products="allProducts"
/>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { Loading, PullRefresh } from 'vant'
import Icon from '../../components/icon/Icon.vue'
import PageBar from '../../components/ui/PageBar.vue'
@@ -148,11 +148,9 @@ import Filters from '../../components/search/Filters.vue'
import SectionHead from '../../components/home/SectionHead.vue'
import ProductCard from '../../components/product/ProductCard.vue'
import { useFiltersStore } from '../../stores/filtersStore'
import { useNavStore } from '../../stores/navStore'
import { api, type ProductSummary } from '../../api/client'
const filters = useFiltersStore()
const nav = useNavStore()
// Starts true: the first render happens before onMounted kicks off the fetch,
// and with false the "nothing found" box flashed up for a frame before the
@@ -256,50 +254,14 @@ function setCats(id: string | undefined) {
}
function showFilters() {
showFilter.value = true
nav.setNavVisibility(false)
}
function hideFilters(event?: { resetFilters: boolean }) {
showFilter.value = false
nav.setNavVisibility(true)
if (event?.resetFilters) {
filters.reset()
filterKeys.value = []
}
}
// While the filter sheet is open the bar's back button closes the sheet
// rather than leaving the route, matching the legacy "بازگشت" affordance.
function onBarClick(e: MouseEvent) {
if (!showFilter.value) return
const btn = (e.target as HTMLElement)?.closest('button')
if (btn) {
e.stopPropagation()
hideFilters()
}
}
// The filter sheet deliberately hides the bottom tab bar while it is open, so
// its own fixed action bar ("اعمال فیلتر" / "حذف فیلترها") stays clickable —
// the tab bar sits at the same bottom edge with a higher z-index. But leaving
// the page with the sheet still open (browser/swipe back, or tapping a
// collection tile) never ran hideFilters, so that hide leaked: showNav stayed
// false, and on any destination outside the five tab routes the user was left
// with no tab bar at all until a full page refresh reset the store.
//
// Restoring it here rather than in onBeforeUnmount is deliberate: this guard
// runs BEFORE router.afterEach, which then writes the correct visibility for
// the destination and gets the last word. onBeforeUnmount runs after it, and
// would wrongly re-show the bar on full-page routes like /product/:id.
onBeforeRouteLeave(() => {
if (showFilter.value) nav.setNavVisibility(true)
})
onMounted(loadProducts)
</script>
<style scoped lang="less">
.search {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -0,0 +1,340 @@
<!--
تنظیمات ظاهری — the colour theme and the "fast & light" motion switch.
Reached from the dashboard; open to signed-out users too (these are device
preferences, not account data). State lives in stores/appearanceStore.ts,
which applies it to <html> immediately — nothing to save or confirm.
The theme previews are drawn with FIXED colours, not theme tokens: each one
has to show what that theme looks like, whichever theme is active now.
STRUCTURE IS SHARED, LOOK IS NOT: template and script are identical in
user-app and user-app-v2; only the "Look" block differs, per app.
-->
<template>
<div class="ap">
<PageBar title="تنظیمات ظاهری" back-to="userPage::hasSubRoute" />
<container class="ap__body">
<!-- ── Theme ───────────────────────────────────────────── -->
<section class="ap__section">
<header class="ap__head">
<h2 id="ap-theme" class="ap__title">پوسته برنامه</h2>
<p class="ap__sub">ظاهر روشن یا تیره، یا هماهنگ با تنظیمات گوشی شما</p>
</header>
<div class="ap__themes" role="radiogroup" aria-labelledby="ap-theme">
<button
v-for="opt in THEMES"
:key="opt.value"
type="button"
role="radio"
class="ap__theme"
:class="{ 'is-on': appearance.theme === opt.value }"
:aria-checked="appearance.theme === opt.value"
@click="appearance.setTheme(opt.value)"
>
<span class="ap__preview" :class="`ap__preview--${opt.value}`" aria-hidden="true">
<span class="ap__pvBar"></span>
<span class="ap__pvCard"></span>
<span class="ap__pvLine"></span>
<span class="ap__pvLine ap__pvLine--short"></span>
<span class="ap__pvBtn"></span>
</span>
<span class="ap__themeLabel">
<Icon :icon="opt.icon" size="15" />
{{ opt.label }}
</span>
<span class="ap__check" aria-hidden="true">
<Icon icon="vuesax-linear:tick-circle" size="18" />
</span>
</button>
</div>
<p v-if="appearance.theme === 'system'" class="ap__note">
الان {{ appearance.resolvedTheme === 'dark' ? 'تیره' : 'روشن' }} است، مطابق تنظیمات گوشی.
</p>
</section>
<!-- ── Motion ──────────────────────────────────────────── -->
<section class="ap__section">
<header class="ap__head">
<h2 class="ap__title">انیمیشن‌ها</h2>
<p class="ap__sub">برای سرعت بیشتر روی گوشی‌های ضعیف‌تر</p>
</header>
<!-- The whole row is the switch: one tap target, one announcement. -->
<button
type="button"
role="switch"
class="ap__row"
:aria-checked="appearance.reduceMotion"
@click="appearance.setReduceMotion(!appearance.reduceMotion)"
>
<span class="ap__rowIcon" aria-hidden="true">
<Icon icon="vuesax-linear:flash" size="18" />
</span>
<span class="ap__rowText">
<b>حالت سبک و سریع</b>
<small>
حرکت‌های سنگین، افکت‌های شیشه‌ای و اسلاید خودکار خاموش می‌شوند؛ بازخورد کوتاه لمس‌ها و جابه‌جایی‌های سریع
باقی می‌مانند تا برنامه خشک و بی‌روح نشود.
</small>
</span>
<ToggleSwitch :on="appearance.reduceMotion" />
</button>
</section>
</container>
</div>
</template>
<script setup lang="ts">
import Icon from '../../components/icon/Icon.vue'
import PageBar from '../../components/ui/PageBar.vue'
import ToggleSwitch from '../../components/ui/ToggleSwitch.vue'
import { useAppearanceStore, type ThemeChoice } from '../../stores/appearanceStore'
const appearance = useAppearanceStore()
const THEMES: { value: ThemeChoice; label: string; icon: string }[] = [
{ value: 'light', label: 'روشن', icon: 'vuesax-linear:sun-1' },
{ value: 'dark', label: 'تیره', icon: 'vuesax-linear:moon' },
{ value: 'system', label: 'مطابق سیستم', icon: 'vuesax-linear:monitor-mobbile' },
]
</script>
<style scoped lang="less">
/* ── Structure (same in both apps) ──────────────────────────── */
.ap {
min-height: 100vh;
}
.ap__body {
padding-top: 76px;
padding-bottom: 90px;
display: flex;
flex-direction: column;
gap: 26px;
}
.ap__title,
.ap__sub,
.ap__note {
margin: 0;
}
.ap__themes {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.ap__theme {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
cursor: pointer;
font-family: inherit;
-webkit-tap-highlight-color: transparent;
transition: transform 0.15s ease, border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
}
.ap__theme:active {
transform: scale(0.97);
}
.ap__themeLabel {
display: inline-flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.ap__check {
position: absolute;
top: 8px;
inset-inline-end: 8px;
display: grid;
place-items: center;
opacity: 0;
transform: scale(0.6);
transition: opacity 0.18s ease, transform 0.22s cubic-bezier(0.32, 0.72, 0, 1);
}
.ap__theme.is-on .ap__check {
opacity: 1;
transform: none;
}
// Mini screen, fixed colours on purpose (see the header).
.ap__preview {
position: relative;
width: 100%;
aspect-ratio: 3 / 4;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 6%;
padding: 10% 12%;
--pv-bg: #ffffff;
--pv-bar: #f4f5f6;
--pv-card: #ecedef;
--pv-line: #dadce0;
--pv-btn: #14161a;
background: var(--pv-bg);
}
.ap__preview--dark {
--pv-bg: #16171a;
--pv-bar: #1e2024;
--pv-card: #2b2e34;
--pv-line: #3a3e45;
--pv-btn: #f1f2f4;
}
// System: the light screen with its lower-left half in dark.
.ap__preview--system::after {
content: '';
position: absolute;
inset: 0;
background: #16171a;
clip-path: polygon(100% 0, 100% 100%, 0 100%);
opacity: 0.94;
}
.ap__pvBar {
height: 9%;
border-radius: 4px;
background: var(--pv-bar);
}
.ap__pvCard {
flex: 1;
border-radius: 7px;
background: var(--pv-card);
}
.ap__pvLine {
height: 6%;
border-radius: 3px;
background: var(--pv-line);
}
.ap__pvLine--short {
width: 60%;
}
.ap__pvBtn {
height: 11%;
border-radius: 999px;
background: var(--pv-btn);
}
.ap__row {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
font-family: inherit;
text-align: start;
-webkit-tap-highlight-color: transparent;
transition: transform 0.15s ease, border-color 0.2s ease;
}
.ap__row:active {
transform: scale(0.985);
}
.ap__rowIcon {
flex: 0 0 auto;
display: grid;
place-items: center;
}
.ap__rowText {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
/* ── Look: user-app ─────────────────────────────────────────── */
// White hairline-bordered cards at the 16px radius on the soft-grey ground,
// chosen = a brand-green border with a green check (the app's chips and
// variant picker); the motion row is a card with the dashboard tiles' grey
// icon tile.
.ap {
background: @canvasSoft;
}
.ap__head {
margin-bottom: 13px;
}
.ap__title {
font-size: 15px;
font-weight: 800;
color: @ink;
letter-spacing: -0.1px;
}
.ap__sub {
margin-top: 5px;
font-size: 11.5px;
color: @inkFaint;
}
.ap__note {
margin-top: 10px;
font-size: 11.5px;
color: @inkMuted;
}
.ap__theme {
padding: 8px 8px 11px;
border: 1px solid @hairline;
border-radius: 16px;
background: @canvas;
color: @inkMuted;
}
.ap__theme.is-on {
color: @ink;
border-color: @primary;
box-shadow: 0 0 0 1px @primary;
}
.ap__preview {
border-radius: 11px;
box-shadow: 0 0 0 1px rgba(20, 22, 26, 0.07);
}
.ap__themeLabel {
font-size: 12px;
font-weight: 700;
}
.ap__check {
width: 22px;
height: 22px;
border-radius: 50%;
color: #fff;
background: @primary;
}
.ap__row {
padding: 14px;
border: 1px solid @hairline;
border-radius: 16px;
background: @canvas;
}
.ap__rowIcon {
width: 38px;
height: 38px;
border-radius: 12px;
color: @inkSoft;
background: @hairlineSoft;
}
.ap__rowText b {
font-size: 13px;
font-weight: 800;
color: @ink;
}
.ap__rowText small {
font-size: 11px;
line-height: 1.9;
color: @inkFaint;
}
</style>
@@ -153,7 +153,7 @@ function finish() {
.cpf__error {
margin: 2px;
font-size: 12.5px;
color: #dc2626;
color: @red;
}
.cpf__submit {
+20 -9
View File
@@ -130,8 +130,8 @@
</template>
<template v-slot:title>آیا می خواهید از برنامه خارج شوید؟</template>
<template v-slot:button>
<div class="text-center text-[#23493c]" @click="logOut">بله</div>
<div class="text-center text-[#6b7280]" @click="data.notify = false">بی خیال</div>
<div class="text-center text-[color:var(--c-primary)]" @click="logOut">بله</div>
<div class="text-center text-[color:var(--c-inkMuted)]" @click="data.notify = false">بی خیال</div>
</template>
</DialogVant>
@@ -146,7 +146,7 @@
</div>
</template>
<template v-slot:button>
<div class="text-center text-[#23493c]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center text-[color:var(--c-primary)]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center text-danger" @click="data.loginNotify = false">بیخیال</div>
</template>
</DialogVant>
@@ -208,11 +208,22 @@ const EXPERT_TILES = [
{ id: 5, icon: 'vuesax-linear:profile-2user', title: 'مشتریان من', disabled: false },
]
const list = computed(() =>
session.isSalesExpert ? [...BASE_TILES, ...EXPERT_TILES] : BASE_TILES,
)
// Appearance (theme + reduced motion) — always last, and open to everyone:
// device preferences, not account data. See goToDetails.
const APPEARANCE_TILE = { id: 7, icon: 'vuesax-linear:brush', title: 'ظاهر', disabled: false }
const list = computed(() => [
...BASE_TILES,
...(session.isSalesExpert ? EXPERT_TILES : []),
APPEARANCE_TILE,
])
function goToDetails(id: number) {
// The only tile that needs no login.
if (id === 7) {
router.push({ name: 'appearanceSettings' })
return
}
if (!session.isLoggedIn) {
data.loginNotify = true
return
@@ -260,7 +271,7 @@ onMounted(async () => {
<style scoped lang="less">
.dash {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -270,7 +281,7 @@ onMounted(async () => {
left: 0;
right: 0;
z-index: 20;
background: rgba(255, 255, 255, 0.88);
background: alpha(@canvas, 88%);
backdrop-filter: blur(14px);
border-bottom: 1px solid @hairline;
}
@@ -368,7 +379,7 @@ onMounted(async () => {
}
.dash__seal.is-creditor {
color: @ink;
color: #14161A;
background: rgba(255, 255, 255, 0.9);
}
@@ -64,7 +64,7 @@ onMounted(async () => {
<style scoped lang="less">
.favs {
background: @canvas;
background: @page;
min-height: 100vh;
}
+1 -1
View File
@@ -204,7 +204,7 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
<style scoped lang="less">
.auth {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -24,7 +24,7 @@ import PageBar from '../../components/ui/PageBar.vue'
<style scoped lang="less">
.notif {
background: @canvas;
background: @page;
min-height: 100vh;
}
+2 -2
View File
@@ -225,7 +225,7 @@ onMounted(loadOrders)
<style scoped lang="less">
.ord {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -313,7 +313,7 @@ onMounted(loadOrders)
gap: 12px;
padding: 11px 16px;
border-bottom: 1px solid @hairline;
background: rgba(255, 255, 255, 0.92);
background: alpha(@canvas, 92%);
backdrop-filter: blur(14px);
}
+2 -2
View File
@@ -125,7 +125,7 @@ onMounted(async () => {
<style scoped lang="less">
.prof {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -179,7 +179,7 @@ onMounted(async () => {
.prof__seal.is-ok {
color: @green;
background: #f0fdf4;
background: alpha(@green, 10%);
}
.prof__seal.is-pending {
@@ -127,7 +127,7 @@ const register = async () => {
<style scoped lang="less">
.auth {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -152,7 +152,7 @@ onMounted(async () => {
<style scoped lang="less">
.tx {
background: @canvas;
background: @page;
min-height: 100vh;
}
@@ -193,7 +193,7 @@ onMounted(async () => {
.tx__mark.is-in {
color: @green;
background: #f0fdf4;
background: alpha(@green, 10%);
}
.tx__mark.is-out {