This commit is contained in:
缪进兴
2025-11-13 09:51:53 +08:00
commit cc82c1f177
54 changed files with 9373 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

9
.vscode/extendsions.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"recommendations": [
"vue.volar",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"streetsidesoftware.code-spell-checker",
"christian-kohler.path-intellisense"
]
}

19
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,19 @@
{
// Vue 组件名称驼峰提示
"volar.completion.preferredTagNameCase": "camelCase",
// 属性名称驼峰提示
"volar.completion.preferredAttrNameCase": "camelCase",
// 禁用 Vue 2 兼容性提示
"volar.completion.vue2BuiltIn": false,
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.classAttributes": ["class", "ui"],
"tailwindCSS.experimental.classRegex": [
["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}

75
.vscode/vue3.code-snippets vendored Normal file
View File

@@ -0,0 +1,75 @@
{
// Place your 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Print Vue3 SFC": {
"scope": "vue",
"prefix": "v3",
"body": [
"<template>",
" <div class=\"\">$3</div>",
"</template>\n",
"<script lang=\"ts\" setup>",
"defineOptions({",
" name: '$1'",
"})",
"</script>\n",
"<style lang=\"scss\" scoped>",
"//$4",
"</style>\n",
],
},
"Print style": {
"scope": "vue",
"prefix": "st",
"body": [
"<style lang=\"scss\" scoped>",
"//",
"</style>\n"
],
},
"Print script": {
"scope": "vue",
"prefix": "sc",
"body": [
"<script lang=\"ts\" setup>",
"//$1",
"</script>\n"
],
},
"Print script with definePage": {
"scope": "vue",
"prefix": "scdp",
"body": [
"<script lang=\"ts\" setup>",
"definePage({",
" style: {",
" navigationBarTitleText: '$1',",
" },",
"})",
"</script>\n"
],
},
"Print template": {
"scope": "vue",
"prefix": "te",
"body": [
"<template>",
" <div class=\"\">$1</div>",
"</template>\n"
],
},
}

75
README.md Normal file
View File

@@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

7
app/app.config.ts Normal file
View File

@@ -0,0 +1,7 @@
export default defineAppConfig({
ui: {
container:{
base:""
}
},
});

15
app/app.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
<div class="w-full h-screen">
<UApp>
<!-- <SearchFilter/> -->
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</div>
</template>
<script lang="ts" setup>
import SearchFilter from './components/search-filter/index.vue'
</script>

42
app/assets/css/global.css Normal file
View File

@@ -0,0 +1,42 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-primary: #dc5f3f;
/* text */
--color-text-primary: #fff;
/* 灰 */
--color-gray-1:#ffffff;
--color-gray-2:#fafafa;
--color-gray-3:#f5f5f5;
--color-gray-4:#f0f0f0;
--color-gray-5:#d9d9d9;
--color-gray-6:#bfbfbf;
--color-gray-7:#8c8c8c;
--color-gray-8:#595959;
--color-gray-9:#434343;
--color-gray-10:#262626;
}
@theme {
--color-primary:'#dc5f3f'
}
@theme dark {
--color-primary: #dc5f3f;
--colors-primary: 255, 108, 71;
--colors-green: 39, 214, 57;
--colors-yellow: 255, 189, 0;
--colors-red: 255, 38, 73;
}

3
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
@import "@nuxt/ui";
@import "./global.css";

View File

BIN
app/assets/videos/beep.mp3 Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

View File

@@ -0,0 +1,13 @@
<template>
<div class="h-[250px]">
<USkeleton>
</USkeleton>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'GoodsSkeleton'
})
</script>

View File

