新增:基础组件

This commit is contained in:
ivan 2026-02-04 19:58:25 +08:00
parent 25ab98b90a
commit 7ceb5c4e38
15 changed files with 7947 additions and 10 deletions

279
design/pencil-new.pen Normal file
View File

@ -0,0 +1,279 @@
{
"version": "2.6",
"children": [
{
"type": "frame",
"id": "bi8Au",
"x": 0,
"y": 0,
"name": "Login Page",
"theme": {
"Mode": "Light",
"Base": "Neutral",
"Accent": "Default"
},
"clip": true,
"width": 800,
"height": 600,
"fill": "#f8b6b6ff",
"layout": "none",
"children": [
{
"type": "frame",
"id": "loginCard",
"x": 216,
"y": 36,
"width": 305,
"height": 533,
"fill": "#f9ebebff",
"cornerRadius": 8,
"stroke": {
"align": "inside",
"thickness": 1,
"fill": "#000000"
},
"layout": "vertical",
"gap": 24,
"padding": 24,
"children": [
{
"type": "frame",
"id": "logoContainer",
"width": "fill_container",
"layout": "vertical",
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "logo",
"fill": "#000000",
"content": "PolyMarket",
"lineHeight": 1.2,
"textAlign": "center",
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 24,
"fontWeight": "600"
}
]
},
{
"type": "frame",
"id": "titleContainer",
"width": "fill_container",
"layout": "vertical",
"gap": 8,
"children": [
{
"type": "text",
"id": "title",
"fill": "#000000",
"content": "Sign in to your account",
"lineHeight": 1.5,
"textAlign": "center",
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 18,
"fontWeight": "500"
},
{
"type": "text",
"id": "subtitle",
"fill": "#000000",
"content": "Enter your email and password to sign in",
"lineHeight": 1.4285714285714286,
"textAlign": "center",
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "emailInputGroup",
"width": "fill_container",
"layout": "vertical",
"gap": 6,
"children": [
{
"type": "frame",
"id": "emailLabelContainer",
"width": "fill_container",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "emailLabel",
"fill": "#000000",
"content": "Email",
"lineHeight": 1.4285714285714286,
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "emailInput",
"width": "fill_container",
"height": 40,
"fill": "#000000",
"cornerRadius": 6,
"stroke": {
"align": "inside",
"thickness": 1,
"fill": "#000000"
},
"padding": [
8,
12
],
"children": [
{
"type": "text",
"id": "emailPlaceholder",
"fill": "#000000",
"content": "your.email@example.com",
"lineHeight": 1.4285714285714286,
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
}
]
}
]
},
{
"type": "frame",
"id": "passwordInputGroup",
"width": "fill_container",
"layout": "vertical",
"gap": 6,
"children": [
{
"type": "frame",
"id": "W4jnH",
"width": "fill_container",
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "passwordLabel",
"fill": "#000000",
"content": "Password",
"lineHeight": 1.4285714285714286,
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "500"
},
{
"type": "text",
"id": "forgotPassword",
"fill": "#000000",
"content": "Forgot password?",
"lineHeight": 1.4285714285714286,
"textAlign": "right",
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "passwordInput",
"width": "fill_container",
"height": 40,
"fill": "#000000",
"cornerRadius": 6,
"stroke": {
"align": "inside",
"thickness": 1,
"fill": "#000000"
},
"padding": [
8,
12
],
"children": [
{
"type": "text",
"id": "passwordPlaceholder",
"fill": "#000000",
"content": "••••••••",
"lineHeight": 1.4285714285714286,
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
}
]
}
]
},
{
"type": "frame",
"id": "signInButton",
"width": "fill_container",
"height": 40,
"fill": "#000000",
"cornerRadius": 6,
"gap": 6,
"padding": [
8,
16
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "signInText",
"fill": "#000000",
"content": "Sign in",
"lineHeight": 1.4285714285714286,
"textAlign": "center",
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "createAccountContainer",
"width": "fill_container",
"layout": "vertical",
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "createAccount",
"fill": "#000000",
"content": "Create account",
"lineHeight": 1.4285714285714286,
"textAlign": "center",
"textAlignVertical": "middle",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
}
]
}
]
}
]
}
]
}

View File

