Initial commit: Monisuo - 虚拟货币模拟交易平台

功能模块:
- 用户注册/登录/KYC
- 资金账户/交易账户
- 实时行情/币种管理
- 即时交易/充提审核
- 管理后台

技术栈:
- 后端: SpringBoot 2.2.4 + MyBatis Plus
- 前端: uni-app x (Vue3 + UTS)
- 数据库: MySQL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sion
2026-03-21 20:52:33 +08:00
commit 7694a34ade
108 changed files with 12563 additions and 0 deletions

448
app/pages/asset/asset.uvue Normal file
View File

@@ -0,0 +1,448 @@
<template>
<view class="asset-container">
<!-- 总资产卡片 -->
<view class="total-card">
<text class="total-label">总资产估值(USDT)</text>
<text class="total-value">${{ totalAssets }}</text>
</view>
<!-- 账户切换 -->
<view class="account-tabs">
<text :class="['tab', activeAccount === 'fund' ? 'active' : '']" @click="activeAccount = 'fund'">资金账户</text>
<text :class="['tab', activeAccount === 'trade' ? 'active' : '']" @click="activeAccount = 'trade'">交易账户</text>
</view>
<!-- 资金账户 -->
<view v-if="activeAccount === 'fund'" class="account-section">
<view class="balance-card">
<text class="balance-label">USDT余额</text>
<text class="balance-value">{{ fundBalance }}</text>
</view>
<view class="action-btns">
<button class="action-btn deposit" @click="showDeposit = true">
<text class="btn-text">充值</text>
</button>
<button class="action-btn withdraw" @click="showWithdraw = true">
<text class="btn-text">提现</text>
</button>
<button class="action-btn transfer" @click="showTransfer = true">
<text class="btn-text">划转</text>
</button>
</view>
</view>
<!-- 交易账户 -->
<view v-if="activeAccount === 'trade'" class="trade-section">
<view class="position-list">
<view class="position-item" v-for="(pos, index) in positions" :key="index">
<view class="pos-left">
<text class="pos-icon">{{ pos.icon }}</text>
<view class="pos-info">
<text class="pos-code">{{ pos.coinCode }}</text>
<text class="pos-name">{{ pos.coinName }}</text>
</view>
</view>
<view class="pos-right">
<text class="pos-quantity">{{ pos.quantity }}</text>
<text class="pos-value">≈ ${{ pos.value }}</text>
</view>
</view>
</view>
</view>
<!-- 充值弹窗 -->
<view v-if="showDeposit" class="modal-mask" @click="showDeposit = false">
<view class="modal-content" @click.stop="">
<text class="modal-title">充值</text>
<view class="modal-form">
<text class="form-label">充值金额(USDT)</text>
<input class="form-input" type="digit" v-model="depositAmount" placeholder="请输入金额" />
</view>
<view class="modal-btns">
<button class="modal-btn cancel" @click="showDeposit = false"><text class="btn-text">取消</text></button>
<button class="modal-btn confirm" @click="handleDeposit"><text class="btn-text">确认</text></button>
</view>
</view>
</view>
<!-- 提现弹窗 -->
<view v-if="showWithdraw" class="modal-mask" @click="showWithdraw = false">
<view class="modal-content" @click.stop="">
<text class="modal-title">提现</text>
<view class="modal-form">
<text class="form-label">提现金额(USDT)</text>
<input class="form-input" type="digit" v-model="withdrawAmount" placeholder="请输入金额" />
</view>
<view class="modal-btns">
<button class="modal-btn cancel" @click="showWithdraw = false"><text class="btn-text">取消</text></button>
<button class="modal-btn confirm" @click="handleWithdraw"><text class="btn-text">确认</text></button>
</view>
</view>
</view>
<!-- 划转弹窗 -->
<view v-if="showTransfer" class="modal-mask" @click="showTransfer = false">
<view class="modal-content" @click.stop="">
<text class="modal-title">资金划转</text>
<view class="transfer-direction">
<text :class="['dir-item', transferDir === 1 ? 'active' : '']" @click="transferDir = 1">资金→交易</text>
<text :class="['dir-item', transferDir === 2 ? 'active' : '']" @click="transferDir = 2">交易→资金</text>
</view>
<view class="modal-form">
<text class="form-label">划转金额(USDT)</text>
<input class="form-input" type="digit" v-model="transferAmount" placeholder="请输入金额" />
</view>
<view class="modal-btns">
<button class="modal-btn cancel" @click="showTransfer = false"><text class="btn-text">取消</text></button>
<button class="modal-btn confirm" @click="handleTransfer"><text class="btn-text">确认</text></button>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import { getOverview, getTradeAccount, transfer } from '@/api/asset.uts'
import { deposit, withdraw } from '@/api/fund.uts'
type PositionItem = {
coinCode: string
coinName: string
quantity: string
value: string
icon: string
}
const totalAssets = ref('0.00')
const fundBalance = ref('0.00')
const positions = ref<PositionItem[]>([])
const activeAccount = ref('fund')
const showDeposit = ref(false)
const showWithdraw = ref(false)
const showTransfer = ref(false)
const depositAmount = ref('')
const withdrawAmount = ref('')
const transferAmount = ref('')
const transferDir = ref(1)
onShow(() => {
loadData()
})
func loadData () {
getOverview()
.then((res) => {
const data = res.data as UTSJSONObject
totalAssets.value = formatAmount(data['totalAssets'] as number)
fundBalance.value = formatAmount(data['fundBalance'] as number)
})
.catch(() => {})
getTradeAccount()
.then((res) => {
const data = res.data as UTSJSONObject
const list = data['positions'] as Array<UTSJSONObject>
const items: PositionItem[] = []
for (let i = 0; i < list.length; i++) {
const item = list[i]
items.push({
coinCode: item['coinCode'] as string,
coinName: item['coinName'] as string,
quantity: (item['quantity'] as number).toFixed(4),
value: formatAmount(item['value'] as number),
icon: getCoinIcon(item['coinCode'] as string)
})
}
positions.value = items
})
.catch(() => {})
}
func handleDeposit () {
if (depositAmount.value === '' || parseFloat(depositAmount.value) <= 0) {
uni.showToast({ title: '请输入有效金额', icon: 'none' })
return
}
deposit(depositAmount.value, null)
.then(() => {
uni.showToast({ title: '申请成功,等待审批', icon: 'success' })
showDeposit.value = false
depositAmount.value = ''
loadData()
})
.catch(() => {})
}
func handleWithdraw () {
if (withdrawAmount.value === '' || parseFloat(withdrawAmount.value) <= 0) {
uni.showToast({ title: '请输入有效金额', icon: 'none' })
return
}
withdraw(withdrawAmount.value, null)
.then(() => {
uni.showToast({ title: '申请成功,等待审批', icon: 'success' })
showWithdraw.value = false
withdrawAmount.value = ''
loadData()
})
.catch(() => {})
}
func handleTransfer () {
if (transferAmount.value === '' || parseFloat(transferAmount.value) <= 0) {
uni.showToast({ title: '请输入有效金额', icon: 'none' })
return
}
transfer(transferDir.value, transferAmount.value)
.then(() => {
uni.showToast({ title: '划转成功', icon: 'success' })
showTransfer.value = false
transferAmount.value = ''
loadData()
})
.catch(() => {})
}
func formatAmount (value: number): string {
return value.toFixed(2)
}
func getCoinIcon (code: string): string {
const icons: Map<string, string> = new Map()
icons.set('BTC', '₿')
icons.set('ETH', 'Ξ')
icons.set('SOL', '◎')
icons.set('USDT', '₮')
return icons.get(code) || '●'
}
</script>
<style lang="scss">
.asset-container {
min-height: 100vh;
background: #1A1A2E;
padding: 24rpx;
padding-bottom: 120rpx;
}
.total-card {
background: linear-gradient(135deg, #00D4AA 0%, #00B894 100%);
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
}
.total-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 8rpx;
}
.total-value {
font-size: 48rpx;
font-weight: bold;
color: #fff;
}
.account-tabs {
display: flex;
flex-direction: row;
background: #16213E;
border-radius: 16rpx;
padding: 8rpx;
margin-bottom: 24rpx;
}
.tab {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
border-radius: 12rpx;
}
.tab.active {
background: rgba(0, 212, 170, 0.2);
color: #00D4AA;
}
.balance-card {
background: #16213E;
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 24rpx;
}
.balance-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 8rpx;
}
.balance-value {
font-size: 40rpx;
font-weight: bold;
color: #00D4AA;
}
.action-btns {
display: flex;
flex-direction: row;
gap: 16rpx;
}
.action-btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.action-btn.deposit { background: #00C853; }
.action-btn.withdraw { background: #FF5252; }
.action-btn.transfer { background: #00D4AA; }
.btn-text {
font-size: 28rpx;
font-weight: bold;
color: #fff;
}
.position-list {
background: #16213E;
border-radius: 20rpx;
padding: 24rpx;
}
.position-item {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 20rpx 0;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.position-item:last-child { border-bottom: none; }
.pos-left {
display: flex;
flex-direction: row;
align-items: center;
}
.pos-icon {
font-size: 32rpx;
width: 48rpx;
height: 48rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
}
.pos-info { display: flex; flex-direction: column; }
.pos-code { font-size: 28rpx; font-weight: bold; color: #fff; }
.pos-name { font-size: 22rpx; color: rgba(255, 255, 255, 0.5); }
.pos-right { display: flex; flex-direction: column; align-items: flex-end; }
.pos-quantity { font-size: 28rpx; color: #fff; }
.pos-value { font-size: 22rpx; color: rgba(255, 255, 255, 0.5); }
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 600rpx;
background: #16213E;
border-radius: 24rpx;
padding: 32rpx;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #fff;
text-align: center;
margin-bottom: 32rpx;
}
.modal-form {
margin-bottom: 32rpx;
}
.form-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 12rpx;
}
.form-input {
width: 100%;
height: 80rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 32rpx;
color: #fff;
box-sizing: border-box;
}
.transfer-direction {
display: flex;
flex-direction: row;
margin-bottom: 24rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 8rpx;
}
.dir-item {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
border-radius: 8rpx;
}
.dir-item.active {
background: #00D4AA;
color: #fff;
}
.modal-btns {
display: flex;
flex-direction: row;
gap: 24rpx;
}
.modal-btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.modal-btn.cancel { background: rgba(255, 255, 255, 0.1); }
.modal-btn.confirm { background: #00D4AA; }
</style>

305
app/pages/index/index.uvue Normal file
View File

@@ -0,0 +1,305 @@
<template>
<view class="index-container">
<!-- 资产概览卡片 -->
<view class="asset-card">
<text class="asset-label">总资产(USDT)</text>
<text class="asset-value">{{ totalAsset }}</text>
<view class="asset-row">
<view class="asset-item">
<text class="asset-item-label">资金账户</text>
<text class="asset-item-value">{{ fundBalance }}</text>
</view>
<view class="asset-item">
<text class="asset-item-label">交易账户</text>
<text class="asset-item-value">{{ tradeBalance }}</text>
</view>
</view>
</view>
<!-- 快捷操作 -->
<view class="quick-actions">
<view class="action-item" @click="goToRecharge">
<text class="action-icon">充</text>
<text class="action-text">充值</text>
</view>
<view class="action-item" @click="goToWithdraw">
<text class="action-icon">提</text>
<text class="action-text">提现</text>
</view>
<view class="action-item" @click="goToTransfer">
<text class="action-icon">转</text>
<text class="action-text">划转</text>
</view>
<view class="action-item" @click="goToTrade">
<text class="action-icon">币</text>
<text class="action-text">交易</text>
</view>
</view>
<!-- 持仓列表 -->
<view class="holding-section">
<text class="section-title">我的持仓</text>
<view class="holding-list">
<view class="holding-item" v-for="(item, index) in holdings" :key="index">
<view class="holding-info">
<text class="coin-code">{{ item.coinCode }}</text>
<text class="coin-quantity">{{ item.quantity }}</text>
</view>
<view class="holding-value">
<text class="value-text">{{ item.value }} USDT</text>
<text :class="['profit-text', item.profit >= 0 ? 'up' : 'down']">
{{ item.profit >= 0 ? '+' : '' }}{{ item.profit }}%
</text>
</view>
</view>
<view class="empty-hint" v-if="holdings.length === 0">
<text class="empty-text">暂无持仓</text>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import { getAssetOverview, getTradeAccounts } from '@/api/asset.uts'
type HoldingItem = {
coinCode: string
quantity: string
value: string
profit: number
}
export default {
data() {
return {
totalAsset: '0.00' as string,
fundBalance: '0.00' as string,
tradeBalance: '0.00' as string,
holdings: [] as HoldingItem[]
}
},
onShow() {
this.loadAssetData()
},
methods: {
async loadAssetData() {
try {
const res = await getAssetOverview()
const data = res.data
this.totalAsset = this.formatAmount(data['totalAsset'] as number)
this.fundBalance = this.formatAmount(data['fundBalance'] as number)
this.tradeBalance = this.formatAmount(data['tradeBalance'] as number)
// 加载持仓
const tradeRes = await getTradeAccounts()
const list = tradeRes.data['list'] as any[]
this.holdings = []
if (list !== null && list.length > 0) {
for (let i = 0; i < list.length; i++) {
const item = list[i]
const qty = item['quantity'] as number
if (qty > 0) {
this.holdings.push({
coinCode: item['coinCode'] as string,
quantity: qty.toFixed(6),
value: (item['totalValue'] as number).toFixed(2),
profit: item['profitRate'] as number
})
}
}
}
} catch (e) {
console.error('加载资产数据失败', e)
}
},
formatAmount(value: number): string {
if (value === null || value === undefined) {
return '0.00'
}
return value.toFixed(2)
},
goToRecharge() {
uni.navigateTo({ url: '/pages/asset/asset?action=deposit' })
},
goToWithdraw() {
uni.navigateTo({ url: '/pages/asset/asset?action=withdraw' })
},
goToTransfer() {
uni.navigateTo({ url: '/pages/asset/asset?action=transfer' })
},
goToTrade() {
uni.switchTab({ url: '/pages/trade/trade' })
}
}
}
</script>
<style lang="scss">
.index-container {
min-height: 100vh;
background: $bg-color-dark;
padding: $spacing-base;
box-sizing: border-box;
}
.asset-card {
background: linear-gradient(135deg, $primary-color 0%, $primary-color-dark 100%);
border-radius: $border-radius-lg;
padding: $spacing-lg;
margin-bottom: $spacing-lg;
}
.asset-label {
font-size: $font-size-base;
color: rgba(255, 255, 255, 0.8);
display: block;
margin-bottom: 8rpx;
}
.asset-value {
font-size: 56rpx;
font-weight: bold;
color: #FFFFFF;
display: block;
margin-bottom: $spacing-base;
}
.asset-row {
display: flex;
justify-content: space-between;
}
.asset-item {
display: flex;
flex-direction: column;
}
.asset-item-label {
font-size: $font-size-sm;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 4rpx;
}
.asset-item-value {
font-size: $font-size-lg;
color: #FFFFFF;
font-weight: 500;
}
.quick-actions {
display: flex;
justify-content: space-between;
background: $bg-color-card;
border-radius: $border-radius-lg;
padding: $spacing-base;
margin-bottom: $spacing-lg;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.action-icon {
width: 80rpx;
height: 80rpx;
background: rgba(0, 212, 170, 0.2);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: $font-size-lg;
color: $primary-color;
margin-bottom: 8rpx;
}
.action-text {
font-size: $font-size-sm;
color: $text-color;
}
.holding-section {
background: $bg-color-card;
border-radius: $border-radius-lg;
padding: $spacing-base;
}
.section-title {
font-size: $font-size-lg;
color: $text-color;
font-weight: bold;
display: block;
margin-bottom: $spacing-base;
}
.holding-list {
min-height: 100rpx;
}
.holding-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: $spacing-base 0;
border-bottom: 1rpx solid $border-color;
}
.holding-item:last-child {
border-bottom: none;
}
.holding-info {
display: flex;
flex-direction: column;
}
.coin-code {
font-size: $font-size-lg;
color: $text-color;
font-weight: bold;
}
.coin-quantity {
font-size: $font-size-sm;
color: $text-color-secondary;
margin-top: 4rpx;
}
.holding-value {
text-align: right;
}
.value-text {
font-size: $font-size-base;
color: $text-color;
display: block;
}
.profit-text {
font-size: $font-size-sm;
margin-top: 4rpx;
}
.profit-text.up {
color: $up-color;
}
.profit-text.down {
color: $down-color;
}
.empty-hint {
display: flex;
justify-content: center;
align-items: center;
height: 100rpx;
}
.empty-text {
font-size: $font-size-base;
color: $text-color-secondary;
}
</style>

169
app/pages/login/login.uvue Normal file
View File

@@ -0,0 +1,169 @@
<template>
<view class="login-container">
<view class="login-header">
<text class="app-title">模拟所</text>
<text class="app-subtitle">虚拟货币模拟交易平台</text>
</view>
<view class="login-form">
<view class="form-item">
<input
class="input"
type="text"
v-model="username"
placeholder="请输入用户名"
placeholder-class="placeholder"
/>
</view>
<view class="form-item">
<input
class="input"
type="password"
v-model="password"
placeholder="请输入密码"
placeholder-class="placeholder"
/>
</view>
<button class="login-btn" @click="handleLogin">登录</button>
<view class="register-link">
<text class="link-text" @click="goToRegister">还没有账号?立即注册</text>
</view>
</view>
</view>
</template>
<script lang="uts">
import { post } from '@/api/request.uts'
import { login } from '@/api/user.uts'
type LoginResponse = {
token: string
user: UTSJSONObject
}
export default {
data() {
return {
username: '' as string,
password: '' as string
}
},
methods: {
async handleLogin() {
if (this.username === '' || this.username === null) {
uni.showToast({ title: '请输入用户名', icon: 'none' })
return
}
if (this.password === '' || this.password === null) {
uni.showToast({ title: '请输入密码', icon: 'none' })
return
}
try {
const res = await login({
username: this.username,
password: this.password
} as UTSJSONObject)
const token = res.data['token'] as string
uni.setStorageSync('token', token)
if (res.data['user'] !== null) {
uni.setStorageSync('userInfo', JSON.stringify(res.data['user']))
}
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
} catch (e) {
console.error('登录失败', e)
}
},
goToRegister() {
uni.navigateTo({ url: '/pages/register/register' })
}
}
}
</script>
<style lang="scss">
.login-container {
min-height: 100vh;
background: $bg-color-dark;
padding: 120rpx 48rpx;
box-sizing: border-box;
}
.login-header {
text-align: center;
margin-bottom: 80rpx;
}
.app-title {
font-size: 56rpx;
font-weight: bold;
color: $primary-color;
display: block;
margin-bottom: 16rpx;
}
.app-subtitle {
font-size: $font-size-base;
color: $text-color-secondary;
}
.login-form {
margin-top: 48rpx;
}
.form-item {
margin-bottom: 32rpx;
}
.input {
width: 100%;
height: 96rpx;
background: $bg-color-card;
border-radius: $border-radius-base;
padding: 0 32rpx;
font-size: $font-size-base;
color: $text-color;
border: 2rpx solid $border-color;
box-sizing: border-box;
}
.placeholder {
color: $text-color-placeholder;
}
.login-btn {
width: 100%;
height: 96rpx;
background: $primary-color;
border-radius: $border-radius-base;
color: #FFFFFF;
font-size: $font-size-lg;
font-weight: bold;
margin-top: 48rpx;
border: none;
}
.login-btn:active {
background: $primary-color-dark;
}
.register-link {
text-align: center;
margin-top: 32rpx;
}
.link-text {
color: $primary-color;
font-size: $font-size-base;
}
</style>

View File

@@ -0,0 +1,300 @@
<template>
<view class="market-container">
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
type="text"
v-model="keyword"
placeholder="搜索币种"
placeholder-class="placeholder"
@input="onSearch"
/>
</view>
<!-- 分类标签 -->
<view class="tabs">
<text :class="['tab', activeTab === 'all' ? 'active' : '']" @click="activeTab = 'all'">全部</text>
<text :class="['tab', activeTab === 'realtime' ? 'active' : '']" @click="activeTab = 'realtime'">实时</text>
<text :class="['tab', activeTab === 'hot' ? 'active' : '']" @click="activeTab = 'hot'">热门</text>
</view>
<!-- 币种列表 -->
<view class="coin-list">
<view class="list-header">
<text class="header-item name">币种</text>
<text class="header-item price">最新价</text>
<text class="header-item change">涨跌幅</text>
</view>
<scroll-view scroll-y class="scroll-area">
<view class="coin-item" v-for="(coin, index) in filteredCoins" :key="index" @click="goTrade(coin)">
<view class="coin-left">
<text class="coin-icon">{{ coin.icon }}</text>
<view class="coin-info">
<text class="coin-code">{{ coin.code }}/USDT</text>
<text class="coin-name">{{ coin.name }}</text>
</view>
</view>
<view class="coin-center">
<text class="coin-price">${{ coin.price }}</text>
</view>
<view class="coin-right">
<view :class="['change-box', coin.change >= 0 ? 'up' : 'down']">
<text class="change-text">{{ coin.change >= 0 ? '+' : '' }}{{ coin.change }}%</text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script lang="uts">
import { getCoinList } from '@/api/market.uts'
type CoinItem = {
code: string
name: string
price: string
change: number
icon: string
priceType: number
}
const keyword = ref('')
const activeTab = ref('all')
const allCoins = ref<CoinItem[]>([])
const filteredCoins = ref<CoinItem[]>([])
onShow(() => {
loadCoins()
})
func loadCoins () {
getCoinList()
.then((res) => {
const data = res.data as UTSJSONObject
const list = data['list'] as Array<UTSJSONObject>
const coins: CoinItem[] = []
for (let i = 0; i < list.length; i++) {
const item = list[i]
coins.push({
code: item['code'] as string,
name: item['name'] as string,
price: formatPrice(item['price'] as number),
change: (item['change24h'] as number) || 0,
icon: getCoinIcon(item['code'] as string),
priceType: item['priceType'] as number
})
}
allCoins.value = coins
filterCoins()
})
.catch(() => {})
}
func filterCoins () {
let result: CoinItem[] = []
if (activeTab.value === 'realtime') {
result = allCoins.value.filter((c) => c.priceType === 1)
} else if (activeTab.value === 'hot') {
result = allCoins.value.slice(0, 6)
} else {
result = allCoins.value
}
if (keyword.value.trim() !== '') {
const kw = keyword.value.trim().toLowerCase()
result = result.filter((c) =>
c.code.toLowerCase().includes(kw) || c.name.toLowerCase().includes(kw)
)
}
filteredCoins.value = result
}
func onSearch () {
filterCoins()
}
watch(activeTab, () => {
filterCoins()
})
func formatPrice (value: number): string {
if (value >= 1000) return value.toFixed(2)
if (value >= 1) return value.toFixed(4)
return value.toFixed(6)
}
func getCoinIcon (code: string): string {
const icons: Map<string, string> = new Map()
icons.set('BTC', '₿')
icons.set('ETH', 'Ξ')
icons.set('SOL', '◎')
icons.set('USDT', '₮')
icons.set('DOGE', '🐕')
icons.set('XRP', '✕')
return icons.get(code) || '●'
}
func goTrade (coin: CoinItem) {
uni.switchTab({ url: '/pages/trade/trade' })
}
</script>
<style lang="scss">
.market-container {
min-height: 100vh;
background: #1A1A2E;
display: flex;
flex-direction: column;
}
.search-bar {
padding: 16rpx 24rpx;
}
.search-input {
height: 72rpx;
background: #16213E;
border-radius: 36rpx;
padding: 0 32rpx;
font-size: 28rpx;
color: #fff;
}
.placeholder {
color: rgba(255, 255, 255, 0.3);
}
.tabs {
display: flex;
flex-direction: row;
padding: 0 24rpx 16rpx;
}
.tab {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
margin-right: 32rpx;
padding-bottom: 8rpx;
}
.tab.active {
color: #00D4AA;
border-bottom: 4rpx solid #00D4AA;
}
.coin-list {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 24rpx;
}
.list-header {
display: flex;
flex-direction: row;
padding: 16rpx 0;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
}
.header-item {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
}
.header-item.name { width: 40%; }
.header-item.price { width: 30%; text-align: center; }
.header-item.change { width: 30%; text-align: right; }
.scroll-area {
flex: 1;
}
.coin-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.coin-left {
width: 40%;
display: flex;
flex-direction: row;
align-items: center;
}
.coin-icon {
font-size: 36rpx;
width: 56rpx;
height: 56rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
}
.coin-info {
display: flex;
flex-direction: column;
}
.coin-code {
font-size: 28rpx;
font-weight: bold;
color: #fff;
}
.coin-name {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
}
.coin-center {
width: 30%;
text-align: center;
}
.coin-price {
font-size: 28rpx;
color: #fff;
}
.coin-right {
width: 30%;
display: flex;
justify-content: flex-end;
}
.change-box {
padding: 8rpx 16rpx;
border-radius: 8rpx;
}
.change-box.up {
background: rgba(0, 200, 83, 0.2);
}
.change-box.down {
background: rgba(255, 82, 82, 0.2);
}
.change-text {
font-size: 26rpx;
}
.change-box.up .change-text {
color: #00C853;
}
.change-box.down .change-text {
color: #FF5252;
}
</style>

209
app/pages/mine/mine.uvue Normal file
View File

@@ -0,0 +1,209 @@
<template>
<view class="mine-container">
<!-- 用户信息 -->
<view class="user-card">
<view class="avatar">
<text class="avatar-text">{{ avatarText }}</text>
</view>
<view class="user-info">
<text class="username">{{ username }}</text>
<text class="user-level">普通用户</text>
</view>
</view>
<!-- 功能列表 -->
<view class="menu-list">
<view class="menu-item" @click="goToKyc">
<text class="menu-icon">实名</text>
<text class="menu-text">实名认证</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="goToSecurity">
<text class="menu-icon">安全</text>
<text class="menu-text">安全设置</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="goToAbout">
<text class="menu-icon">关于</text>
<text class="menu-text">关于我们</text>
<text class="menu-arrow">></text>
</view>
</view>
<!-- 退出按钮 -->
<button class="logout-btn" @click="handleLogout">退出登录</button>
</view>
</template>
<script lang="uts">
export default {
data() {
return {
username: '' as string
}
},
computed: {
avatarText(): string {
if (this.username !== null && this.username.length > 0) {
return this.username.substring(0, 1).toUpperCase()
}
return 'U'
}
},
onShow() {
this.loadUserInfo()
},
methods: {
loadUserInfo() {
const userInfoStr = uni.getStorageSync('userInfo') as string
if (userInfoStr !== null && userInfoStr !== '') {
try {
const userInfo = JSON.parse(userInfoStr) as UTSJSONObject
this.username = userInfo['username'] as string
} catch (e) {
this.username = '用户'
}
} else {
this.username = '用户'
}
},
goToKyc() {
uni.showToast({ title: '功能开发中', icon: 'none' })
},
goToSecurity() {
uni.showToast({ title: '功能开发中', icon: 'none' })
},
goToAbout() {
uni.showToast({ title: '模拟所 v1.0.0', icon: 'none' })
},
handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
uni.reLaunch({ url: '/pages/login/login' })
}
}
})
}
}
}
</script>
<style lang="scss">
.mine-container {
min-height: 100vh;
background: $bg-color-dark;
padding: $spacing-base;
box-sizing: border-box;
}
.user-card {
display: flex;
align-items: center;
padding: $spacing-lg;
background: $bg-color-card;
border-radius: $border-radius-lg;
margin-bottom: $spacing-lg;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: $primary-color;
display: flex;
justify-content: center;
align-items: center;
margin-right: $spacing-base;
}
.avatar-text {
font-size: 48rpx;
color: #FFFFFF;
font-weight: bold;
}
.user-info {
display: flex;
flex-direction: column;
}
.username {
font-size: $font-size-xl;
color: $text-color;
font-weight: bold;
margin-bottom: 8rpx;
}
.user-level {
font-size: $font-size-sm;
color: $text-color-secondary;
}
.menu-list {
background: $bg-color-card;
border-radius: $border-radius-lg;
overflow: hidden;
margin-bottom: $spacing-lg;
}
.menu-item {
display: flex;
align-items: center;
padding: $spacing-base $spacing-lg;
border-bottom: 1rpx solid $border-color;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background: rgba(255, 255, 255, 0.05);
}
.menu-icon {
width: 64rpx;
height: 64rpx;
background: $primary-color;
border-radius: $border-radius-base;
display: flex;
justify-content: center;
align-items: center;
font-size: $font-size-sm;
color: #FFFFFF;
margin-right: $spacing-base;
}
.menu-text {
flex: 1;
font-size: $font-size-base;
color: $text-color;
}
.menu-arrow {
font-size: $font-size-base;
color: $text-color-secondary;
}
.logout-btn {
width: 100%;
height: 88rpx;
background: $error-color;
border-radius: $border-radius-base;
color: #FFFFFF;
font-size: $font-size-lg;
border: none;
margin-top: 48rpx;
}
.logout-btn:active {
opacity: 0.8;
}
</style>