@@ -0,0 +1,76 @@
<template>
<NuxtLink
:class="[
wrapperCls,
exteriorCls,
'transition-transform duration-200 ease-out hover:-translate-y-1 active:-translate-y-1 focus:-translate-y-1 '
]"
>
<!-- top tags -->
<div class="px-2 pt-2 w-full top-0 left-0 flex items-center">
<!-- exterior 外观 -->
<div class="text-xs flex-1 [&>:nth-child(n+2)::before]:content-['/'] [&>:nth-child(n+2)::before]:mx-2 [&>:nth-child(n+2)::before]:text-text-primary/60 [&>:nth-child(n+2)::before]:font-bold">
<span class="">
略有磨损
</span>
<span class=" text-[#c169ff]">ST</span>
<span class="text-amber-400">SV</span>
</div>
<!-- 销售 -->
<div class="text-right flex items-center justify-end text-sm text-text-primary/60">
<span>88</span>
<span class="ml-1 bg-[url('/images/sale.png')] bg-cover bg-center bg-no-repeat w-3 h-3"></span>
</div>
</div>
<!-- cover -->
<div class="relative overflow-hidden">
<div class="group-hover:scale-105 flex justify-center items-center h-32 mt-2 mb-2 delay-300 animate-scaletransition delay-150 duration-300 ">
<img alt="MP7 | 橘皮涂装" loading="lazy" width="0" height="0" decoding="async" data-nimg="1" class="block " src="https://img.zbt.com/e/steam/item/730/U291dmVuaXIgTVA3IHwgT3JhbmdlIFBlZWwgKEZhY3RvcnkgTmV3KQ==.png?x-oss-process=image/resize,w_220" style="color: transparent; object-fit: contain; width: 162px; height: 122px;">
</div>
</div>
<!-- footer -->
<div class="px-3 pb-3">
<div class="flex justify-start items-center ">
<div class="text-2xl text-text-primary">
<span class="pr-0.5">HK$</span>
<span class="text-2xl text-text-primary">359.04</span>
</div>
<!-- sale info -->
<div class="ml-4 text-white text-xs w-10 h-4 flex justify-center items-center bg-[url('/images/goods-item/discount.png')] bg-center bg-no-repeat bg-cover" style="background-size: 100% 100%; --ml: 16px;">
-38%
</div>
</div>
<!-- suggested price -->
<div class="text-xs font-medium text-text-primary/20 truncate flex items-center mt-1 mb-3">
<!-- suggested price -->
<div :class="false ? 'line-through' : undefined">建议价格 </div>
<div class="text-xs text-text-primary/20 pl-1">
<span class="pr-0.5">HK$</span>
<span class="numFont text-xs text-text-primary/20 ">575.42</span>
</div>
</div>
<h4 class="text-sm font-bold text-text-primary/60 truncate">格洛克 18 | 子弹皇后 (久经沙场)</h4></div>
</NuxtLink>
</template>
<script lang="ts" setup>
defineOptions({
name: 'GoodsItem'
})
const wrapperCls = "w-full h-[266px] cursor-pointer relative bg-linear-to-b from-[rgb(46,49,50)] to-[rgb(35,39,40)] rounded-sm overflow-hidden"
const exteriorCls = computed(()=>[
'text-lime-700'
])
const exteriorClsMap:Record<string,string> = {
// 完好
"0":'text-teal-200',
//
"1": 'text-lime-700',
"2":""
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<!-- 虚拟列表容器开启滚动 -->
<div
class="grid gap-2 h-100"
v-bind="containerProps"
>
<!-- wrapper -->
<div
v-bind="wrapperProps" class="border border-gray-2 mb-2 w-50 h-[266px] flex items-center justify-center"
v-for="{ index , data } in list"
>
Row {{ index }} <span class="text-gray-1 ml-1">{{ data.content }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { useVirtualList } from '@vueuse/core'
defineOptions({
name: 'GoodsVirtuaList'
})
// 2. 生成测试数据1000 条)
const totalCount = 100000
const data = Array.from({ length: totalCount }, (_, i) => ({
id: i,
content: `Item ${i + 1}`
}))
// 5. 使用 useVirtualList 计算可视项目
const { list, containerProps, wrapperProps, scrollTo , } = useVirtualList(
data,
{
itemHeight:266,
overscan: 10,
},
)
</script>

View File

@@ -0,0 +1,13 @@
<template>
<!-- wrapper -->
<div class="grid gap-1 w-60">
<slot />
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'GoodsWrapper'
})
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div class="">
<!-- adv -->
<!-- search fliter -->
<FilterGroup title="价格">
<PriceRangeInput v-model="priceRange" :max="6000" />
<StatusInput />
</FilterGroup>
<TypeFilter />
</div>
</template>
<script lang="ts" setup>
import FilterGroup from './widgets/filter-group/index.vue'
import PriceRangeInput from './widgets/price-range-input/index.vue'
import StatusInput from './widgets/status-input/index.vue'
import TypeFilter from './widgets/type-filter/index.vue'
defineOptions({
name: 'SearchFilter'
})
const priceRange = ref<[number,number]>([0 , 5000])
const data = reactive({
minPrice: 0,
maxPrice: 5000,
})
</script>

