first commit
All checks were successful
Deploy to Server / deploy (push) Successful in 19s

This commit is contained in:
爽哒哒
2026-03-20 23:30:57 +08:00
commit 290be5d5be
328 changed files with 37215 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
// Tech/Cyberpunk Theme Variables & Mixins
// Colors
$bg-dark: #050505;
$primary-cyan: #00f0ff;
$primary-green: #00b96b;
$primary-purple: #bd00ff;
$text-main: #ffffff;
$text-secondary: rgba(255, 255, 255, 0.7);
$text-muted: rgba(255, 255, 255, 0.4);
// Mixins
@mixin page-container {
min-height: 100vh;
background-color: $bg-dark;
background-image:
radial-gradient(circle at 10% 10%, rgba(0, 240, 255, 0.1) 0%, transparent 40%),
radial-gradient(circle at 90% 90%, rgba(189, 0, 255, 0.1) 0%, transparent 40%);
color: $text-main;
padding: 30px;
box-sizing: border-box;
}
@mixin glass-card {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
}
@mixin neon-text($color: $primary-cyan) {
color: $color;
text-shadow: 0 0 10px rgba($color, 0.5), 0 0 20px rgba($color, 0.3);
}
@mixin neon-button($color: $primary-cyan) {
background: rgba($color, 0.1);
border: 1px solid rgba($color, 0.5);
color: $color;
box-shadow: 0 0 15px rgba($color, 0.2);
transition: all 0.3s ease;
&:active {
background: rgba($color, 0.2);
box-shadow: 0 0 25px rgba($color, 0.4);
transform: scale(0.98);
}
}
@mixin tech-border {
position: relative;
&::before {
content: '';
position: absolute;
top: -1px; left: -1px;
width: 20px; height: 20px;
border-top: 2px solid $primary-cyan;
border-left: 2px solid $primary-cyan;
border-radius: 4px 0 0 0;
}
&::after {
content: '';
position: absolute;
bottom: -1px; right: -1px;
width: 20px; height: 20px;
border-bottom: 2px solid $primary-cyan;
border-right: 2px solid $primary-cyan;
border-radius: 0 0 4px 0;
}
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '收益明细'
})

View File

@@ -0,0 +1,56 @@
@import './_shared.scss';
.page-container {
@include page-container;
}
.item {
@include glass-card;
padding: 30px;
margin-bottom: 24px;
.row {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
align-items: center;
.type {
font-size: 28px;
color: $text-main;
font-weight: bold;
}
.amount {
font-size: 32px;
@include neon-text($primary-green);
font-weight: bold;
font-family: 'DIN Alternate', sans-serif;
}
.source {
font-size: 24px;
color: $text-secondary;
}
.status {
font-size: 24px;
color: $primary-cyan;
background: rgba(0, 240, 255, 0.1);
padding: 4px 12px;
border-radius: 4px;
}
}
.time {
font-size: 22px;
color: $text-muted;
display: block;
margin-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 12px;
}
}
.empty {
padding: 100px 0;
text-align: center;
color: $text-muted;
font-size: 28px;
}

View File

@@ -0,0 +1,55 @@
import { View, Text } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { useState } from 'react'
import { distributorEarnings } from '../../api'
import './earnings.scss'
export default function Earnings() {
const [list, setList] = useState<any[]>([])
const [loading, setLoading] = useState(true)
useLoad(() => {
fetchData()
})
const fetchData = async () => {
try {
const res: any = await distributorEarnings()
// Pagination support check? The backend returns { count, next, previous, results } or just list if no pagination
if (res.results) {
setList(res.results)
} else if (Array.isArray(res)) {
setList(res)
}
} catch (err) {
console.error(err)
Taro.showToast({ title: '加载失败', icon: 'none' })
} finally {
setLoading(false)
}
}
return (
<View className='page-container'>
{list.length > 0 ? (
list.map((item: any) => (
<View className='item' key={item.id}>
<View className='row'>
<Text className='type'>{item.level === 1 ? '直接推广' : '团队奖励'}</Text>
<Text className='amount'>+{item.amount}</Text>
</View>
<View className='row'>
<Text className='source'>
{item.order_info?.customer_name} - ¥{item.order_info?.total_price}
</Text>
<Text className='status'>{item.status === 'settled' ? '已结算' : '待结算'}</Text>
</View>
<Text className='time'>{item.created_at?.replace('T', ' ').substring(0, 19)}</Text>
</View>
))
) : (
<View className='empty'></View>
)}
</View>
)
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '分销中心'
})