@ -0,0 +1,215 @@
{
"version": "2.6",
"children": [
{
"type": "frame",
"id": "simpleMarketCard",
"x": 0,
"y": 0,
"name": "Simple Market Card",
"theme": {
"Mode": "Light",
"Base": "Neutral",
"Accent": "Default"
},
"enabled": false,
"clip": true,
"width": 350,
"height": 200,
"fill": "#ffffffff",
"layout": "none"
},
{
"type": "frame",
"id": "marketCard",
"x": 20,
"y": 20,
"width": 310,
"height": 160,
"fill": "#ffffffff",
"cornerRadius": 8,
"stroke": {
"align": "inside",
"thickness": 1,
"fill": "#e7e7e7ff"
},
"layout": "none",
"children": [
{
"type": "frame",
"id": "marketImage",
"x": 12,
"y": 12,
"width": 40,
"height": 40,
"fill": "#f0f0f0",
"cornerRadius": 4,
"layout": "none"
},
{
"type": "text",
"id": "marketTitle",
"x": 60,
"y": 12,
"name": "Mamdan opens city-owned grocery store b...",
"fill": "#000000",
"textGrowth": "fixed-width-height",
"width": 169,
"height": 16,
"content": "Mamdan opens city-owned grocery store b...",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "500"
},
{
"type": "frame",
"id": "chanceContainer",
"x": 258,
"y": 12,
"width": 42,
"height": 40,
"layout": "none",
"children": [
{
"type": "frame",
"id": "progressBar",
"x": 0,
"y": 0,
"width": 40,
"height": 40,
"fill": "#ffffff",
"cornerRadius": 20,
"stroke": {
"align": "center",
"thickness": 4,
"fill": "#e0e0e0"
},
"layout": "none",
"children": [
{
"type": "frame",
"id": "progressFill",
"x": 2,
"y": 2,
"width": 36,
"height": 36,
"fill": "#ffffff",
"cornerRadius": 18,
"stroke": {
"align": "center",
"thickness": 4,
"fill": "#ff0000"
},
"layout": "none"
},
{
"type": "text",
"id": "chanceValue",
"x": 10,
"y": 12,
"fill": "#ff0000",
"content": "17%",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "600"
}
]
},
{
"type": "text",
"id": "chanceLabel",
"x": 0,
"y": 42,
"fill": "#808080",
"content": "chance",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "optionYes",
"x": 12,
"y": 72,
"width": 149,
"height": 40,
"fill": "#e6f9e6",
"cornerRadius": 4,
"layout": "none",
"children": [
{
"type": "text",
"id": "optionTextYes",
"x": 61,
"y": 10,
"fill": "#008000",
"content": "Yes",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "optionNo",
"x": 166,
"y": 72,
"width": 132,
"height": 40,
"fill": "#ffe6e6",
"cornerRadius": 4,
"layout": "none",
"children": [
{
"type": "text",
"id": "optionTextNo",
"x": 55,
"y": 10,
"fill": "#ff0000",
"content": "No",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "500"
}
]
},
{
"type": "text",
"id": "marketInfo",
"x": 12,
"y": 124,
"fill": "#808080",
"content": "$155k Vol.",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "icon_font",
"id": "giftIcon",
"x": 240,
"y": 124,
"width": 16,
"height": 16,
"iconFontName": "gift",
"iconFontFamily": "mdi",
"fill": "#808080"
},
{
"type": "icon_font",
"id": "bookmarkIcon",
"x": 270,
"y": 124,
"width": 16,
"height": 16,
"iconFontName": "bookmark",
"iconFontFamily": "mdi",
"fill": "#808080"
}
]
}
]
}

5676
design/pencil-shadcn.pen Normal file

File diff suppressed because it is too large Load Diff

37
package-lock.json generated
View File

@ -8,9 +8,11 @@
"name": "polyclientvuetify", "name": "polyclientvuetify",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.27", "vue": "^3.5.27",
"vue-router": "^5.0.1" "vue-router": "^5.0.1",
"vuetify": "^4.0.0-beta.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.1", "@playwright/test": "^1.58.1",
@ -1445,6 +1447,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@mdi/font": {
"version": "7.4.47",
"resolved": "https://registry.npmmirror.com/@mdi/font/-/font-7.4.47.tgz",
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
"license": "Apache-2.0"
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -6820,6 +6828,33 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/vuetify": {
"version": "4.0.0-beta.0",
"resolved": "https://registry.npmmirror.com/vuetify/-/vuetify-4.0.0-beta.0.tgz",
"integrity": "sha512-efBGozz9obv7bAqqFgSC1PAxO2NZltJzor+EDZx0Qxp8GssvIOzMQoMnK3axRbldIa5yfWdPAsbZ+lO8tJxgjg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/johnleider"
},
"peerDependencies": {
"typescript": ">=4.7",
"vite-plugin-vuetify": ">=2.1.0",
"vue": "^3.5.0",
"webpack-plugin-vuetify": ">=3.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
},
"vite-plugin-vuetify": {
"optional": true
},
"webpack-plugin-vuetify": {
"optional": true
}
}
},
"node_modules/w3c-xmlserializer": { "node_modules/w3c-xmlserializer": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",

View File

@ -17,9 +17,11 @@
"format": "prettier --write --experimental-cli src/" "format": "prettier --write --experimental-cli src/"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.27", "vue": "^3.5.27",
"vue-router": "^5.0.1" "vue-router": "^5.0.1",
"vuetify": "^4.0.0-beta.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.1", "@playwright/test": "^1.58.1",

View File

@ -1,11 +1,32 @@
<script setup lang="ts"></script> <script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const currentRoute = computed(() => {
return route.path
})
</script>
<template> <template>
<h1>You did it!</h1> <v-app>
<p> <v-app-bar color="primary" dark>
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the <v-app-bar-title>PolyMarket</v-app-bar-title>
documentation <v-spacer></v-spacer>
</p> <v-btn text to="/" :class="{ active: currentRoute === '/' }"> Home </v-btn>
<v-btn text to="/trade" :class="{ active: currentRoute === '/trade' }"> Trade </v-btn>
</v-app-bar>
<v-main>
<router-view />
</v-main>
</v-app>
</template> </template>
<style scoped></style> <style scoped>
/* Global styles can be added here */
.active {
font-weight: bold;
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<div class="horizontal-progress-bar" :class="className">
<div class="progress-track" :style="trackStyle"></div>
<div class="progress-fill" :style="{ ...fillStyle, width: `${progressPercentage}%` }"></div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps({
//
max: {
type: Number,
default: 100,
},
//
value: {
type: Number,
default: 0,
},
//
className: {
type: String,
default: '',
},
//
trackStyle: {
type: Object,
default: () => ({}),
},
//
fillStyle: {
type: Object,
default: () => ({}),
},
})
//
const progressPercentage = computed(() => {
return Math.min(100, Math.max(0, (props.value / props.max) * 100))
})
</script>
<style scoped>
.horizontal-progress-bar {
position: relative;
width: 100%;
height: 8px;
overflow: hidden;
border-radius: 4px;
}
.progress-track {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #e0e0e0;
}
.progress-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #0066cc;
transition: width 0.3s ease;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,199 @@
<template>
<v-card class="market-card" elevation="0" :rounded="'lg'">
<!-- Market Image -->
<v-avatar class="market-image" :size="40" :color="'#f0f0f0'" :rounded="'sm'">
<!-- Placeholder for market image -->
</v-avatar>
<!-- Market Title -->
<v-card-title class="market-title" :text="true" :shrink="true">
{{ marketTitle }}
</v-card-title>
<!-- Chance Container -->
<div class="chance-container">
<v-progress-circular
class="progress-bar"
:size="60"
:width="4"
:value="chanceValue"
:color="progressColor"
:background="'#e0e0e0'"
>
<template v-slot:default>
<span class="chance-value">{{ chanceValue }}%</span>
</template>
</v-progress-circular>
<span class="chance-label">chance</span>
</div>
<!-- Options -->
<v-btn
class="option-yes"
:width="140"
:height="40"
:color="'#e6f9e6'"
:rounded="'sm'"
:text="true"
>
<span class="option-text-yes">Yes</span>
</v-btn>
<v-btn
class="option-no"
:width="140"
:height="40"
:color="'#ffe6e6'"
:rounded="'sm'"
:text="true"
>
<span class="option-text-no">No</span>
</v-btn>
<!-- Market Info -->
<span class="market-info">{{ marketInfo }}</span>
<v-icon class="gift-icon" size="16">mdi-gift</v-icon>
<v-icon class="bookmark-icon" size="16">mdi-bookmark</v-icon>
</v-card>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps({
marketTitle: {
type: String,
default: 'Mamdan opens city-owned grocery store b...',
},
chanceValue: {
type: Number,
default: 17,
},
marketInfo: {
type: String,
default: '$155k Vol.',
},
})
// (0%)绿(100%)
const progressColor = computed(() => {
// HSL0绿120
const hue = (props.chanceValue / 100) * 120
return `hsl(${hue}, 100%, 50%)`
})
</script>
<style scoped>
.market-card {
position: relative;
width: 310px;
height: 160px;
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e7e7e7;
}
.market-image {
position: absolute;
left: 12px;
top: 12px;
}
.market-title {
position: absolute;
left: 60px;
top: 12px;
width: 180px;
max-height: 32px;
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 500;
color: #000000;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.chance-container {
position: absolute;
left: 248px;
top: 10px;
width: 60px;
height: 60px;
display: flex;
flex-direction: column;
align-items: center;
}
.progress-bar {
margin-bottom: 2px;
}
.chance-value {
font-family: 'Inter', sans-serif;
font-size: 12px;
font-weight: 600;
color: #000000;
}
.chance-label {
font-family: 'Inter', sans-serif;
font-size: 10px;
font-weight: normal;
color: #808080;
margin-top: 4px;
}
.option-yes {
position: absolute;
left: 12px;
top: 72px;
background-color: #e6f9e6;
}
.option-text-yes {
font-family: 'Inter', sans-serif;
font-size: 16px;
font-weight: 500;
color: #008000;
}
.option-no {
position: absolute;
left: 158px;
top: 72px;
background-color: #ffe6e6;
}
.option-text-no {
font-family: 'Inter', sans-serif;
font-size: 16px;
font-weight: 500;
color: #ff0000;
}
.market-info {
position: absolute;
left: 12px;
top: 124px;
font-family: 'Inter', sans-serif;
font-size: 12px;
font-weight: normal;
color: #808080;
}
.gift-icon {
position: absolute;
left: 240px;
top: 124px;
color: #808080;
}
.bookmark-icon {
position: absolute;
left: 270px;
top: 124px;
color: #808080;
}
</style>

View File

@ -0,0 +1,486 @@
<template>
<v-card class="order-book">
<!-- Header -->
<div class="order-book-header">
<h3 class="order-book-title">Order Book</h3>
<div class="order-book-vol">
$4.4k Vol.
<v-icon size="14" class="order-book-icon">mdi-chevron-up</v-icon>
</div>
</div>
<!-- Trade Tabs -->
<div class="trade-tabs-container">
<v-tabs v-model="activeTrade" class="trade-tabs" density="comfortable">
<v-tab value="up" class="trade-tab">Trade Up</v-tab>
<v-tab value="down" class="trade-tab">Trade Down</v-tab>
</v-tabs>
<div class="maker-rebate">
<span class="maker-rebate-text">Maker Rebate</span>
<v-icon size="14">mdi-refresh</v-icon>
</div>
</div>
<!-- Order Book Content -->
<div class="order-book-content">
<!-- Order List -->
<div class="order-list">
<div class="order-list-header">
<div class="order-list-header-price">PRICE</div>
<div class="order-list-header-shares">SHARES</div>
<div class="order-list-header-total">TOTAL</div>
</div>
<!-- Asks Orders -->
<div class="asks-label">Asks</div>
<div v-for="(ask, index) in asksWithCumulativeTotal" :key="index" class="order-item">
<div class="order-progress">
<HorizontalProgressBar
:max="maxAsksTotal"
:value="ask.total"
:fillStyle="{ backgroundColor: '#ffb3ba' }"
:trackStyle="{ backgroundColor: 'transparent' }"
/>
</div>
<div class="order-price asks-price">{{ ask.price }}¢</div>
<div class="order-shares">{{ ask.shares.toFixed(2) }}</div>
<div class="order-total">{{ ask.cumulativeTotal.toFixed(2) }}</div>
</div>
<!-- Bids Orders -->
<div class="bids-label">Bids</div>
<div
v-for="(bid, index) in bidsWithCumulativeTotal"
:key="index"
class="order-item bids-item"
>
<div class="order-progress">
<HorizontalProgressBar
:max="maxBidsTotal"
:value="bid.total"
:fillStyle="{ backgroundColor: '#b3ffb3' }"
:trackStyle="{ backgroundColor: 'transparent' }"
/>
</div>
<div class="order-price bids-price">{{ bid.price }}¢</div>
<div class="order-shares">{{ bid.shares.toFixed(2) }}</div>
<div class="order-total">{{ bid.cumulativeTotal.toFixed(2) }}</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="order-book-footer">
<div class="last-price">Last: {{ lastPrice }}¢</div>
<div class="spread">Spread: {{ spread }}¢</div>
</div>
</v-card>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import HorizontalProgressBar from './HorizontalProgressBar.vue'
// State
const activeTrade = ref('up')
const lastPrice = ref(37)
const spread = ref(1)
// Mock asks data ()
const asks = ref([
{ price: 45, shares: 1000.0 },
{ price: 44, shares: 2500.0 },
{ price: 43, shares: 1800.0 },
{ price: 42, shares: 3200.0 },
{ price: 41, shares: 2000.0 },
{ price: 40, shares: 1500.0 },
{ price: 39, shares: 800.0 },
{ price: 38, shares: 500.0 },
{ price: 37, shares: 300.0 },
])
// Mock bids data ()
const bids = ref([
{ price: 36, shares: 200.0 },
{ price: 35, shares: 500.0 },
{ price: 34, shares: 1000.0 },
{ price: 33, shares: 1500.0 },
{ price: 32, shares: 2000.0 },
{ price: 31, shares: 2500.0 },
{ price: 30, shares: 3000.0 },
{ price: 29, shares: 2800.0 },
{ price: 28, shares: 2500.0 },
{ price: 27, shares: 2000.0 },
{ price: 26, shares: 1500.0 },
{ price: 25, shares: 1000.0 },
])
// Simulate dynamic data updates
setInterval(() => {
// Update random ask price and shares
const randomAskIndex = Math.floor(Math.random() * asks.value.length)
asks.value[randomAskIndex] = {
...asks.value[randomAskIndex],
shares: Math.max(0, asks.value[randomAskIndex].shares + Math.floor(Math.random() * 100) - 50),
}
// Update random bid price and shares
const randomBidIndex = Math.floor(Math.random() * bids.value.length)
bids.value[randomBidIndex] = {
...bids.value[randomBidIndex],
shares: Math.max(0, bids.value[randomBidIndex].shares + Math.floor(Math.random() * 100) - 50),
}
// Update last price
lastPrice.value = lastPrice.value + Math.floor(Math.random() * 3) - 1
// Update spread
spread.value = spread.value + Math.floor(Math.random() * 2) - 1
if (spread.value < 1) spread.value = 1
}, 2000) // Update every 2 seconds
// Calculate cumulative total for asks
const asksWithCumulativeTotal = computed(() => {
let cumulativeTotal = 0
// Sort asks by price in descending order
const sortedAsks = [...asks.value].sort((a, b) => a.price - b.price)
return sortedAsks
.map((ask) => {
// Calculate current ask's value
const askValue = (ask.price * ask.shares) / 100 // Convert cents to dollars
cumulativeTotal += askValue
return {
...ask,
total: cumulativeTotal, // Use calculated total instead of fixed value
cumulativeTotal,
}
})
.sort((a, b) => b.price - a.price)
})
// Calculate cumulative total for bids
const bidsWithCumulativeTotal = computed(() => {
let cumulativeTotal = 0
const sortedBids = [...bids.value].sort((a, b) => b.price - a.price)
return sortedBids.map((bid) => {
// Calculate current bid's value
const bidValue = (bid.price * bid.shares) / 100 // Convert cents to dollars
cumulativeTotal += bidValue
return {
...bid,
total: cumulativeTotal, // Use calculated total instead of fixed value
cumulativeTotal,
}
})
// Reverse to display in descending order
})
// Calculate max total from cumulative totals
const maxAsksTotal = computed(() => {
const askTotals = asksWithCumulativeTotal.value.map((item) => item.cumulativeTotal)
const allTotals = [...askTotals]
return Math.max(...allTotals)
})
const maxBidsTotal = computed(() => {
const bidTotals = bidsWithCumulativeTotal.value.map((item) => item.cumulativeTotal)
const allTotals = [...bidTotals]
return Math.max(...allTotals)
})
</script>
<style scoped>
.order-book {
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.horizontal-progress-bar {
height: 33px !important;
border-radius: 0px !important;
}
.progress-fill {
border-radius: 0px !important;
}
.order-book-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: none;
background-color: transparent;
}
.order-book-title {
font-size: 16px;
font-weight: 500;
margin: 0;
color: #333333;
}
.order-book-vol {
font-size: 14px;
color: #666666;
display: flex;
align-items: center;
gap: 4px;
}
.order-book-icon {
color: #666666;
}
.trade-tabs-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px 12px 16px;
border-bottom: none;
background-color: transparent;
}
.trade-tabs {
border-bottom: none;
background-color: transparent;
}
.trade-tab {
font-size: 14px;
text-transform: none;
color: #666666;
min-width: 120px;
margin-right: 8px;
background-color: transparent;
border: none;
border-radius: 0;
}
.trade-tab.v-tab--active {
color: #0066cc;
font-weight: 500;
background-color: transparent;
border: none;
}
.trade-tab:not(.v-tab--active) {
background-color: transparent;
border: none;
}
.maker-rebate {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
}
.maker-rebate-text {
font-size: 14px;
color: #ff9800;
font-weight: 500;
}
.order-book-content {
display: flex;
padding: 12px 16px;
gap: 16px;
}
.depth-chart {
width: 50%;
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: column;
gap: 4px;
}
.depth-chart-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.depth-bar-container {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
height: 24px;
}
.depth-bar {
border-radius: 2px 0 0 2px;
height: 80%;
min-width: 4px;
align-self: center;
}
.asks-bar {
background-color: #ffb3ba;
}
.bids-bar {
background-color: #b3ffb3;
}
.order-list {
flex: 1;
display: flex;
flex-direction: column;
max-height: 400px;
overflow-y: auto;
}
.order-list-header {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: none;
margin-bottom: 8px;
}
.order-list-header-price,
.order-list-header-shares,
.order-list-header-total {
font-size: 12px;
font-weight: 500;
color: #666666;
text-transform: uppercase;
}
.order-list-header-price {
width: 80px;
}
.order-list-header-shares {
flex: 1;
text-align: right;
}
.order-list-header-total {
width: 100px;
text-align: right;
}
.asks-label,
.bids-label {
font-size: 12px;
font-weight: 500;
margin: 8px 0 4px 0;
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
}
.asks-label {
color: #ffffff;
background-color: #ff0000;
}
.bids-label {
color: #ffffff;
background-color: #008000;
}
.order-item {
display: flex;
align-items: center;
padding: 0px 0;
border-bottom: none;
gap: 12px;
}
.order-progress {
flex: 1;
}
.asks-price {
color: #ff0000;
}
.bids-price {
color: #008000;
}
.order-price {
width: 80px;
font-size: 14px;
font-weight: 500;
}
.order-shares {
width: 100px;
text-align: right;
font-size: 14px;
color: #333333;
}
.order-total {
width: 100px;
text-align: right;
font-size: 14px;
color: #333333;
}
.bids-item {
background-color: transparent;
}
.order-book-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-top: none;
background-color: transparent;
}
.last-price,
.spread {
font-size: 14px;
color: #666666;
}
/* Responsive Adjustments */
@media (max-width: 600px) {
.order-book-content {
padding: 8px 12px;
gap: 12px;
}
.order-progress {
flex: 2;
}
.depth-chart {
width: 80px;
}
.order-list-header-price {
width: 60px;
}
.order-list-header-total {
width: 80px;
}
.order-price {
width: 60px;
font-size: 13px;
}
.order-shares {
font-size: 13px;
}
.order-total {
width: 80px;
font-size: 13px;
}
}
</style>

View File

@ -0,0 +1,756 @@
<template>
<v-card class="trade-component">
<!-- Header -->
<div class="header">
<v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact">
<v-tab value="buy" class="minimal-tab">Buy</v-tab>
<v-tab value="sell" class="minimal-tab">Sell</v-tab>
</v-tabs>
<v-menu>
<template v-slot:activator="{ props, isActive }">
<v-btn v-bind="props" class="limit-btn" text end>
{{ limitType }}
<v-icon right>{{ isActive ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="limitType = 'Market'">
<v-list-item-title>Market</v-list-item-title>
</v-list-item>
<v-list-item @click="limitType = 'Limit'">
<v-list-item-title>Limit</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-menu append :location="'end'">
<template v-slot:activator="{ props }">
<v-list-item v-bind="props" @click.stop>
<v-list-item-title>More</v-list-item-title>
<template v-slot:append>
<v-icon icon="mdi-chevron-right" size="x-small"></v-icon>
</template>
</v-list-item>
</template>
<v-list>
<v-list-item>
<v-list-item-title>Merge</v-list-item-title>
</v-list-item>
<v-list-item>
<v-list-item-title>Split</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-list>
</v-menu>
</div>
<!-- Market Mode View -->
<template v-if="isMarketMode">
<!-- Balance > 0: Show Trade Interface -->
<template v-if="balance > 0">
<!-- Price Options -->
<div class="price-options">
<v-btn
class="yes-btn"
:class="{ active: selectedOption === 'yes' }"
text
@click="handleOptionChange('yes')"
>
Yes {{ selectedOption === 'yes' ? '19¢' : '18¢' }}
</v-btn>
<v-btn
class="no-btn"
:class="{ active: selectedOption === 'no' }"
text
@click="handleOptionChange('no')"
>
No {{ selectedOption === 'no' ? '82¢' : '81¢' }}
</v-btn>
</div>
<!-- Total and To Win (Buy模式) You'll receive (Sell模式) -->
<div class="total-section">
<!-- Buy模式 -->
<template v-if="activeTab === 'buy'">
<div class="total-row">
<span class="label">Total</span>
<span class="total-value">${{ totalPrice }}</span>
</div>
<div class="total-row">
<span class="label">To win</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
$20
</span>
</div>
</template>
<!-- Sell模式 -->
<template v-else>
<div class="total-row">
<span class="label">You'll receive</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
${{ totalPrice }}
</span>
</div>
</template>
</div>
<!-- Action Button -->
<v-btn class="action-btn">
{{ actionButtonText }}
</v-btn>
</template>
<!-- Balance <= 0: Show Deposit Interface -->
<template v-else>
<!-- Price Options -->
<div class="price-options">
<v-btn
class="yes-btn"
:class="{ active: selectedOption === 'yes' }"
text
@click="handleOptionChange('yes')"
>
Yes {{ selectedOption === 'yes' ? '19¢' : '18¢' }}
</v-btn>
<v-btn
class="no-btn"
:class="{ active: selectedOption === 'no' }"
text
@click="handleOptionChange('no')"
>
No {{ selectedOption === 'no' ? '82¢' : '81¢' }}
</v-btn>
</div>
<!-- Amount Section -->
<div class="input-group">
<div class="amount-header">
<div>
<span class="label amount-label">Amount</span>
<span class="balance-label">Balance ${{ balance.toFixed(2) }}</span>
</div>
<div class="amount-value">${{ amount.toFixed(2) }}</div>
</div>
<!-- Amount Buttons -->
<div class="amount-buttons">
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
</div>
</div>
<!-- Deposit Button -->
<v-btn
class="deposit-btn"
@click="deposit"
>
Deposit
</v-btn>
</template>
</template>
<!-- Limit Mode View -->
<template v-else>
<!-- Price Options -->
<div class="price-options">
<v-btn
class="yes-btn"
:class="{ active: selectedOption === 'yes' }"
text
@click="handleOptionChange('yes')"
>
Yes {{ selectedOption === 'yes' ? '19¢' : '18¢' }}
</v-btn>
<v-btn
class="no-btn"
:class="{ active: selectedOption === 'no' }"
text
@click="handleOptionChange('no')"
>
No {{ selectedOption === 'no' ? '82¢' : '81¢' }}
</v-btn>
</div>
<!-- Limit Price -->
<div class="input-group limit-price-group">
<div class="limit-price-header">
<span class="label">Limit Price</span>
<div class="price-input">
<v-btn class="adjust-btn" icon @click="decreasePrice">
<v-icon>mdi-minus</v-icon>
</v-btn>
<v-text-field
v-model.number="limitPrice"
type="number"
min="0.01"
step="0.01"
class="price-input-field"
hide-details
density="compact"
></v-text-field>
<v-btn class="adjust-btn" icon @click="increasePrice">
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
</div>
</div>
<!-- Shares -->
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<v-text-field
v-model.number="shares"
type="number"
min="0"
class="shares-input-field"
hide-details
density="compact"
></v-text-field>
</div>
</div>
<!-- Buy模式的份额调整按钮 -->
<div v-if="activeTab === 'buy'" class="shares-buttons">
<v-btn class="share-btn" @click="adjustShares(-100)">-100</v-btn>
<v-btn class="share-btn" @click="adjustShares(-10)">-10</v-btn>
<v-btn class="share-btn" @click="adjustShares(10)">+10</v-btn>
<v-btn class="share-btn" @click="adjustShares(100)">+100</v-btn>
<v-btn class="share-btn" @click="adjustShares(200)">+200</v-btn>
</div>
<!-- Sell模式的份额调整按钮 -->
<div v-else class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
</div>
<div v-if="activeTab === 'buy'" class="matching-info">
<v-icon size="14">mdi-information</v-icon>
<span>20.00 matching</span>
</div>
</div>
<!-- Set Expiration -->
<div class="input-group expiration-group">
<div class="expiration-header">
<span class="label">Set Expiration</span>
<v-switch v-model="expirationEnabled" class="expiration-switch" hide-details></v-switch>
</div>
<!-- Expiration Time Dropdown -->
<v-select
v-if="expirationEnabled"
v-model="expirationTime"
:items="expirationOptions"
class="expiration-select"
hide-details
density="compact"
></v-select>
</div>
<!-- Total and To Win (Buy模式) You'll receive (Sell模式) -->
<div class="total-section">
<!-- Buy模式 -->
<template v-if="activeTab === 'buy'">
<div class="total-row">
<span class="label">Total</span>
<span class="total-value">${{ totalPrice }}</span>
</div>
<div class="total-row">
<span class="label">To win</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
$20
</span>
</div>
</template>
<!-- Sell模式 -->
<template v-else>
<div class="total-row">
<span class="label">You'll receive</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
${{ totalPrice }}
</span>
</div>
</template>
</div>
<!-- Action Button -->
<v-btn class="action-btn">
{{ actionButtonText }}
</v-btn>
</template>
</v-card>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
// State
const activeTab = ref('buy')
const limitType = ref('Limit')
const expirationEnabled = ref(false)
const selectedOption = ref('no') // 'yes' or 'no'
const limitPrice = ref(0.82) //
const shares = ref(20) //
const expirationTime = ref('5m') //
const expirationOptions = ref(['5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d']) //
// Market mode state
const isMarketMode = computed(() => limitType.value === 'Market')
const amount = ref(0) // Market mode amount
const balance = ref(0) // Market mode balance
// Emits
const emit = defineEmits(['optionChange'])
// Computed properties
const currentPrice = computed(() => {
return `${(limitPrice.value * 100).toFixed(0)}¢`
})
const totalPrice = computed(() => {
return (limitPrice.value * shares.value).toFixed(2)
})
const actionButtonText = computed(() => {
return `${activeTab.value} ${selectedOption.value.charAt(0).toUpperCase() + selectedOption.value.slice(1)}`
})
// Methods
const handleOptionChange = (option: 'yes' | 'no') => {
selectedOption.value = option
//
if (option === 'yes') {
limitPrice.value = 0.19
} else {
limitPrice.value = 0.82
}
emit('optionChange', option)
}
//
const decreasePrice = () => {
limitPrice.value = Math.max(0.01, limitPrice.value - 0.01)
}
const increasePrice = () => {
limitPrice.value += 0.01
}
//
const adjustShares = (amount: number) => {
shares.value = Math.max(0, shares.value + amount)
}
// Sell使
const setSharesPercentage = (percentage: number) => {
// 100
const maxShares = 100
shares.value = Math.round((maxShares * percentage) / 100)
}
// Market mode methods
const adjustAmount = (value: number) => {
amount.value += value
if (amount.value < 0) amount.value = 0
}
const setMaxAmount = () => {
amount.value = balance.value
}
const deposit = () => {
console.log('Depositing amount:', amount.value)
// API
}
</script>
<style scoped>
.trade-component {
width: 100%;
max-width: 400px;
border-radius: 8px;
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid #e0e0e0;
}
.buy-sell-tabs {
flex: 1;
}
.minimal-tabs {
--tabs-height: 32px !important;
margin: 0 !important;
}
.minimal-tab {
height: 32px !important;
min-height: 32px !important;
padding: 4px 12px !important;
font-size: 14px !important;
}
/* 移除了.tab-btn样式使用v-tabs的默认样式 */
.limit-btn {
font-size: 14px;
text-transform: none;
color: #666666;
}
.more-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.price-options {
display: flex;
gap: 10px;
padding: 12px;
}
.yes-btn {
flex: 1;
background-color: #f0f0f0;
border-radius: 4px;
text-transform: none;
font-size: 14px;
}
.yes-btn.active {
background-color: #e6f9e6;
color: #008000;
}
.no-btn {
flex: 1;
background-color: #f0f0f0;
color: #000000;
border-radius: 4px;
text-transform: none;
font-size: 14px;
}
.no-btn.active {
background-color: #ff0000;
color: #ffffff;
}
.input-group {
padding: 12px;
border-top: 1px solid #e0e0e0;
}
/* Reduce height for Set Expiration section */
.expiration-group {
padding: 8px 12px !important;
margin: 0 !important;
}
/* Limit Price header flex layout */
.limit-price-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
/* Adjust label margin for flex layout */
.limit-price-header .label {
margin-bottom: 0;
flex-shrink: 0;
}
/* Adjust price input width for flex layout */
.limit-price-header .price-input {
flex: 1;
max-width: 200px;
margin-left: 12px;
}
.label {
font-size: 14px;
color: #666666;
margin-bottom: 6px;
display: block;
}
.price-input {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #f8f8f8;
border-radius: 4px;
padding: 6px 10px;
}
.adjust-btn {
color: #666666;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
.price-value {
font-size: 15px;
font-weight: 500;
}
.price-input-field {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 500;
background-color: transparent;
border: none;
box-shadow: none;
max-width: 100px;
}
/* Shares header flex layout */
.shares-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 8px;
}
/* Adjust label margin for flex layout */
.shares-header .label {
margin-bottom: 0;
flex-shrink: 0;
}
/* Adjust shares input width for flex layout */
.shares-header .shares-input {
flex: 1;
max-width: 200px;
margin-left: 12px;
text-align: right;
}
.shares-input {
background-color: #f8f8f8;
border-radius: 4px;
margin-bottom: 8px;
}
.shares-value {
font-size: 15px;
font-weight: 500;
text-align: right;
}
.shares-input-field {
width: 100%;
text-align: right;
font-size: 15px;
font-weight: 500;
background-color: transparent;
border: none;
box-shadow: none;
}
.shares-buttons {
display: flex;
gap: 6px;
margin-bottom: 6px;
}
.share-btn {
flex: 1;
background-color: #f0f0f0;
border-radius: 4px;
text-transform: none;
font-size: 12px;
padding: 4px;
}
.matching-info {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #4caf50;
}
.expiration-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.expiration-switch {
margin: 0;
}
.expiration-select {
margin-top: 8px;
width: 100%;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.total-section {
padding: 12px;
border-top: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
}
.total-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.total-value {
font-size: 15px;
font-weight: 500;
color: #0066cc;
}
.to-win-value {
font-size: 15px;
font-weight: 500;
color: #4caf50;
}
.action-btn {
width: 100%;
background-color: #0066cc;
color: #ffffff;
border-radius: 0;
padding: 16px;
font-size: 15px;
font-weight: 500;
text-transform: none;
}
/* Market Mode Styles */
.amount-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 12px;
}
.amount-label {
font-size: 16px;
font-weight: 500;
margin-bottom: 4px;
}
.balance-label {
font-size: 12px;
color: #999999;
display: block;
}
.amount-value {
font-size: 24px;
font-weight: 500;
color: #000000;
}
.amount-buttons {
display: flex;
gap: 8px;
justify-content: space-between;
}
.amount-btn {
flex: 1;
background-color: #f0f0f0;
border-radius: 4px;
text-transform: none;
font-size: 14px;
padding: 8px;
}
.deposit-btn {
width: 100%;
background-color: #0066cc;
color: #ffffff;
border-radius: 0;
padding: 16px;
font-size: 15px;
font-weight: 500;
text-transform: none;
}
/* 响应式调整 - 小屏幕设备 */
@media (max-width: 600px) {
.header {
padding: 10px;
}
.price-options {
padding: 10px;
}
.input-group {
padding: 10px;
}
.total-section {
padding: 10px;
}
.label {
font-size: 13px;
margin-bottom: 5px;
}
.price-value,
.shares-value,
.total-value,
.to-win-value {
font-size: 14px;
}
.action-btn {
padding: 15px;
font-size: 14px;
}
.share-btn {
font-size: 11px;
padding: 3px;
}
.amount-label {
font-size: 14px;
}
.amount-value {
font-size: 20px;
}
.amount-btn {
font-size: 13px;
padding: 6px;
}
.deposit-btn {
padding: 15px;
font-size: 14px;
}
}
</style>

View File

@ -3,10 +3,12 @@ import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import vuetify from './plugins/vuetify'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(vuetify)
app.mount('#app') app.mount('#app')

43
src/plugins/vuetify.ts Normal file
View File

@ -0,0 +1,43 @@
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
export default createVuetify({
components,
directives,
theme: {
defaultTheme: 'light',
themes: {
light: {
dark: false,
colors: {
primary: '#1A73E8',
secondary: '#5F6368',
accent: '#34A853',
error: '#EA4335',
info: '#4285F4',
success: '#34A853',
warning: '#FBBC05',
surface: '#FFFFFF',
background: '#F5F5F5'
}
},
dark: {
dark: true,
colors: {
primary: '#1E88E5',
secondary: '#9E9E9E',
accent: '#4CAF50',
error: '#F44336',
info: '#2196F3',
success: '#4CAF50',
warning: '#FFC107',
surface: '#1E1E1E',
background: '#121212'
}
}
}
}
})

View File

@ -1,8 +1,21 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Trade from '../views/Trade.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [], routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/trade',
name: 'trade',
component: Trade
}
],
}) })
export default router export default router