View File

@@ -0,0 +1,54 @@
<template>
<UCollapsible :open="checked" class="relative flex flex-col w-60">
<!-- header -->
<div class="flex items-center text-sm font-semibold h-10 px-3 group cursor-pointer hover:bg-text-primary/10 relative">
<!-- 标题区域 - 点击不触发展开/关闭 -->
<span class="flex-1">
{{ title }} {{ unit }}
</span>
<!-- 箭头图标 - 点击触发展开/关闭 -->
<button type="button" @click.stop="toggleAccordion" class="w-3 h-3 absolute right-2 top-1/2 -mt-1.5 p-0 m-0 leading-0">
<UIcon name="ri:play-large-fill" :class="triggerCls"></UIcon>
</button>
</div>
<!-- content -->
<template #content>
<div class="h-auto">
<slot />
</div>
</template>
</UCollapsible>
</template>
<script lang="ts" setup>
defineOptions({
name: 'FilterGroup'
})
const props = withDefaults(defineProps<{
title?: string,
unit?: string
}>(), {
title: '',
unit: 'CNY'
})
const checked = ref(false)
const toggleAccordion = () => {
console.log(1111);
checked.value = !checked.value
}
const triggerCls = computed(() => {
return [
"size-3 text-text-primary/60 transition-transform duration-250 left-0 top-0",
checked.value ? "-rotate-90" : 'rotate-90'
]
})
</script>

View File

@@ -0,0 +1,56 @@
<template>
<div class="w-full relative py-4">
<div class="flex flex-col gap-4">
<!-- Slider -->
<div class="h-10 flex items-center px-4">
<USlider :min="min" :max="max" v-model="value" />
</div>
<!-- input -->
<div class="relative flex items-center gap-4 box-content border border-text-primary/40 h-13 rounded-md">
<!-- input item -->
<div class="flex flex-col justify-start items-start gap-1">
<label class="text-text-primary/60 font-bold text-xs ml-2 mt-2 p-0 leading-none">{{ `最小 (RMB)` }}</label>
<UInputNumber
name="minPrice"
:min="min"
:max="value[1]"
:increment="false"
:decrement="false"
v-model="value[0]"
:ui="{ base:'border-0 focus-visible:ring-0 ring-0 border-text-primary/40 text-text-primary/80 font-medium text-sm bg-[none] py-0' }"
/>
</div>
<div class="w-px "></div>
<div class="flex flex-col justify-start items-start gap-1">
<label class="text-text-primary/60 font-bold text-xs ml-2 mt-2 p-0 leading-none">{{ `最大 (RMB)` }}</label>
<UInputNumber
name="minPrice"
:min="min"
:max="value[1]"
:increment="false"
:decrement="false"
v-model="value[0]"
:ui="{ base:'border-0 focus-visible:ring-0 ring-0 border-text-primary/40 text-text-primary/80 font-medium text-sm bg-[none] py-0' }"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PriceRangeInputProps } from './interface'
defineOptions({ name: 'PriceRangeInput' })
const value = defineModel<[number,number]>({default:()=>[0,1600]})
withDefaults(defineProps<PriceRangeInputProps>(), {
min: 0,
max: 1600
})
</script>

View File

@@ -0,0 +1,7 @@
export interface PriceRangeInputProps {
// 最小值
min?: number;
// 最大值
max?: number;
}

View File

@@ -0,0 +1,41 @@
<template>
<div class="relative bg-black/10">
<UCheckboxGroup
v-model="value"
:items="options"
:ui="{
item:'h-8 hover:bg-white/10 items-center px-4'
}"
/>
</div>
</template>
<script lang="ts">
const options = [
{
label:"可出售",
value:1,
},
{
label:"售卖中",
value:2,
},
{
label:"不可出售",
value:3,
},
{
label:"冷却期",
value:4,
}
] as Option[]
</script>
<script lang="ts" setup>
import type { Option } from 'types'
defineOptions({
name: 'StatusInput'
})
const value = defineModel<number[]>({default:[]})
</script>

