Skip to content

Commit

Permalink
feat(market): add market client components
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 17, 2024
1 parent 88bfaea commit e63d15a
Show file tree
Hide file tree
Showing 28 changed files with 1,303 additions and 0 deletions.
203 changes: 203 additions & 0 deletions packages/market/client/components/filter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
<template>
<div class="market-filter-group">
<div class="market-filter-title">
<h2 class="text">{{ t('type.sort') }}</h2>
</div>
<template v-for="(item, key) in comparators" :key="key">
<div
v-if="!item.hidden"
class="market-filter-item"
:class="{ active: activeSort[0] === key }"
@click="toggleSort('sort:' + key, $event)">
<span class="icon"><market-icon :name="item.icon"></market-icon></span>
<span class="text">{{ t(`sort.${key}`) }}</span>
<span class="spacer"></span>
<span class="order"><market-icon :name="activeSort[1]"></market-icon></span>
</div>
</template>
</div>
<div class="market-filter-group">
<div class="market-filter-title">
<h2 class="text">{{ t('type.filter') }}</h2>
</div>
<template v-for="(item, key) in badges" :key="key">
<div
v-if="!item.hidden?.(config ?? {}, 'filter')"
class="market-filter-item"
:class="{ [key]: true, active: words.includes(item.query), disabled: words.includes(item.negate) }"
@click="toggleQuery(item, $event)">
<span class="icon"><market-icon :name="key"></market-icon></span>
<span class="text">{{ t(`badge.${key}`) }}</span>
<span class="spacer"></span>
<span class="count" v-if="data">
{{ data.filter(x => validate(x, item.query, config)).length }}
</span>
</div>
</template>
</div>
</template>

<script lang="ts" setup>
import { computed, inject, ref, watch } from 'vue'
import { Badge, badges, kConfig, validate, comparators, useMarketI18n } from '../utils'
import { SearchObject } from '@cordisjs/registry'
import MarketIcon from '../icons'
const props = defineProps<{
modelValue: string[]
data?: SearchObject[]
}>()
const emit = defineEmits(['update:modelValue'])
const { t } = useMarketI18n()
const config = inject(kConfig, {})
const words = ref<string[]>([])
watch(() => props.modelValue, (value) => {
words.value = value.slice()
}, { immediate: true, deep: true })
const activeSort = computed<string[]>(() => {
let word = words.value.find(w => w.startsWith('sort:'))
if (!word) return ['default', 'desc']
word = word.slice(5)
if (word.endsWith('-desc')) {
return [word.slice(0, -5), 'desc']
} else if (word.endsWith('-asc')) {
return [word.slice(0, -4), 'asc']
} else {
return [word, 'desc']
}
})
function addWord(word: string) {
if (!words.value[words.value.length - 1]) {
words.value.pop()
}
words.value.push(word, '')
}
function toggleSort(word: string, event: MouseEvent) {
const index = words.value.findIndex(x => x.startsWith('sort:'))
if (index === -1) {
if (word === 'sort:default') {
addWord('sort:default-asc')
} else {
addWord(word)
}
} else if (words.value[index] === word || words.value[index] === word + '-desc') {
words.value[index] = word + '-asc'
} else if (words.value[index] === word + '-asc') {
words.value[index] = word
} else {
words.value[index] = word
}
emit('update:modelValue', words.value)
}
function toggleCategory(word: string, event: MouseEvent) {
const index = words.value.findIndex(x => x.startsWith('category:'))
if (index === -1) {
addWord(word)
} else if (words[index] === word) {
words.value.splice(index, 1)
} else {
words.value[index] = word
}
emit('update:modelValue', words.value)
}
function toggleQuery(item: Badge, event: MouseEvent) {
const { query, negate } = item
const index = words.value.findIndex(x => x === query || x === negate)
if (index === -1) {
addWord(query)
} else if (words.value[index] === query) {
words.value[index] = negate
} else {
words.value.splice(index, 1)
}
emit('update:modelValue', words.value)
}
</script>

<style lang="scss" scoped>
.market-filter-item {
display: flex;
margin: 4px 0;
color: var(--k-text-normal);
transition: color 0.5s;
align-items: center;
z-index: 2;
height: 24px;
cursor: pointer;
&:hover {
color: var(--k-text-dark);
}
&.active {
color: var(--k-text-active);
&.verified, &.newborn {
color: var(--k-color-success);
}
&.preview, &.portable {
color: var(--k-color-warning);
}
&.insecure {
color: var(--k-color-danger);
}
}
&.disabled {
opacity: 0.5;
text-decoration: line-through 2px;
}
.icon {
display: inline-flex;
width: 1.75rem;
margin-right: 4px;
align-items: center;
justify-content: center;
}
&:not(.active) .order {
display: none;
}
.order {
display: inline-flex;
width: 1.75rem;
align-items: center;
justify-content: center;
}
svg {
height: 1rem;
max-width: 1.125rem;
}
.text, .count {
line-height: 20px;
font-size: 14px;
font-weight: 500;
}
.count {
margin-right: 4px;
}
.spacer {
flex: 1;
}
}
</style>
114 changes: 114 additions & 0 deletions packages/market/client/components/list.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<template>
<slot name="header" v-bind="{ all, packages, hasFilter: hasFilter(modelValue) }"></slot>
<template v-if="packages.length">
<el-pagination
class="pagination"
background
v-model:current-page="page"
:pager-count="5"
:page-size="limit"
:total="packages.length"
layout="prev, pager, next"
/>
<div class="package-list">
<market-package
v-for="data in pages[page - 1]"
:key="data.package.name"
class="k-card"
:data="data"
:gravatar="gravatar"
@query="onQuery"
#action
>
<slot name="action" v-bind="data"></slot>
</market-package>
</div>
<el-pagination
class="pagination"
background
v-model:current-page="page"
:pager-count="5"
:page-size="limit"
:total="packages.length"
layout="prev, pager, next"
/>
</template>
<k-empty v-else>
没有搜索到相关插件。
</k-empty>
</template>

<script lang="ts" setup>
import { computed, inject, ref, watch } from 'vue'
import { SearchObject } from '@cordisjs/registry'
import { getSorted, getFiltered, hasFilter, kConfig } from '../utils'
import MarketPackage from './package.vue'
const props = defineProps<{
modelValue: string[],
data: SearchObject[],
installed?: (data: SearchObject) => boolean,
gravatar?: string,
}>()
const emit = defineEmits(['update:modelValue', 'update:page'])
const config = inject(kConfig, {})
const all = computed(() => getSorted(props.data, props.modelValue))
const packages = computed(() => getFiltered(all.value, props.modelValue, config))
const limit = computed(() => {
for (const word of props.modelValue) {
if (word.startsWith('limit:')) {
const size = parseInt(word.slice(6))
if (size) return size
}
}
return 24
})
const page = ref(1)
watch(page, (page) => emit('update:page', page))
const pages = computed(() => {
const result: SearchObject[][] = []
for (let i = 0; i < packages.value.length; i += limit.value) {
result.push(packages.value.slice(i, i + limit.value))
}
return result
})
function onQuery(word: string) {
const words = props.modelValue.slice()
if (!words[words.length - 1]) words.pop()
if (!words.includes(word)) words.push(word)
words.push('')
emit('update:modelValue', words)
}
</script>

<style lang="scss" scoped>
.package-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(336px, 1fr));
gap: var(--card-margin);
justify-items: center;
flex: 1 0 auto;
}
.pagination {
margin: var(--card-margin) 0;
justify-content: center;
}
.k-empty {
flex: 1 0 auto;
}
</style>
Loading

0 comments on commit e63d15a

Please sign in to comment.