View File

@@ -0,0 +1,112 @@
@import './_shared.scss';
.page-container {
@include page-container;
}
.header-card {
@include glass-card;
@include tech-border;
padding: 40px 30px;
margin-bottom: 30px;
text-align: center;
position: relative;
overflow: hidden;
// Background accent
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(0, 240, 255, 0.05), rgba(0, 185, 107, 0.05));
z-index: -1;
}
.label {
font-size: 16px;
color: $text-secondary;
display: block;
margin-bottom: 16px;
letter-spacing: 1px;
text-transform: uppercase;
}
.amount {
font-size: 56px;
font-weight: bold;
display: block;
margin-bottom: 30px;
@include neon-text($primary-cyan);
font-family: 'DIN Alternate', sans-serif; // Use a tech-looking font if available
}
.btn-withdraw {
@include neon-button($primary-green);
border-radius: 30px;
font-size: 18px;
padding: 0 40px;
height: 50px;
line-height: 50px;
display: inline-block;
font-weight: bold;
letter-spacing: 2px;
}
}
.stats-grid {
display: flex;
@include glass-card;
padding: 30px 0;
margin-bottom: 30px;
.item {
flex: 1;
text-align: center;
border-right: 1px solid rgba(255, 255, 255, 0.1);
&:last-child { border-right: none; }
.val {
font-size: 24px;
font-weight: bold;
color: $text-main;
display: block;
margin-bottom: 8px;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
}
.lbl {
font-size: 14px;
color: $text-secondary;
}
}
}
.menu-list {
@include glass-card;
padding: 0 20px;
.menu-item {
display: flex;
justify-content: space-between;
align-items: center;
height: 70px; // Larger touch target
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 18px;
color: $text-main;
&:last-child { border-bottom: none; }
.arrow {
color: $primary-cyan;
opacity: 0.7;
font-family: monospace;
}
&:active {
background: rgba(255, 255, 255, 0.02);
}
}
}

View File

@@ -0,0 +1,83 @@
import { View, Text, Button } from '@tarojs/components'
import Taro, { useDidShow } from '@tarojs/taro'
import { useState } from 'react'
import { distributorInfo } from '../../api'
import './index.scss'
export default function DistributorIndex() {
const [info, setInfo] = useState<any>(null)
const [loading, setLoading] = useState(true)
useDidShow(() => {
fetchInfo()
})
const fetchInfo = async () => {
try {
const res = await distributorInfo()
setInfo(res)
} catch (err: any) {
if (err.statusCode === 404) {
// Not registered
Taro.redirectTo({ url: '/subpackages/distributor/register' })
} else {
Taro.showToast({ title: '加载失败', icon: 'none' })
}
} finally {
setLoading(false)
}
}
const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' })
const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' })
const goTeam = () => Taro.navigateTo({ url: '/subpackages/distributor/team' })
const goEarnings = () => Taro.navigateTo({ url: '/subpackages/distributor/earnings' })
const goOrders = () => Taro.navigateTo({ url: '/subpackages/distributor/orders' })
if (loading) return <View>Loading...</View>
if (!info) return <View>Error</View>
return (
<View className='page-container'>
<View className='header-card'>
<Text className='label'></Text>
<Text className='amount'>¥{info.withdrawable_balance}</Text>
<Button className='btn-withdraw' onClick={goWithdraw}></Button>
</View>
<View className='stats-grid'>
<View className='item'>
<Text className='val'>¥{info.total_earnings}</Text>
<Text className='lbl'></Text>
</View>
<View className='item'>
<Text className='val'>Lv.{info.level}</Text>
<Text className='lbl'></Text>
</View>
<View className='item'>
<Text className='val'>{(Number(info.commission_rate) * 100).toFixed(1)}%</Text>
<Text className='lbl'></Text>
</View>
</View>
<View className='menu-list'>
<View className='menu-item' onClick={goInvite}>
<Text>广</Text>
<Text className='arrow'>{'>'}</Text>
</View>
<View className='menu-item' onClick={goTeam}>
<Text></Text>
<Text className='arrow'>{'>'}</Text>
</View>
<View className='menu-item' onClick={goEarnings}>
<Text></Text>
<Text className='arrow'>{'>'}</Text>
</View>
<View className='menu-item' onClick={goOrders}>
<Text></Text>
<Text className='arrow'>{'>'}</Text>
</View>
</View>
</View>
)
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '推广邀请'
})