View File

@@ -0,0 +1,3 @@
export interface StatusInputProps {
}

View File

@@ -0,0 +1,147 @@
<template>
<FilterGroup title="类型" :unit="''">
<UTree
:items="items"
@select="onSelect"
bubble-select
:as="{link:'div'}"
propagate-select
multiple
:nested="false"
>
<!-- 自定义节点前缀 -->
<template #item-leading="{ selected, indeterminate, handleSelect }">
<UCheckbox
:model-value="indeterminate ? 'indeterminate' : selected"
tabindex="-1"
@change="handleSelect"
@click.stop
/>
</template>
<!-- 自定义节点label -->
<template #item-label="{ item }" >
<span @click.stop>{{ item.label }}</span>
</template>
</UTree>
</FilterGroup>
</template>
<script lang="ts">
const items: TreeOption<string>[] = [
{
label: "武器",
value: "weapons",
children: [
{ label: "长剑", value: "longsword" },
{ label: "短剑", value: "shortsword" },
{ label: "巨剑", value: "greatsword" },
{ label: "武士刀", value: "katana" }
]
},
{
label: "防具",
value: "armor",
children: [
{ label: "头盔", value: "helmet" },
{ label: "胸甲", value: "chestplate" },
{ label: "护腿", value: "leggings" },
{ label: "靴子", value: "boots" }
]
},
{
label: "工具",
value: "tools",
children: [
{ label: "斧头", value: "axe" },
{ label: "镐", value: "pickaxe" },
{ label: "铲子", value: "shovel" },
{ label: "锤子", value: "hammer" }
]
},
{
label: "魔法物品",
value: "magic_items",
children: [
{ label: "法杖", value: "staff" },
{ label: "魔杖", value: "wand" },
{ label: "魔法书", value: "spellbook" },
{ label: "水晶球", value: "crystal_ball" }
]
},
{
label: "消耗品",
value: "consumables",
children: [
{ label: "治疗药水", value: "healing_potion" },
{ label: "法力药水", value: "mana_potion" },
{ label: "解毒剂", value: "antidote" },
{ label: "食物", value: "food" }
]
},
{
label: "材料",
value: "materials",
children: [
{ label: "木材", value: "wood" },
{ label: "矿石", value: "ore" },
{ label: "皮革", value: "leather" },
{ label: "布料", value: "cloth" }
]
},
{
label: "饰品",
value: "accessories",
children: [
{ label: "戒指", value: "ring" },
{ label: "项链", value: "necklace" },
{ label: "耳环", value: "earring" },
{ label: "手镯", value: "bracelet" }
]
},
{
label: "远程武器",
value: "ranged_weapons",
children: [
{ label: "弓", value: "bow" },
{ label: "弩", value: "crossbow" },
{ label: "飞镖", value: "dart" },
{ label: "投石索", value: "sling" }
]
},
{
label: "盾牌",
value: "shields",
children: [
{ label: "圆盾", value: "buckler" },
{ label: "塔盾", value: "tower_shield" },
{ label: "骑士盾", value: "knight_shield" },
{ label: "魔法盾", value: "magic_shield" }
]
},
{
label: "特殊物品",
value: "special_items",
children: [
{ label: "钥匙", value: "key" },
{ label: "地图", value: "map" },
{ label: "宝石", value: "gem" },
{ label: "古董", value: "antique" }
]
}
];
</script>
<script lang="ts" setup>
import type { TreeItemSelectEvent } from 'reka-ui'
import type { TreeOption } from 'types'
import FilterGroup from '../filter-group/index.vue'
defineOptions({ name: 'TypeFilter' })
const onSelect = (e: TreeItemSelectEvent<TreeOption<string>>) => {
if (e.detail.originalEvent.type === 'click') {
e.preventDefault()
}
}
</script>

View File