View File

@@ -0,0 +1,203 @@
<template>
<view class="register-container">
<!-- Logo区域 -->
<view class="logo-section">
<text class="logo-text">₿</text>
<text class="app-name">注册账号</text>
</view>
<!-- 注册表单 -->
<view class="form-section">
<view class="form-item">
<text class="form-label">账号</text>
<input
class="form-input"
type="text"
v-model="username"
placeholder="请输入账号(4-20位字母数字)"
placeholder-class="placeholder"
/>
</view>
<view class="form-item">
<text class="form-label">密码</text>
<input
class="form-input"
type="password"
v-model="password"
placeholder="请输入密码(至少6位)"
placeholder-class="placeholder"
/>
</view>
<view class="form-item">
<text class="form-label">确认密码</text>
<input
class="form-input"
type="password"
v-model="confirmPassword"
placeholder="请再次输入密码"
placeholder-class="placeholder"
/>
</view>
<!-- 注册按钮 -->
<button class="register-btn" :disabled="loading" @click="handleRegister">
<text class="btn-text">{{ loading ? '注册中...' : '注 册' }}</text>
</button>
<!-- 登录入口 -->
<view class="login-row">
<text class="login-text">已有账号?</text>
<text class="login-link" @click="goLogin">立即登录</text>
</view>
</view>
</view>
</template>
<script lang="uts">
import { register } from '@/api/user.uts'
const username = ref('')
const password = ref('')
const confirmPassword = ref('')
const loading = ref(false)
func handleRegister () {
const usernameValue = username.value.trim()
const passwordValue = password.value.trim()
const confirmPasswordValue = confirmPassword.value.trim()
if (usernameValue === '' || usernameValue.length < 4) {
uni.showToast({ title: '账号至少4位', icon: 'none' })
return
}
if (passwordValue === '' || passwordValue.length < 6) {
uni.showToast({ title: '密码至少6位', icon: 'none' })
return
}
if (passwordValue !== confirmPasswordValue) {
uni.showToast({ title: '两次密码不一致', icon: 'none' })
return
}
loading.value = true
register(usernameValue, passwordValue)
.then((res) => {
const data = res.data as UTSJSONObject
uni.setStorageSync('token', data['token'] as string)
uni.setStorageSync('userInfo', JSON.stringify(data['userInfo']))
uni.showToast({ title: '注册成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
})
.catch((error) => {
console.error('注册失败:', error)
})
.finally(() => {
loading.value = false
})
}
func goLogin () {
uni.navigateBack()
}
</script>
<style lang="scss">
.register-container {
min-height: 100vh;
background: linear-gradient(180deg, #1A1A2E 0%, #16213E 100%);
padding: 0 60rpx;
}
.logo-section {
padding-top: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.logo-text {
font-size: 80rpx;
color: #00D4AA;
margin-bottom: 24rpx;
}
.app-name {
font-size: 40rpx;
font-weight: bold;
color: #fff;
margin-bottom: 48rpx;
}
.form-section {
margin-top: 48rpx;
}
.form-item {
margin-bottom: 32rpx;
}
.form-label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 16rpx;
display: block;
}
.form-input {
width: 100%;
height: 96rpx;
background: rgba(255, 255, 255, 0.08);
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
padding: 0 32rpx;
font-size: 32rpx;
color: #fff;
box-sizing: border-box;
}
.placeholder {
color: rgba(255, 255, 255, 0.3);
}
.register-btn {
margin-top: 48rpx;
height: 96rpx;
background: linear-gradient(90deg, #00D4AA 0%, #00B894 100%);
border-radius: 48rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.btn-text {
font-size: 34rpx;
font-weight: bold;
color: #fff;
}
.login-row {
margin-top: 32rpx;
display: flex;
justify-content: center;
align-items: center;
}
.login-text {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
}
.login-link {
font-size: 26rpx;
color: #00D4AA;
margin-left: 8rpx;
}
</style>

361
app/pages/trade/trade.uvue Normal file
View File

@@ -0,0 +1,361 @@
<template>
<view class="trade-container">
<!-- 交易对选择 -->
<view class="pair-selector" @click="showPairPicker = true">
<text class="pair-text">{{ selectedPair }}</text>
<text class="pair-arrow">▼</text>
</view>
<!-- 价格信息 -->
<view class="price-section">
<text class="current-price">${{ currentPrice }}</text>
<text :class="['price-change', priceChange >= 0 ? 'up' : 'down']">
{{ priceChange >= 0 ? '+' : '' }}{{ priceChange }}%
</text>
</view>
<!-- 买卖切换 -->
<view class="trade-tabs">
<text :class="['trade-tab', tradeType === 'buy' ? 'active buy' : '']" @click="tradeType = 'buy'">买入</text>
<text :class="['trade-tab', tradeType === 'sell' ? 'active sell' : '']" @click="tradeType = 'sell'">卖出</text>
</view>
<!-- 交易表单 -->
<view class="trade-form">
<view class="form-item">
<text class="form-label">价格(USDT)</text>
<view class="input-row">
<input class="form-input" type="digit" v-model="price" />
<text class="input-unit">USDT</text>
</view>
</view>
<view class="form-item">
<text class="form-label">数量</text>
<view class="input-row">
<input class="form-input" type="digit" v-model="quantity" />
<text class="input-unit">{{ coinCode }}</text>
</view>
</view>
<view class="amount-row">
<text class="amount-label">交易金额</text>
<text class="amount-value">{{ totalAmount }} USDT</text>
</view>
<view class="balance-row">
<text class="balance-label">可用</text>
<text class="balance-value">{{ availableBalance }} USDT</text>
</view>
<!-- 交易按钮 -->
<button :class="['trade-btn', tradeType]" @click="handleTrade">
<text class="btn-text">{{ tradeType === 'buy' ? '买入' : '卖出' }} {{ coinCode }}</text>
</button>
</view>
<!-- 持仓信息 -->
<view class="position-section">
<text class="section-title">当前持仓</text>
<view class="position-info">
<text class="position-label">{{ coinCode }}</text>
<text class="position-value">{{ positionQuantity }}</text>
</view>
</view>
</view>
</template>
<script lang="uts">
import { buy, sell } from '@/api/trade.uts'
import { getOverview, getTradeAccount } from '@/api/asset.uts'
import { getCoinDetail } from '@/api/market.uts'
const coinCode = ref('BTC')
const selectedPair = ref('BTC/USDT')
const currentPrice = ref('0.00')
const priceChange = ref(0)
const tradeType = ref('buy')
const price = ref('')
const quantity = ref('')
const availableBalance = ref('0.00')
const positionQuantity = ref('0.0000')
const showPairPicker = ref(false)
const totalAmount = computed((): string => {
const p = parseFloat(price.value) || 0
const q = parseFloat(quantity.value) || 0
return (p * q).toFixed(2)
})
onShow(() => {
loadData()
})
func loadData () {
loadBalance()
loadCoinInfo()
loadPosition()
}
func loadBalance () {
getOverview()
.then((res) => {
const data = res.data as UTSJSONObject
// 获取交易账户USDT余额
const positions = data['positions'] as Array<UTSJSONObject>
for (let i = 0; i < positions.length; i++) {
const pos = positions[i]
if (pos['coinCode'] === 'USDT') {
availableBalance.value = (pos['quantity'] as number).toFixed(2)
break
}
}
})
.catch(() => {})
}
func loadCoinInfo () {
getCoinDetail(coinCode.value)
.then((res) => {
const data = res.data as UTSJSONObject
currentPrice.value = formatPrice(data['price'] as number)
priceChange.value = (data['change24h'] as number) || 0
price.value = currentPrice.value
})
.catch(() => {})
}
func loadPosition () {
getTradeAccount()
.then((res) => {
const data = res.data as UTSJSONObject
const positions = data['positions'] as Array<UTSJSONObject>
for (let i = 0; i < positions.length; i++) {
const pos = positions[i]
if (pos['coinCode'] === coinCode.value) {
positionQuantity.value = (pos['quantity'] as number).toFixed(4)
break
}
}
})
.catch(() => {})
}
func handleTrade () {
const priceValue = parseFloat(price.value) || 0
const quantityValue = parseFloat(quantity.value) || 0
if (priceValue <= 0) {
uni.showToast({ title: '请输入有效价格', icon: 'none' })
return
}
if (quantityValue <= 0) {
uni.showToast({ title: '请输入有效数量', icon: 'none' })
return
}
const api = tradeType.value === 'buy' ? buy : sell
api(coinCode.value, price.value, quantity.value)
.then((res) => {
uni.showToast({ title: tradeType.value === 'buy' ? '买入成功' : '卖出成功', icon: 'success' })
price.value = ''
quantity.value = ''
loadData()
})
.catch(() => {})
}
func formatPrice (value: number): string {
if (value >= 1000) return value.toFixed(2)
if (value >= 1) return value.toFixed(4)
return value.toFixed(6)
}
</script>
<style lang="scss">
.trade-container {
min-height: 100vh;
background: #1A1A2E;
padding: 24rpx;
}
.pair-selector {
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx 0;
}
.pair-text {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
.pair-arrow {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
margin-left: 8rpx;
}
.price-section {
display: flex;
flex-direction: row;
align-items: baseline;
margin-top: 16rpx;
}
.current-price {
font-size: 56rpx;
font-weight: bold;
color: #fff;
margin-right: 16rpx;
}
.price-change {
font-size: 28rpx;
}
.price-change.up { color: #00C853; }
.price-change.down { color: #FF5252; }
.trade-tabs {
display: flex;
flex-direction: row;
margin-top: 32rpx;
background: #16213E;
border-radius: 16rpx;
padding: 8rpx;
}
.trade-tab {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 30rpx;
color: rgba(255, 255, 255, 0.5);
border-radius: 12rpx;
}
.trade-tab.active.buy {
background: rgba(0, 200, 83, 0.2);
color: #00C853;
}
.trade-tab.active.sell {
background: rgba(255, 82, 82, 0.2);
color: #FF5252;
}
.trade-form {
margin-top: 32rpx;
background: #16213E;
border-radius: 20rpx;
padding: 24rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 12rpx;
display: block;
}
.input-row {
display: flex;
flex-direction: row;
align-items: center;
background: rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 0 24rpx;
}
.form-input {
flex: 1;
height: 80rpx;
font-size: 32rpx;
color: #fff;
}
.input-unit {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
}
.amount-row, .balance-row {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 16rpx 0;
}
.amount-label, .balance-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
}
.amount-value, .balance-value {
font-size: 26rpx;
color: #fff;
}
.trade-btn {
margin-top: 24rpx;
height: 96rpx;
border-radius: 48rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.trade-btn.buy {
background: linear-gradient(90deg, #00C853 0%, #00A844 100%);
}
.trade-btn.sell {
background: linear-gradient(90deg, #FF5252 0%, #E04040 100%);
}
.btn-text {
font-size: 34rpx;
font-weight: bold;
color: #fff;
}
.position-section {
margin-top: 32rpx;
background: #16213E;
border-radius: 20rpx;
padding: 24rpx;
}
.section-title {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 16rpx;
display: block;
}
.position-info {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.position-label {
font-size: 30rpx;
color: #fff;
}
.position-value {
font-size: 30rpx;
font-weight: bold;
color: #00D4AA;
}
</style>