View File

@@ -0,0 +1,49 @@
@import './_shared.scss';
.page-container {
@include page-container;
display: flex;
flex-direction: column;
align-items: center;
}
.qr-card {
@include glass-card;
@include tech-border;
padding: 40px;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 40px;
.qr-img {
width: 400px;
height: 400px;
background: rgba(255, 255, 255, 0.1);
margin-bottom: 40px;
border: 1px solid $primary-cyan;
padding: 10px;
border-radius: 8px;
}
.tip {
color: $text-main;
font-size: 28px;
text-align: center;
line-height: 1.6;
opacity: 0.9;
}
}
.btn-save {
margin-top: 60px;
width: 100%;
@include neon-button($primary-cyan);
height: 90px;
line-height: 90px;
font-size: 32px;
font-weight: bold;
border-radius: 45px;
}

View File

@@ -0,0 +1,57 @@
import { View, Text, Image, Button } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { useState } from 'react'
import { distributorInvite } from '../../api'
import './invite.scss'
export default function Invite() {
const [qrCode, setQrCode] = useState('')
const [loading, setLoading] = useState(true)
useLoad(() => {
fetchQr()
})
const fetchQr = async () => {
try {
const res: any = await distributorInvite()
setQrCode(res.qr_code_url)
} catch (err) {
console.error(err)
Taro.showToast({ title: '获取二维码失败', icon: 'none' })
} finally {
setLoading(false)
}
}
const saveImage = () => {
if (!qrCode) return
Taro.downloadFile({
url: qrCode,
success: (res) => {
Taro.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => Taro.showToast({ title: '已保存', icon: 'success' }),
fail: () => Taro.showToast({ title: '保存失败', icon: 'none' })
})
}
})
}
return (
<View className='page-container'>
<View className='qr-card'>
{loading ? (
<View className='qr-img' style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Text>Loading...</Text>
</View>
) : (
<Image src={qrCode} className='qr-img' mode='aspectFit' />
)}
<Text className='tip'>{'\n'}广</Text>
</View>
<Button className='btn-save' onClick={saveImage} disabled={!qrCode}></Button>
</View>
)
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '分销订单'
})

View File

@@ -0,0 +1,72 @@
@import './_shared.scss';
.page-container {
@include page-container;
}
.item {
@include glass-card;
padding: 30px;
margin-bottom: 24px;
.row {
display: flex;
justify-content: space-between;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 16px;
margin-bottom: 16px;
.order-no {
font-size: 24px;
color: $text-secondary;
font-family: monospace;
}
.status {
font-size: 24px;
color: $primary-green;
}
}
.content {
display: flex;
margin-bottom: 16px;
.img {
width: 120px;
height: 120px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
margin-right: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.title {
font-size: 28px;
color: $text-main;
line-height: 1.4;
}
.price {
font-size: 32px;
color: $primary-cyan;
font-weight: bold;
}
}
}
.footer {
display: flex;
justify-content: space-between;
font-size: 24px;
color: $text-muted;
}
}
.empty {
padding: 100px 0;
text-align: center;
color: $text-muted;
font-size: 28px;
}

View File