@@ -0,0 +1,256 @@
<template>
<div :class="cn('relative h-full w-full', props.containerClass)">
<Motion
ref="containerRef"
as="div"
:initial="{ opacity: 0 }"
:animate="{ opacity: 1 }"
class="absolute inset-0 z-0 flex size-full items-center justify-center bg-transparent"
>
<canvas ref="canvasRef"></canvas>
</Motion>
<div :class="cn('relative z-10', props.class)">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { createNoise3D } from "simplex-noise";
import { onMounted, onUnmounted } from "vue";
import { useDebounceFn, templateRef } from "@vueuse/core";
import { cn } from "@/lib/utils";
const TAU = 2 * Math.PI;
const BASE_TTL = 50;
const RANGE_TTL = 150;
const PARTICLE_PROP_COUNT = 9;
const RANGE_HUE = 100;
const NOISE_STEPS = 3;
const X_OFF = 0.00125;
const Y_OFF = 0.00125;
const Z_OFF = 0.0005;
interface VortexProps {
class?: string;
containerClass?: string;
particleCount?: number;
rangeY?: number;
baseHue?: number;
baseSpeed?: number;
rangeSpeed?: number;
baseRadius?: number;
rangeRadius?: number;
backgroundColor?: string;
}
const props = withDefaults(defineProps<VortexProps>(), {
particleCount: 700,
rangeY: 100,
baseSpeed: 0.0,
rangeSpeed: 1.5,
baseRadius: 1,
rangeRadius: 2,
baseHue: 220,
backgroundColor: "#000000",
});
const tick = ref<number>(0);
const animationFrame = ref<number | null>(null);
const particleProps = shallowRef<Float32Array | null>(null);
const center = ref<[number, number]>([0, 0]);
const ctx = shallowRef<CanvasRenderingContext2D | null>(null);
const canvasRef = templateRef<HTMLCanvasElement | null>("canvasRef");
const containerRef = templateRef<HTMLElement | null>("containerRef");
const particleCache = {
x: 0,
y: 0,
vx: 0,
vy: 0,
life: 0,
ttl: 0,
speed: 0,
radius: 0,
hue: 0,
};
const noise3D = createNoise3D();
function rand(n: number) {
return n * Math.random();
}
function randRange(n: number): number {
return n - rand(2 * n);
}
function fadeInOut(t: number, m: number): number {
const hm = 0.5 * m;
return Math.abs(((t + hm) % m) - hm) / hm;
}
function lerp(n1: number, n2: number, speed: number): number {
return (1 - speed) * n1 + speed * n2;
}
function initParticle(i: number) {
if (!particleProps.value || !canvasRef.value) return;
const canvas = canvasRef.value;
particleCache.x = rand(canvas.width);
particleCache.y = center.value[1] + randRange(props.rangeY);
particleCache.vx = 0;
particleCache.vy = 0;
particleCache.life = 0;
particleCache.ttl = BASE_TTL + rand(RANGE_TTL);
particleCache.speed = props.baseSpeed + rand(props.rangeSpeed);
particleCache.radius = props.baseRadius + rand(props.rangeRadius);
particleCache.hue = props.baseHue + rand(RANGE_HUE);
particleProps.value.set(
[
particleCache.x,
particleCache.y,
particleCache.vx,
particleCache.vy,
particleCache.life,
particleCache.ttl,
particleCache.speed,
particleCache.radius,
particleCache.hue,
],
i,
);
}
function updateParticle(i: number) {
if (!particleProps.value || !canvasRef.value || !ctx.value) return;
const canvas = canvasRef.value;
const props = particleProps.value as Record<number,any>;
const context = ctx.value;
//
particleCache.x = props[i];
particleCache.y = props[i + 1];
particleCache.vx = props[i + 2];
particleCache.vy = props[i + 3];
particleCache.life = props[i + 4];
particleCache.ttl = props[i + 5];
particleCache.speed = props[i + 6];
particleCache.radius = props[i + 7];
particleCache.hue = props[i + 8];
const n =
noise3D(particleCache.x * X_OFF, particleCache.y * Y_OFF, tick.value * Z_OFF) *
NOISE_STEPS *
TAU;
const nextVx = lerp(particleCache.vx, Math.cos(n), 0.5);
const nextVy = lerp(particleCache.vy, Math.sin(n), 0.5);
const nextX = particleCache.x + nextVx * particleCache.speed;
const nextY = particleCache.y + nextVy * particleCache.speed;
context.save();
context.lineCap = "round";
context.lineWidth = particleCache.radius;
context.strokeStyle = `hsla(${particleCache.hue},100%,60%,${fadeInOut(
particleCache.life,
particleCache.ttl,
)})`;
context.beginPath();
context.moveTo(particleCache.x, particleCache.y);
context.lineTo(nextX, nextY);
context.stroke();
context.restore();
props[i] = nextX;
props[i + 1] = nextY;
props[i + 2] = nextVx;
props[i + 3] = nextVy;
props[i + 4] = particleCache.life + 1;
if (
nextX > canvas.width ||
nextX < 0 ||
nextY > canvas.height ||
nextY < 0 ||
particleCache.life > particleCache.ttl
) {
initParticle(i);
}
}
function draw() {
if (!canvasRef.value || !ctx.value || !particleProps.value) return;
const canvas = canvasRef.value;
const context = ctx.value;
tick.value++;
context.fillStyle = props.backgroundColor;
context.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < particleProps.value.length; i += PARTICLE_PROP_COUNT) {
updateParticle(i);
}
context.save();
context.filter = "blur(8px) brightness(200%)";
context.globalCompositeOperation = "lighter";
context.drawImage(canvas, 0, 0);
context.restore();
context.save();
context.filter = "blur(4px) brightness(200%)";
context.globalCompositeOperation = "lighter";
context.drawImage(canvas, 0, 0);
context.restore();
animationFrame.value = requestAnimationFrame(draw);
}
const handleResize = useDebounceFn(() => {
if (!canvasRef.value) return;
const canvas = canvasRef.value;
const { innerWidth, innerHeight } = window;
canvas.width = innerWidth;
canvas.height = innerHeight;
center.value = [0.5 * canvas.width, 0.5 * canvas.height];
}, 150);
onMounted(() => {
const canvas = canvasRef.value;
if (!canvas) return;
ctx.value = canvas.getContext("2d");
if (!ctx.value) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
center.value = [0.5 * canvas.width, 0.5 * canvas.height];
const particlePropsLength = props.particleCount * PARTICLE_PROP_COUNT;
particleProps.value = new Float32Array(particlePropsLength);
for (let i = 0; i < particlePropsLength; i += PARTICLE_PROP_COUNT) {
initParticle(i);
}
draw();
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value);
}
window.removeEventListener("resize", handleResize);
ctx.value = null;
particleProps.value = null;
});
</script>

