项目初版完成

This commit is contained in:
tianfeng 2025-08-13 18:28:28 +08:00
commit 0c96d3fa1f
52 changed files with 12075 additions and 0 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

114
README.md Normal file
View File

@ -0,0 +1,114 @@
# Likei H5 项目
这是一个基于 Vue 3 + Vite 的移动端 H5 应用,包含申请加入团队和主播中心两个主要页面。
## 项目结构
```
src/
├── components/
│ ├── MobileHeader.vue # 公共头部导航组件
│ └── HelloWorld.vue # 示例组件
├── views/
│ ├── HomeView.vue # 首页(带导航按钮)
│ ├── ApplyView.vue # 申请加入团队页面
│ ├── HostCenterView.vue # 主播中心页面
│ └── AboutView.vue # 关于页面
├── router/
│ └── index.js # 路由配置
├── assets/ # 静态资源
├── App.vue # 根组件
└── main.js # 入口文件
```
## 页面说明
### 1. 申请加入团队页面 (`/apply`)
- 包含申请表单
- 用户可以输入代理商ID
- 有帮助提示功能
- 支持表单验证
### 2. 主播中心页面 (`/host-center`)
- 显示用户信息和头像
- 显示收入统计
- 显示今日任务进度
- 包含交换金币、转账、提现等功能按钮
- 点击Date按钮跳转到历史薪资页面
- 点击Exchange按钮跳转到兑换金币页面
### 3. 主播设置页面 (`/host-setting`)
- 显示代理商信息
- 银行卡管理
- KYC认证信息
- 支持离开代理商操作
### 4. 历史薪资页面 (`/history-salary`)
- 按月份查看薪资记录
- 详细的收入分解(基础薪资、奖金、佣金、扣款)
- 支付状态显示
- 月度统计汇总
### 5. 兑换金币页面 (`/exchange-gold-coins`)
- 显示可用余额
- 收款人选择和管理
- 金币选择网格
- 支持多选金币进行转账
- 点击收款人可跳转到搜索页面
### 6. 搜索收款人页面 (`/search-payee`)
- 实时搜索功能
- 支持按ID或姓名搜索
- 显示用户标签Host、Agency、Salary
- 搜索结果选择和添加
### 3. 公共组件
#### MobileHeader.vue
可复用的移动端头部导航组件,支持:
- 自定义标题
- 返回按钮(可选)
- 帮助按钮(可选)
- 自定义事件处理
## 技术特点
1. **Vue 3 Composition API**: 使用最新的 Vue 3 语法
2. **响应式设计**: 适配手机和桌面端
3. **组件化开发**: 公共组件可复用
4. **简洁的样式**: 使用原生CSS无外部UI库依赖
5. **路由管理**: 使用 Vue Router 4
## 开发运行
```bash
# 安装依赖
npm install
# 开发模式运行
npm run dev
# 构建生产版本
npm run build
# 预览生产版本
npm run preview
```
## 代码特点
- **适合初学者**: 代码结构清晰,注释完整
- **无复杂依赖**: 只使用必要的Vue生态工具
- **移动端优先**: 专为移动端体验设计
- **易于扩展**: 组件化架构便于添加新功能
## 访问路径
- 首页: `/`
- 申请页面: `/apply`
- 主播中心: `/host-center`
- 主播设置: `/host-setting`
- 历史薪资: `/history-salary`
- 兑换金币: `/exchange-gold-coins`
- 搜索收款人: `/search-payee`
- 关于页面: `/about`

85
START.md Normal file
View File

@ -0,0 +1,85 @@
# 快速开始
## 项目说明
这是一个Vue3移动端H5项目的演示包含两个主要页面
1. **申请加入团队页面** - 模拟用户申请成为主播的流程
2. **主播中心页面** - 显示主播的收入、任务等信息
## 启动步骤
1. **安装依赖**
```bash
npm install
```
2. **启动开发服务器**
```bash
npm run dev
```
3. **访问应用**
- 在浏览器中打开显示的本地地址(通常是 http://localhost:5173
- 建议用浏览器的移动端模拟器查看效果
## 页面预览
### 首页
- 包含三个导航按钮
- 点击可以跳转到对应的功能页面
### 申请页面 (/apply)
- 包含申请表单
- 输入代理商ID
- 点击帮助按钮查看提示
### 主播中心 (/host-center)
- 查看用户信息
- 查看收入统计
- 查看任务进度
- 操作按钮(交换、转账、提现)
- Date按钮跳转到历史薪资
- Exchange按钮跳转到兑换金币
### 主播设置 (/host-setting)
- 查看代理商信息
- 管理银行卡信息
- KYC认证管理
### 历史薪资 (/history-salary)
- 按月份查看薪资
- 详细的收入分解
- 月度统计信息
### 兑换金币 (/exchange-gold-coins)
- 金币选择和转账
- 收款人管理
- 点击收款人跳转搜索
### 搜索收款人 (/search-payee)
- 实时搜索用户
- 按ID或名称搜索
## 技术亮点
**Vue 3** - 使用最新的Composition API语法
**响应式设计** - 适配移动端和桌面端
**组件化** - MobileHeader作为公共组件复用
**路由管理** - 使用Vue Router进行页面跳转
**简洁样式** - 原生CSS无复杂依赖
**易于理解** - 代码结构清晰适合Vue3初学者
## 文件结构
```
src/
├── components/MobileHeader.vue # 公共头部组件
├── views/
│ ├── HomeView.vue # 首页
│ ├── ApplyView.vue # 申请页面
│ ├── HostCenterView.vue # 主播中心
│ ├── HostSettingView.vue # 主播设置
│ ├── HistorySalaryView.vue # 历史薪资
│ ├── ExchangeGoldCoinsView.vue # 兑换金币
│ └── SearchPayeeView.vue # 搜索收款人
├── router/index.js # 路由配置
└── App.vue # 根组件
```

24
eslint.config.js Normal file
View File

@ -0,0 +1,24 @@
import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals'
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
export default defineConfig([
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
{
languageOptions: {
globals: {
...globals.browser,
},
},
},
js.configs.recommended,
...pluginVue.configs['flat/essential'],
])

24
index.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Likei H5</title>
<style>
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

3963
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "likei-h5",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --fix"
},
"dependencies": {
"vue": "^3.5.18",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@vitejs/plugin-vue": "^6.0.1",
"eslint": "^9.31.0",
"eslint-plugin-vue": "~10.3.0",
"globals": "^16.3.0",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

36
src/App.vue Normal file
View File

@ -0,0 +1,36 @@
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<div id="app">
<RouterView />
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: transparent;
overflow-x: hidden;
}
#app {
width: 100%;
min-height: 100vh;
margin: 0;
padding: 0;
}
</style>

64
src/api/wallet.js Normal file
View File

@ -0,0 +1,64 @@
import { get } from '../utils/http.js'
/**
* 获取钱包流水详情
* @returns {Promise} 返回流水详情数据
*/
export function getWalletDetails() {
return get('/wallet/salary-diamond/balance/water/details')
}
/**
* 获取银行余额
* @returns {Promise} 返回银行余额数据
*/
export function getBankBalance() {
return get('/wallet/bank/balance')
}
/**
* 转账角色检查
* @returns {Promise} 返回用户角色和权限信息
*/
export function userBankCheckTransfer() {
return get('/wallet/bank/check/transfer')
}
/**
* 搜索用户资料
* @param {string} type - 用户类型 (AGENT/BD/ANCHOR)
* @param {string} account - 用户账号/ID
* @returns {Promise} 返回用户资料
*/
export function userBankSearchUserProfile(type, account) {
return get(`/wallet/bank/search/user-profile?type=${type}&account=${account}`)
}
/**
* 格式化时间戳为可读格式
* @param {number} timestamp - 时间戳毫秒
* @returns {string} 格式化后的时间字符串
*/
export function formatDateTime(timestamp) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${day}/${month}/${year} ${hours}:${minutes}`
}
/**
* 格式化事件描述
* @param {string} eventDescribe - 原始事件描述
* @returns {string} 格式化后的事件描述
*/
export function formatEventDescribe(eventDescribe) {
// 移除 [system] 前缀并简化描述
return eventDescribe
.replace(/^\[system\]\s*/i, '[System] ')
.replace(/Settlement Agent Salary/i, 'Create An Account')
.replace(/Settlement Member salary/i, 'Transfer')
}

86
src/assets/base.css Normal file
View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1,120 @@
/* 全局渐变背景样式 */
.gradient-background {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
/* 备选渐变背景(更柔和的紫色渐变) */
.gradient-background-soft {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 50%, #4facfe 100%);
min-height: 100vh;
}
/* 淡紫色渐变背景(接近图片效果) */
.gradient-background-light {
background: linear-gradient(180deg, #e8d5ff 0%, #f5f1ff 50%, #ffffff 100%);
min-height: 100vh;
}
/* 圆圈晕染效果背景 */
.gradient-background-circles {
position: relative;
min-height: 100vh;
background: linear-gradient(180deg, #e8d5ff 0%, #f5f1ff 35%, #ffffff 100%);
overflow: hidden;
}
/* 左上角紫色大圆 */
.gradient-background-circles::before {
content: '';
position: absolute;
top: -40%;
left: -30%;
width: 80%;
height: 80%;
background: radial-gradient(
circle,
rgba(139, 92, 246, 0.35) 0%,
rgba(139, 92, 246, 0.2) 25%,
rgba(139, 92, 246, 0.1) 45%,
rgba(139, 92, 246, 0.05) 65%,
transparent 85%
);
border-radius: 50%;
filter: blur(60px);
z-index: 0;
animation: slowPulse 12s ease-in-out infinite;
}
/* 右上角蓝色大圆 */
.gradient-background-circles::after {
content: '';
position: absolute;
top: -10%;
right: -35%;
width: 100%;
height: 100%;
background: radial-gradient(
circle,
rgba(59, 130, 246, 0.3) 0%,
rgba(59, 130, 246, 0.15) 30%,
rgba(59, 130, 246, 0.08) 50%,
rgba(59, 130, 246, 0.03) 70%,
transparent 90%
);
border-radius: 50%;
filter: blur(80px);
z-index: 0;
animation: rotate 20s linear infinite;
}
/* 动画效果 */
@keyframes slowPulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.9;
}
}
@keyframes rotate {
0% {
transform: rotate(0deg) scale(1);
}
50% {
transform: rotate(180deg) scale(1.03);
}
100% {
transform: rotate(360deg) scale(1);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0px) scale(1);
opacity: 0.8;
}
33% {
transform: translateY(-15px) scale(1.02);
opacity: 1;
}
66% {
transform: translateY(10px) scale(0.98);
opacity: 0.9;
}
}
/* 确保内容在圆圈效果之上 */
.gradient-background-circles > * {
position: relative;
z-index: 1;
}
.gradient-background-circles .content {
position: relative;
z-index: 2;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

36
src/assets/main.css Normal file
View File

@ -0,0 +1,36 @@
@import './base.css';
@import './gradient-background.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,44 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="mobile-header">
<button v-if="showBack" class="back-btn" @click="goBack">
<span></span>
</button>
<h1 class="title">{{ title }}</h1>
<button v-if="showHelp" class="help-btn" @click="$emit('help')">
<span>?</span>
</button>
<div v-else class="placeholder"></div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
// props
defineProps({
title: {
type: String,
required: true
},
showBack: {
type: Boolean,
default: true
},
showHelp: {
type: Boolean,
default: false
}
})
// emits
defineEmits(['help'])
const router = useRouter()
const goBack = () => {
router.go(-1)
}
</script>
<style scoped>
.mobile-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: white;
border-bottom: 1px solid #e0e0e0;
position: sticky;
top: 0;
z-index: 100;
}
.back-btn, .help-btn {
width: 40px;
height: 40px;
border: none;
background: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #333;
cursor: pointer;
}
.back-btn:hover, .help-btn:hover {
background-color: #f0f0f0;
border-radius: 20px;
}
.title {
font-size: 18px;
font-weight: 600;
margin: 0;
color: #333;
text-align: center;
flex: 1;
}
.placeholder {
width: 40px;
height: 40px;
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<div class="page-container">
<slot />
</div>
</template>
<script setup>
//
</script>
<style scoped>
.page-container {
background: linear-gradient(180deg, #e8d5ff 0%, #f5f1ff 50%, #ffffff 100%);
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
</style>

View File

@ -0,0 +1,94 @@
<script setup>
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@ -0,0 +1,86 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

11
src/main.js Normal file
View File

@ -0,0 +1,11 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')

105
src/router/index.js Normal file
View File

@ -0,0 +1,105 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
},
{
path: '/apply',
name: 'apply',
component: () => import('../views/ApplyView.vue'),
},
{
path: '/host-center',
name: 'host-center',
component: () => import('../views/HostCenterView.vue'),
},
{
path: '/host-setting',
name: 'host-setting',
component: () => import('../views/HostSettingView.vue'),
},
{
path: '/history-salary',
name: 'history-salary',
component: () => import('../views/HistorySalaryView.vue'),
},
{
path: '/transfer',
name: 'transfer',
component: () => import('../views/TransferView.vue'),
},
{
path: '/search-payee',
name: 'search-payee',
component: () => import('../views/SearchPayeeView.vue'),
},
{
path: '/information-details',
name: 'information-details',
component: () => import('../views/InformationDetailsView.vue'),
},
{
path: '/message',
name: 'message',
component: () => import('../views/MessageView.vue'),
},
{
path: '/invite-members',
name: 'invite-members',
component: () => import('../views/InviteMembersView.vue'),
},
{
path: '/team-bill',
name: 'team-bill',
component: () => import('../views/TeamBillView.vue'),
},
{
path: '/team-member',
name: 'team-member',
component: () => import('../views/TeamMemberView.vue'),
},
{
path: '/platform-policy',
name: 'platform-policy',
component: () => import('../views/PlatformPolicyView.vue'),
},
{
path: '/coin-seller',
name: 'coin-seller',
component: () => import('../views/CoinSellerView.vue'),
},
{
path: '/coin-seller-search',
name: 'coin-seller-search',
component: () => import('../views/CoinSellerSearchView.vue'),
},
{
path: '/seller-records',
name: 'seller-records',
component: () => import('../views/SellerRecordsView.vue'),
},
{
path: '/exchange-gold-coins',
name: 'exchange-gold-coins',
component: () => import('../views/ExchangeGoldCoinsView.vue'),
},
{
path: '/agency-center',
name: 'agency-center',
component: () => import('../views/AgencyCenterView.vue'),
},
],
})
export default router

View File

@ -0,0 +1,27 @@
import { ref, reactive } from 'vue'
// Coin Seller 全局状态管理
const coinSellerStore = reactive({
selectedUser: {
id: '',
name: ''
}
})
// 设置选中的用户
export function setSelectedUser(user) {
coinSellerStore.selectedUser = user
}
// 获取选中的用户
export function getSelectedUser() {
return coinSellerStore.selectedUser
}
// 清除选中的用户
export function clearSelectedUser() {
coinSellerStore.selectedUser = {
id: '',
name: ''
}
}

111
src/utils/http.js Normal file
View File

@ -0,0 +1,111 @@
// HTTP请求配置
const API_BASE_URL = 'https://api.likeichat.com'
// 公共请求头配置
const COMMON_HEADERS = {
'authorization': 'Bearer 1850459451220E4F1FABF65F36A43E76.djIlM0ExOTU0MDU5NzkzOTYzMzkzMDI1JTNBTElLRUklM0ExNzU3NjQ4ODMwMjM3JTNBMTc1NTA1NjgzMDIzNw==',
'req-app-intel': 'version=1.0.0;build=1;model=SM-G9550;sysVersion=9;channel=Google',
'req-client': 'Android',
'req-imei': '8fa54d728ab449e04f9292329ed44bda',
'req-lang': 'id-ID',
'req-sys-origin': 'origin=LIKEI;originChild=LIKEI',
'req-version': 'V2',
'req-zone': 'Asia/Shanghai',
}
/**
* 通用HTTP请求函数
* @param {string} url - 请求URL
* @param {object} options - 请求选项
* @returns {Promise} - 返回Promise对象
*/
export async function request(url, options = {}) {
const {
method = 'GET',
data = null,
headers = {},
...otherOptions
} = options
const fullUrl = url.startsWith('http') ? url : `${API_BASE_URL}${url}`
const config = {
method,
headers: {
...COMMON_HEADERS,
...headers
},
...otherOptions
}
// 如果是POST、PUT等方法且有数据添加body
if (data && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
config.body = JSON.stringify(data)
}
try {
console.log('🚀 API Request:', fullUrl, config)
const response = await fetch(fullUrl, config)
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`)
}
const result = await response.json()
console.log('✅ API Response:', result)
// 检查业务状态
if (result.status === false) {
throw new Error(result.message || `API Error: ${result.errorCode}`)
}
return result
} catch (error) {
console.error('❌ API Error:', error)
throw error
}
}
/**
* GET请求
* @param {string} url - 请求URL
* @param {object} options - 请求选项
* @returns {Promise}
*/
export function get(url, options = {}) {
return request(url, { ...options, method: 'GET' })
}
/**
* POST请求
* @param {string} url - 请求URL
* @param {object} data - 请求数据
* @param {object} options - 请求选项
* @returns {Promise}
*/
export function post(url, data, options = {}) {
return request(url, { ...options, method: 'POST', data })
}
/**
* PUT请求
* @param {string} url - 请求URL
* @param {object} data - 请求数据
* @param {object} options - 请求选项
* @returns {Promise}
*/
export function put(url, data, options = {}) {
return request(url, { ...options, method: 'PUT', data })
}
/**
* DELETE请求
* @param {string} url - 请求URL
* @param {object} options - 请求选项
* @returns {Promise}
*/
export function del(url, options = {}) {
return request(url, { ...options, method: 'DELETE' })
}