@@ -0,0 +1,58 @@
import { View, Text, Image } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { useState } from 'react'
import { distributorOrders } from '../../api'
import './orders.scss'
export default function Orders() {
const [list, setList] = useState<any[]>([])
const [loading, setLoading] = useState(true)
useLoad(() => {
fetchData()
})
const fetchData = async () => {
try {
const res: any = await distributorOrders()
if (res.results) {
setList(res.results)
} else if (Array.isArray(res)) {
setList(res)
}
} catch (err) {
console.error(err)
Taro.showToast({ title: '加载失败', icon: 'none' })
} finally {
setLoading(false)
}
}
return (
<View className='page-container'>
{list.length > 0 ? (
list.map((item: any) => (
<View className='item' key={item.id}>
<View className='row'>
<Text className='order-no'>: {item.wechat_trade_no || item.id}</Text>
<Text className='status'>{item.status === 'paid' ? '已支付' : item.status}</Text>
</View>
<View className='content'>
<Image className='img' src={item.config_image || ''} mode='aspectFill' />
<View className='info'>
<Text className='title'>{item.config_name || item.course_title || '商品'}</Text>
<Text className='price'>¥{item.total_price}</Text>
</View>
</View>
<View className='footer'>
<Text className='customer'>: {item.customer_name}</Text>
<Text className='time'>{item.created_at?.split('T')[0]}</Text>
</View>
</View>
))
) : (
<View className='empty'></View>
)}
</View>
)
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '申请分销员'
})

View File

@@ -0,0 +1,46 @@
@import './_shared.scss';
.page-container {
@include page-container;
display: flex;
justify-content: center;
align-items: center;
}
.card {
@include glass-card;
@include tech-border;
padding: 50px 30px;
width: 100%;
text-align: center;
.title {
font-size: 32px;
font-weight: bold;
@include neon-text($primary-purple);
display: block;
margin-bottom: 16px;
letter-spacing: 2px;
}
.desc {
font-size: 16px;
color: $text-secondary;
display: block;
margin-bottom: 50px;
}
.btn-register {
@include neon-button($primary-green);
background: linear-gradient(90deg, rgba(0, 185, 107, 0.2), rgba(0, 240, 255, 0.2));
border: 1px solid $primary-green;
border-radius: 30px;
height: 56px;
line-height: 56px;
font-size: 20px;
font-weight: bold;
width: 80%;
margin: 0 auto;
letter-spacing: 4px;
}
}

View File

@@ -0,0 +1,32 @@
import { View, Button, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { distributorRegister } from '../../api'
import './register.scss'
export default function Register() {
const handleRegister = async () => {
try {
await distributorRegister({})
Taro.showToast({ title: '申请已提交', icon: 'success' })
setTimeout(() => {
Taro.redirectTo({ url: '/subpackages/distributor/index' })
}, 1500)
} catch (err: any) {
if (err.data?.error === 'Already registered') {
Taro.redirectTo({ url: '/subpackages/distributor/index' })
} else {
Taro.showToast({ title: '申请失败', icon: 'none' })
}
}
}
return (
<View className='page-container'>
<View className='card'>
<Text className='title'></Text>
<Text className='desc'></Text>
<Button className='btn-register' onClick={handleRegister}></Button>
</View>
</View>
)
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '我的团队'
})

View File

@@ -0,0 +1,85 @@
@import './_shared.scss';
.page-container {
@include page-container;
padding-bottom: 40px;
}
.header {
@include glass-card;
padding: 30px;
display: flex;
justify-content: space-around;
margin-bottom: 24px;
.stat {
display: flex;
flex-direction: column;
align-items: center;
.val {
font-size: 36px;
font-weight: bold;
@include neon-text($primary-cyan);
}
.lbl {
font-size: 24px;
color: $text-secondary;
margin-top: 10px;
}
}
}
.list {
@include glass-card;
padding: 0;
overflow: hidden;
.list-header {
padding: 24px 30px;
font-size: 28px;
font-weight: bold;
color: $text-main;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.02);
}
.item {
display: flex;
align-items: center;
padding: 24px 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin-right: 24px;
background: #333;
border: 2px solid $primary-purple;
}
.info {
flex: 1;
display: flex;
flex-direction: column;
.name {
font-size: 28px;
color: $text-main;
}
.time {
font-size: 22px;
color: $text-muted;
margin-top: 8px;
}
}
.level {
font-size: 24px;
color: $primary-purple;
font-weight: bold;
}
}
.empty {
padding: 50px;
text-align: center;
color: $text-muted;
}
}

View File