93
src/views/Home.vue Normal file
View File

@ -0,0 +1,93 @@
<template>
<v-container class="home-container">
<v-row justify="center" align="center" class="home-header">
<h1 class="home-title">PolyMarket</h1>
</v-row>
<v-row justify="center" align="center" class="home-subtitle">
<p>Welcome to PolyMarket - Your Gateway to Decentralized Trading</p>
</v-row>
<v-row justify="center" align="center" class="home-content">
<v-col cols="12" md="8" lg="6">
<v-card class="home-card">
<v-card-title>Market Overview</v-card-title>
<v-card-text>
<p>Explore the latest market data and trading opportunities.</p>
<v-btn
class="home-btn"
color="primary"
@click="navigateToTrade"
>
Go to Trading
</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const navigateToTrade = () => {
// Navigate to trade page (we'll add this route later)
console.log('Navigate to trade page')
}
</script>
<style scoped>
.home-container {
padding: 40px 20px;
min-height: 100vh;
}
.home-header {
margin-bottom: 20px;
}
.home-title {
font-size: 3rem;
font-weight: bold;
color: #0066cc;
margin: 0;
}
.home-subtitle {
margin-bottom: 40px;
}
.home-subtitle p {
font-size: 1.2rem;
color: #666666;
margin: 0;
}
.home-content {
margin-top: 40px;
}
.home-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 24px;
text-align: center;
}
.home-card-title {
font-size: 1.5rem;
font-weight: 500;
margin-bottom: 16px;
}
.home-card-text {
margin-bottom: 24px;
}
.home-btn {
margin-top: 16px;
}
</style>

45
src/views/Trade.vue Normal file
View File

@ -0,0 +1,45 @@
<template>
<v-container class="trade-container">
<v-row justify="center" align="center" class="trade-header">
<h1 class="trade-title">Trading</h1>
</v-row>
<v-row justify="center" align="center" class="trade-content">
<v-col cols="12" md="10" lg="8">
<v-card class="trade-card">
<v-card-title>Order Book</v-card-title>
<v-card-text>
<OrderBook />
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import OrderBook from '../components/OrderBook.vue'
</script>
<style scoped>
.trade-container {
padding: 40px 20px;
min-height: 100vh;
}
.trade-header {
margin-bottom: 40px;
}
.trade-title {
font-size: 2rem;
font-weight: bold;
color: #0066cc;
margin: 0;
}
.trade-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>