14
app/error.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<div>
<h1>{{ error?.statusCode }}</h1>
<NuxtLink to="/">Go back home</NuxtLink>
</div>
</template>
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps({
error: Object as () => NuxtError,
})
</script>

View File

@@ -0,0 +1,12 @@
<template>
<UContainer class="w-full max-w-full mx-auto sm:px-0 lg:px-0">
<slot />
</UContainer>
</template>
<script lang="ts" setup>
defineOptions({
name: ''
})
</script>

View File

@@ -0,0 +1,58 @@
<template>
<UFooter>
<template #left>
<p class="text-muted text-sm">Copyright © {{ new Date().getFullYear() }}</p>
</template>
<UNavigationMenu :items="items" variant="link" />
<template #right>
<!-- <UButton
icon="i-simple-icons-discord"
color="neutral"
variant="ghost"
to="/"
target="_blank"
aria-label="Discord"
/>
<UButton
icon="i-simple-icons-x"
color="neutral"
variant="ghost"
to="/"
target="_blank"
aria-label="X"
/>
<UButton
icon="i-simple-icons-github"
color="neutral"
variant="ghost"
to="/"
target="_blank"
aria-label="GitHub"
/> -->
</template>
</UFooter>
</template>
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'
const items: NavigationMenuItem[] = [
{
label: 'Figma Kit',
to: 'https://go.nuxt.com/figma-ui',
target: '_blank'
},
{
label: 'Playground',
to: 'https://stackblitz.com/edit/nuxt-ui',
target: '_blank'
},
{
label: 'Releases',
to: 'https://github.com/nuxt/ui/releases',
target: '_blank'
}
]
</script>

View File