@@ -0,0 +1,62 @@
import { View, Text, Image } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { useState } from 'react'
import { distributorTeam } from '../../api'
import './team.scss'
export default function Team() {
const [data, setData] = useState<any>(null)
const [loading, setLoading] = useState(true)
useLoad(() => {
fetchData()
})
const fetchData = async () => {
try {
const res = await distributorTeam()
setData(res)
} catch (err) {
console.error(err)
Taro.showToast({ title: '加载失败', icon: 'none' })
} finally {
setLoading(false)
}
}
if (loading) return <View className='page-container'>Loading...</View>
if (!data) return <View className='page-container'>Error</View>
return (
<View className='page-container'>
<View className='header'>
<View className='stat'>
<Text className='val'>{data.children_count}</Text>
<Text className='lbl'></Text>
</View>
<View className='stat'>
<Text className='val'>¥{data.second_level_earnings}</Text>
<Text className='lbl'></Text>
</View>
</View>
<View className='list'>
<View className='list-header'></View>
{data.children?.length > 0 ? (
data.children.map((item: any) => (
<View className='item' key={item.id}>
<Image className='avatar' src={item.user_info?.avatar_url || ''} />
<View className='info'>
<Text className='name'>{item.user_info?.nickname || '用户'}</Text>
<Text className='time'>: {item.created_at?.split('T')[0]}</Text>
</View>
<Text className='level'>Lv.{item.level}</Text>
</View>
))
) : (
<View className='empty'></View>
)}
</View>
</View>
)
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '申请提现'
})

View File

@@ -0,0 +1,60 @@
@import './_shared.scss';
.page-container {
@include page-container;
}
.card {
@include glass-card;
padding: 40px;
.label {
font-size: 28px;
color: $text-secondary;
margin-bottom: 24px;
display: block;
}
.input-box {
display: flex;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding-bottom: 20px;
margin-bottom: 24px;
.symbol {
font-size: 48px;
font-weight: bold;
color: $text-main;
margin-right: 20px;
}
.input {
flex: 1;
height: 60px;
font-size: 48px;
font-weight: bold;
color: $primary-green;
}
}
.balance-tip {
font-size: 24px;
color: $text-muted;
.all {
color: $primary-cyan;
margin-left: 16px;
}
}
.btn-submit {
margin-top: 80px;
@include neon-button($primary-green);
height: 88px;
line-height: 88px;
font-size: 32px;
font-weight: bold;
border-radius: 44px;
}
}

View File

@@ -0,0 +1,73 @@
import { View, Text, Button, Input } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { useState } from 'react'
import { distributorInfo, distributorWithdraw } from '../../api'
import './withdraw.scss'
export default function Withdraw() {
const [balance, setBalance] = useState(0)
const [amount, setAmount] = useState('')
const [loading, setLoading] = useState(false)
useLoad(() => {
fetchInfo()
})
const fetchInfo = async () => {
try {
const res: any = await distributorInfo()
setBalance(Number(res.withdrawable_balance))
} catch (err) {
console.error(err)
}
}
const handleWithdraw = async () => {
const val = Number(amount)
if (!val || val <= 0) {
Taro.showToast({ title: '请输入有效金额', icon: 'none' })
return
}
if (val > balance) {
Taro.showToast({ title: '余额不足', icon: 'none' })
return
}
setLoading(true)
try {
await distributorWithdraw(val)
Taro.showToast({ title: '申请已提交', icon: 'success' })
setTimeout(() => {
Taro.navigateBack()
}, 1500)
} catch (err) {
Taro.showToast({ title: '提现失败', icon: 'none' })
} finally {
setLoading(false)
}
}
return (
<View className='page-container'>
<View className='card'>
<Text className='label'></Text>
<View className='input-box'>
<Text className='symbol'>¥</Text>
<Input
className='input'
type='digit'
value={amount}
onInput={(e) => setAmount(e.detail.value)}
placeholder='0.00'
/>
</View>
<View className='balance-tip'>
<Text> ¥{balance.toFixed(2)}</Text>
<Text className='all' onClick={() => setAmount(balance.toString())}></Text>
</View>
<Button className='btn-submit' onClick={handleWithdraw} loading={loading}></Button>
</View>
</View>
)
}