21
src/utils/payeeStore.js Normal file
View File

@ -0,0 +1,21 @@
import { ref, reactive } from 'vue'
// 全局状态管理
const selectedPayeeStore = reactive({
payee: null
})
// 设置选中的收款人
export function setSelectedPayee(payee) {
selectedPayeeStore.payee = payee
}
// 获取选中的收款人
export function getSelectedPayee() {
return selectedPayeeStore.payee
}
// 清除选中的收款人
export function clearSelectedPayee() {
selectedPayeeStore.payee = null
}

27
src/views/AboutView.vue Normal file
View File

@ -0,0 +1,27 @@
<template>
<div class="about gradient-background-circles">
<h1>This is an about page</h1>
</div>
</template>
<style>
.about {
padding: 20px;
text-align: center;
}
.about h1 {
position: relative;
z-index: 2;
color: #333;
}
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@ -0,0 +1,753 @@
<template>
<div class="agency-center gradient-background-circles">
<MobileHeader title="Host Center" />
<div class="content">
<!-- 用户信息卡片 -->
<div class="user-card">
<div class="user-info">
<div class="avatar">
<span>{{ userInfo.name.charAt(0) }}</span>
</div>
<div class="user-details">
<h3>{{ userInfo.name }}</h3>
<p>ID: {{ userInfo.id }}</p>
</div>
<button class="notification-btn" @click="goToNotification">
<span>🔔</span>
</button>
</div>
<!-- 身份切换标签 -->
<div class="identity-tabs">
<button
v-for="identity in identities"
:key="identity.value"
@click="handleIdentityChange(identity.value)"
:class="['identity-tab', { active: currentIdentity === identity.value }]"
>
{{ identity.label }}
</button>
</div>
<!-- 账户状态标签 -->
<div class="account-tags">
<span class="tag agency-tag">🏢 Agency</span>
<span class="tag salary-tag">💰 Salary</span>
</div>
</div>
<!-- Host Agency 标签页 -->
<div class="host-agency-tabs">
<button
@click="taskTab = 'host'"
:class="['host-agency-tab', { active: taskTab === 'host' }]"
>
Host
</button>
<button
@click="taskTab = 'agency'"
:class="['host-agency-tab', { active: taskTab === 'agency' }]"
>
Agency
<button v-if="taskTab === 'agency'" class="help-btn" @click.stop="goToInviteMembers">
?
</button>
</button>
</div>
<!-- My salary 区域 -->
<div class="salary-section">
<div class="salary-header">
<h3>My salary:</h3>
<button class="personal-salary-btn" @click="goToPersonalSalary">
Personal Salary <span class="arrow">></span>
</button>
</div>
<div class="salary-content">
<div class="salary-item">
<p class="salary-label">History Salary:</p>
<p class="salary-amount">${{ salaryInfo.historySalary }}</p>
</div>
<div class="salary-separator"></div>
<div class="salary-item">
<p class="salary-label">Current Salary:</p>
<p class="salary-amount">${{ salaryInfo.currentSalary }}</p>
</div>
</div>
</div>
<!-- Host Agency 内容 -->
<div class="host-agency-content">
<!-- Host 任务内容 -->
<div v-if="taskTab === 'host'" class="task-content">
<h3>Today's Task:</h3>
<!-- 任务表格 -->
<div class="task-table">
<div class="table-row table-header">
<div class="table-cell">Anchor type</div>
<div class="table-cell">Minutes</div>
<div class="table-cell">Gift Task</div>
</div>
<div class="table-row table-data">
<div class="table-cell">{{ taskInfo.anchorType }}</div>
<div class="table-cell">
<span class="minutes-progress">{{ taskInfo.minutesProgress }}</span>
<span class="minutes-completed">/{{ taskInfo.minutesTotal }}</span>
</div>
<div class="table-cell">{{ taskInfo.giftTask }}</div>
</div>
</div>
<!-- Platform Policy 链接 -->
<!-- <div class="policy-link" @click="goToPlatformPolicy">
<span>Platform Policy</span>
<span class="arrow">></span>
</div>-->
</div>
<!-- Agency 任务内容 -->
<div v-else class="agency-task-content">
<!-- Team Bill 卡片 -->
<div class="team-bill-card" @click="goToTeamBill">
<div class="card-content">
<div class="card-date">{{ teamBillInfo.date }}</div>
<div class="card-link">
<span>Team Bill</span>
<span class="arrow">></span>
</div>
<div class="card-details">
<div class="detail-row">
<span class="detail-label">Host Salary:</span>
<span class="detail-value">{{ teamBillInfo.hostSalary }}</span>
<span class="detail-label">Agent Salary:</span>
<span class="detail-value">{{ teamBillInfo.agentSalary }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Total:</span>
<span class="detail-value">{{ teamBillInfo.total }}</span>
<span class="status-badge">{{ teamBillInfo.status }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Team Member 卡片独立在外面 -->
<div class="team-member-section">
<div class="team-member-card" @click="goToTeamMember">
<div class="card-content">
<span class="member-title">Team Member ({{ teamMemberCount }})</span>
<span class="arrow">></span>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button class="action-btn exchange-btn" @click="exchangeGoldCoins">
Exchange<br>Gold Coins
</button>
<button class="action-btn transfer-btn" @click="transfer">
Transfer
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
//
const currentIdentity = ref('agency')
//
const taskTab = ref('agency')
//
const teamMemberCount = ref(87)
//
const salaryInfo = reactive({
historySalary: 12345,
currentSalary: 12345
})
//
const identities = [
{ label: 'Host', value: 'host' },
{ label: 'Agency', value: 'agency' },
{ label: 'Coin Seller', value: 'coin-seller' }
]
//
const userInfo = reactive({
name: 'User1',
id: '1234567890'
})
//
const taskInfo = reactive({
anchorType: 'Chat',
minutesProgress: '120',
minutesTotal: '120',
giftTask: 0
})
// Team Bill
const teamBillInfo = reactive({
date: '2025.08',
hostSalary: '',
agentSalary: '',
total: '',
status: 'Pending'
})
//
const handleIdentityChange = (identity) => {
switch (identity) {
case 'host':
router.push('/host-center')
break
case 'coin-seller':
router.push('/coin-seller')
break
case 'agency':
default:
//
currentIdentity.value = identity
break
}
}
//
const goToNotification = () => {
router.push('/message')
}
const goToInviteMembers = () => {
router.push('/invite-members')
}
const goToTeamBill = () => {
router.push('/team-bill')
}
const goToTeamMember = () => {
router.push('/team-member')
}
const goToPlatformPolicy = () => {
router.push('/platform-policy')
}
const exchangeGoldCoins = () => {
router.push('/exchange-gold-coins')
}
const transfer = () => {
router.push('/transfer')
}
const goToPersonalSalary = () => {
router.push('/platform-policy')
}
</script>
<style scoped>
.agency-center {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 用户信息卡片 */
.user-card {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
font-weight: 600;
margin-right: 12px;
}
.user-details {
flex: 1;
}
.user-details h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.user-details p {
margin: 0;
font-size: 14px;
color: #666;
}
.notification-btn {
width: 32px;
height: 32px;
border: none;
background: none;
font-size: 16px;
cursor: pointer;
}
/* 身份切换标签 */
.identity-tabs {
display: flex;
gap: 8px;
margin: 12px 0;
}
.identity-tab {
padding: 6px 12px;
border: 1px solid #e5e7eb;
border-radius: 16px;
background-color: white;
color: #666;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.identity-tab.active {
background-color: #8B5CF6;
color: white;
border-color: #8B5CF6;
}
.identity-tab:not(.active):hover {
border-color: #8B5CF6;
color: #8B5CF6;
}
.account-tags {
display: flex;
gap: 8px;
}
.tag {
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
}
.agency-tag {
background-color: #DBEAFE;
color: #1E40AF;
}
.salary-tag {
background-color: #FEF3C7;
color: #D97706;
}
/* Host Agency 标签页 */
.host-agency-tabs {
display: flex;
background-color: white;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.host-agency-tab {
flex: 1;
padding: 12px 16px;
border: none;
background-color: #f8f9fa;
color: #666;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
border-bottom: 3px solid transparent;
}
.host-agency-tab.active {
background-color: white;
color: #333;
border-bottom-color: #8B5CF6;
font-weight: 600;
}
.host-agency-tab:not(.active):hover {
background-color: #e9ecef;
color: #8B5CF6;
}
.host-agency-tab .help-btn {
width: 16px;
height: 16px;
border-radius: 8px;
background-color: #8B5CF6;
color: white;
border: none;
font-size: 10px;
cursor: pointer;
margin-left: 4px;
}
/* My salary 区域 */
.salary-section {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.salary-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.salary-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.personal-salary-btn {
background: none;
border: none;
color: #8B5CF6;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.2s;
}
.personal-salary-btn:hover {
color: #7C3AED;
}
.salary-content {
display: flex;
align-items: center;
gap: 20px;
}
.salary-item {
flex: 1;
text-align: center;
}
.salary-label {
margin: 0 0 8px 0;
font-size: 14px;
color: #666;
}
.salary-amount {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
.salary-separator {
width: 1px;
height: 40px;
background-color: #8B5CF6;
}
/* Host Agency 内容区域 */
.host-agency-content {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.task-content h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
/* Team Member 独立区域 */
.team-member-section {
margin-bottom: 12px;
}
.team-member-card {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.2s;
border: 1px solid #e5e7eb;
}
.team-member-card:active {
transform: scale(0.98);
}
.team-member-card .card-content {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 任务表格 */
.task-table {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
margin-bottom: 12px;
}
.table-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.table-header {
background-color: #f8f9fa;
border-bottom: 1px solid #e5e7eb;
}
.table-data {
background-color: white;
}
.table-cell {
padding: 12px 8px;
text-align: center;
border-right: 1px solid #e5e7eb;
font-size: 14px;
}
.table-cell:last-child {
border-right: none;
}
.table-header .table-cell {
font-weight: 600;
color: #374151;
}
.table-data .table-cell {
color: #111827;
}
.minutes-progress {
color: #6B7280;
}
.minutes-completed {
color: #10B981;
font-weight: 600;
}
/* Agency 卡片区域 */
.agency-task-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.team-bill-card {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.2s;
border: 2px solid #3B82F6;
}
.team-bill-card:active {
transform: scale(0.98);
}
.team-bill-card .card-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* 通用卡片内容样式 */
.card-content {
padding: 16px;
}
.card-date {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.card-link {
display: flex;
justify-content: space-between;
align-items: center;
color: #6B7280;
font-size: 14px;
}
.card-details {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-row {
display: grid;
grid-template-columns: auto auto auto auto;
gap: 16px;
align-items: center;
}
.detail-label {
font-size: 14px;
color: #6B7280;
}
.detail-value {
font-size: 14px;
font-weight: 500;
color: #1F2937;
}
.status-badge {
background-color: #DBEAFE;
color: #1D4ED8;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-align: center;
}
.member-title {
font-size: 16px;
font-weight: 600;
color: #1F2937;
}
/* Platform Policy 链接 */
.policy-link {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
margin-top: 12px;
background-color: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #e9ecef;
}
.policy-link:hover {
background-color: #e9ecef;
border-color: #8B5CF6;
}
.policy-link:active {
transform: scale(0.98);
}
.policy-link span:first-child {
font-size: 14px;
font-weight: 500;
color: #333;
}
/* 操作按钮 */
.action-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.action-btn {
padding: 14px 12px;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
text-align: center;
line-height: 1.2;
transition: all 0.2s;
width: 100%;
}
.action-btn:active {
transform: scale(0.98);
}
.exchange-btn {
background-color: #FDE68A;
color: #D97706;
}
.transfer-btn {
background-color: #8B5CF6;
color: white;
}
/* 通用箭头样式 */
.arrow {
font-size: 16px;
font-weight: bold;
color: #8B5CF6;
}
</style>

224
src/views/ApplyView.vue Normal file
View File

@ -0,0 +1,224 @@
<template>
<div class="apply-page gradient-background-circles">
<!-- 顶部导航 -->
<MobileHeader
title="Apply to join the team"
:show-help="true"
@help="showHelp"
/>
<!-- 主要内容 -->
<div class="content">
<!-- 提醒信息 -->
<div class="reminder">
<h3>Reminder before application:</h3>
<p>Enter the ID of your agent, and you will become the host after the agency is approved. After becoming an anchor, your income will be entrusted to your agent</p>
</div>
<!-- 申请表单 -->
<div class="apply-form">
<div class="form-group">
<label class="form-label">Apply ID:</label>
<div class="input-group">
<input
v-model="agentId"
type="text"
placeholder="Please enter the agent ID"
class="form-input"
/>
<button class="apply-btn" @click="submitApplication" :disabled="!agentId">
Apply
</button>
</div>
</div>
</div>
</div>
<!-- 帮助弹窗 -->
<div v-if="showHelpModal" class="modal-overlay" @click="showHelpModal = false">
<div class="modal" @click.stop>
<h3>Help Information</h3>
<p>Please contact your agent to get the correct Agent ID before applying.</p>
<button @click="showHelpModal = false">Close</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
const agentId = ref('')
const showHelpModal = ref(false)
//
const showHelp = () => {
showHelpModal.value = true
}
//
const submitApplication = () => {
if (!agentId.value.trim()) {
alert('Please enter agent ID')
return
}
//
alert(`Application submitted with Agent ID: ${agentId.value}`)
//
// router.push('/host-center')
}
</script>
<style scoped>
.apply-page {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
.reminder {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.reminder h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.reminder p {
margin: 0;
font-size: 14px;
line-height: 1.5;
color: #666;
}
.apply-form {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.input-group {
display: flex;
gap: 12px;
align-items: center;
}
.form-input {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 16px;
background-color: #f9f9f9;
}
.form-input:focus {
outline: none;
border-color: #007AFF;
background-color: white;
}
.apply-btn {
padding: 12px 24px;
background-color: #8B5CF6;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.apply-btn:hover:not(:disabled) {
background-color: #7C3AED;
}
.apply-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 16px;
}
.modal {
background: white;
padding: 20px;
border-radius: 12px;
width: 100%;
max-width: 300px;
text-align: center;
}
.modal h3 {
margin: 0 0 16px 0;
font-size: 18px;
}
.modal p {
margin: 0 0 20px 0;
font-size: 14px;
color: #666;
}
.modal button {
padding: 8px 20px;
background-color: #007AFF;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
/* 手机适配 */
.input-group {
flex-direction: column;
align-items: stretch;
}
.apply-btn {
width: 100%;
margin-top: 8px;
}
</style>

View File

@ -0,0 +1,373 @@
<template>
<div class="coin-seller-search gradient-background-circles">
<MobileHeader title="Search user" />
<div class="content">
<!-- 搜索框 -->
<div class="search-section">
<div class="search-box">
<span class="search-icon">🔍</span>
<input
v-model="searchQuery"
type="text"
placeholder="Please enter the host ID"
class="search-input"
@keyup.enter="performSearch"
/>
<button
v-if="searchQuery"
@click="clearSearch"
class="clear-btn"
>
×
</button>
<button
class="action-btn"
@click="searchQuery ? confirmUser : cancelSearch"
>
{{ searchQuery ? 'Confirm' : 'Cancel' }}
</button>
</div>
</div>
<!-- 搜索结果 -->
<div v-if="searchResults.length > 0" class="results-section">
<div
v-for="user in searchResults"
:key="user.id"
class="user-item"
@click="selectUser(user)"
>
<div class="user-info">
<div class="user-avatar">
<span>{{ user.name.charAt(0) }}</span>
</div>
<div class="user-details">
<h4>{{ user.name }}</h4>
<p>ID: {{ user.id }}</p>
</div>
</div>
<div class="add-icon">
<span></span>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="isSearching" class="loading-state">
<div class="loading-spinner"></div>
<p>Searching...</p>
</div>
<!-- 空状态 -->
<div v-else-if="searchQuery && !isSearching && searchResults.length === 0" class="empty-state">
<div class="empty-icon">🔍</div>
<p>No users found</p>
<span>Try searching with a different ID</span>
</div>
<!-- 搜索提示 -->
<div v-else-if="!searchQuery" class="search-hint">
<div class="hint-icon">👤</div>
<p>Search for users</p>
<span>Enter host ID to find users</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { setSelectedUser } from '../utils/coinSellerStore.js'
const router = useRouter()
const searchQuery = ref('')
const searchResults = ref([])
const isSearching = ref(false)
//
const mockUsers = [
{
id: '1234567890',
name: 'User1'
},
{
id: '0987654321',
name: 'Alice Johnson'
},
{
id: '1122334455',
name: 'Bob Smith'
}
]
//
const performSearch = () => {
if (!searchQuery.value.trim()) {
searchResults.value = []
return
}
isSearching.value = true
//
setTimeout(() => {
const query = searchQuery.value.toLowerCase()
searchResults.value = mockUsers.filter(user =>
user.name.toLowerCase().includes(query) ||
user.id.includes(query)
)
isSearching.value = false
}, 500)
}
//
const clearSearch = () => {
searchQuery.value = ''
searchResults.value = []
}
//
const cancelSearch = () => {
router.go(-1)
}
//
const confirmUser = () => {
if (searchResults.value.length > 0) {
selectUser(searchResults.value[0])
} else {
performSearch()
}
}
//
const selectUser = (user) => {
//
setSelectedUser({
id: user.id,
name: user.name
})
console.log('Selected user:', user)
//
router.go(-1)
}
</script>
<style scoped>
.coin-seller-search {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 搜索部分 */
.search-section {
margin-bottom: 20px;
}
.search-box {
display: flex;
align-items: center;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.search-icon {
margin-right: 8px;
color: #9ca3af;
}
.search-input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #9ca3af;
}
.clear-btn {
background: none;
border: none;
font-size: 18px;
color: #9ca3af;
cursor: pointer;
padding: 0 8px;
}
.action-btn {
background: none;
border: none;
color: #8B5CF6;
font-size: 14px;
font-weight: 500;
cursor: pointer;
padding: 4px 8px;
margin-left: 8px;
}
.action-btn:active {
color: #7C3AED;
}
/* 搜索结果 */
.results-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.user-item {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: space-between;
}
.user-item:active {
transform: scale(0.98);
}
.user-info {
display: flex;
align-items: center;
flex: 1;
}
.user-avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #F59E0B;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
font-weight: 600;
margin-right: 12px;
}
.user-details h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.user-details p {
margin: 0;
font-size: 14px;
color: #666;
}
.add-icon {
color: #10B981;
font-size: 20px;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #F59E0B;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
margin: 0;
font-size: 14px;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 500;
}
.empty-state span {
font-size: 14px;
color: #9ca3af;
}
/* 搜索提示 */
.search-hint {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.hint-icon {
font-size: 48px;
margin-bottom: 16px;
}
.search-hint p {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 500;
}
.search-hint span {
font-size: 14px;
color: #9ca3af;
}
</style>

View File

@ -0,0 +1,435 @@
<template>
<div class="coin-seller gradient-background-circles">
<MobileHeader title="Coin Seller" />
<div class="content">
<!-- 用户信息卡片 -->
<div class="user-card">
<div class="user-info">
<div class="avatar">
<span>{{ userInfo.name.charAt(0) }}</span>
</div>
<div class="user-details">
<h3>{{ userInfo.name }}</h3>
<p>ID: {{ userInfo.id }}</p>
</div>
<button class="settings-btn" @click="goToSettings">
<span></span>
</button>
</div>
<!-- 账户状态标签 -->
<div class="account-tags">
<span class="tag seller-tag">🪙 Seller</span>
</div>
</div>
<!-- 我的金币 -->
<div class="coins-section">
<h3>My coins:</h3>
<div class="coins-display">
<span class="coin-icon">🪙</span>
<span class="coins-amount">{{ coinsAmount }}</span>
</div>
</div>
<!-- 用户搜索部分 -->
<div class="user-section">
<h3>User:</h3>
<div v-if="selectedUser.id" class="selected-user">
<div class="user-info">
<div class="user-avatar">
<span>{{ selectedUser.name.charAt(0) }}</span>
</div>
<div class="user-details">
<div class="user-name">{{ selectedUser.name }}</div>
<div class="user-id">ID: {{ selectedUser.id }}</div>
</div>
</div>
<button class="change-btn" @click="searchUser">Change</button>
</div>
<div v-else class="search-placeholder" @click="searchUser">
<span>Search</span>
</div>
</div>
<!-- 充值部分 -->
<div class="recharge-section">
<h3>Recharge:</h3>
<div class="recharge-input">
<input
v-model="rechargeAmount"
type="number"
placeholder="Please Enter The Amount Of Gold Coins To Transfer"
class="amount-input"
/>
<span class="currency-suffix">0/50</span>
</div>
<button
class="recharge-btn"
:disabled="!canRecharge"
@click="rechargeNow"
>
Recharge now
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { getSelectedUser, clearSelectedUser } from '../utils/coinSellerStore.js'
const router = useRouter()
//
const userInfo = ref({
name: 'User1',
id: '1234567890'
})
//
const coinsAmount = ref('123456478')
//
const selectedUser = ref({
id: '',
name: ''
})
//
const initializeUser = () => {
const savedUser = getSelectedUser()
if (savedUser && savedUser.id) {
selectedUser.value = { ...savedUser }
}
}
//
const rechargeAmount = ref('')
//
const canRecharge = computed(() => {
return selectedUser.value.id && rechargeAmount.value && parseFloat(rechargeAmount.value) > 0
})
//
const goToSettings = () => {
router.push('/seller-records')
}
//
const searchUser = () => {
router.push('/coin-seller-search')
}
//
const rechargeNow = () => {
if (!canRecharge.value) return
alert(`Recharge ${rechargeAmount.value} coins to ${selectedUser.value.name}`)
//
rechargeAmount.value = ''
//
}
//
onMounted(() => {
initializeUser()
})
</script>
<style scoped>
.coin-seller {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 用户信息卡片 */
.user-card {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #F59E0B;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
font-weight: 600;
margin-right: 12px;
}
.user-details {
flex: 1;
}
.user-details h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.user-details p {
margin: 0;
font-size: 14px;
color: #666;
}
.settings-btn {
width: 32px;
height: 32px;
border: none;
background: none;
font-size: 16px;
cursor: pointer;
}
.account-tags {
display: flex;
gap: 8px;
}
.tag {
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
}
.seller-tag {
background-color: #FEF3C7;
color: #D97706;
}
/* 金币部分 */
.coins-section {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.coins-section h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.coins-display {
display: flex;
align-items: center;
gap: 8px;
}
.coin-icon {
font-size: 24px;
}
.coins-amount {
font-size: 20px;
font-weight: 600;
color: #F59E0B;
}
/* 用户搜索部分 */
.user-section {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.user-section h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.selected-user {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.selected-user .user-info {
display: flex;
align-items: center;
margin-bottom: 0;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
font-weight: 600;
margin-right: 12px;
}
.selected-user .user-details {
flex: 1;
}
.user-name {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.user-id {
font-size: 12px;
color: #666;
}
.change-btn {
background-color: #C084FC;
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.change-btn:active {
background-color: #A855F7;
}
.search-placeholder {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
border: 2px dashed #ddd;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background-color: #fafafa;
}
.search-placeholder:hover {
border-color: #F59E0B;
background-color: #fef5e7;
}
.search-placeholder span {
color: #666;
font-size: 14px;
font-weight: 500;
}
/* 充值部分 */
.recharge-section {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.recharge-section h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.recharge-input {
position: relative;
margin-bottom: 16px;
}
.amount-input {
width: 100%;
padding: 12px 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
background-color: #f9fafb;
box-sizing: border-box;
}
.amount-input:focus {
outline: none;
border-color: #F59E0B;
background-color: white;
}
.amount-input::placeholder {
color: #9ca3af;
}
.currency-suffix {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
color: #666;
}
.recharge-btn {
width: 100%;
padding: 14px;
background-color: #8B5CF6;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.recharge-btn:not(:disabled):hover {
background-color: #7C3AED;
}
.recharge-btn:not(:disabled):active {
background-color: #6D28D9;
}
.recharge-btn:disabled {
background-color: #D1D5DB;
cursor: not-allowed;
}
</style>

View File

@ -0,0 +1,289 @@
<template>
<div class="exchange-gold-coins gradient-background-circles">
<MobileHeader title="Exchange Gold Coins" />
<div class="content">
<!-- 信息部分 -->
<div class="info-section">
<div class="info-header">
<button class="details-btn" @click="showDetails">
Details
</button>
</div>
<div class="available-salary">
<span>Available salary: </span>
<span v-if="loading" class="loading-text">Loading...</span>
<span v-else class="amount">{{ availableSalary }}</span>
</div>
</div>
<!-- 兑换部分 -->
<div class="exchange-section">
<h3>Exchange Gold Coins</h3>
<!-- 金币选择 -->
<div class="coins-grid">
<div
v-for="coin in coinOptions"
:key="coin.id"
@click="selectCoin(coin)"
:class="['coin-item', { selected: selectedCoin === coin.id }]"
>
<div class="coin-icon">🪙</div>
<div class="coin-amount">{{ coin.amount }}</div>
<div class="coin-price">${{ coin.price }}</div>
</div>
</div>
<!-- 兑换按钮 -->
<button
class="exchange-btn"
@click="exchangeCoins"
:disabled="!selectedCoin"
>
Exchange
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { getBankBalance } from '../api/wallet.js'
const router = useRouter()
const availableSalary = ref('0')
const selectedCoin = ref(null)
const loading = ref(false)
//
const coinOptions = reactive([
{ id: 1, amount: 10000, price: 1 },
{ id: 2, amount: 10000, price: 1 },
{ id: 3, amount: 10000, price: 1 },
{ id: 4, amount: 10000, price: 1 },
{ id: 5, amount: 10000, price: 1 },
{ id: 6, amount: 10000, price: 1 }
])
//
const fetchBankBalance = async () => {
loading.value = true
try {
const response = await getBankBalance()
if (response.status && typeof response.body === 'number') {
// 2
availableSalary.value = response.body.toFixed(2)
} else {
availableSalary.value = '0.00'
}
} catch (error) {
console.error('Failed to fetch bank balance:', error)
availableSalary.value = '0.00'
//
alert('Failed to load balance. Please try again.')
} finally {
loading.value = false
}
}
//
const showDetails = () => {
router.push('/information-details')
}
const selectCoin = (coin) => {
//
if (selectedCoin.value === coin.id) {
selectedCoin.value = null
} else {
selectedCoin.value = coin.id
}
}
const exchangeCoins = () => {
if (!selectedCoin.value) {
alert('Please select a coin amount first')
return
}
const selectedCoinData = coinOptions.find(coin => coin.id === selectedCoin.value)
//
const confirmExchange = confirm(`Do you want to exchange ${selectedCoinData.amount} coins for $${selectedCoinData.price}?`)
if (confirmExchange) {
alert(`Successfully exchanged ${selectedCoinData.amount} coins for $${selectedCoinData.price}`)
//
selectedCoin.value = null
}
}
//
onMounted(() => {
fetchBankBalance()
})
</script>
<style scoped>
.exchange-gold-coins {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 信息部分 */
.info-section {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.info-header {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.details-btn {
background: none;
border: none;
color: #8B5CF6;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
}
.details-btn:hover {
background-color: #f3f4f6;
}
.details-btn:active {
background-color: #e5e7eb;
color: #7C3AED;
}
.available-salary {
font-size: 16px;
color: #333;
}
.available-salary .amount {
font-weight: 600;
color: #333;
}
.available-salary .loading-text {
font-weight: 500;
color: #8B5CF6;
font-style: italic;
}
/* 兑换部分 */
.exchange-section {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.exchange-section h3 {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
/* 金币网格 */
.coins-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.coin-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
border: 2px solid #e5e7eb;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
background-color: white;
}
.coin-item:hover {
border-color: #8B5CF6;
}
.coin-item.selected {
border-color: #8B5CF6;
background-color: #F3F4F6;
}
.coin-item:active {
transform: scale(0.98);
}
.coin-icon {
font-size: 24px;
margin-bottom: 8px;
}
.coin-amount {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.coin-price {
font-size: 12px;
color: #666;
}
/* 兑换按钮 */
.exchange-btn {
width: 100%;
padding: 14px;
background-color: #FCD34D;
color: #92400E;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.exchange-btn:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
.exchange-btn:not(:disabled):active {
background-color: #F59E0B;
}
</style>

View File

@ -0,0 +1,362 @@
<template>
<div class="history-salary gradient-background-circles">
<MobileHeader title="History Salary" />
<div class="content">
<!-- Host Salary 卡片 -->
<div class="host-salary-card">
<div class="salary-header">
<h3>Host Salary</h3>
<button class="info-btn" @click="showSalaryInfo">
<span>?</span>
</button>
</div>
<div class="salary-amount">${{ totalSalary }}</div>
</div>
<!-- Work Report 部分 -->
<div class="work-report-section">
<h3>Work report:</h3>
<div class="report-list">
<div
v-for="report in workReports"
:key="report.id"
class="report-item"
>
<div class="report-header">
<span class="report-date">{{ report.date }}</span>
<span :class="['status-badge', report.status.toLowerCase().replace(' ', '-')]">
{{ report.status }}
</span>
</div>
<div class="report-details">
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">Target:</span>
<span class="detail-value">{{ report.target }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Salary:</span>
<span class="detail-value">{{ report.salary }}</span>
</div>
</div>
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">Time(Days):</span>
<span class="detail-value">{{ report.timeDays }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Diamond:</span>
<span class="detail-value">{{ report.diamond }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 薪资信息弹窗 -->
<div v-if="showSalaryModal" class="modal-overlay" @click="showSalaryModal = false">
<div class="modal" @click.stop>
<h3>Salary Information</h3>
<div class="salary-breakdown">
<div class="breakdown-item">
<span>Base Salary:</span>
<span>$8,000</span>
</div>
<div class="breakdown-item">
<span>Performance Bonus:</span>
<span>$3,000</span>
</div>
<div class="breakdown-item">
<span>Commission:</span>
<span>$1,345</span>
</div>
<div class="breakdown-total">
<span>Total:</span>
<span>$12,345</span>
</div>
</div>
<button @click="showSalaryModal = false">Close</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import MobileHeader from '../components/MobileHeader.vue'
const totalSalary = ref('12345')
const showSalaryModal = ref(false)
//
const workReports = reactive([
{
id: 1,
date: '2025.07',
status: 'In Progress',
target: '-',
salary: '-',
timeDays: '-',
diamond: '-'
},
{
id: 2,
date: '2025.07',
status: 'Pending',
target: 'A',
salary: '$10',
timeDays: '20',
diamond: '10000'
},
{
id: 3,
date: '2025.07',
status: 'Completed',
target: 'B',
salary: '$50',
timeDays: '20',
diamond: '50000'
},
{
id: 4,
date: '2025.07',
status: 'Out Of Account',
target: '-',
salary: '-',
timeDays: '1',
diamond: '1869'
}
])
//
const showSalaryInfo = () => {
showSalaryModal.value = true
}
</script>
<style scoped>
.history-salary {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* Host Salary 卡片 */
.host-salary-card {
background-color: white;
padding: 20px;
border-radius: 16px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.salary-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.salary-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #666;
}
.info-btn {
width: 24px;
height: 24px;
border-radius: 12px;
border: 1px solid #ddd;
background-color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #666;
cursor: pointer;
}
.salary-amount {
font-size: 32px;
font-weight: 700;
color: #333;
margin: 0;
}
/* Work Report 部分 */
.work-report-section {
margin-bottom: 20px;
}
.work-report-section h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 500;
color: #666;
}
.report-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 报告项目 */
.report-item {
background-color: white;
padding: 16px;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.report-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.report-date {
font-size: 18px;
font-weight: 600;
color: #333;
}
.status-badge {
padding: 6px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
color: white;
}
.status-badge.in-progress {
background-color: #3B82F6;
}
.status-badge.pending {
background-color: #EF4444;
}
.status-badge.completed {
background-color: #10B981;
}
.status-badge.out-of-account {
background-color: #9CA3AF;
}
/* 报告详情 */
.report-details {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-row {
display: flex;
justify-content: space-between;
gap: 20px;
}
.detail-item {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.detail-value {
font-size: 14px;
color: #333;
font-weight: 600;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 16px;
}
.modal {
background: white;
padding: 24px;
border-radius: 16px;
width: 100%;
max-width: 320px;
}
.modal h3 {
margin: 0 0 20px 0;
font-size: 18px;
text-align: center;
color: #333;
}
.salary-breakdown {
margin-bottom: 20px;
}
.breakdown-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 14px;
color: #666;
border-bottom: 1px solid #f0f0f0;
}
.breakdown-total {
display: flex;
justify-content: space-between;
padding: 12px 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #333;
border-top: 2px solid #e0e0e0;
margin-top: 8px;
}
.modal button {
width: 100%;
padding: 12px;
background-color: #3B82F6;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
}
.modal button:active {
background-color: #2563EB;
}
</style>

120
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,120 @@
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const goToApply = () => {
router.push('/apply')
}
const goToHostCenter = () => {
router.push('/host-center')
}
const goToHostSetting = () => {
router.push('/host-setting')
}
</script>
<template>
<main class="gradient-background-circles">
<div class="home-content">
<h1>Likei H5 App</h1>
<p>Welcome to the demo pages</p>
<div class="nav-buttons">
<button @click="goToApply" class="nav-btn apply-btn">
<span>📝</span>
<span>Apply to Join Team</span>
</button>
<button @click="goToHostCenter" class="nav-btn host-btn">
<span>👑</span>
<span>Host Center</span>
</button>
<button @click="goToHostSetting" class="nav-btn setting-btn">
<span></span>
<span>Host Setting</span>
</button>
</div>
</div>
</main>
</template>
<style scoped>
.home-content {
padding: 20px 16px;
text-align: center;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #333;
margin: 0;
position: relative;
z-index: 2;
}
h1 {
font-size: 2rem;
margin-bottom: 0.8rem;
font-weight: 700;
}
p {
font-size: 1rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.nav-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 280px;
padding: 0 16px;
}
.nav-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 16px 24px;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
width: 100%;
}
.nav-btn:active {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.apply-btn {
background: linear-gradient(135deg, #8B5CF6, #A855F7);
color: white;
}
.host-btn {
background: linear-gradient(135deg, #3B82F6, #1D4ED8);
color: white;
}
.setting-btn {
background: linear-gradient(135deg, #10B981, #059669);
color: white;
}
.nav-btn span:first-child {
font-size: 1.3rem;
}
</style>

View File

@ -0,0 +1,614 @@
<template>
<div class="host-center gradient-background-circles">
<!-- 顶部导航 -->
<MobileHeader title="Host Center" />
<!-- 主要内容 -->
<div class="content">
<!-- 用户信息卡片 -->
<div class="user-card">
<div class="user-info">
<div class="avatar">
<span>{{ userInfo.name.charAt(0) }}</span>
</div>
<div class="user-details">
<h3>{{ userInfo.name }}</h3>
<p>ID: {{ userInfo.id }}</p>
</div>
<button class="notification-btn" @click="goToNotification">
<span></span>
</button>
</div>
<!-- 身份切换标签 -->
<div class="identity-tabs">
<button
v-for="identity in identities"
:key="identity.value"
@click="handleIdentityChange(identity.value)"
:class="['identity-tab', { active: currentIdentity === identity.value }]"
>
{{ identity.label }}
</button>
</div>
<!-- 账户状态标签 -->
<div class="account-tags">
<span class="tag host-tag">👑 Host</span>
<span class="tag salary-tag">💰 Salary</span>
</div>
</div>
<!-- My salary 区域 -->
<div class="salary-section">
<div class="salary-header">
<h3>My salary:</h3>
<button class="personal-salary-btn" @click="showDatePicker">
Personal Salary <span class="arrow">></span>
</button>
</div>
<div class="salary-content">
<div class="salary-item">
<p class="salary-label">History Salary:</p>
<p class="salary-amount">${{ salaryInfo.hostSalary }}</p>
</div>
<div class="salary-separator"></div>
<div class="salary-item">
<p class="salary-label">Current Salary:</p>
<p class="salary-amount">${{ salaryInfo.currentSalary }}</p>
</div>
</div>
</div>
<!-- Today's Task -->
<div class="task-section">
<h3>Today's Task:</h3>
<!-- 任务表格 -->
<div class="task-table">
<div class="table-row table-header">
<div class="table-cell">Anchor type</div>
<div class="table-cell">Minutes</div>
<div class="table-cell">Gift Task</div>
</div>
<div class="table-row table-data">
<div class="table-cell">{{ taskInfo.anchorType }}</div>
<div class="table-cell">
<span class="minutes-completed">{{ taskInfo.minutes }}</span>
</div>
<div class="table-cell">{{ taskInfo.giftTask }}</div>
</div>
</div>
<!-- Platform Policy 链接 -->
<!-- <div class="policy-link" @click="goToPlatformPolicy">
<span>Platform Policy</span>
<span class="arrow">></span>
</div>-->
</div>
<!-- 进度信息 -->
<div class="progress-section">
<div class="progress-header">
<span class="progress-date">{{ progressInfo.date }}</span>
<span class="progress-status">{{ progressInfo.status }}</span>
</div>
<div class="progress-details">
<div class="progress-item">
<span>Target: {{ progressInfo.target }}</span>
<span>Salary: ${{ progressInfo.salary }}</span>
</div>
<div class="progress-item">
<span>Time(Days): {{ progressInfo.timeDays }}</span>
<span>Diamond: {{ progressInfo.diamond }}</span>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button class="action-btn exchange-btn" @click="exchangeGoldCoins">
Exchange<br>Gold Coins
</button>
<button class="action-btn transfer-btn" @click="transfer">
Transfer
</button>
<!--
<button class="action-btn withdraw-btn" @click="cashWithdraw">
Cash<br>Withdraw
</button>
-->
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
//
const currentIdentity = ref('host')
//
const identities = [
{ label: 'Host', value: 'host' },
{ label: 'Agency', value: 'agency' },
{ label: 'Coin Seller', value: 'coin-seller' }
]
//
const userInfo = reactive({
name: 'User1',
id: '1234567890'
})
//
const salaryInfo = reactive({
hostSalary: 12345,
currentSalary: 111
})
//
const taskInfo = reactive({
anchorType: 'Chat',
minutes: '0/120',
giftTask: 0
})
//
const progressInfo = reactive({
date: '2025.08',
status: 'Pending',
target: 'B',
salary: 50,
timeDays: 20,
diamond: 50000,
hostSalary: '',
agentSalary: '',
total: ''
})
const showDatePicker = () => {
router.push('/history-salary')
}
const goToNotification = () => {
router.push('/host-setting')
}
//
const handleIdentityChange = (identity) => {
switch (identity) {
case 'agency':
router.push('/agency-center')
break
case 'coin-seller':
router.push('/coin-seller')
break
case 'host':
default:
//
currentIdentity.value = identity
break
}
}
const exchangeGoldCoins = () => {
router.push('/exchange-gold-coins')
}
const transfer = () => {
router.push('/transfer')
}
</script>
<style scoped>
.host-center {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 用户信息卡片 */
.user-card {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
font-weight: 600;
margin-right: 12px;
}
.user-details {
flex: 1;
}
.user-details h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.user-details p {
margin: 0;
font-size: 14px;
color: #666;
}
.edit-btn {
width: 32px;
height: 32px;
border: none;
background: none;
font-size: 16px;
cursor: pointer;
}
.notification-btn {
width: 32px;
height: 32px;
border: none;
background: none;
font-size: 16px;
cursor: pointer;
}
/* 身份切换标签 */
.identity-tabs {
display: flex;
gap: 8px;
margin: 12px 0;
}
.identity-tab {
padding: 6px 12px;
border: 1px solid #e5e7eb;
border-radius: 16px;
background-color: white;
color: #666;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.identity-tab.active {
background-color: #8B5CF6;
color: white;
border-color: #8B5CF6;
}
.identity-tab:not(.active):hover {
border-color: #8B5CF6;
color: #8B5CF6;
}
.account-tags {
display: flex;
gap: 8px;
}
.tag {
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
}
.host-tag {
background-color: #E0E7FF;
color: #5B21B6;
}
.agency-tag {
background-color: #DBEAFE;
color: #1E40AF;
}
.coin-tag {
background-color: #FEF3C7;
color: #D97706;
}
.salary-tag {
background-color: #FEF3C7;
color: #D97706;
}
/* My salary 区域 */
.salary-section {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.salary-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.salary-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.personal-salary-btn {
background: none;
border: none;
color: #8B5CF6;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.2s;
}
.personal-salary-btn:hover {
color: #7C3AED;
}
.salary-content {
display: flex;
align-items: center;
gap: 20px;
}
.salary-item {
flex: 1;
text-align: center;
}
.salary-label {
margin: 0 0 8px 0;
font-size: 14px;
color: #666;
}
.salary-amount {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
.salary-separator {
width: 1px;
height: 40px;
background-color: #8B5CF6;
}
/* Today's Task 区域 */
.task-section {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.task-section h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
/* 任务表格 */
.task-table {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
margin-bottom: 12px;
}
.table-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.table-header {
background-color: #f8f9fa;
border-bottom: 1px solid #e5e7eb;
}
.table-data {
background-color: white;
}
.table-cell {
padding: 12px 8px;
text-align: center;
border-right: 1px solid #e5e7eb;
font-size: 14px;
}
.table-cell:last-child {
border-right: none;
}
.table-header .table-cell {
font-weight: 600;
color: #374151;
}
.table-data .table-cell {
color: #111827;
}
.minutes-progress {
color: #6B7280;
}
.minutes-completed {
color: #10B981;
font-weight: 600;
}
/* Platform Policy 链接 */
.policy-link {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
margin-top: 12px;
background-color: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #e9ecef;
}
.policy-link:hover {
background-color: #e9ecef;
border-color: #8B5CF6;
}
.policy-link:active {
transform: scale(0.98);
}
.policy-link span:first-child {
font-size: 14px;
font-weight: 500;
color: #333;
}
/* 进度信息 */
.progress-section {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.progress-date {
font-size: 16px;
font-weight: 600;
color: #333;
}
.progress-status {
background-color: #3B82F6;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.progress-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.progress-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.progress-item span {
font-size: 14px;
color: #333;
}
/* 操作按钮 */
.action-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.action-btn {
padding: 14px 12px;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
text-align: center;
line-height: 1.2;
transition: all 0.2s;
width: 100%;
}
.action-btn:active {
transform: scale(0.98);
}
.exchange-btn {
background-color: #FDE68A;
color: #D97706;
}
.transfer-btn {
background-color: #8B5CF6;
color: white;
}
.withdraw-btn {
background-color: #10B981;
color: white;
}
/* 通用箭头样式 */
.arrow {
font-size: 16px;
font-weight: bold;
color: #8B5CF6;
}
</style>

View File

@ -0,0 +1,149 @@
<template>
<div class="host-setting gradient-background-circles">
<MobileHeader title="Host Setting" />
<div class="content">
<!-- My Agent 部分 -->
<div class="section">
<div class="section-header">
<h3>My Agent</h3>
<span class="agency-tag">🏢 Agency</span>
</div>
<div class="agent-card">
<div class="agent-info">
<div class="avatar">
<span>{{ userInfo.name.charAt(0) }}</span>
</div>
<div class="agent-details">
<h4>{{ userInfo.name }}</h4>
<p>ID: {{ userInfo.id }}</p>
</div>
<button class="leave-btn" @click="leaveAgent">Leave</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
//
const userInfo = reactive({
name: 'User1',
id: '1234567890'
})
//
const leaveAgent = () => {
if (confirm('Are you sure you want to leave this agent?')) {
alert('Leave agent request submitted')
//
}
}
</script>
<style scoped>
.host-setting {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
.section {
margin-bottom: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.section-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.agency-tag {
background-color: #DBEAFE;
color: #1E40AF;
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
}
/* Agent Card */
.agent-card {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.agent-info {
display: flex;
align-items: center;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
font-weight: 600;
margin-right: 12px;
}
.agent-details {
flex: 1;
}
.agent-details h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.agent-details p {
margin: 0;
font-size: 14px;
color: #666;
}
.leave-btn {
background-color: #EF4444;
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.leave-btn:active {
background-color: #DC2626;
}
</style>

View File

@ -0,0 +1,241 @@
<template>
<div class="information-details gradient-background-circles">
<MobileHeader title="Information details" />
<div class="content">
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<div class="loading-spinner"></div>
<p>Loading...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-state">
<div class="error-icon"></div>
<p>{{ error }}</p>
<button @click="fetchData" class="retry-btn">Retry</button>
</div>
<!-- 交易记录列表 -->
<div v-else class="transaction-list">
<div
v-for="transaction in transactions"
:key="transaction.id"
class="transaction-item"
>
<div class="transaction-content">
<div class="transaction-info">
<h3>{{ transaction.eventDescribe }}</h3>
<p class="transaction-time">{{ transaction.createTime }}</p>
</div>
<div class="transaction-amount" :class="transaction.amount > 0 ? 'positive' : 'negative'">
{{ transaction.amount > 0 ? '+' : '' }}{{ Math.abs(transaction.amount) }}
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && !error && transactions.length === 0" class="empty-state">
<div class="empty-icon">📄</div>
<p>No transaction records</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import MobileHeader from '../components/MobileHeader.vue'
import { getWalletDetails, formatDateTime, formatEventDescribe } from '../api/wallet.js'
const transactions = ref([])
const loading = ref(false)
const error = ref(null)
//
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await getWalletDetails()
// API
if (response.body && Array.isArray(response.body)) {
transactions.value = response.body.map(item => ({
id: item.id,
eventDescribe: formatEventDescribe(item.eventDescribe),
createTime: formatDateTime(item.createTime),
amount: item.type === 0 ? -Math.abs(item.amount) : item.amount, // type=0
balance: item.balance,
event: item.event
}))
} else {
transactions.value = []
}
} catch (err) {
console.error('Failed to fetch wallet details:', err)
error.value = err.message || 'Failed to load data'
} finally {
loading.value = false
}
}
//
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.information-details {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 交易记录列表 */
.transaction-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.transaction-item {
background-color: white;
padding: 16px;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.transaction-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.transaction-info {
flex: 1;
}
.transaction-info h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.transaction-time {
margin: 0;
font-size: 14px;
color: #666;
}
.transaction-amount {
font-size: 18px;
font-weight: 600;
min-width: 40px;
text-align: right;
}
.transaction-amount.positive {
color: #10B981;
}
.transaction-amount.negative {
color: #EF4444;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #8B5CF6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
margin: 0;
font-size: 14px;
}
/* 错误状态 */
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.error-state p {
margin: 0 0 20px 0;
font-size: 14px;
line-height: 1.5;
}
.retry-btn {
padding: 10px 24px;
background-color: #8B5CF6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.retry-btn:active {
background-color: #7C3AED;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
margin: 0;
font-size: 16px;
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<div class="invite-members gradient-background-circles">
<MobileHeader title="Invite Members" />
<div class="content">
<div class="instructions">
<div class="instruction-item">
<span class="step-number">1.</span>
<span>Invite users to download Likei APP</span>
</div>
<div class="instruction-item">
<span class="step-number">2.</span>
<span>Invited users need to log in to the Likei APP!</span>
</div>
<div class="instruction-item">
<span class="step-number">3.</span>
<span>Me > Host Center >Apply to join the team.</span>
</div>
<div class="instruction-item">
<span class="step-number">4.</span>
<span>Enter the team ID,click send to join.</span>
</div>
</div>
<div class="team-info">
<div class="team-id-section">
<h3>Team ID:</h3>
<div class="team-id-display">
<span class="team-id">{{ teamId }}</span>
<button class="copy-btn" @click="copyTeamId">
📋
</button>
</div>
</div>
<div class="share-section">
<h3>Share:</h3>
<div class="share-buttons">
<button class="share-btn" @click="shareToWeChat">
<span class="share-icon">💬</span>
WeChat
</button>
<button class="share-btn" @click="shareLink">
<span class="share-icon">🔗</span>
Link
</button>
<button class="share-btn" @click="shareQRCode">
<span class="share-icon">📱</span>
QR Code
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
const teamId = ref('123456789')
// ID
const copyTeamId = async () => {
try {
await navigator.clipboard.writeText(teamId.value)
alert('Team ID copied to clipboard!')
} catch (error) {
//
const textArea = document.createElement('textarea')
textArea.value = teamId.value
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
alert('Team ID copied to clipboard!')
}
}
//
const shareToWeChat = () => {
alert('Share to WeChat functionality')
}
//
const shareLink = () => {
const shareText = `Join my team! Team ID: ${teamId.value}\nDownload Likei APP: https://likei.app`
if (navigator.share) {
navigator.share({
title: 'Join my team on Likei',
text: shareText,
url: 'https://likei.app'
})
} else {
//
copyTeamId()
alert('Link copied! Share with your friends.')
}
}
//
const shareQRCode = () => {
alert('QR Code sharing functionality')
}
</script>
<style scoped>
.invite-members {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 说明步骤 */
.instructions {
background-color: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.instruction-item {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
line-height: 1.5;
}
.instruction-item:last-child {
margin-bottom: 0;
}
.step-number {
color: #8B5CF6;
font-weight: 600;
margin-right: 8px;
flex-shrink: 0;
}
.instruction-item span:last-child {
color: #333;
font-size: 14px;
}
/* 团队信息 */
.team-info {
background-color: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.team-id-section {
margin-bottom: 24px;
}
.team-id-section h3,
.share-section h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.team-id-display {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #f8f9fa;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.team-id {
font-size: 18px;
font-weight: 600;
color: #333;
letter-spacing: 1px;
}
.copy-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.copy-btn:hover {
background-color: #e9ecef;
}
.copy-btn:active {
background-color: #dee2e6;
}
/* 分享按钮 */
.share-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.share-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 12px;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
flex: 1;
min-width: 80px;
}
.share-btn:hover {
background-color: #e9ecef;
border-color: #8B5CF6;
}
.share-btn:active {
transform: scale(0.98);
}
.share-icon {
font-size: 24px;
margin-bottom: 4px;
}
.share-btn span:last-child {
font-size: 12px;
font-weight: 500;
color: #666;
}
</style>

264
src/views/MessageView.vue Normal file
View File

@ -0,0 +1,264 @@
<template>
<div class="message gradient-background-circles">
<MobileHeader title="User's message" />
<div class="content">
<div v-if="loading" class="loading-state">
<div class="loading-spinner"></div>
<p>Loading messages...</p>
</div>
<div v-else class="message-list">
<div
v-for="message in messages"
:key="message.id"
class="message-item"
>
<div class="user-info">
<div class="user-avatar">
<img v-if="message.avatar" :src="message.avatar" :alt="message.name" />
<span v-else>{{ message.name.charAt(0) }}</span>
</div>
<div class="user-details">
<h4>{{ message.name }}</h4>
<p>ID: {{ message.id }}</p>
</div>
</div>
<div class="action-buttons">
<button class="agree-btn" @click="handleAgree(message)">
Agree
</button>
<button class="refuse-btn" @click="handleRefuse(message)">
Refuse
</button>
</div>
</div>
</div>
<div v-if="!loading && messages.length === 0" class="empty-state">
<div class="empty-icon">📨</div>
<p>No messages</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
const loading = ref(false)
const messages = ref([])
//
const mockMessages = [
{
id: '1234567890',
name: 'User1',
avatar: '',
type: 'team_join_request'
},
{
id: '1234567890',
name: 'User1',
avatar: '',
type: 'team_join_request'
},
{
id: '1234567890',
name: 'User1',
avatar: '',
type: 'team_join_request'
}
]
//
const fetchMessages = async () => {
loading.value = true
try {
// API
setTimeout(() => {
messages.value = mockMessages
loading.value = false
}, 800)
} catch (error) {
console.error('Failed to fetch messages:', error)
loading.value = false
}
}
//
const handleAgree = (message) => {
alert(`Agreed to ${message.name}'s request`)
//
const index = messages.value.findIndex(m => m === message)
if (index > -1) {
messages.value.splice(index, 1)
}
}
//
const handleRefuse = (message) => {
alert(`Refused ${message.name}'s request`)
//
const index = messages.value.findIndex(m => m === message)
if (index > -1) {
messages.value.splice(index, 1)
}
}
onMounted(() => {
fetchMessages()
})
</script>
<style scoped>
.message {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #8B5CF6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 消息列表 */
.message-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.message-item {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: space-between;
}
.user-info {
display: flex;
align-items: center;
flex: 1;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
font-weight: 600;
margin-right: 12px;
overflow: hidden;
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-details h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.user-details p {
margin: 0;
font-size: 14px;
color: #666;
}
.action-buttons {
display: flex;
gap: 8px;
}
.agree-btn, .refuse-btn {
padding: 8px 16px;
border: none;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.agree-btn {
background-color: #C084FC;
color: white;
}
.agree-btn:active {
background-color: #A855F7;
}
.refuse-btn {
background-color: #E5E7EB;
color: #6B7280;
}
.refuse-btn:active {
background-color: #D1D5DB;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
margin: 0;
font-size: 16px;
}
</style>

View File

@ -0,0 +1,175 @@
<template>
<div class="platform-policy gradient-background-circles">
<MobileHeader title="Platform Policy" />
<div class="content">
<div class="policy-list">
<div
v-for="policy in policies"
:key="policy.level"
class="policy-item"
>
<div class="policy-header">
<h3>{{ policy.level }}</h3>
</div>
<div class="policy-details">
<div class="policy-grid">
<div class="policy-cell">
<span class="cell-label">Time (hours)</span>
<span class="cell-value">{{ policy.timeHours }}</span>
</div>
<div class="policy-cell">
<span class="cell-label">Target</span>
<span class="cell-value">{{ policy.target }}</span>
</div>
<div class="policy-cell">
<span class="cell-label">Host Salary</span>
<span class="cell-value">${{ policy.hostSalary }}</span>
</div>
</div>
<div class="policy-grid bottom-row">
<div class="policy-cell">
<span class="cell-label">Agent Salary</span>
<span class="cell-value">${{ policy.agentSalary }}</span>
</div>
<div class="policy-cell">
<span class="cell-label">Total Salary</span>
<span class="cell-value">${{ policy.totalSalary }}</span>
</div>
<div class="policy-cell empty"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
//
const policies = ref([
{
level: 'Lv.1',
timeHours: 20,
target: 32500,
hostSalary: 3,
agentSalary: 1,
totalSalary: 4
},
{
level: 'Lv.2',
timeHours: 20,
target: 65000,
hostSalary: 6,
agentSalary: 2,
totalSalary: 8
},
{
level: 'Lv.3',
timeHours: 15,
target: 108000,
hostSalary: 10,
agentSalary: 3,
totalSalary: 13
}
])
</script>
<style scoped>
.platform-policy {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
.policy-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.policy-item {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.policy-header {
padding: 16px 16px 8px 16px;
border-bottom: 1px solid #f0f0f0;
}
.policy-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.policy-details {
padding: 16px;
}
.policy-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.policy-grid.bottom-row {
margin-bottom: 0;
}
.policy-cell {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.policy-cell.empty {
visibility: hidden;
}
.cell-label {
font-size: 12px;
color: #666;
margin-bottom: 8px;
line-height: 1.3;
}
.cell-value {
font-size: 16px;
font-weight: 600;
color: #333;
}
/* 响应式调整 */
@media (max-width: 480px) {
.policy-grid {
gap: 12px;
}
.cell-label {
font-size: 11px;
}
.cell-value {
font-size: 14px;
}
}
</style>

View File

@ -0,0 +1,606 @@
<template>
<div class="search-payee gradient-background-circles">
<MobileHeader title="Search Payee" />
<div class="content">
<!-- 搜索框 -->
<div class="search-section">
<!-- 用户类型选择 -->
<div v-if="!isLoadingRole && availableUserTypes.length > 1" class="user-type-selector">
<label>搜索类型:</label>
<div class="type-buttons">
<button
v-for="type in availableUserTypes"
:key="type.value"
@click="selectedUserType = type.value"
:class="['type-btn', { active: selectedUserType === type.value }]"
>
{{ type.label }}
</button>
</div>
</div>
<div class="search-box">
<input
v-model="searchQuery"
type="text"
placeholder="Search by ID or name"
class="search-input"
@keyup.enter="performSearch"
/>
<button
v-if="searchQuery"
@click="clearSearch"
class="clear-btn"
>
×
</button>
<button class="search-btn" @click="performSearch" :disabled="!selectedUserType">
Search
</button>
</div>
</div>
<!-- 角色检查加载状态 -->
<div v-if="isLoadingRole" class="loading-state">
<div class="loading-spinner"></div>
<p>Checking permissions...</p>
</div>
<!-- 搜索结果 -->
<div v-else-if="searchResults.length > 0" class="results-section">
<div
v-for="user in searchResults"
:key="user.id"
class="user-item"
>
<div class="user-info">
<div class="user-avatar">
<img v-if="user.avatar" :src="user.avatar" :alt="user.name" />
<span v-else>{{ user.name.charAt(0) }}</span>
</div>
<div class="user-details">
<h4>{{ user.name }}</h4>
<p>ID: {{ user.account || user.id }}</p>
<div class="user-tags">
<span v-if="user.isHost" class="tag host-tag">👑 Host</span>
<span v-if="user.isAgency" class="tag agency-tag">🏢 Agency</span>
<span v-if="user.hasSalary" class="tag salary-tag">💰 Salary</span>
</div>
</div>
</div>
<button class="add-btn" @click="selectUser(user)">
Add
</button>
</div>
</div>
<!-- 空状态 -->
<div v-else-if="searchQuery && !isSearching && !isLoadingRole" class="empty-state">
<div class="empty-icon">🔍</div>
<p>No users found</p>
<span>Try searching with a different keyword</span>
</div>
<!-- 搜索提示 -->
<div v-else-if="!searchQuery && !isLoadingRole" class="search-hint">
<div class="hint-icon">👥</div>
<p>Search for users</p>
<span v-if="userRole">You can search for: {{ getRoleDescription(userRole) }}</span>
<span v-else>Enter user ID or name to find payees</span>
</div>
<!-- 加载状态 -->
<div v-if="isSearching" class="loading-state">
<div class="loading-spinner"></div>
<p>Searching...</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { userBankCheckTransfer, userBankSearchUserProfile } from '../api/wallet.js'
import { setSelectedPayee } from '../utils/payeeStore.js'
const router = useRouter()
const searchQuery = ref('')
const searchResults = ref([])
const isSearching = ref(false)
const userRole = ref('')
const isLoadingRole = ref(false)
const selectedUserType = ref('')
const showTypeSelector = ref(false)
// -
const allUsers = {
'TEAM_OWN': [
{
id: '1234567890',
name: 'Agent User',
type: 'AGENT',
isHost: false,
isAgency: true,
hasSalary: false
},
{
id: '0987654321',
name: 'BD Manager',
type: 'BD',
isHost: false,
isAgency: true,
hasSalary: true
},
{
id: '1122334455',
name: 'Host Anchor',
type: 'ANCHOR',
isHost: true,
isAgency: false,
hasSalary: true
}
],
'TEAM_MEMBER': [
{
id: '1234567890',
name: 'Team Agent',
type: 'AGENT',
isHost: false,
isAgency: true,
hasSalary: false
},
{
id: '0987654321',
name: 'BD Manager',
type: 'BD',
isHost: false,
isAgency: true,
hasSalary: true
}
],
'BD': [
{
id: '1234567890',
name: 'Agent User',
type: 'AGENT',
isHost: false,
isAgency: true,
hasSalary: false
}
],
'default': [
{
id: '1234567890',
name: 'Default User',
type: 'USER',
isHost: false,
isAgency: false,
hasSalary: false
}
]
}
//
const availableUsers = computed(() => {
return allUsers[userRole.value] || allUsers['default']
})
//
const availableUserTypes = computed(() => {
switch (userRole.value) {
case 'TEAM_OWN':
return [
{ label: 'Agent', value: 'AGENT' },
{ label: 'BD Manager', value: 'BD' },
{ label: 'Anchor', value: 'ANCHOR' }
]
case 'TEAM_MEMBER':
return [
{ label: 'Agent', value: 'AGENT' },
{ label: 'BD Manager', value: 'BD' }
]
case 'BD':
return [
{ label: 'Agent', value: 'AGENT' }
]
default:
return [
{ label: 'User', value: 'USER' }
]
}
})
//
const checkUserRole = async () => {
isLoadingRole.value = true
try {
const response = await userBankCheckTransfer()
if (response.status && response.body) {
// role
userRole.value = response.body.role || ''
console.log('用户角色:', userRole.value)
//
if (userRole.value === 'TEAM_OWN') {
selectedUserType.value = 'ANCHOR' // TEAM_OWN ANCHOR
} else {
selectedUserType.value = 'AGENT' // AGENT
}
} else {
userRole.value = ''
selectedUserType.value = 'USER'
}
} catch (error) {
console.error('Failed to check user role:', error)
userRole.value = ''
selectedUserType.value = 'USER'
} finally {
isLoadingRole.value = false
}
}
//
const performSearch = async () => {
if (!searchQuery.value.trim()) {
searchResults.value = []
return
}
if (!selectedUserType.value) {
alert('Please select user type first')
return
}
isSearching.value = true
try {
const response = await userBankSearchUserProfile(selectedUserType.value, searchQuery.value.trim())
if (response.status && response.body) {
// API
const user = {
id: response.body.id,
account: response.body.account,
name: response.body.userNickname,
avatar: response.body.userAvatar,
type: selectedUserType.value,
sex: response.body.userSex,
age: response.body.age,
countryName: response.body.countryName,
countryCode: response.body.countryCode,
accountStatus: response.body.accountStatus,
//
isHost: selectedUserType.value === 'ANCHOR',
isAgency: ['AGENT', 'BD'].includes(selectedUserType.value),
hasSalary: ['ANCHOR', 'BD'].includes(selectedUserType.value)
}
searchResults.value = [user]
} else {
searchResults.value = []
}
} catch (error) {
console.error('Search failed:', error)
searchResults.value = []
alert('User not found or search failed')
} finally {
isSearching.value = false
}
}
//
const clearSearch = () => {
searchQuery.value = ''
searchResults.value = []
}
//
const selectUser = (user) => {
//
setSelectedPayee({
id: user.id,
account: user.account,
name: user.name,
avatar: user.avatar,
type: user.type,
isHost: user.isHost,
isAgency: user.isAgency,
hasSalary: user.hasSalary
})
console.log('Selected payee:', user)
// Transfer
router.push('/transfer')
}
//
const getRoleDescription = (role) => {
switch (role) {
case 'TEAM_OWN':
return 'Agents, BD Managers, Anchors'
case 'TEAM_MEMBER':
return 'Agents, BD Managers'
case 'BD':
return 'Agents'
default:
return 'Users'
}
}
//
onMounted(() => {
checkUserRole()
})
</script>
<style scoped>
.search-payee {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 搜索部分 */
.search-section {
margin-bottom: 20px;
}
/* 用户类型选择器 */
.user-type-selector {
margin-bottom: 12px;
}
.user-type-selector label {
display: block;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.type-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.type-btn {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 16px;
background-color: white;
color: #666;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.type-btn.active {
background-color: #8B5CF6;
color: white;
border-color: #8B5CF6;
}
.type-btn:active {
transform: scale(0.98);
}
.search-box {
display: flex;
align-items: center;
background-color: white;
border-radius: 12px;
padding: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.search-input {
flex: 1;
border: none;
padding: 12px 16px;
font-size: 16px;
background: transparent;
outline: none;
}
.search-input::placeholder {
color: #999;
}
.clear-btn {
background: none;
border: none;
padding: 8px;
color: #999;
font-size: 18px;
cursor: pointer;
margin-right: 4px;
}
.search-btn {
background-color: #8B5CF6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.search-btn:active {
background-color: #7C3AED;
}
.search-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* 搜索结果 */
.results-section {
margin-bottom: 20px;
}
.user-item {
display: flex;
align-items: center;
justify-content: space-between;
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: background-color 0.2s;
}
.user-item:active {
background-color: #f9f9f9;
}
.user-info {
display: flex;
align-items: center;
flex: 1;
}
.user-avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
font-weight: 600;
margin-right: 12px;
overflow: hidden;
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-details h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.user-details p {
margin: 0 0 8px 0;
font-size: 14px;
color: #666;
}
.user-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.tag {
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
}
.host-tag {
background-color: #E0E7FF;
color: #5B21B6;
}
.agency-tag {
background-color: #DBEAFE;
color: #1E40AF;
}
.salary-tag {
background-color: #FEF3C7;
color: #D97706;
}
.add-btn {
background-color: #10B981;
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.add-btn:active {
background-color: #059669;
}
/* 空状态和提示 */
.empty-state, .search-hint {
text-align: center;
padding: 40px 20px;
color: #666;
}
.empty-icon, .hint-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p, .search-hint p {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
color: #333;
}
.empty-state span, .search-hint span {
font-size: 14px;
color: #666;
}
/* 加载状态 */
.loading-state {
text-align: center;
padding: 40px 20px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #8B5CF6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
margin: 0;
color: #666;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,235 @@
<template>
<div class="seller-records gradient-background-circles">
<MobileHeader title="Seller records" />
<div class="content">
<div v-if="loading" class="loading-state">
<div class="loading-spinner"></div>
<p>Loading records...</p>
</div>
<div v-else class="records-list">
<div
v-for="record in records"
:key="record.id"
class="record-item"
>
<div class="record-info">
<div class="user-avatar">
<span>{{ record.userName.charAt(0) }}</span>
</div>
<div class="record-details">
<h4>{{ record.userName }}</h4>
<p>ID: {{ record.userId }}</p>
<div class="record-time">{{ record.time }}</div>
</div>
</div>
<div class="record-amount">
<span class="coins-amount">{{ record.amount }} coins</span>
</div>
</div>
</div>
<div v-if="!loading && records.length === 0" class="empty-state">
<div class="empty-icon">📋</div>
<p>No records found</p>
<span>No selling records yet</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
const loading = ref(false)
const records = ref([])
//
const mockRecords = [
{
id: 1,
userName: 'User1033...',
userId: '452319866',
amount: '-10000',
time: '2025.08.01 10:59:59'
},
{
id: 2,
userName: 'User1033...',
userId: '452319866',
amount: '-10000',
time: '2025.08.01 10:59:59'
},
{
id: 3,
userName: 'User1033...',
userId: '452319866',
amount: '-10000',
time: '2025.08.01 10:59:59'
}
]
//
const fetchRecords = async () => {
loading.value = true
try {
// API
setTimeout(() => {
records.value = mockRecords
loading.value = false
}, 800)
} catch (error) {
console.error('Failed to fetch records:', error)
loading.value = false
}
}
onMounted(() => {
fetchRecords()
})
</script>
<style scoped>
.seller-records {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #F59E0B;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
margin: 0;
font-size: 14px;
}
/* 记录列表 */
.records-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.record-item {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: space-between;
}
.record-info {
display: flex;
align-items: center;
flex: 1;
}
.user-avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #9CA3AF;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
font-weight: 600;
margin-right: 12px;
}
.record-details {
flex: 1;
}
.record-details h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.record-details p {
margin: 0 0 4px 0;
font-size: 14px;
color: #666;
}
.record-time {
font-size: 12px;
color: #9ca3af;
}
.record-amount {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.coins-amount {
font-size: 14px;
font-weight: 600;
color: #EF4444;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 500;
}
.empty-state span {
font-size: 14px;
color: #9ca3af;
}
</style>

750
src/views/TeamBillView.vue Normal file
View File

@ -0,0 +1,750 @@
<template>
<div class="team-bill gradient-background-circles">
<MobileHeader title="Team Bill" />
<div class="content">
<!-- 历史团队收入 -->
<div class="history-income-card">
<div class="income-header">
<span class="income-label">History Team Income:</span>
<button class="help-btn" @click="showHelpInfo">?</button>
</div>
<div class="income-amount">${{ historyIncome }}</div>
</div>
<!-- 工作报告标题 -->
<div class="work-report-title">
<span>Work report:</span>
</div>
<!-- 账单列表 -->
<div class="bill-list">
<div
v-for="bill in bills"
:key="bill.id"
class="bill-item"
@click="showBillDetail(bill)"
>
<div class="bill-header">
<span class="bill-period">{{ bill.period }}</span>
<span :class="['bill-status', bill.status.toLowerCase()]">
{{ bill.statusText }}
</span>
</div>
<div class="bill-details">
<div class="salary-row">
<div class="salary-item">
<span class="salary-label">Host Salary:</span>
<span class="salary-value">{{ bill.hostSalary }}</span>
</div>
<div class="salary-item">
<span class="salary-label">Agent Salary:</span>
<span class="salary-value">{{ bill.agentSalary }}</span>
</div>
</div>
<div class="total-row">
<span class="total-label">Total:</span>
<span class="total-value">{{ bill.total }}</span>
</div>
</div>
<div class="more-btn">
<span>More</span>
<span class="arrow">></span>
</div>
</div>
</div>
</div>
<!-- 帮助模态框 -->
<div v-if="showHelpModal" class="modal-overlay" @click="closeHelpModal">
<div class="help-modal-content" @click.stop>
<div class="help-modal-header">
<h3>Help Information</h3>
<button class="close-btn" @click="closeHelpModal">×</button>
</div>
<div class="help-modal-body">
<div class="help-section">
<p class="help-description">
Shown below is the work report of the members "More" can view more work detailed.
</p>
</div>
<div class="help-section">
<h4>Status Description:</h4>
<div class="status-list">
<div class="status-item">
<span class="status-number">(1)</span>
<span class="status-name">Completed:</span>
<span class="status-desc">The system has completed the settlement report.</span>
</div>
<div class="status-item">
<span class="status-number">(2)</span>
<span class="status-name">Pending:</span>
<span class="status-desc">There is currently a dispute, and the system cannot perform a settlement.</span>
</div>
<div class="status-item">
<span class="status-number">(3)</span>
<span class="status-name">Out of account:</span>
<span class="status-desc">waiting for system verification</span>
</div>
<div class="status-item">
<span class="status-number">(4)</span>
<span class="status-name">In progress:</span>
<span class="status-desc">work now in progress.</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 成员收入弹窗 -->
<div v-if="showMemberModal" class="modal-overlay" @click="closeMemberModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>Member Income</h3>
<button class="close-btn" @click="closeMemberModal">×</button>
</div>
<div class="modal-body">
<div class="modal-period">
<span>{{ selectedBill?.period }}</span>
<span :class="['modal-status', selectedBill?.status.toLowerCase()]">
{{ selectedBill?.statusText }}
</span>
</div>
<div class="search-section">
<div class="search-box">
<span class="search-icon">🔍</span>
<input
type="text"
placeholder="Please enter the host ID"
v-model="searchQuery"
class="search-input"
/>
</div>
</div>
<div class="member-list">
<div
v-for="member in filteredMembers"
:key="member.id"
class="member-item"
>
<div class="member-info">
<div class="member-avatar">
<span>{{ member.name.charAt(0) }}</span>
</div>
<div class="member-details">
<div class="member-name">{{ member.name }}</div>
<div class="member-id">ID: {{ member.id }}</div>
<div class="member-target">Target: {{ member.target }}</div>
</div>
</div>
<div class="member-stats">
<div class="member-stat">
<span class="stat-icon">💰</span>
<span>{{ member.id }}</span>
</div>
<div class="member-time">Time (Days): {{ member.days }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
const historyIncome = ref('12345')
const showMemberModal = ref(false)
const showHelpModal = ref(false)
const selectedBill = ref(null)
const searchQuery = ref('')
//
const bills = ref([
{
id: 1,
period: '2025.08',
status: 'pending',
statusText: '进行中 (Pending)',
hostSalary: '',
agentSalary: '',
total: ''
},
{
id: 2,
period: '2025.07',
status: 'pending',
statusText: '待处理 (Pending)',
hostSalary: '$100',
agentSalary: '$10',
total: '110'
},
{
id: 3,
period: '2025.06',
status: 'completed',
statusText: '已完成 (Completed)',
hostSalary: '$200',
agentSalary: '$30',
total: '$230'
},
{
id: 4,
period: '2025.05',
status: 'out',
statusText: '挂起 (Out of account)',
hostSalary: '$500',
agentSalary: '$100',
total: '$600'
}
])
//
const members = ref([
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
}
])
//
const filteredMembers = computed(() => {
if (!searchQuery.value) return members.value
return members.value.filter(member =>
member.id.includes(searchQuery.value) ||
member.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
//
const showBillDetail = (bill) => {
selectedBill.value = bill
showMemberModal.value = true
}
//
const closeMemberModal = () => {
showMemberModal.value = false
selectedBill.value = null
searchQuery.value = ''
}
//
const showHelpInfo = () => {
showHelpModal.value = true
}
//
const closeHelpModal = () => {
showHelpModal.value = false
}
</script>
<style scoped>
.team-bill {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 历史收入卡片 */
.history-income-card {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.income-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.income-label {
font-size: 14px;
color: #666;
}
.help-btn {
width: 20px;
height: 20px;
border-radius: 10px;
background-color: #666;
color: white;
border: none;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.help-btn:hover {
background-color: #8B5CF6;
}
.help-btn:active {
transform: scale(0.95);
}
.income-amount {
font-size: 24px;
font-weight: 600;
color: #333;
}
/* 工作报告标题 */
.work-report-title {
font-size: 16px;
color: #666;
margin-bottom: 12px;
}
/* 账单列表 */
.bill-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.bill-item {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.2s;
}
.bill-item:active {
transform: scale(0.98);
}
.bill-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.bill-period {
font-size: 16px;
font-weight: 600;
color: #333;
}
.bill-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.bill-status.pending {
background-color: #FEF3C7;
color: #D97706;
}
.bill-status.completed {
background-color: #D1FAE5;
color: #059669;
}
.bill-status.out {
background-color: #FEE2E2;
color: #DC2626;
}
.bill-details {
margin-bottom: 12px;
}
.salary-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.salary-item {
display: flex;
flex-direction: column;
align-items: center;
}
.salary-label {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.salary-value {
font-size: 14px;
font-weight: 600;
color: #333;
}
.total-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.total-label {
font-size: 14px;
color: #666;
}
.total-value {
font-size: 14px;
font-weight: 600;
color: #333;
}
.more-btn {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 4px;
color: #C084FC;
font-size: 14px;
}
.arrow {
font-weight: bold;
}
/* 模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 16px;
}
.modal-content {
background-color: white;
border-radius: 12px;
width: 100%;
max-width: 400px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #e5e7eb;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 16px;
overflow-y: auto;
}
.modal-period {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.modal-status.pending {
background-color: #FEF3C7;
color: #D97706;
}
.search-section {
margin-bottom: 16px;
}
.search-box {
display: flex;
align-items: center;
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
}
.search-icon {
margin-right: 8px;
color: #9ca3af;
}
.search-input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #9ca3af;
}
.member-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.member-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background-color: #f9fafb;
border-radius: 8px;
}
.member-info {
display: flex;
align-items: center;
flex: 1;
}
.member-avatar {
width: 32px;
height: 32px;
border-radius: 16px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
font-weight: 600;
margin-right: 12px;
}
.member-details {
flex: 1;
}
.member-name {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.member-id {
font-size: 12px;
color: #666;
margin-bottom: 2px;
}
.member-target {
font-size: 12px;
color: #666;
}
.member-stats {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.member-stat {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #333;
}
.stat-icon {
font-size: 10px;
}
.member-time {
font-size: 11px;
color: #666;
}
/* 帮助模态框 */
.help-modal-content {
background-color: white;
border-radius: 12px;
width: 100%;
max-width: 400px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.help-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #e5e7eb;
background-color: #f8f9fa;
}
.help-modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.help-modal-body {
padding: 20px;
overflow-y: auto;
}
.help-section {
margin-bottom: 20px;
}
.help-section:last-child {
margin-bottom: 0;
}
.help-description {
font-size: 14px;
line-height: 1.5;
color: #555;
margin: 0;
padding: 12px;
background-color: #f0f9ff;
border-radius: 8px;
border-left: 4px solid #3b82f6;
}
.help-section h4 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.status-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.status-item {
display: flex;
gap: 8px;
padding: 12px;
background-color: #f9fafb;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.status-number {
font-weight: 600;
color: #8B5CF6;
flex-shrink: 0;
}
.status-name {
font-weight: 600;
color: #333;
flex-shrink: 0;
min-width: 100px;
}
.status-desc {
color: #666;
line-height: 1.4;
flex: 1;
}
</style>

View File

@ -0,0 +1,426 @@
<template>
<div class="team-member gradient-background-circles">
<MobileHeader title="Team Member (87)" />
<div class="content">
<!-- 搜索框 -->
<div class="search-section">
<div class="search-box">
<span class="search-icon">🔍</span>
<input
type="text"
placeholder="Search by ID or name"
v-model="searchQuery"
class="search-input"
/>
</div>
</div>
<!-- 筛选标签 -->
<div class="filter-tabs">
<button
v-for="tab in filterTabs"
:key="tab.value"
@click="activeFilter = tab.value"
:class="['filter-tab', { active: activeFilter === tab.value }]"
>
{{ tab.label }}
</button>
</div>
<!-- 成员列表 -->
<div class="member-list">
<div
v-for="member in filteredMembers"
:key="member.id"
class="member-item"
@click="viewMemberDetail(member)"
>
<div class="member-info">
<div class="member-avatar">
<img v-if="member.avatar" :src="member.avatar" :alt="member.name" />
<span v-else>{{ member.name.charAt(0) }}</span>
</div>
<div class="member-details">
<h4>{{ member.name }}</h4>
<p>ID: {{ member.id }}</p>
<div class="member-tags">
<span v-if="member.isHost" class="tag host-tag">👑 Host</span>
<span v-if="member.isAgent" class="tag agent-tag">👤 Agent</span>
<span v-if="member.isActive" class="tag active-tag">🟢 Active</span>
<span v-else class="tag inactive-tag">🔴 Inactive</span>
</div>
</div>
</div>
<div class="member-stats">
<div class="stat-item">
<span class="stat-label">Target:</span>
<span class="stat-value">{{ member.target }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Days:</span>
<span class="stat-value">{{ member.activeDays }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Salary:</span>
<span class="stat-value">${{ member.salary }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredMembers.length === 0" class="empty-state">
<div class="empty-icon">👥</div>
<p>No members found</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
const router = useRouter()
const searchQuery = ref('')
const activeFilter = ref('all')
//
const filterTabs = [
{ label: 'All', value: 'all' },
{ label: 'Host', value: 'host' },
{ label: 'Agent', value: 'agent' },
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' }
]
//
const members = ref([
{
id: '1234567890',
name: 'Alice Johnson',
avatar: '',
isHost: true,
isAgent: false,
isActive: true,
target: 'A',
activeDays: 25,
salary: 450
},
{
id: '0987654321',
name: 'Bob Smith',
avatar: '',
isHost: false,
isAgent: true,
isActive: true,
target: 'B',
activeDays: 18,
salary: 320
},
{
id: '1122334455',
name: 'Carol Davis',
avatar: '',
isHost: true,
isAgent: false,
isActive: false,
target: 'A',
activeDays: 8,
salary: 120
},
{
id: '5566778899',
name: 'David Wilson',
avatar: '',
isHost: false,
isAgent: true,
isActive: true,
target: 'C',
activeDays: 30,
salary: 580
},
{
id: '9988776655',
name: 'Eva Brown',
avatar: '',
isHost: false,
isAgent: true,
isActive: false,
target: 'B',
activeDays: 5,
salary: 80
},
{
id: '4433221100',
name: 'Frank Miller',
avatar: '',
isHost: true,
isAgent: false,
isActive: true,
target: 'A',
activeDays: 22,
salary: 390
}
])
//
const filteredMembers = computed(() => {
let filtered = members.value
//
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(member =>
member.name.toLowerCase().includes(query) ||
member.id.includes(query)
)
}
//
switch (activeFilter.value) {
case 'host':
filtered = filtered.filter(member => member.isHost)
break
case 'agent':
filtered = filtered.filter(member => member.isAgent)
break
case 'active':
filtered = filtered.filter(member => member.isActive)
break
case 'inactive':
filtered = filtered.filter(member => !member.isActive)
break
}
return filtered
})
//
const viewMemberDetail = (member) => {
alert(`View details for ${member.name}`)
}
</script>
<style scoped>
.team-member {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 搜索部分 */
.search-section {
margin-bottom: 16px;
}
.search-box {
display: flex;
align-items: center;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.search-icon {
margin-right: 8px;
color: #9ca3af;
}
.search-input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #9ca3af;
}
/* 筛选标签 */
.filter-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
overflow-x: auto;
padding-bottom: 4px;
}
.filter-tab {
padding: 6px 12px;
border: 1px solid #e5e7eb;
border-radius: 16px;
background-color: white;
color: #666;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.filter-tab.active {
background-color: #8B5CF6;
color: white;
border-color: #8B5CF6;
}
.filter-tab:not(.active):hover {
border-color: #8B5CF6;
color: #8B5CF6;
}
/* 成员列表 */
.member-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.member-item {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.2s;
}
.member-item:active {
transform: scale(0.98);
}
.member-info {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.member-avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
font-weight: 600;
margin-right: 12px;
overflow: hidden;
}
.member-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.member-details {
flex: 1;
}
.member-details h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.member-details p {
margin: 0 0 8px 0;
font-size: 14px;
color: #666;
}
.member-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tag {
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 600;
}
.host-tag {
background-color: #E0E7FF;
color: #5B21B6;
}
.agent-tag {
background-color: #DBEAFE;
color: #1E40AF;
}
.active-tag {
background-color: #D1FAE5;
color: #059669;
}
.inactive-tag {
background-color: #FEE2E2;
color: #DC2626;
}
.member-stats {
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-label {
font-size: 11px;
color: #666;
margin-bottom: 2px;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #333;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
margin: 0;
font-size: 16px;
}
</style>

487
src/views/TransferView.vue Normal file
View File

@ -0,0 +1,487 @@
<template>
<div class="transfer gradient-background-circles">
<MobileHeader title="Transfer" />
<div class="content">
<!-- 信息部分 -->
<div class="info-section">
<div class="info-header">
<h3>Information</h3>
<button class="details-btn" @click="showDetails">
Details <span>></span>
</button>
</div>
<div class="available-salary">
<span>Available salary: </span>
<span v-if="loading" class="loading-text">Loading...</span>
<span v-else class="amount">{{ availableSalary }}</span>
</div>
</div>
<!-- 转账部分 -->
<div class="transfer-section">
<h3>Transfer to others</h3>
<!-- 收款人信息 -->
<div v-if="selectedPayee.id" class="payee-info" @click="searchPayee">
<div class="payee-details">
<div class="payee-avatar">
<img v-if="selectedPayee.avatar" :src="selectedPayee.avatar" :alt="selectedPayee.name" />
<span v-else>{{ selectedPayee.name.charAt(0) }}</span>
</div>
<div class="payee-text">
<h4>{{ selectedPayee.name }}</h4>
<p>ID: {{ selectedPayee.account || selectedPayee.id }}</p>
<div class="payee-tags">
<span v-if="selectedPayee.isHost" class="tag host-tag">👑 Host</span>
<span v-if="selectedPayee.isAgency" class="tag agency-tag">🏢 Agency</span>
<span v-if="selectedPayee.hasSalary" class="tag salary-tag">💰 Salary</span>
</div>
</div>
</div>
<button class="change-btn" @click.stop="clearPayee">Change</button>
</div>
<!-- 空状态 - 默认显示 -->
<div v-else class="empty-payee-state" @click="searchPayee">
<div class="search-placeholder">
<div class="search-icon">🔍</div>
<span>Search</span>
</div>
</div>
<!-- 转账金额选择 -->
<div class="coins-grid">
<div
v-for="coin in coinOptions"
:key="coin.id"
@click="selectCoin(coin)"
:class="['coin-item', { selected: selectedCoin === coin.id }]"
>
<div class="coin-icon">🪙</div>
<div class="coin-amount">{{ coin.amount }}</div>
<div class="coin-price">${{ coin.price }}</div>
</div>
</div>
<!-- 转账按钮 -->
<button
class="transfer-btn"
@click="transfer"
:disabled="!selectedCoin || !selectedPayee.id"
>
Transfer
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { getBankBalance } from '../api/wallet.js'
import { getSelectedPayee, clearSelectedPayee } from '../utils/payeeStore.js'
const router = useRouter()
const availableSalary = ref('0')
const selectedCoin = ref(null)
const loading = ref(false)
//
const selectedPayee = reactive({
id: null,
account: '',
name: '',
avatar: '',
type: '',
isHost: false,
isAgency: false,
hasSalary: false
})
//
const initializePayee = () => {
const savedPayee = getSelectedPayee()
if (savedPayee) {
Object.assign(selectedPayee, savedPayee)
}
}
//
const coinOptions = reactive([
{ id: 1, amount: 10000, price: 1 },
{ id: 2, amount: 50000, price: 5 },
{ id: 3, amount: 100000, price: 10 },
{ id: 4, amount: 500000, price: 50 },
{ id: 5, amount: 1000000, price: 100 },
{ id: 6, amount: 2000000, price: 200 }
])
//
const fetchBankBalance = async () => {
loading.value = true
try {
const response = await getBankBalance()
if (response.status && typeof response.body === 'number') {
// 2
availableSalary.value = response.body.toFixed(2)
} else {
availableSalary.value = '0.00'
}
} catch (error) {
console.error('Failed to fetch bank balance:', error)
availableSalary.value = '0.00'
//
alert('Failed to load balance. Please try again.')
} finally {
loading.value = false
}
}
//
const showDetails = () => {
router.push('/information-details')
}
const searchPayee = () => {
router.push('/search-payee')
}
//
const clearPayee = () => {
Object.assign(selectedPayee, {
id: null,
account: '',
name: '',
avatar: '',
type: '',
isHost: false,
isAgency: false,
hasSalary: false
})
clearSelectedPayee()
}
const selectCoin = (coin) => {
//
if (selectedCoin.value === coin.id) {
selectedCoin.value = null
} else {
selectedCoin.value = coin.id
}
}
const transfer = () => {
if (!selectedCoin.value) {
alert('Please select an amount first')
return
}
if (!selectedPayee.id) {
alert('Please select a payee first')
return
}
const selectedCoinData = coinOptions.find(coin => coin.id === selectedCoin.value)
alert(`Transfer ${selectedCoinData.amount} (${selectedCoinData.price} USD) to ${selectedPayee.name}`)
//
selectedCoin.value = null
}
//
onMounted(() => {
fetchBankBalance()
initializePayee()
})
</script>
<style scoped>
.transfer {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 信息部分 */
.info-section {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.info-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.details-btn {
background: none;
border: none;
color: #666;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.available-salary {
font-size: 14px;
color: #666;
}
.available-salary .amount {
font-weight: 600;
color: #333;
}
.available-salary .loading-text {
font-weight: 500;
color: #8B5CF6;
font-style: italic;
}
/* 转账部分 */
.transfer-section {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.transfer-section h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
/* 收款人信息 */
.payee-info {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 12px;
margin-bottom: 20px;
cursor: pointer;
transition: background-color 0.2s;
}
.payee-info:active {
background-color: #f9f9f9;
}
.payee-details {
display: flex;
align-items: center;
flex: 1;
}
.payee-avatar {
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
font-weight: 600;
margin-right: 12px;
overflow: hidden;
}
.payee-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.payee-text h4 {
margin: 0 0 4px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.payee-text p {
margin: 0 0 8px 0;
font-size: 12px;
color: #666;
}
.payee-tags {
display: flex;
gap: 4px;
}
.tag {
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
}
.host-tag {
background-color: #E0E7FF;
color: #5B21B6;
}
.agency-tag {
background-color: #DBEAFE;
color: #1E40AF;
}
.salary-tag {
background-color: #FEF3C7;
color: #D97706;
}
.change-btn {
background-color: #8B5CF6;
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.change-btn:active {
background-color: #7C3AED;
}
/* 空状态样式 */
.empty-payee-state {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
border: 2px dashed #ddd;
border-radius: 12px;
margin-bottom: 20px;
cursor: pointer;
transition: all 0.2s;
background-color: #fafafa;
}
.empty-payee-state:hover {
border-color: #8B5CF6;
background-color: #f8f5ff;
}
.empty-payee-state:active {
background-color: #f0e6ff;
}
.search-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: #666;
}
.search-icon {
font-size: 24px;
}
.search-placeholder span {
font-size: 14px;
font-weight: 500;
}
/* 金币网格 */
.coins-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.coin-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
border: 2px solid #e0e0e0;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
background-color: white;
}
.coin-item.selected {
border-color: #8B5CF6;
background-color: #F3F4F6;
}
.coin-item:active {
transform: scale(0.98);
}
.coin-icon {
font-size: 24px;
margin-bottom: 8px;
}
.coin-amount {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.coin-price {
font-size: 12px;
color: #666;
}
/* 转账按钮 */
.transfer-btn {
width: 100%;
padding: 14px;
background-color: #8B5CF6;
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.transfer-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.transfer-btn:not(:disabled):active {
background-color: #7C3AED;
}
</style>

18
vite.config.js Normal file
View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})