@@ -0,0 +1,54 @@
<template>
<UHeader>
<template #title>
<div class="h-6 w-auto" />
</template>
<UNavigationMenu :items="items" />
<template #right>
<UColorModeButton />
<UTooltip text="Open on GitHub" :kbds="['meta', 'G']">
<UButton
color="neutral"
variant="ghost"
to="https://github.com/nuxt/ui"
target="_blank"
aria-label="GitHub"
/>
</UTooltip>
</template>
</UHeader>
</template>
<script lang="ts" setup>
import type { NavigationMenuItem } from '@nuxt/ui'
const route = useRoute()
const items = computed<NavigationMenuItem[]>(() => [
{
label: 'Docs',
to: 'https://go.nuxt.com/figma-ui',
active: route.path.startsWith('/docs/getting-started')
},
{
label: 'Components',
to: 'https://go.nuxt.com/figma-ui',
active: route.path.startsWith('/docs/components')
},
{
label: 'Figma',
to: 'https://go.nuxt.com/figma-ui',
target: '_blank'
},
{
label: 'Releases',
to: 'https://github.com/nuxt/ui/releases',
target: '_blank'
}
])
</script>

15
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
<div class="w-full h-screen">
<Header />
<Contairner >
<slot />
</Contairner>
<Footer />
</div>
</template>
<script lang="ts" setup>
import Header from './components/header.vue'
import Footer from './components/footer.vue'
import Contairner from './components/contairner.vue';
</script>

7
app/lib/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { ClassValue } from "clsx"
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

32
app/pages/dev/index.vue Normal file
View File

@@ -0,0 +1,32 @@
<template>
<!-- <GoodsWrapper>
<GoodsItem
/>
</GoodsWrapper> -->
<GoodsVirtualList />
</template>
<script lang="ts" setup>
// import SearchFilter from '@/components/search-filter/index.vue'
// import GoodsWrapper from '@/components/goods-wrapper/index.vue'
// import GoodsItem from '@/components/goods-item/index.vue'
// import Adv from '@/components/adv/index.vue'
import GoodsVirtualList from '@/components/goods-virtual-list/index.vue'
definePageMeta({
layout: 'default',
title: '开发',
})
const listData = Array.from({ length:100 }).fill(1).map((item, index) => {
return {
id: index,
name: `商品${index}`,
price: Math.floor(Math.random() * 1000),
img: 'https://picsum.photos/200/300',
}
})
</script>

11
app/pages/index/index.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<div class="w-full h-screen">
村上春树
</div>
</template>
<script lang="ts" setup>
definePageMeta({
layout: 'default'
})
</script>

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/assets/css/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"composables": "@/composables"
},
"registries": {}
}

32
nuxt.config.ts Normal file
View File

@@ -0,0 +1,32 @@
import tailwindcss from "@tailwindcss/vite";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
css: [
'./app/assets/css/main.css'
],
vite:{
plugins:[
tailwindcss()
]
},
modules: [
'@nuxt/ui',
'motion-v/nuxt',
],
// 字体
fonts:{
// 切换google 字体源
provider: 'bunny'
},
router:{
// 默认路由模式
// options:{
// hashMode: true
// }
},
ssr:false
})

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "app-pc",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/ui": "^4.1.0",
"@vueuse/core": "^13.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.552.0",
"motion-v": "^1.7.4",
"nprogress": "^0.2.0",
"nuxt": "^4.2.0",
"reka-ui": "^2.6.0",
"shared": "workspace:*",
"simplex-noise": "^4.0.3",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vue": "^3.5.22",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@tailwindcss/vite": "^4.1.16",
"tailwindcss": "^4.1.16",
"types": "workspace:*"
},
"workspaces": [
"packages/*"
]
}

View File

@@ -0,0 +1,4 @@
{
"name": "shared",
"main": "src"
}

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,4 @@
{
"name": "types",
"main": "src"
}

1
packages/types/src/common/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from './option.d'

10
packages/types/src/common/option.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
export interface Option<T = any> {
label: string;
value:T
}
export interface TreeOption<T = any> {
label: string;
value:T;
children?: TreeOption<T>[];
}

View File

@@ -0,0 +1 @@
export * from './common'

8006
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
packages:
- 'packages/*'

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/images/noData.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
public/images/sale.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

0
tailwind.config.ts Normal file
View File

18
tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}