Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -20,16 +20,16 @@
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
const { checkAndPromptUpdate } = useAppUpdate()
|
const { checkAndPromptUpdate } = useAppUpdate();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 应用启动 3 秒后检查更新
|
// 应用启动 3 秒后检查更新
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
checkAndPromptUpdate()
|
checkAndPromptUpdate();
|
||||||
}, 3000)
|
}, 3000);
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,6 +38,18 @@ onMounted(async () => {
|
|||||||
在设置页面添加"检查更新"按钮:
|
在设置页面添加"检查更新"按钮:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
const {
|
||||||
|
isChecking,
|
||||||
|
currentVersion,
|
||||||
|
checkAndPromptUpdate,
|
||||||
|
} = useAppUpdate();
|
||||||
|
|
||||||
|
async function handleCheckUpdate() {
|
||||||
|
await checkAndPromptUpdate();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ion-page>
|
<ion-page>
|
||||||
<ion-header>
|
<ion-header>
|
||||||
@@ -59,18 +71,6 @@ onMounted(async () => {
|
|||||||
</ion-content>
|
</ion-content>
|
||||||
</ion-page>
|
</ion-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const {
|
|
||||||
isChecking,
|
|
||||||
currentVersion,
|
|
||||||
checkAndPromptUpdate,
|
|
||||||
} = useAppUpdate()
|
|
||||||
|
|
||||||
async function handleCheckUpdate() {
|
|
||||||
await checkAndPromptUpdate()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 自定义更新提示
|
### 3. 自定义更新提示
|
||||||
@@ -86,19 +86,20 @@ const {
|
|||||||
updateMessage,
|
updateMessage,
|
||||||
checkForUpdate,
|
checkForUpdate,
|
||||||
openStoreUpdate,
|
openStoreUpdate,
|
||||||
} = useAppUpdate()
|
} = useAppUpdate();
|
||||||
|
|
||||||
async function checkUpdate() {
|
async function checkUpdate() {
|
||||||
const result = await checkForUpdate()
|
const result = await checkForUpdate();
|
||||||
|
|
||||||
if (result.hasUpdate) {
|
if (result.hasUpdate) {
|
||||||
// 自定义提示逻辑
|
// 自定义提示逻辑
|
||||||
if (result.forceUpdate) {
|
if (result.forceUpdate) {
|
||||||
// 强制更新 - 阻止用户继续使用
|
// 强制更新 - 阻止用户继续使用
|
||||||
showForceUpdateModal()
|
showForceUpdateModal();
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
// 可选更新 - 显示提示但允许跳过
|
// 可选更新 - 显示提示但允许跳过
|
||||||
showOptionalUpdateToast()
|
showOptionalUpdateToast();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,26 +109,28 @@ async function checkUpdate() {
|
|||||||
### 4. 显示版本信息
|
### 4. 显示版本信息
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
|
const { currentVersion, hasUpdate, getCurrentVersion, checkForUpdate } = useAppUpdate();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getCurrentVersion();
|
||||||
|
await checkForUpdate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>应用版本</h2>
|
<h2>应用版本</h2>
|
||||||
<p>{{ currentVersion || '获取中...' }}</p>
|
<p>{{ currentVersion || '获取中...' }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-badge v-if="hasUpdate" color="danger">有更新</ion-badge>
|
<ion-badge v-if="hasUpdate" color="danger">
|
||||||
|
有更新
|
||||||
|
</ion-badge>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
|
|
||||||
const { currentVersion, hasUpdate, getCurrentVersion, checkForUpdate } = useAppUpdate()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await getCurrentVersion()
|
|
||||||
await checkForUpdate()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## API 参考
|
## API 参考
|
||||||
|
|||||||
@@ -51,23 +51,23 @@ curl https://your-domain.com/version.json
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Node.js / Elysia 示例
|
// Node.js / Elysia 示例
|
||||||
app.get('/api/app/version', async (ctx) => {
|
app.get("/api/app/version", async (ctx) => {
|
||||||
// 从前端静态资源读取版本
|
// 从前端静态资源读取版本
|
||||||
const response = await fetch('https://your-frontend-domain.com/version.json')
|
const response = await fetch("https://your-frontend-domain.com/version.json");
|
||||||
const frontendVersion = await response.json()
|
const frontendVersion = await response.json();
|
||||||
|
|
||||||
const { platform, currentVersion } = ctx.query
|
const { platform, currentVersion } = ctx.query;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: frontendVersion.version,
|
version: frontendVersion.version,
|
||||||
forceUpdate: compareVersion(currentVersion, frontendVersion.version) < 0,
|
forceUpdate: compareVersion(currentVersion, frontendVersion.version) < 0,
|
||||||
updateMessage: '修复了一些问题',
|
updateMessage: "修复了一些问题",
|
||||||
updateUrl: platform === 'ios'
|
updateUrl: platform === "ios"
|
||||||
? 'https://apps.apple.com/app/xxx'
|
? "https://apps.apple.com/app/xxx"
|
||||||
: 'https://play.google.com/store/apps/details?id=xxx',
|
: "https://play.google.com/store/apps/details?id=xxx",
|
||||||
minSupportVersion: '0.0.1',
|
minSupportVersion: "0.0.1",
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**方式 2:部署时同步到后端**
|
**方式 2:部署时同步到后端**
|
||||||
@@ -88,18 +88,18 @@ app.get('/api/app/version', async (ctx) => {
|
|||||||
后端直接读取本地文件:
|
后端直接读取本地文件:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import fs from 'fs'
|
import fs from "node:fs";
|
||||||
|
|
||||||
app.get('/api/app/version', async (ctx) => {
|
app.get("/api/app/version", async (ctx) => {
|
||||||
const versionFile = fs.readFileSync('/app/frontend-version.json', 'utf-8')
|
const versionFile = fs.readFileSync("/app/frontend-version.json", "utf-8");
|
||||||
const { version } = JSON.parse(versionFile)
|
const { version } = JSON.parse(versionFile);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version,
|
version,
|
||||||
forceUpdate: false,
|
forceUpdate: false,
|
||||||
// ...
|
// ...
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -162,27 +162,27 @@ versionName "1.2.3" // 与 package.json 保持一致
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import fs from 'fs'
|
import { execSync } from "node:child_process";
|
||||||
import { execSync } from 'child_process'
|
import fs from "node:fs";
|
||||||
|
|
||||||
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf-8'))
|
const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8"));
|
||||||
const version = packageJson.version
|
const version = packageJson.version;
|
||||||
|
|
||||||
console.log(`Syncing version ${version} to native projects...`)
|
console.log(`Syncing version ${version} to native projects...`);
|
||||||
|
|
||||||
// iOS
|
// iOS
|
||||||
const infoPlist = './ios/App/App/Info.plist'
|
const infoPlist = "./ios/App/App/Info.plist";
|
||||||
let plistContent = fs.readFileSync(infoPlist, 'utf-8')
|
let plistContent = fs.readFileSync(infoPlist, "utf-8");
|
||||||
plistContent = plistContent.replace(
|
plistContent = plistContent.replace(
|
||||||
/<key>CFBundleShortVersionString<\/key>\s*<string>.*?<\/string>/,
|
/<key>CFBundleShortVersionString<\/key>\s*<string>.*?<\/string>/,
|
||||||
`<key>CFBundleShortVersionString</key>\n\t<string>${version}</string>`
|
`<key>CFBundleShortVersionString</key>\n\t<string>${version}</string>`
|
||||||
)
|
);
|
||||||
fs.writeFileSync(infoPlist, plistContent)
|
fs.writeFileSync(infoPlist, plistContent);
|
||||||
|
|
||||||
// Android (需要安装 gradle 解析库或手动更新)
|
// Android (需要安装 gradle 解析库或手动更新)
|
||||||
console.log('Please manually update android/app/build.gradle versionName')
|
console.log("Please manually update android/app/build.gradle versionName");
|
||||||
|
|
||||||
console.log('✓ Version synced!')
|
console.log("✓ Version synced!");
|
||||||
```
|
```
|
||||||
|
|
||||||
在 `package.json` 中添加脚本:
|
在 `package.json` 中添加脚本:
|
||||||
@@ -241,7 +241,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: "18"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
@@ -284,107 +284,108 @@ jobs:
|
|||||||
|
|
||||||
**一次配置,永久受益!** 🎉
|
**一次配置,永久受益!** 🎉
|
||||||
|
|
||||||
|
|
||||||
## 后端 API 实现示例 - 版本检查接口
|
## 后端 API 实现示例 - 版本检查接口
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { Elysia, t } from 'elysia'
|
// 方案二:从本地文件读取(适用于前后端部署在同一服务器)
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
|
||||||
// 版本比较工具函数
|
// 版本比较工具函数
|
||||||
function compareVersion(version1: string, version2: string): number {
|
function compareVersion(version1: string, version2: string): number {
|
||||||
const v1Parts = version1.split('.').map(Number)
|
const v1Parts = version1.split(".").map(Number);
|
||||||
const v2Parts = version2.split('.').map(Number)
|
const v2Parts = version2.split(".").map(Number);
|
||||||
const maxLength = Math.max(v1Parts.length, v2Parts.length)
|
const maxLength = Math.max(v1Parts.length, v2Parts.length);
|
||||||
|
|
||||||
for (let i = 0; i < maxLength; i++) {
|
for (let i = 0; i < maxLength; i++) {
|
||||||
const v1Part = v1Parts[i] || 0
|
const v1Part = v1Parts[i] || 0;
|
||||||
const v2Part = v2Parts[i] || 0
|
const v2Part = v2Parts[i] || 0;
|
||||||
|
|
||||||
if (v1Part < v2Part) return -1
|
if (v1Part < v2Part)
|
||||||
if (v1Part > v2Part) return 1
|
return -1;
|
||||||
|
if (v1Part > v2Part)
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方案一:从前端静态资源读取版本(推荐)
|
// 方案一:从前端静态资源读取版本(推荐)
|
||||||
async function getFrontendVersionFromURL(): Promise<{
|
async function getFrontendVersionFromURL(): Promise<{
|
||||||
version: string
|
version: string;
|
||||||
buildTime: string
|
buildTime: string;
|
||||||
gitCommit: string
|
gitCommit: string;
|
||||||
environment: string
|
environment: string;
|
||||||
}> {
|
}> {
|
||||||
const response = await fetch('https://your-frontend-domain.com/version.json')
|
const response = await fetch("https://your-frontend-domain.com/version.json");
|
||||||
return await response.json()
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方案二:从本地文件读取(适用于前后端部署在同一服务器)
|
|
||||||
import fs from 'fs'
|
|
||||||
|
|
||||||
function getFrontendVersionFromFile(): {
|
function getFrontendVersionFromFile(): {
|
||||||
version: string
|
version: string;
|
||||||
buildTime: string
|
buildTime: string;
|
||||||
gitCommit: string
|
gitCommit: string;
|
||||||
environment: string
|
environment: string;
|
||||||
} {
|
} {
|
||||||
const content = fs.readFileSync('/app/frontend-version.json', 'utf-8')
|
const content = fs.readFileSync("/app/frontend-version.json", "utf-8");
|
||||||
return JSON.parse(content)
|
return JSON.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用商店链接配置
|
// 应用商店链接配置
|
||||||
const APP_STORE_URLS = {
|
const APP_STORE_URLS = {
|
||||||
ios: 'https://apps.apple.com/app/id123456789',
|
ios: "https://apps.apple.com/app/id123456789",
|
||||||
android: 'https://play.google.com/store/apps/details?id=riwa.ionic.app',
|
android: "https://play.google.com/store/apps/details?id=riwa.ionic.app",
|
||||||
}
|
};
|
||||||
|
|
||||||
// 版本策略配置(可存储在数据库)
|
// 版本策略配置(可存储在数据库)
|
||||||
const VERSION_POLICIES = {
|
const VERSION_POLICIES = {
|
||||||
minSupportVersion: '0.0.1', // 最低支持版本
|
minSupportVersion: "0.0.1", // 最低支持版本
|
||||||
forceUpdateVersions: ['0.0.1'], // 需要强制更新的版本列表
|
forceUpdateVersions: ["0.0.1"], // 需要强制更新的版本列表
|
||||||
updateMessages: {
|
updateMessages: {
|
||||||
'zh-CN': '修复了一些问题并优化了性能',
|
"zh-CN": "修复了一些问题并优化了性能",
|
||||||
'en-US': 'Bug fixes and performance improvements',
|
"en-US": "Bug fixes and performance improvements",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// Elysia 路由定义
|
// Elysia 路由定义
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.get(
|
.get(
|
||||||
'/api/app/version',
|
"/api/app/version",
|
||||||
async ({ query, headers }) => {
|
async ({ query, headers }) => {
|
||||||
const { platform, currentVersion } = query
|
const { platform, currentVersion } = query;
|
||||||
const lang = headers['accept-language']?.split(',')[0] || 'en-US'
|
const lang = headers["accept-language"]?.split(",")[0] || "en-US";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取前端版本信息
|
// 获取前端版本信息
|
||||||
const frontendVersion = await getFrontendVersionFromURL()
|
const frontendVersion = await getFrontendVersionFromURL();
|
||||||
// 或使用本地文件: const frontendVersion = getFrontendVersionFromFile()
|
// 或使用本地文件: const frontendVersion = getFrontendVersionFromFile()
|
||||||
|
|
||||||
// 判断是否需要更新
|
// 判断是否需要更新
|
||||||
const hasUpdate = compareVersion(currentVersion, frontendVersion.version) < 0
|
const hasUpdate = compareVersion(currentVersion, frontendVersion.version) < 0;
|
||||||
|
|
||||||
// 判断是否强制更新
|
// 判断是否强制更新
|
||||||
let forceUpdate = VERSION_POLICIES.forceUpdateVersions.includes(currentVersion)
|
let forceUpdate = VERSION_POLICIES.forceUpdateVersions.includes(currentVersion);
|
||||||
|
|
||||||
// 检查是否低于最低支持版本
|
// 检查是否低于最低支持版本
|
||||||
if (compareVersion(currentVersion, VERSION_POLICIES.minSupportVersion) < 0) {
|
if (compareVersion(currentVersion, VERSION_POLICIES.minSupportVersion) < 0) {
|
||||||
forceUpdate = true
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取更新链接
|
// 获取更新链接
|
||||||
const updateUrl = platform === 'ios'
|
const updateUrl = platform === "ios"
|
||||||
? APP_STORE_URLS.ios
|
? APP_STORE_URLS.ios
|
||||||
: platform === 'android'
|
: platform === "android"
|
||||||
? APP_STORE_URLS.android
|
? APP_STORE_URLS.android
|
||||||
: ''
|
: "";
|
||||||
|
|
||||||
// 获取更新说明(多语言)
|
// 获取更新说明(多语言)
|
||||||
const updateMessage = VERSION_POLICIES.updateMessages[lang]
|
const updateMessage = VERSION_POLICIES.updateMessages[lang]
|
||||||
|| VERSION_POLICIES.updateMessages['en-US']
|
|| VERSION_POLICIES.updateMessages["en-US"];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: frontendVersion.version,
|
version: frontendVersion.version,
|
||||||
buildNumber: parseInt(frontendVersion.version.replace(/\./g, '')),
|
buildNumber: Number.parseInt(frontendVersion.version.replace(/\./g, "")),
|
||||||
buildTime: frontendVersion.buildTime,
|
buildTime: frontendVersion.buildTime,
|
||||||
gitCommit: frontendVersion.gitCommit,
|
gitCommit: frontendVersion.gitCommit,
|
||||||
forceUpdate,
|
forceUpdate,
|
||||||
@@ -392,26 +393,27 @@ const app = new Elysia()
|
|||||||
updateUrl,
|
updateUrl,
|
||||||
minSupportVersion: VERSION_POLICIES.minSupportVersion,
|
minSupportVersion: VERSION_POLICIES.minSupportVersion,
|
||||||
releaseNotes: [
|
releaseNotes: [
|
||||||
'修复了已知问题',
|
"修复了已知问题",
|
||||||
'优化了应用性能',
|
"优化了应用性能",
|
||||||
'改进了用户界面',
|
"改进了用户界面",
|
||||||
],
|
],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
catch (error) {
|
||||||
console.error('Failed to get frontend version:', error)
|
console.error("Failed to get frontend version:", error);
|
||||||
|
|
||||||
// 降级处理:返回当前版本,不强制更新
|
// 降级处理:返回当前版本,不强制更新
|
||||||
return {
|
return {
|
||||||
version: currentVersion,
|
version: currentVersion,
|
||||||
forceUpdate: false,
|
forceUpdate: false,
|
||||||
updateMessage: '',
|
updateMessage: "",
|
||||||
updateUrl: '',
|
updateUrl: "",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
platform: t.Union([t.Literal('ios'), t.Literal('android'), t.Literal('web')]),
|
platform: t.Union([t.Literal("ios"), t.Literal("android"), t.Literal("web")]),
|
||||||
currentVersion: t.String(),
|
currentVersion: t.String(),
|
||||||
}),
|
}),
|
||||||
response: t.Object({
|
response: t.Object({
|
||||||
@@ -426,9 +428,9 @@ const app = new Elysia()
|
|||||||
releaseNotes: t.Optional(t.Array(t.String())),
|
releaseNotes: t.Optional(t.Array(t.String())),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
export default app
|
export default app;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用示例:
|
* 使用示例:
|
||||||
@@ -470,5 +472,4 @@ export default app
|
|||||||
* [platform]
|
* [platform]
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"@ionic/vue-router": "^8.7.11",
|
"@ionic/vue-router": "^8.7.11",
|
||||||
"@riwa/api-types": "http://192.168.1.7:9527/api/riwa-eden-0.0.118.tgz",
|
"@riwa/api-types": "http://192.168.1.7:9527/api/riwa-eden-0.0.118.tgz",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@vee-validate/yup": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@vueuse/integrations": "^14.1.0",
|
"@vueuse/integrations": "^14.1.0",
|
||||||
"@vueuse/router": "^14.1.0",
|
"@vueuse/router": "^14.1.0",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"vue": "^3.5.25",
|
"vue": "^3.5.25",
|
||||||
"vue-i18n": "^11.2.2",
|
"vue-i18n": "^11.2.2",
|
||||||
"vue-router": "^4.6.3",
|
"vue-router": "^4.6.3",
|
||||||
"yup": "^1.7.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^6.6.1",
|
"@antfu/eslint-config": "^6.6.1",
|
||||||
|
|||||||
@@ -50,19 +50,19 @@ pnpm generate
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const currentVersion: AppVersion = {
|
export const currentVersion: AppVersion = {
|
||||||
version: '1.0.0',
|
version: "1.0.0",
|
||||||
buildNumber: '100',
|
buildNumber: "100",
|
||||||
releaseDate: '2025-12-30',
|
releaseDate: "2025-12-30",
|
||||||
releaseNotes: {
|
releaseNotes: {
|
||||||
'zh-CN': ['更新内容...'],
|
"zh-CN": ["更新内容..."],
|
||||||
'en-US': ['What\'s new...'],
|
"en-US": ["What's new..."],
|
||||||
},
|
},
|
||||||
downloads: {
|
downloads: {
|
||||||
ios: 'https://example.com/app.ipa',
|
ios: "https://example.com/app.ipa",
|
||||||
android: 'https://example.com/app.apk',
|
android: "https://example.com/app.apk",
|
||||||
h5: 'https://app.example.com',
|
h5: "https://app.example.com",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 接入真实 API
|
### 接入真实 API
|
||||||
@@ -81,9 +81,9 @@ Nuxt UI 使用 TailwindCSS 4,可在 `nuxt.config.ts` 中配置:
|
|||||||
```typescript
|
```typescript
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
colorMode: {
|
colorMode: {
|
||||||
preference: 'system', // 'light' | 'dark' | 'system'
|
preference: "system", // 'light' | 'dark' | 'system'
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export default defineAppConfig({
|
export default defineAppConfig({
|
||||||
ui: {
|
ui: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: 'blue',
|
primary: "blue",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -6,4 +6,3 @@
|
|||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</UApp>
|
</UApp>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,8 @@
|
|||||||
|
|
||||||
/* 发光脉冲效果 */
|
/* 发光脉冲效果 */
|
||||||
@keyframes glow-pulse {
|
@keyframes glow-pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
|
box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@@ -96,7 +97,8 @@
|
|||||||
|
|
||||||
/* 悬浮动画 */
|
/* 悬浮动画 */
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@@ -208,7 +210,8 @@
|
|||||||
|
|
||||||
/* 脉冲缩放动画 */
|
/* 脉冲缩放动画 */
|
||||||
@keyframes pulse-scale {
|
@keyframes pulse-scale {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@@ -250,7 +253,8 @@
|
|||||||
|
|
||||||
/* 微妙的悬浮动画 */
|
/* 微妙的悬浮动画 */
|
||||||
@keyframes float-subtle {
|
@keyframes float-subtle {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translateY(0) translateZ(0);
|
transform: translateY(0) translateZ(0);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@@ -284,7 +288,8 @@
|
|||||||
|
|
||||||
/* 发光脉冲动画 */
|
/* 发光脉冲动画 */
|
||||||
@keyframes pulse-glow {
|
@keyframes pulse-glow {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@@ -298,7 +303,8 @@
|
|||||||
|
|
||||||
/* 微妙的脉冲动画 */
|
/* 微妙的脉冲动画 */
|
||||||
@keyframes pulse-subtle {
|
@keyframes pulse-subtle {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@@ -362,7 +368,8 @@
|
|||||||
|
|
||||||
/* 震动效果 */
|
/* 震动效果 */
|
||||||
@keyframes shake {
|
@keyframes shake {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
25% {
|
25% {
|
||||||
@@ -401,7 +408,8 @@
|
|||||||
|
|
||||||
/* 呼吸灯效果 */
|
/* 呼吸灯效果 */
|
||||||
@keyframes breathe {
|
@keyframes breathe {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
const { canInstall, isInstalled, install } = usePWAInstall()
|
const { canInstall, isInstalled, install } = usePWAInstall();
|
||||||
const installing = ref(false)
|
const installing = ref(false);
|
||||||
const dismissed = ref(false)
|
const dismissed = ref(false);
|
||||||
|
|
||||||
// 从 localStorage 读取是否已关闭,但只在未安装状态下有效
|
// 从 localStorage 读取是否已关闭,但只在未安装状态下有效
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 如果应用未安装,检查用户是否之前关闭过横幅
|
// 如果应用未安装,检查用户是否之前关闭过横幅
|
||||||
if (!isInstalled.value) {
|
if (!isInstalled.value) {
|
||||||
dismissed.value = localStorage.getItem('pwa-banner-dismissed') === 'true'
|
dismissed.value = localStorage.getItem("pwa-banner-dismissed") === "true";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 如果应用已安装,清除关闭记录(为了卸载后能再次提示)
|
// 如果应用已安装,清除关闭记录(为了卸载后能再次提示)
|
||||||
localStorage.removeItem('pwa-banner-dismissed')
|
localStorage.removeItem("pwa-banner-dismissed");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// 监听安装状态变化
|
// 监听安装状态变化
|
||||||
watch(isInstalled, (newValue) => {
|
watch(isInstalled, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
// 应用安装后,清除关闭记录
|
// 应用安装后,清除关闭记录
|
||||||
localStorage.removeItem('pwa-banner-dismissed')
|
localStorage.removeItem("pwa-banner-dismissed");
|
||||||
dismissed.value = false
|
dismissed.value = false;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// 监听 canInstall 变化(卸载后会重新触发 beforeinstallprompt)
|
// 监听 canInstall 变化(卸载后会重新触发 beforeinstallprompt)
|
||||||
watch(canInstall, (newValue) => {
|
watch(canInstall, (newValue) => {
|
||||||
if (newValue && !isInstalled.value) {
|
if (newValue && !isInstalled.value) {
|
||||||
// 如果可以安装且未安装,清除之前的关闭记录
|
// 如果可以安装且未安装,清除之前的关闭记录
|
||||||
// 这样卸载后再次访问就会重新显示横幅
|
// 这样卸载后再次访问就会重新显示横幅
|
||||||
localStorage.removeItem('pwa-banner-dismissed')
|
localStorage.removeItem("pwa-banner-dismissed");
|
||||||
dismissed.value = false
|
dismissed.value = false;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
async function handleInstall() {
|
async function handleInstall() {
|
||||||
installing.value = true
|
installing.value = true;
|
||||||
try {
|
try {
|
||||||
const success = await install()
|
const success = await install();
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log('PWA 安装成功')
|
console.log("PWA 安装成功");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
installing.value = false
|
installing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissBanner() {
|
function dismissBanner() {
|
||||||
dismissed.value = true
|
dismissed.value = true;
|
||||||
localStorage.setItem('pwa-banner-dismissed', 'true')
|
localStorage.setItem("pwa-banner-dismissed", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
const showBanner = computed(() => canInstall.value && !isInstalled.value && !dismissed.value)
|
const showBanner = computed(() => canInstall.value && !isInstalled.value && !dismissed.value);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
const { canInstall, isInstalled, install } = usePWAInstall()
|
const { canInstall, isInstalled, install } = usePWAInstall();
|
||||||
const installing = ref(false)
|
const installing = ref(false);
|
||||||
|
|
||||||
async function handleInstall() {
|
async function handleInstall() {
|
||||||
installing.value = true
|
installing.value = true;
|
||||||
try {
|
try {
|
||||||
const success = await install()
|
const success = await install();
|
||||||
if (success) {
|
if (success) {
|
||||||
// 可以显示成功提示
|
// 可以显示成功提示
|
||||||
console.log('PWA 安装成功')
|
console.log("PWA 安装成功");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
installing.value = false
|
installing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { onMounted, ref } from "vue";
|
||||||
|
|
||||||
export function usePWAInstall() {
|
export function usePWAInstall() {
|
||||||
const deferredPrompt = ref<any>(null)
|
const deferredPrompt = ref<any>(null);
|
||||||
const canInstall = ref(false)
|
const canInstall = ref(false);
|
||||||
const isInstalled = ref(false)
|
const isInstalled = ref(false);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 检查是否已安装
|
// 检查是否已安装
|
||||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
if (window.matchMedia("(display-mode: standalone)").matches) {
|
||||||
isInstalled.value = true
|
isInstalled.value = true;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听安装提示事件
|
// 监听安装提示事件
|
||||||
window.addEventListener('beforeinstallprompt', (e) => {
|
window.addEventListener("beforeinstallprompt", (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
deferredPrompt.value = e
|
deferredPrompt.value = e;
|
||||||
canInstall.value = true
|
canInstall.value = true;
|
||||||
})
|
});
|
||||||
|
|
||||||
// 监听安装成功事件
|
// 监听安装成功事件
|
||||||
window.addEventListener('appinstalled', () => {
|
window.addEventListener("appinstalled", () => {
|
||||||
deferredPrompt.value = null
|
deferredPrompt.value = null;
|
||||||
canInstall.value = false
|
canInstall.value = false;
|
||||||
isInstalled.value = true
|
isInstalled.value = true;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
if (!deferredPrompt.value) {
|
if (!deferredPrompt.value) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deferredPrompt.value.prompt()
|
await deferredPrompt.value.prompt();
|
||||||
const { outcome } = await deferredPrompt.value.userChoice
|
const { outcome } = await deferredPrompt.value.userChoice;
|
||||||
|
|
||||||
if (outcome === 'accepted') {
|
if (outcome === "accepted") {
|
||||||
deferredPrompt.value = null
|
deferredPrompt.value = null;
|
||||||
canInstall.value = false
|
canInstall.value = false;
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('安装失败:', error)
|
console.error("安装失败:", error);
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,5 +54,5 @@ export function usePWAInstall() {
|
|||||||
canInstall,
|
canInstall,
|
||||||
isInstalled,
|
isInstalled,
|
||||||
install,
|
install,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import type { Platform } from '~/types'
|
import type { Platform } from "~/types";
|
||||||
|
|
||||||
export function usePlatformDetection() {
|
export function usePlatformDetection() {
|
||||||
const platform = useState<Platform>('platform', () => 'unknown')
|
const platform = useState<Platform>("platform", () => "unknown");
|
||||||
|
|
||||||
function detectPlatform(): Platform {
|
function detectPlatform(): Platform {
|
||||||
if (import.meta.server)
|
if (import.meta.server)
|
||||||
return 'unknown'
|
return "unknown";
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase()
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
|
||||||
if (/iphone|ipad|ipod/.test(ua))
|
if (/iphone|ipad|ipod/.test(ua))
|
||||||
return 'ios'
|
return "ios";
|
||||||
else if (/android/.test(ua))
|
else if (/android/.test(ua))
|
||||||
return 'android'
|
return "android";
|
||||||
else if (/windows|macintosh|linux/.test(ua))
|
else if (/windows|macintosh|linux/.test(ua))
|
||||||
return 'desktop'
|
return "desktop";
|
||||||
|
|
||||||
return 'unknown'
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
platform.value = detectPlatform()
|
platform.value = detectPlatform();
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
platform: readonly(platform),
|
platform: readonly(platform),
|
||||||
isIOS: computed(() => platform.value === 'ios'),
|
isIOS: computed(() => platform.value === "ios"),
|
||||||
isAndroid: computed(() => platform.value === 'android'),
|
isAndroid: computed(() => platform.value === "android"),
|
||||||
isDesktop: computed(() => platform.value === 'desktop'),
|
isDesktop: computed(() => platform.value === "desktop"),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +1,34 @@
|
|||||||
import type { AppVersion, DownloadStats } from '~/types'
|
import type { AppVersion, DownloadStats } from "~/types";
|
||||||
|
|
||||||
// 当前版本信息
|
// 当前版本信息
|
||||||
export const currentVersion: AppVersion = {
|
export const currentVersion: AppVersion = {
|
||||||
version: '1.0.0',
|
version: "1.0.0",
|
||||||
buildNumber: '100',
|
buildNumber: "100",
|
||||||
releaseDate: '2025-12-30',
|
releaseDate: "2025-12-30",
|
||||||
releaseNotes: {
|
releaseNotes: {
|
||||||
'zh-CN': [
|
"zh-CN": [
|
||||||
'🎉 首次发布',
|
"🎉 首次发布",
|
||||||
'✨ 全新的用户界面设计',
|
"✨ 全新的用户界面设计",
|
||||||
'🔐 增强的安全特性',
|
"🔐 增强的安全特性",
|
||||||
'⚡ 性能优化,响应速度提升 30%',
|
"⚡ 性能优化,响应速度提升 30%",
|
||||||
'🌍 支持多语言切换',
|
"🌍 支持多语言切换",
|
||||||
'🌙 深色模式支持',
|
"🌙 深色模式支持",
|
||||||
],
|
],
|
||||||
'en-US': [
|
"en-US": [
|
||||||
'🎉 Initial Release',
|
"🎉 Initial Release",
|
||||||
'✨ Brand new user interface',
|
"✨ Brand new user interface",
|
||||||
'🔐 Enhanced security features',
|
"🔐 Enhanced security features",
|
||||||
'⚡ Performance optimization, 30% faster response',
|
"⚡ Performance optimization, 30% faster response",
|
||||||
'🌍 Multi-language support',
|
"🌍 Multi-language support",
|
||||||
'🌙 Dark mode support',
|
"🌙 Dark mode support",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
downloads: {
|
downloads: {
|
||||||
ios: 'https://example.com/riwa-ios-1.0.0.ipa',
|
ios: "https://example.com/riwa-ios-1.0.0.ipa",
|
||||||
android: 'https://example.com/riwa-android-1.0.0.apk',
|
android: "https://example.com/riwa-android-1.0.0.apk",
|
||||||
h5: 'http://localhost:5173',
|
h5: "http://localhost:5173",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// 模拟下载统计数据
|
// 模拟下载统计数据
|
||||||
export const mockDownloadStats: DownloadStats = {
|
export const mockDownloadStats: DownloadStats = {
|
||||||
@@ -36,4 +36,4 @@ export const mockDownloadStats: DownloadStats = {
|
|||||||
today: 156,
|
today: 156,
|
||||||
ios: 7234,
|
ios: 7234,
|
||||||
android: 5346,
|
android: 5346,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ services:
|
|||||||
# nginx conf
|
# nginx conf
|
||||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
|
||||||
# version: '3.8'
|
# version: '3.8'
|
||||||
|
|
||||||
# services:
|
# services:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import antfu from '@antfu/eslint-config'
|
import antfu from "@antfu/eslint-config";
|
||||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
import withNuxt from "./.nuxt/eslint.config.mjs";
|
||||||
|
|
||||||
export default withNuxt(
|
export default withNuxt(
|
||||||
antfu({
|
antfu({
|
||||||
formatters: true,
|
formatters: true,
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxt/ui',
|
"@nuxt/ui",
|
||||||
'@nuxtjs/i18n',
|
"@nuxtjs/i18n",
|
||||||
'@nuxt/eslint',
|
"@nuxt/eslint",
|
||||||
'@vite-pwa/nuxt',
|
"@vite-pwa/nuxt",
|
||||||
],
|
],
|
||||||
|
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
pwa: {
|
pwa: {
|
||||||
registerType: 'autoUpdate',
|
registerType: "autoUpdate",
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'Riwa应用商店',
|
name: "Riwa应用商店",
|
||||||
short_name: 'Riwa应用商店',
|
short_name: "Riwa应用商店",
|
||||||
description: 'Riwa App 下载 - iOS, Android, H5',
|
description: "Riwa App 下载 - iOS, Android, H5",
|
||||||
theme_color: '#3b82f6',
|
theme_color: "#3b82f6",
|
||||||
background_color: '#ffffff',
|
background_color: "#ffffff",
|
||||||
display: 'standalone',
|
display: "standalone",
|
||||||
scope: '/',
|
scope: "/",
|
||||||
start_url: '/',
|
start_url: "/",
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/favicon.svg',
|
src: "/favicon.svg",
|
||||||
sizes: '512x512',
|
sizes: "512x512",
|
||||||
type: 'image/svg+xml',
|
type: "image/svg+xml",
|
||||||
purpose: 'any',
|
purpose: "any",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/favicon.svg',
|
src: "/favicon.svg",
|
||||||
sizes: '192x192',
|
sizes: "192x192",
|
||||||
type: 'image/svg+xml',
|
type: "image/svg+xml",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
navigateFallback: '/',
|
navigateFallback: "/",
|
||||||
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
|
globPatterns: ["**/*.{js,css,html,png,svg,ico}"],
|
||||||
cleanupOutdatedCaches: true,
|
cleanupOutdatedCaches: true,
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
||||||
handler: 'CacheFirst',
|
handler: "CacheFirst",
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'google-fonts-cache',
|
cacheName: "google-fonts-cache",
|
||||||
expiration: {
|
expiration: {
|
||||||
maxEntries: 10,
|
maxEntries: 10,
|
||||||
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
||||||
@@ -55,9 +55,9 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
|
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
|
||||||
handler: 'CacheFirst',
|
handler: "CacheFirst",
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'gstatic-fonts-cache',
|
cacheName: "gstatic-fonts-cache",
|
||||||
expiration: {
|
expiration: {
|
||||||
maxEntries: 10,
|
maxEntries: 10,
|
||||||
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
||||||
@@ -75,81 +75,81 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
devOptions: {
|
devOptions: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
type: 'module',
|
type: "module",
|
||||||
},
|
},
|
||||||
injectManifest: {
|
injectManifest: {
|
||||||
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
|
globPatterns: ["**/*.{js,css,html,png,svg,ico}"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
css: [
|
css: [
|
||||||
'~/assets/css/main.css',
|
"~/assets/css/main.css",
|
||||||
'~/assets/css/animations.css',
|
"~/assets/css/animations.css",
|
||||||
],
|
],
|
||||||
|
|
||||||
colorMode: {
|
colorMode: {
|
||||||
preference: 'light',
|
preference: "light",
|
||||||
},
|
},
|
||||||
|
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: 'zh-CN',
|
defaultLocale: "zh-CN",
|
||||||
locales: [
|
locales: [
|
||||||
{
|
{
|
||||||
code: 'zh-CN',
|
code: "zh-CN",
|
||||||
name: '简体中文',
|
name: "简体中文",
|
||||||
file: 'zh-CN.json',
|
file: "zh-CN.json",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'en-US',
|
code: "en-US",
|
||||||
name: 'English',
|
name: "English",
|
||||||
file: 'en-US.json',
|
file: "en-US.json",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
strategy: 'no_prefix',
|
strategy: "no_prefix",
|
||||||
detectBrowserLanguage: {
|
detectBrowserLanguage: {
|
||||||
useCookie: true,
|
useCookie: true,
|
||||||
cookieKey: 'i18n_locale',
|
cookieKey: "i18n_locale",
|
||||||
redirectOn: 'root',
|
redirectOn: "root",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
charset: 'utf-8',
|
charset: "utf-8",
|
||||||
viewport: 'width=device-width, initial-scale=1',
|
viewport: "width=device-width, initial-scale=1",
|
||||||
title: 'Riwa App 下载',
|
title: "Riwa App 下载",
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'description', content: 'Riwa App 下载 - iOS, Android, H5' },
|
{ name: "description", content: "Riwa App 下载 - iOS, Android, H5" },
|
||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
devServer:{
|
devServer: {
|
||||||
host: '0.0.0.0'
|
host: "0.0.0.0",
|
||||||
},
|
},
|
||||||
|
|
||||||
nitro: {
|
nitro: {
|
||||||
prerender: {
|
prerender: {
|
||||||
routes: ['/'],
|
routes: ["/"],
|
||||||
crawlLinks: true,
|
crawlLinks: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
hooks: {
|
hooks: {
|
||||||
async 'nitro:config'(nitroConfig) {
|
"nitro:config": async function (nitroConfig) {
|
||||||
// 预渲染所有应用详情页
|
// 预渲染所有应用详情页
|
||||||
const { apps } = await import('./data/apps')
|
const { apps } = await import("./data/apps");
|
||||||
const routes = apps.map(app => `/apps/${app.id}`)
|
const routes = apps.map(app => `/apps/${app.id}`);
|
||||||
nitroConfig.prerender = nitroConfig.prerender || {}
|
nitroConfig.prerender = nitroConfig.prerender || {};
|
||||||
nitroConfig.prerender.routes = [
|
nitroConfig.prerender.routes = [
|
||||||
...(nitroConfig.prerender.routes || []),
|
...(nitroConfig.prerender.routes || []),
|
||||||
...routes,
|
...routes,
|
||||||
]
|
];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
compatibilityDate: '2025-12-30',
|
compatibilityDate: "2025-12-30",
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@riwa/distribute",
|
"name": "@riwa/distribute",
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
|
|||||||
@@ -1,72 +1,72 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AppInfo } from '~/types'
|
import type { AppInfo } from "~/types";
|
||||||
import { apps } from '~/data/apps'
|
import { apps } from "~/data/apps";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
// 直接从数据文件获取应用详情
|
// 直接从数据文件获取应用详情
|
||||||
const app = computed(() => apps.find(a => a.id === route.params.id))
|
const app = computed(() => apps.find(a => a.id === route.params.id));
|
||||||
|
|
||||||
// 如果应用不存在,跳转回首页
|
// 如果应用不存在,跳转回首页
|
||||||
if (!app.value) {
|
if (!app.value) {
|
||||||
navigateTo('/')
|
navigateTo("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载处理
|
// 下载处理
|
||||||
async function handleDownload(type: 'ios' | 'android' | 'h5') {
|
async function handleDownload(type: "ios" | "android" | "h5") {
|
||||||
if (!app.value) {
|
if (!app.value) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = app.value.downloads[type]
|
const url = app.value.downloads[type];
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'h5') {
|
if (type === "h5") {
|
||||||
navigateTo(url, { external: true, open: { target: '_blank' } })
|
navigateTo(url, { external: true, open: { target: "_blank" } });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
navigateTo(url, { external: true })
|
navigateTo(url, { external: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await $fetch(`/api/track/${type}`, {
|
await $fetch(`/api/track/${type}`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: { appId: app.value.id },
|
body: { appId: app.value.id },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回首页
|
// 返回首页
|
||||||
function goBack() {
|
function goBack() {
|
||||||
router.back()
|
router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
useHead({
|
useHead({
|
||||||
title: app.value ? `${app.value.name} - Riwa App Store` : 'Riwa App Store',
|
title: app.value ? `${app.value.name} - Riwa App Store` : "Riwa App Store",
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: "description",
|
||||||
content: app.value?.shortDescription[locale.value as 'zh-CN' | 'en-US'] || '',
|
content: app.value?.shortDescription[locale.value as "zh-CN" | "en-US"] || "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="app" class="min-h-screen relative overflow-hidden bg-gradient-to-br from-gray-50 via-blue-50/30 to-purple-50/30 dark:from-gray-950 dark:via-blue-950/20 dark:to-purple-950/20">
|
<div v-if="app" class="min-h-screen relative overflow-hidden bg-gradient-to-br from-gray-50 via-blue-50/30 to-purple-50/30 dark:from-gray-950 dark:via-blue-950/20 dark:to-purple-950/20">
|
||||||
<!-- 科技感网格背景 -->
|
<!-- 科技感网格背景 -->
|
||||||
<div class="fixed inset-0 opacity-30 dark:opacity-20 pointer-events-none">
|
<div class="fixed inset-0 opacity-30 dark:opacity-20 pointer-events-none">
|
||||||
<div class="absolute inset-0" style="background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px); background-size: 50px 50px;"></div>
|
<div class="absolute inset-0" style="background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px); background-size: 50px 50px;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 动态发光球体背景 -->
|
<!-- 动态发光球体背景 -->
|
||||||
<div class="fixed inset-0 pointer-events-none overflow-hidden">
|
<div class="fixed inset-0 pointer-events-none overflow-hidden">
|
||||||
<div class="absolute top-1/4 -left-48 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl animate-pulse"></div>
|
<div class="absolute top-1/4 -left-48 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl animate-pulse" />
|
||||||
<div class="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl animate-pulse" style="animation-delay: 1s;"></div>
|
<div class="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl animate-pulse" style="animation-delay: 1s;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UContainer class="relative z-10">
|
<UContainer class="relative z-10">
|
||||||
@@ -91,10 +91,10 @@ useHead({
|
|||||||
<!-- App Header -->
|
<!-- App Header -->
|
||||||
<div class="flex items-start gap-6 mb-8 group">
|
<div class="flex items-start gap-6 mb-8 group">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute -inset-2 bg-gradient-to-r from-primary-500 via-purple-500 to-blue-500 rounded-3xl opacity-50 blur-xl group-hover:opacity-100 transition-all duration-500 animate-pulse"></div>
|
<div class="absolute -inset-2 bg-gradient-to-r from-primary-500 via-purple-500 to-blue-500 rounded-3xl opacity-50 blur-xl group-hover:opacity-100 transition-all duration-500 animate-pulse" />
|
||||||
<div class="size-24 rounded-3xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-4xl shrink-0 shadow-2xl shadow-blue-500/50 relative overflow-hidden group-hover:scale-110 transition-all duration-500 p-3">
|
<div class="size-24 rounded-3xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-4xl shrink-0 shadow-2xl shadow-blue-500/50 relative overflow-hidden group-hover:scale-110 transition-all duration-500 p-3">
|
||||||
<div class="absolute inset-0 bg-gradient-to-tr from-white/0 via-white/30 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></div>
|
<div class="absolute inset-0 bg-gradient-to-tr from-white/0 via-white/30 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700" />
|
||||||
<img :src="app.icon" :alt="app.name" class="size-full object-contain relative z-10 rounded-2xl" />
|
<img :src="app.icon" :alt="app.name" class="size-full object-contain relative z-10 rounded-2xl">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -115,7 +115,7 @@ useHead({
|
|||||||
<!-- Download Buttons -->
|
<!-- Download Buttons -->
|
||||||
<div class="grid md:grid-cols-3 gap-4 mb-8">
|
<div class="grid md:grid-cols-3 gap-4 mb-8">
|
||||||
<div v-if="app.downloads.ios" class="relative group">
|
<div v-if="app.downloads.ios" class="relative group">
|
||||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-device-phone-mobile"
|
icon="i-heroicons-device-phone-mobile"
|
||||||
size="xl"
|
size="xl"
|
||||||
@@ -124,7 +124,9 @@ useHead({
|
|||||||
@click="handleDownload('ios')"
|
@click="handleDownload('ios')"
|
||||||
>
|
>
|
||||||
<div class="text-left w-full">
|
<div class="text-left w-full">
|
||||||
<div class="font-semibold text-base">iOS</div>
|
<div class="font-semibold text-base">
|
||||||
|
iOS
|
||||||
|
</div>
|
||||||
<div v-if="app.size?.ios" class="text-xs opacity-80">
|
<div v-if="app.size?.ios" class="text-xs opacity-80">
|
||||||
{{ app.size.ios }}
|
{{ app.size.ios }}
|
||||||
</div>
|
</div>
|
||||||
@@ -132,7 +134,7 @@ useHead({
|
|||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="app.downloads.android" class="relative group">
|
<div v-if="app.downloads.android" class="relative group">
|
||||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-device-tablet"
|
icon="i-heroicons-device-tablet"
|
||||||
size="xl"
|
size="xl"
|
||||||
@@ -141,7 +143,9 @@ useHead({
|
|||||||
@click="handleDownload('android')"
|
@click="handleDownload('android')"
|
||||||
>
|
>
|
||||||
<div class="text-left w-full">
|
<div class="text-left w-full">
|
||||||
<div class="font-semibold text-base">Android</div>
|
<div class="font-semibold text-base">
|
||||||
|
Android
|
||||||
|
</div>
|
||||||
<div v-if="app.size?.android" class="text-xs opacity-80">
|
<div v-if="app.size?.android" class="text-xs opacity-80">
|
||||||
{{ app.size.android }}
|
{{ app.size.android }}
|
||||||
</div>
|
</div>
|
||||||
@@ -149,7 +153,7 @@ useHead({
|
|||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="app.downloads.h5" class="relative group">
|
<div v-if="app.downloads.h5" class="relative group">
|
||||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-globe-alt"
|
icon="i-heroicons-globe-alt"
|
||||||
size="xl"
|
size="xl"
|
||||||
@@ -158,7 +162,9 @@ useHead({
|
|||||||
@click="handleDownload('h5')"
|
@click="handleDownload('h5')"
|
||||||
>
|
>
|
||||||
<div class="text-left w-full">
|
<div class="text-left w-full">
|
||||||
<div class="font-semibold text-base">Web</div>
|
<div class="font-semibold text-base">
|
||||||
|
Web
|
||||||
|
</div>
|
||||||
<div class="text-xs opacity-80">
|
<div class="text-xs opacity-80">
|
||||||
PWA
|
PWA
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +176,7 @@ useHead({
|
|||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-primary-500 to-purple-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
<div class="absolute -inset-0.5 bg-gradient-to-r from-primary-500 to-purple-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-primary-500/20 transition-all duration-500 hover:-translate-y-1">
|
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-primary-500/20 transition-all duration-500 hover:-translate-y-1">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-3xl font-bold bg-gradient-to-r from-primary-500 to-purple-500 bg-clip-text text-transparent">
|
<div class="text-3xl font-bold bg-gradient-to-r from-primary-500 to-purple-500 bg-clip-text text-transparent">
|
||||||
@@ -183,7 +189,7 @@ useHead({
|
|||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
<div class="absolute -inset-0.5 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-purple-500/20 transition-all duration-500 hover:-translate-y-1">
|
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-purple-500/20 transition-all duration-500 hover:-translate-y-1">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-3xl font-bold bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
|
<div class="text-3xl font-bold bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||||
@@ -196,7 +202,7 @@ useHead({
|
|||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
<div class="absolute -inset-0.5 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-blue-500/20 transition-all duration-500 hover:-translate-y-1">
|
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-blue-500/20 transition-all duration-500 hover:-translate-y-1">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-3xl font-bold bg-gradient-to-r from-blue-500 to-cyan-500 bg-clip-text text-transparent">
|
<div class="text-3xl font-bold bg-gradient-to-r from-blue-500 to-cyan-500 bg-clip-text text-transparent">
|
||||||
@@ -209,7 +215,7 @@ useHead({
|
|||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
<div class="absolute -inset-0.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-indigo-500/20 transition-all duration-500 hover:-translate-y-1">
|
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-indigo-500/20 transition-all duration-500 hover:-translate-y-1">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-3xl font-bold bg-gradient-to-r from-indigo-500 to-purple-500 bg-clip-text text-transparent">
|
<div class="text-3xl font-bold bg-gradient-to-r from-indigo-500 to-purple-500 bg-clip-text text-transparent">
|
||||||
@@ -225,7 +231,7 @@ useHead({
|
|||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div class="relative group mb-8">
|
<div class="relative group mb-8">
|
||||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-primary-500 via-purple-500 to-blue-500 rounded-xl opacity-0 group-hover:opacity-30 blur transition-all duration-500"></div>
|
<div class="absolute -inset-0.5 bg-gradient-to-r from-primary-500 via-purple-500 to-blue-500 rounded-xl opacity-0 group-hover:opacity-30 blur transition-all duration-500" />
|
||||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-primary-500/10 transition-all duration-500">
|
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-primary-500/10 transition-all duration-500">
|
||||||
<h3 class="text-xl font-semibold bg-gradient-to-r from-gray-900 via-primary-600 to-purple-600 dark:from-white dark:via-primary-400 dark:to-purple-400 bg-clip-text text-transparent mb-4">
|
<h3 class="text-xl font-semibold bg-gradient-to-r from-gray-900 via-primary-600 to-purple-600 dark:from-white dark:via-primary-400 dark:to-purple-400 bg-clip-text text-transparent mb-4">
|
||||||
{{ locale === 'zh-CN' ? '应用介绍' : 'Description' }}
|
{{ locale === 'zh-CN' ? '应用介绍' : 'Description' }}
|
||||||
@@ -238,7 +244,7 @@ useHead({
|
|||||||
|
|
||||||
<!-- What's New -->
|
<!-- What's New -->
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-purple-500 via-pink-500 to-rose-500 rounded-xl opacity-0 group-hover:opacity-30 blur transition-all duration-500"></div>
|
<div class="absolute -inset-0.5 bg-gradient-to-r from-purple-500 via-pink-500 to-rose-500 rounded-xl opacity-0 group-hover:opacity-30 blur transition-all duration-500" />
|
||||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-purple-500/10 transition-all duration-500">
|
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-purple-500/10 transition-all duration-500">
|
||||||
<h3 class="text-xl font-semibold bg-gradient-to-r from-gray-900 via-purple-600 to-pink-600 dark:from-white dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-4">
|
<h3 class="text-xl font-semibold bg-gradient-to-r from-gray-900 via-purple-600 to-pink-600 dark:from-white dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-4">
|
||||||
{{ locale === 'zh-CN' ? '更新内容' : "What's New" }}
|
{{ locale === 'zh-CN' ? '更新内容' : "What's New" }}
|
||||||
|
|||||||
@@ -1,104 +1,104 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AppInfo } from '~/types'
|
import type { AppInfo } from "~/types";
|
||||||
|
|
||||||
import { apps as appsData, categories as categoriesData } from '~/data/apps'
|
import { apps as appsData, categories as categoriesData } from "~/data/apps";
|
||||||
|
|
||||||
const { t, locale, setLocale } = useI18n()
|
const { t, locale, setLocale } = useI18n();
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode();
|
||||||
|
|
||||||
// 直接使用数据文件
|
// 直接使用数据文件
|
||||||
const apps = computed(() => appsData)
|
const apps = computed(() => appsData);
|
||||||
const categories = computed(() => categoriesData)
|
const categories = computed(() => categoriesData);
|
||||||
|
|
||||||
// 当前选中的分类
|
// 当前选中的分类
|
||||||
const selectedCategory = ref('all')
|
const selectedCategory = ref("all");
|
||||||
|
|
||||||
// 搜索关键词
|
// 搜索关键词
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref("");
|
||||||
|
|
||||||
// 过滤后的应用列表
|
// 过滤后的应用列表
|
||||||
const filteredApps = computed(() => {
|
const filteredApps = computed(() => {
|
||||||
let result = apps.value
|
let result = apps.value;
|
||||||
|
|
||||||
// 按分类过滤
|
// 按分类过滤
|
||||||
if (selectedCategory.value !== 'all') {
|
if (selectedCategory.value !== "all") {
|
||||||
result = result.filter(app => app.category === selectedCategory.value)
|
result = result.filter(app => app.category === selectedCategory.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按搜索关键词过滤
|
// 按搜索关键词过滤
|
||||||
if (searchKeyword.value.trim()) {
|
if (searchKeyword.value.trim()) {
|
||||||
const keyword = searchKeyword.value.toLowerCase()
|
const keyword = searchKeyword.value.toLowerCase();
|
||||||
result = result.filter(app =>
|
result = result.filter(app =>
|
||||||
app.name.toLowerCase().includes(keyword)
|
app.name.toLowerCase().includes(keyword)
|
||||||
|| app.shortDescription[locale.value as 'zh-CN' | 'en-US'].toLowerCase().includes(keyword),
|
|| app.shortDescription[locale.value as "zh-CN" | "en-US"].toLowerCase().includes(keyword),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result;
|
||||||
})
|
});
|
||||||
|
|
||||||
// 切换语言
|
// 切换语言
|
||||||
function toggleLanguage() {
|
function toggleLanguage() {
|
||||||
setLocale(locale.value === 'zh-CN' ? 'en-US' : 'zh-CN')
|
setLocale(locale.value === "zh-CN" ? "en-US" : "zh-CN");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开应用详情
|
// 打开应用详情
|
||||||
function openAppDetail(app: AppInfo) {
|
function openAppDetail(app: AppInfo) {
|
||||||
navigateTo(`/apps/${app.id}`)
|
navigateTo(`/apps/${app.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载处理
|
// 下载处理
|
||||||
async function handleDownload(app: AppInfo, type: 'ios' | 'android' | 'h5', event?: MouseEvent | TouchEvent) {
|
async function handleDownload(app: AppInfo, type: "ios" | "android" | "h5", event?: MouseEvent | TouchEvent) {
|
||||||
const url = app.downloads[type]
|
const url = app.downloads[type];
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'h5') {
|
if (type === "h5") {
|
||||||
navigateTo(url, { external: true, open: { target: '_blank' } })
|
navigateTo(url, { external: true, open: { target: "_blank" } });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
navigateTo(url, { external: true })
|
navigateTo(url, { external: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await $fetch(`/api/track/${type}`, {
|
await $fetch(`/api/track/${type}`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: { appId: app.id },
|
body: { appId: app.id },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = computed(() => colorMode.value === 'dark')
|
const isDark = computed(() => colorMode.value === "dark");
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
useHead({
|
useHead({
|
||||||
title: locale.value === 'zh-CN' ? 'Riwa 应用商店' : 'Riwa App Store',
|
title: locale.value === "zh-CN" ? "Riwa 应用商店" : "Riwa App Store",
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: "description",
|
||||||
content: locale.value === 'zh-CN'
|
content: locale.value === "zh-CN"
|
||||||
? '下载 Riwa 系列应用,包括 Riwa 主应用、Riwa 钱包和 Riwa 聊天等'
|
? "下载 Riwa 系列应用,包括 Riwa 主应用、Riwa 钱包和 Riwa 聊天等"
|
||||||
: 'Download Riwa apps including Riwa main app, Riwa Wallet, and Riwa Chat',
|
: "Download Riwa apps including Riwa main app, Riwa Wallet, and Riwa Chat",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen relative overflow-hidden bg-linear-to-br from-gray-50 via-blue-50/30 to-purple-50/30 dark:from-gray-950 dark:via-blue-950/20 dark:to-purple-950/20">
|
<div class="min-h-screen relative overflow-hidden bg-linear-to-br from-gray-50 via-blue-50/30 to-purple-50/30 dark:from-gray-950 dark:via-blue-950/20 dark:to-purple-950/20">
|
||||||
<!-- 科技感网格背景 -->
|
<!-- 科技感网格背景 -->
|
||||||
<div class="fixed inset-0 opacity-30 dark:opacity-20 pointer-events-none">
|
<div class="fixed inset-0 opacity-30 dark:opacity-20 pointer-events-none">
|
||||||
<div class="absolute inset-0 animate-pulse-subtle" style="background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px); background-size: 50px 50px;"></div>
|
<div class="absolute inset-0 animate-pulse-subtle" style="background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px); background-size: 50px 50px;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 动态发光球体背景 -->
|
<!-- 动态发光球体背景 -->
|
||||||
<div class="fixed inset-0 pointer-events-none overflow-hidden">
|
<div class="fixed inset-0 pointer-events-none overflow-hidden">
|
||||||
<div class="absolute top-1/4 -left-48 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl animate-breathe"></div>
|
<div class="absolute top-1/4 -left-48 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl animate-breathe" />
|
||||||
<div class="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl animate-breathe" style="animation-delay: 1s;"></div>
|
<div class="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl animate-breathe" style="animation-delay: 1s;" />
|
||||||
<div class="absolute top-1/2 left-1/2 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl animate-breathe" style="animation-delay: 2s;"></div>
|
<div class="absolute top-1/2 left-1/2 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl animate-breathe" style="animation-delay: 2s;" />
|
||||||
<!-- 额外的动态光球 -->
|
<!-- 额外的动态光球 -->
|
||||||
<div class="absolute top-1/3 right-1/4 w-64 h-64 bg-cyan-500/15 rounded-full blur-2xl animate-float" style="animation-delay: 0.5s;"></div>
|
<div class="absolute top-1/3 right-1/4 w-64 h-64 bg-cyan-500/15 rounded-full blur-2xl animate-float" style="animation-delay: 0.5s;" />
|
||||||
<div class="absolute bottom-1/3 left-1/3 w-72 h-72 bg-indigo-500/15 rounded-full blur-2xl animate-float" style="animation-delay: 1.5s;"></div>
|
<div class="absolute bottom-1/3 left-1/3 w-72 h-72 bg-indigo-500/15 rounded-full blur-2xl animate-float" style="animation-delay: 1.5s;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@@ -107,8 +107,8 @@ useHead({
|
|||||||
<div class="flex items-center justify-between py-4">
|
<div class="flex items-center justify-between py-4">
|
||||||
<div class="flex items-center gap-3 group">
|
<div class="flex items-center gap-3 group">
|
||||||
<div class="size-10 rounded-xl bg-linear-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-xl relative overflow-hidden shadow-lg shadow-blue-500/50">
|
<div class="size-10 rounded-xl bg-linear-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-xl relative overflow-hidden shadow-lg shadow-blue-500/50">
|
||||||
<div class="absolute inset-0 bg-linear-to-tr from-white/0 via-white/20 to-white/0 -translate-x-full group-hover:translate-x-full transition-transform duration-700"></div>
|
<div class="absolute inset-0 bg-linear-to-tr from-white/0 via-white/20 to-white/0 -translate-x-full group-hover:translate-x-full transition-transform duration-700" />
|
||||||
<div class="absolute -inset-1 bg-blue-400/30 rounded-xl blur-md opacity-0 group-hover:opacity-100 animate-pulse-ring"></div>
|
<div class="absolute -inset-1 bg-blue-400/30 rounded-xl blur-md opacity-0 group-hover:opacity-100 animate-pulse-ring" />
|
||||||
<span class="relative z-10">R</span>
|
<span class="relative z-10">R</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-xl font-bold bg-linear-to-r from-gray-900 via-primary-600 to-purple-600 dark:from-white dark:via-primary-400 dark:to-purple-400 bg-clip-text text-transparent animate-gradient">
|
<h1 class="text-xl font-bold bg-linear-to-r from-gray-900 via-primary-600 to-purple-600 dark:from-white dark:via-primary-400 dark:to-purple-400 bg-clip-text text-transparent animate-gradient">
|
||||||
@@ -181,14 +181,14 @@ useHead({
|
|||||||
@click="openAppDetail(app)"
|
@click="openAppDetail(app)"
|
||||||
>
|
>
|
||||||
<!-- 内部发光效果 -->
|
<!-- 内部发光效果 -->
|
||||||
<div class="absolute inset-0 bg-linear-to-br from-primary-500/5 to-purple-500/5"></div>
|
<div class="absolute inset-0 bg-linear-to-br from-primary-500/5 to-purple-500/5" />
|
||||||
|
|
||||||
<div class="flex items-start gap-4 relative z-10">
|
<div class="flex items-start gap-4 relative z-10">
|
||||||
<!-- App Icon -->
|
<!-- App Icon -->
|
||||||
<div class="size-16 rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-2xl shrink-0 shadow-lg shadow-blue-500/50 relative overflow-hidden group-hover:shadow-2xl group-hover:shadow-blue-500/60 transition-all duration-500 group-hover:scale-110 group-hover:rotate-3 group-active:scale-105 group-active:rotate-1 p-2">
|
<div class="size-16 rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-2xl shrink-0 shadow-lg shadow-blue-500/50 relative overflow-hidden group-hover:shadow-2xl group-hover:shadow-blue-500/60 transition-all duration-500 group-hover:scale-110 group-hover:rotate-3 group-active:scale-105 group-active:rotate-1 p-2">
|
||||||
<!-- 动态发光效果 -->
|
<!-- 动态发光效果 -->
|
||||||
<div class="absolute inset-0 bg-linear-to-tr from-white/0 via-white/30 to-white/0 -translate-x-full group-hover:translate-x-full group-active:translate-x-[50%] transition-transform duration-700"></div>
|
<div class="absolute inset-0 bg-linear-to-tr from-white/0 via-white/30 to-white/0 -translate-x-full group-hover:translate-x-full group-active:translate-x-[50%] transition-transform duration-700" />
|
||||||
<img :src="app.icon" :alt="app.name" class="size-full object-contain relative z-10 rounded-lg transition-all duration-300 group-active:scale-95 group-hover:-rotate-3" />
|
<img :src="app.icon" :alt="app.name" class="size-full object-contain relative z-10 rounded-lg transition-all duration-300 group-active:scale-95 group-hover:-rotate-3">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- App Info -->
|
<!-- App Info -->
|
||||||
@@ -217,8 +217,7 @@ useHead({
|
|||||||
block
|
block
|
||||||
class="transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/50 hover:-translate-y-1 active:scale-95 active:shadow-xl active:shadow-blue-500/60 relative overflow-hidden group/btn"
|
class="transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/50 hover:-translate-y-1 active:scale-95 active:shadow-xl active:shadow-blue-500/60 relative overflow-hidden group/btn"
|
||||||
@click.stop="(e) => handleDownload(app, 'ios', e)"
|
@click.stop="(e) => handleDownload(app, 'ios', e)"
|
||||||
>
|
/>
|
||||||
</UButton>
|
|
||||||
<UButton
|
<UButton
|
||||||
v-if="app.downloads.android"
|
v-if="app.downloads.android"
|
||||||
icon="i-heroicons-device-tablet"
|
icon="i-heroicons-device-tablet"
|
||||||
@@ -227,8 +226,7 @@ useHead({
|
|||||||
block
|
block
|
||||||
class="transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/50 hover:-translate-y-1 active:scale-95 active:shadow-xl active:shadow-blue-500/60 relative overflow-hidden group/btn"
|
class="transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/50 hover:-translate-y-1 active:scale-95 active:shadow-xl active:shadow-blue-500/60 relative overflow-hidden group/btn"
|
||||||
@click.stop="(e) => handleDownload(app, 'android', e)"
|
@click.stop="(e) => handleDownload(app, 'android', e)"
|
||||||
>
|
/>
|
||||||
</UButton>
|
|
||||||
<UButton
|
<UButton
|
||||||
v-if="app.downloads.h5"
|
v-if="app.downloads.h5"
|
||||||
icon="i-heroicons-globe-alt"
|
icon="i-heroicons-globe-alt"
|
||||||
@@ -237,8 +235,7 @@ useHead({
|
|||||||
block
|
block
|
||||||
class="transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/50 hover:-translate-y-1 active:scale-95 active:shadow-xl active:shadow-blue-500/60 relative overflow-hidden group/btn"
|
class="transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/50 hover:-translate-y-1 active:scale-95 active:shadow-xl active:shadow-blue-500/60 relative overflow-hidden group/btn"
|
||||||
@click.stop="(e) => handleDownload(app, 'h5', e)"
|
@click.stop="(e) => handleDownload(app, 'h5', e)"
|
||||||
>
|
/>
|
||||||
</UButton>
|
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { getAllApps, categories } from '~/data/apps'
|
import { categories, getAllApps } from "~/data/apps";
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
return {
|
return {
|
||||||
apps: getAllApps(),
|
apps: getAllApps(),
|
||||||
categories,
|
categories,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { getAppById } from '~/data/apps'
|
import { getAppById } from "~/data/apps";
|
||||||
|
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler((event) => {
|
||||||
const id = getRouterParam(event, 'id')
|
const id = getRouterParam(event, "id");
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'App ID is required',
|
message: "App ID is required",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = getAppById(id)
|
const app = getAppById(id);
|
||||||
|
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
message: 'App not found',
|
message: "App not found",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app;
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default defineEventHandler(async () => {
|
export default defineEventHandler(async () => {
|
||||||
return await fetchDownloadStats()
|
return await fetchDownloadStats();
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const platform = getRouterParam(event, 'platform') as 'ios' | 'android' | 'h5'
|
const platform = getRouterParam(event, "platform") as "ios" | "android" | "h5";
|
||||||
const body = await readBody(event)
|
const body = await readBody(event);
|
||||||
const appId = body?.appId
|
const appId = body?.appId;
|
||||||
|
|
||||||
if (!['ios', 'android', 'h5'].includes(platform)) {
|
if (!["ios", "android", "h5"].includes(platform)) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Invalid platform',
|
message: "Invalid platform",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await trackDownload(platform)
|
await trackDownload(platform);
|
||||||
|
|
||||||
return { success: true, appId, platform }
|
return { success: true, appId, platform };
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { currentVersion } from '~/data/versions'
|
import { currentVersion } from "~/data/versions";
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
return currentVersion
|
return currentVersion;
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import type { DownloadStats } from '~/types'
|
import type { DownloadStats } from "~/types";
|
||||||
import { mockDownloadStats } from '~/data/versions'
|
import { mockDownloadStats } from "~/data/versions";
|
||||||
|
|
||||||
// 获取下载统计(可替换为真实 API)
|
// 获取下载统计(可替换为真实 API)
|
||||||
export async function fetchDownloadStats(): Promise<DownloadStats> {
|
export async function fetchDownloadStats(): Promise<DownloadStats> {
|
||||||
// 模拟 API 延迟
|
// 模拟 API 延迟
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return mockDownloadStats
|
return mockDownloadStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录下载事件(可替换为真实 API)
|
// 记录下载事件(可替换为真实 API)
|
||||||
export async function trackDownload(platform: 'ios' | 'android' | 'h5'): Promise<void> {
|
export async function trackDownload(platform: "ios" | "android" | "h5"): Promise<void> {
|
||||||
// 模拟 API 延迟
|
// 模拟 API 延迟
|
||||||
await new Promise(resolve => setTimeout(resolve, 200))
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
console.log(`Download tracked: ${platform}`)
|
console.log(`Download tracked: ${platform}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,65 @@
|
|||||||
export interface AppInfo {
|
export interface AppInfo {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
icon: string
|
icon: string;
|
||||||
shortDescription: {
|
shortDescription: {
|
||||||
'zh-CN': string
|
"zh-CN": string;
|
||||||
'en-US': string
|
"en-US": string;
|
||||||
}
|
};
|
||||||
description: {
|
description: {
|
||||||
'zh-CN': string
|
"zh-CN": string;
|
||||||
'en-US': string
|
"en-US": string;
|
||||||
}
|
};
|
||||||
category: string
|
category: string;
|
||||||
version: string
|
version: string;
|
||||||
buildNumber: string
|
buildNumber: string;
|
||||||
releaseDate: string
|
releaseDate: string;
|
||||||
releaseNotes: {
|
releaseNotes: {
|
||||||
'zh-CN': string[]
|
"zh-CN": string[];
|
||||||
'en-US': string[]
|
"en-US": string[];
|
||||||
}
|
};
|
||||||
downloads: {
|
downloads: {
|
||||||
ios?: string
|
ios?: string;
|
||||||
android?: string
|
android?: string;
|
||||||
h5?: string
|
h5?: string;
|
||||||
}
|
};
|
||||||
screenshots?: string[]
|
screenshots?: string[];
|
||||||
size?: {
|
size?: {
|
||||||
ios?: string
|
ios?: string;
|
||||||
android?: string
|
android?: string;
|
||||||
}
|
};
|
||||||
stats: DownloadStats
|
stats: DownloadStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppVersion {
|
export interface AppVersion {
|
||||||
version: string
|
version: string;
|
||||||
buildNumber: string
|
buildNumber: string;
|
||||||
releaseDate: string
|
releaseDate: string;
|
||||||
releaseNotes: {
|
releaseNotes: {
|
||||||
'zh-CN': string[]
|
"zh-CN": string[];
|
||||||
'en-US': string[]
|
"en-US": string[];
|
||||||
}
|
};
|
||||||
downloads: {
|
downloads: {
|
||||||
ios: string
|
ios: string;
|
||||||
android: string
|
android: string;
|
||||||
h5: string
|
h5: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadStats {
|
export interface DownloadStats {
|
||||||
total: number
|
total: number;
|
||||||
today: number
|
today: number;
|
||||||
ios: number
|
ios: number;
|
||||||
android: number
|
android: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Platform = 'ios' | 'android' | 'desktop' | 'unknown'
|
export type Platform = "ios" | "android" | "desktop" | "unknown";
|
||||||
|
|
||||||
export interface AppCategory {
|
export interface AppCategory {
|
||||||
id: string
|
id: string;
|
||||||
name: {
|
name: {
|
||||||
'zh-CN': string
|
"zh-CN": string;
|
||||||
'en-US': string
|
"en-US": string;
|
||||||
}
|
};
|
||||||
icon?: string
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -62,9 +62,9 @@ importers:
|
|||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.18
|
specifier: ^4.1.18
|
||||||
version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
|
version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
|
||||||
'@vee-validate/yup':
|
'@vee-validate/zod':
|
||||||
specifier: ^4.15.1
|
specifier: ^4.15.1
|
||||||
version: 4.15.1(vue@3.5.25(typescript@5.9.3))(yup@1.7.1)
|
version: 4.15.1(vue@3.5.25(typescript@5.9.3))(zod@3.25.76)
|
||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
||||||
@@ -119,9 +119,9 @@ importers:
|
|||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.6.3
|
specifier: ^4.6.3
|
||||||
version: 4.6.3(vue@3.5.25(typescript@5.9.3))
|
version: 4.6.3(vue@3.5.25(typescript@5.9.3))
|
||||||
yup:
|
zod:
|
||||||
specifier: ^1.7.1
|
specifier: ^3.24.1
|
||||||
version: 1.7.1
|
version: 3.25.76
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@antfu/eslint-config':
|
'@antfu/eslint-config':
|
||||||
specifier: ^6.6.1
|
specifier: ^6.6.1
|
||||||
@@ -3692,10 +3692,10 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@vee-validate/yup@4.15.1':
|
'@vee-validate/zod@4.15.1':
|
||||||
resolution: {integrity: sha512-+u6lI1IZftjHphj+mTCPJRruwBBwv1IKKCI1EFm6ipQroAPibkS5M8UNX+yeVYG5++ix6m1rsv4/SJvJJQTWJg==}
|
resolution: {integrity: sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
yup: ^1.3.2
|
zod: ^3.24.0
|
||||||
|
|
||||||
'@vercel/nft@0.30.4':
|
'@vercel/nft@0.30.4':
|
||||||
resolution: {integrity: sha512-wE6eAGSXScra60N2l6jWvNtVK0m+sh873CpfZW4KI2v8EHuUQp+mSEi4T+IcdPCSEDgCdAS/7bizbhQlkjzrSA==}
|
resolution: {integrity: sha512-wE6eAGSXScra60N2l6jWvNtVK0m+sh873CpfZW4KI2v8EHuUQp+mSEi4T+IcdPCSEDgCdAS/7bizbhQlkjzrSA==}
|
||||||
@@ -9264,6 +9264,9 @@ packages:
|
|||||||
zod@3.22.3:
|
zod@3.22.3:
|
||||||
resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==}
|
resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==}
|
||||||
|
|
||||||
|
zod@3.25.76:
|
||||||
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
zod@4.1.13:
|
zod@4.1.13:
|
||||||
resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
|
resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
|
||||||
|
|
||||||
@@ -10036,7 +10039,7 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
'@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)':
|
'@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/utils': 0.3.0
|
'@better-auth/utils': 0.3.0
|
||||||
'@better-fetch/fetch': 1.1.18
|
'@better-fetch/fetch': 1.1.18
|
||||||
@@ -10047,9 +10050,9 @@ snapshots:
|
|||||||
nanostores: 1.1.0
|
nanostores: 1.1.0
|
||||||
zod: 4.1.13
|
zod: 4.1.13
|
||||||
|
|
||||||
'@better-auth/telemetry@1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0))':
|
'@better-auth/telemetry@1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)
|
'@better-auth/core': 1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)
|
||||||
'@better-auth/utils': 0.3.0
|
'@better-auth/utils': 0.3.0
|
||||||
'@better-fetch/fetch': 1.1.18
|
'@better-fetch/fetch': 1.1.18
|
||||||
|
|
||||||
@@ -12957,11 +12960,11 @@ snapshots:
|
|||||||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vee-validate/yup@4.15.1(vue@3.5.25(typescript@5.9.3))(yup@1.7.1)':
|
'@vee-validate/zod@4.15.1(vue@3.5.25(typescript@5.9.3))(zod@3.25.76)':
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 4.41.0
|
type-fest: 4.41.0
|
||||||
vee-validate: 4.15.1(vue@3.5.25(typescript@5.9.3))
|
vee-validate: 4.15.1(vue@3.5.25(typescript@5.9.3))
|
||||||
yup: 1.7.1
|
zod: 3.25.76
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
@@ -13584,8 +13587,8 @@ snapshots:
|
|||||||
|
|
||||||
better-auth@1.4.6(vue@3.5.25(typescript@5.9.3)):
|
better-auth@1.4.6(vue@3.5.25(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)
|
'@better-auth/core': 1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)
|
||||||
'@better-auth/telemetry': 1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0))
|
'@better-auth/telemetry': 1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0))
|
||||||
'@better-auth/utils': 0.3.0
|
'@better-auth/utils': 0.3.0
|
||||||
'@better-fetch/fetch': 1.1.18
|
'@better-fetch/fetch': 1.1.18
|
||||||
'@noble/ciphers': 2.1.1
|
'@noble/ciphers': 2.1.1
|
||||||
@@ -17476,7 +17479,8 @@ snapshots:
|
|||||||
kleur: 3.0.3
|
kleur: 3.0.3
|
||||||
sisteransi: 1.0.5
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
property-expr@2.0.6: {}
|
property-expr@2.0.6:
|
||||||
|
optional: true
|
||||||
|
|
||||||
prosemirror-changeset@2.3.1:
|
prosemirror-changeset@2.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18408,7 +18412,8 @@ snapshots:
|
|||||||
|
|
||||||
through@2.3.8: {}
|
through@2.3.8: {}
|
||||||
|
|
||||||
tiny-case@1.0.3: {}
|
tiny-case@1.0.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
tiny-inflate@1.0.3: {}
|
tiny-inflate@1.0.3: {}
|
||||||
|
|
||||||
@@ -18464,7 +18469,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
toposort@2.0.2: {}
|
toposort@2.0.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
tosource@2.0.0-alpha.3: {}
|
tosource@2.0.0-alpha.3: {}
|
||||||
|
|
||||||
@@ -18523,7 +18529,8 @@ snapshots:
|
|||||||
|
|
||||||
type-fest@0.8.1: {}
|
type-fest@0.8.1: {}
|
||||||
|
|
||||||
type-fest@2.19.0: {}
|
type-fest@2.19.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
type-fest@4.41.0: {}
|
type-fest@4.41.0: {}
|
||||||
|
|
||||||
@@ -19493,6 +19500,7 @@ snapshots:
|
|||||||
tiny-case: 1.0.3
|
tiny-case: 1.0.3
|
||||||
toposort: 2.0.2
|
toposort: 2.0.2
|
||||||
type-fest: 2.19.0
|
type-fest: 2.19.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
zip-stream@6.0.1:
|
zip-stream@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -19502,6 +19510,8 @@ snapshots:
|
|||||||
|
|
||||||
zod@3.22.3: {}
|
zod@3.22.3: {}
|
||||||
|
|
||||||
|
zod@3.25.76: {}
|
||||||
|
|
||||||
zod@4.1.13: {}
|
zod@4.1.13: {}
|
||||||
|
|
||||||
zwitch@2.0.4: {}
|
zwitch@2.0.4: {}
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
|
catalogMode: prefer
|
||||||
|
shellEmulator: true
|
||||||
|
trustPolicy: no-downgrade
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- "packages/*"
|
- "packages/*"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { PhoneCountry } from "./type";
|
import type { PhoneCountry } from "./type";
|
||||||
import { toTypedSchema } from "@vee-validate/yup";
|
import { toTypedSchema } from "@vee-validate/zod";
|
||||||
import { emailOTPClient, phoneNumberClient, usernameClient } from "better-auth/client/plugins";
|
import { emailOTPClient, phoneNumberClient, usernameClient } from "better-auth/client/plugins";
|
||||||
import { createAuthClient } from "better-auth/vue";
|
import { createAuthClient } from "better-auth/vue";
|
||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
import CircleFlagsCnHk from "~icons/circle-flags/cn-hk";
|
import CircleFlagsCnHk from "~icons/circle-flags/cn-hk";
|
||||||
import CircleFlagsEnUs from "~icons/circle-flags/en-us";
|
import CircleFlagsEnUs from "~icons/circle-flags/en-us";
|
||||||
import CircleFlagsTw from "~icons/circle-flags/tw";
|
import CircleFlagsTw from "~icons/circle-flags/tw";
|
||||||
@@ -23,15 +23,15 @@ export const authClient = createAuthClient({
|
|||||||
plugins: [emailOTPClient(), phoneNumberClient(), usernameClient()],
|
plugins: [emailOTPClient(), phoneNumberClient(), usernameClient()],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailSchema = toTypedSchema(yup.object({
|
export const emailSchema = toTypedSchema(z.object({
|
||||||
email: yup
|
email: z
|
||||||
.string()
|
.string({ message: i18n.global.t("auth.login.validation.emailRequired") })
|
||||||
.required(i18n.global.t("auth.login.validation.emailRequired"))
|
.min(1, i18n.global.t("auth.login.validation.emailRequired"))
|
||||||
.email(i18n.global.t("auth.login.validation.emailInvalid")),
|
.email(i18n.global.t("auth.login.validation.emailInvalid")),
|
||||||
otp: yup
|
otp: z
|
||||||
.string()
|
.string({ message: i18n.global.t("auth.login.validation.otpRequired") })
|
||||||
.required(i18n.global.t("auth.login.validation.otpRequired"))
|
.min(1, i18n.global.t("auth.login.validation.otpRequired"))
|
||||||
.matches(/^\d{6}$/, i18n.global.t("auth.login.validation.otpInvalid")),
|
.regex(/^\d{6}$/, i18n.global.t("auth.login.validation.otpInvalid")),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const countries: PhoneCountry[] = [
|
export const countries: PhoneCountry[] = [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { GenericObject } from "vee-validate";
|
|||||||
import type { EmailVerifyClient } from "@/api/types";
|
import type { EmailVerifyClient } from "@/api/types";
|
||||||
import { toastController } from "@ionic/vue";
|
import { toastController } from "@ionic/vue";
|
||||||
import { Field, Form } from "vee-validate";
|
import { Field, Form } from "vee-validate";
|
||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
import { authClient, emailSchema } from "@/auth";
|
import { authClient, emailSchema } from "@/auth";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -19,7 +19,7 @@ const canResend = computed(() => countdown.value === 0 && !isSending.value);
|
|||||||
const email = ref("");
|
const email = ref("");
|
||||||
const emailError = ref("");
|
const emailError = ref("");
|
||||||
|
|
||||||
let timer: number | null = null;
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
function startCountdown() {
|
function startCountdown() {
|
||||||
countdown.value = 60;
|
countdown.value = 60;
|
||||||
@@ -42,7 +42,7 @@ async function sendOtp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await yup.string().email().validate(emailValue);
|
await z.string().email().parseAsync(emailValue);
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
emailError.value = t("auth.login.validation.emailInvalid");
|
emailError.value = t("auth.login.validation.emailInvalid");
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import type { GenericObject } from "vee-validate";
|
|||||||
import type { PhoneNumberVerifyClient } from "@/api/types";
|
import type { PhoneNumberVerifyClient } from "@/api/types";
|
||||||
import type { PhoneCountry } from "@/auth/type";
|
import type { PhoneCountry } from "@/auth/type";
|
||||||
import { toastController } from "@ionic/vue";
|
import { toastController } from "@ionic/vue";
|
||||||
import { toTypedSchema } from "@vee-validate/yup";
|
import { toTypedSchema } from "@vee-validate/zod";
|
||||||
import { chevronDown } from "ionicons/icons";
|
import { chevronDown } from "ionicons/icons";
|
||||||
import { Field, Form } from "vee-validate";
|
import { Field, Form } from "vee-validate";
|
||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
import { authClient, countries } from "@/auth";
|
import { authClient, countries } from "@/auth";
|
||||||
import Country from "./country.vue";
|
import Country from "./country.vue";
|
||||||
|
|
||||||
@@ -42,19 +42,18 @@ function validatePhoneNumber(phone: string): boolean {
|
|||||||
return currentCountry.value.pattern.test(phone);
|
return currentCountry.value.pattern.test(phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = computed(() => toTypedSchema(yup.object({
|
const schema = computed(() => toTypedSchema(z.object({
|
||||||
phoneNumber: yup
|
phoneNumber: z
|
||||||
.string()
|
.string({ message: t("auth.login.validation.phoneNumberRequired") })
|
||||||
.required(t("auth.login.validation.phoneNumberRequired"))
|
.min(1, t("auth.login.validation.phoneNumberRequired"))
|
||||||
.test(
|
.refine(
|
||||||
"phone-format",
|
value => validatePhoneNumber(value),
|
||||||
t("auth.login.validation.phoneNumberInvalid"),
|
t("auth.login.validation.phoneNumberInvalid"),
|
||||||
value => !value || validatePhoneNumber(value),
|
|
||||||
),
|
),
|
||||||
code: yup
|
code: z
|
||||||
.string()
|
.string({ message: t("auth.login.validation.codeRequired") })
|
||||||
.required(t("auth.login.validation.codeRequired"))
|
.min(1, t("auth.login.validation.codeRequired"))
|
||||||
.matches(/^\d{6}$/, t("auth.login.validation.codeInvalid")),
|
.regex(/^\d{6}$/, t("auth.login.validation.codeInvalid")),
|
||||||
})));
|
})));
|
||||||
|
|
||||||
function startCountdown() {
|
function startCountdown() {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang='ts' setup>
|
<script lang='ts' setup>
|
||||||
import type { GenericObject } from "vee-validate";
|
import type { GenericObject } from "vee-validate";
|
||||||
import type { RwaIssuanceCategoriesData, RwaIssuanceProductBody } from "@/api/types";
|
import type { RwaIssuanceCategoriesData, RwaIssuanceProductBody } from "@/api/types";
|
||||||
import { toTypedSchema } from "@vee-validate/yup";
|
import { toTypedSchema } from "@vee-validate/zod";
|
||||||
import { ErrorMessage, Field, Form } from "vee-validate";
|
import { ErrorMessage, Field, Form } from "vee-validate";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialData: RwaIssuanceProductBody["product"];
|
initialData: RwaIssuanceProductBody["product"];
|
||||||
@@ -16,10 +16,10 @@ const emit = defineEmits<{
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const schema = toTypedSchema(
|
const schema = toTypedSchema(
|
||||||
yup.object({
|
z.object({
|
||||||
name: yup.string().required(t("asset.issue.apply.validation.nameRequired")),
|
name: z.string({ message: t("asset.issue.apply.validation.nameRequired") }).min(1, t("asset.issue.apply.validation.nameRequired")),
|
||||||
code: yup.string().required(t("asset.issue.apply.validation.codeRequired")),
|
code: z.string({ message: t("asset.issue.apply.validation.codeRequired") }).min(1, t("asset.issue.apply.validation.codeRequired")),
|
||||||
categoryId: yup.string().required(t("asset.issue.apply.validation.categoryRequired")),
|
categoryId: z.string({ message: t("asset.issue.apply.validation.categoryRequired") }).min(1, t("asset.issue.apply.validation.categoryRequired")),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang='ts' setup>
|
<script lang='ts' setup>
|
||||||
import type { GenericObject } from "vee-validate";
|
import type { GenericObject } from "vee-validate";
|
||||||
import type { RwaIssuanceProductBody } from "@/api/types";
|
import type { RwaIssuanceProductBody } from "@/api/types";
|
||||||
import { toTypedSchema } from "@vee-validate/yup";
|
import { toTypedSchema } from "@vee-validate/zod";
|
||||||
import { addOutline, removeOutline } from "ionicons/icons";
|
import { addOutline, removeOutline } from "ionicons/icons";
|
||||||
import { ErrorMessage, Field, FieldArray, Form } from "vee-validate";
|
import { ErrorMessage, Field, FieldArray, Form } from "vee-validate";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialData: RwaIssuanceProductBody["editions"];
|
initialData: RwaIssuanceProductBody["editions"];
|
||||||
@@ -35,45 +35,40 @@ const launchDate = ref(new Date().toISOString());
|
|||||||
const subscriptionStartDate = ref(new Date().toISOString());
|
const subscriptionStartDate = ref(new Date().toISOString());
|
||||||
const subscriptionEndDate = ref(new Date().toISOString());
|
const subscriptionEndDate = ref(new Date().toISOString());
|
||||||
|
|
||||||
const schema = toTypedSchema(yup.object({
|
const schema = toTypedSchema(z.object({
|
||||||
editions: yup.array().of(
|
editions: z.array(
|
||||||
yup.object({
|
z.object({
|
||||||
editionName: yup.string().required(t("asset.issue.apply.validation.editionNameRequired")),
|
editionName: z.string({ message: t("asset.issue.apply.validation.editionNameRequired") }).min(1, t("asset.issue.apply.validation.editionNameRequired")),
|
||||||
launchDate: yup.string()
|
launchDate: z.string({ message: t("asset.issue.apply.validation.launchDateRequired") })
|
||||||
.required(t("asset.issue.apply.validation.launchDateRequired"))
|
.min(1, t("asset.issue.apply.validation.launchDateRequired"))
|
||||||
.test("not-past", t("asset.issue.apply.validation.launchDateNotPast"), (value) => {
|
.refine(
|
||||||
if (!value)
|
value => new Date(value) >= new Date(now.value.toDateString()),
|
||||||
return true;
|
t("asset.issue.apply.validation.launchDateNotPast"),
|
||||||
return new Date(value) >= new Date(now.value.toDateString());
|
)
|
||||||
})
|
.refine(
|
||||||
.test("before-subscription", t("asset.issue.apply.validation.launchBeforeSubscription"), (value) => {
|
value => !subscriptionStartDate.value || new Date(value) < new Date(subscriptionStartDate.value),
|
||||||
if (!value || !subscriptionStartDate.value)
|
t("asset.issue.apply.validation.launchBeforeSubscription"),
|
||||||
return true;
|
),
|
||||||
return new Date(value) < new Date(subscriptionStartDate.value);
|
subscriptionStartDate: z.string({ message: t("asset.issue.apply.validation.subscriptionStartDateRequired") })
|
||||||
}),
|
.min(1, t("asset.issue.apply.validation.subscriptionStartDateRequired"))
|
||||||
subscriptionStartDate: yup.string()
|
.refine(
|
||||||
.required(t("asset.issue.apply.validation.subscriptionStartDateRequired"))
|
value => new Date(value) >= new Date(now.value.toDateString()),
|
||||||
.test("not-past", t("asset.issue.apply.validation.subscriptionStartDateNotPast"), (value) => {
|
t("asset.issue.apply.validation.subscriptionStartDateNotPast"),
|
||||||
if (!value)
|
)
|
||||||
return true;
|
.refine(
|
||||||
return new Date(value) >= new Date(now.value.toDateString());
|
value => !launchDate.value || new Date(value) > new Date(launchDate.value),
|
||||||
})
|
t("asset.issue.apply.validation.subscriptionAfterLaunch"),
|
||||||
.test("after-launch", t("asset.issue.apply.validation.subscriptionAfterLaunch"), (value) => {
|
),
|
||||||
if (!value || !launchDate.value)
|
subscriptionEndDate: z.string({ message: t("asset.issue.apply.validation.subscriptionEndDateRequired") })
|
||||||
return true;
|
.min(1, t("asset.issue.apply.validation.subscriptionEndDateRequired"))
|
||||||
return new Date(value) > new Date(launchDate.value);
|
.refine(
|
||||||
}),
|
value => !subscriptionStartDate.value || new Date(value) > new Date(subscriptionStartDate.value),
|
||||||
subscriptionEndDate: yup.string()
|
t("asset.issue.apply.validation.subscriptionEndAfterStart"),
|
||||||
.required(t("asset.issue.apply.validation.subscriptionEndDateRequired"))
|
),
|
||||||
.test("after-start", t("asset.issue.apply.validation.subscriptionEndAfterStart"), (value) => {
|
perUserLimit: z.string({ message: t("asset.issue.apply.validation.perUserLimitRequired") }).min(1, t("asset.issue.apply.validation.perUserLimitRequired")),
|
||||||
if (!value || !subscriptionStartDate.value)
|
totalSupply: z.string({ message: t("asset.issue.apply.validation.totalSupplyRequired") }).min(1, t("asset.issue.apply.validation.totalSupplyRequired")),
|
||||||
return true;
|
unitPrice: z.string({ message: t("asset.issue.apply.validation.unitPriceRequired") }).min(1, t("asset.issue.apply.validation.unitPriceRequired")),
|
||||||
return new Date(value) > new Date(subscriptionStartDate.value);
|
dividendRate: z.string({ message: t("asset.issue.apply.validation.dividendRateRequired") }).min(1, t("asset.issue.apply.validation.dividendRateRequired")),
|
||||||
}),
|
|
||||||
perUserLimit: yup.string().required(t("asset.issue.apply.validation.perUserLimitRequired")),
|
|
||||||
totalSupply: yup.string().required(t("asset.issue.apply.validation.totalSupplyRequired")),
|
|
||||||
unitPrice: yup.string().required(t("asset.issue.apply.validation.unitPriceRequired")),
|
|
||||||
dividendRate: yup.string().required(t("asset.issue.apply.validation.dividendRateRequired")),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang='ts' setup>
|
<script lang='ts' setup>
|
||||||
import type { GenericObject } from "vee-validate";
|
import type { GenericObject } from "vee-validate";
|
||||||
import { SelectChangeEventDetail, toastController } from "@ionic/vue";
|
import { SelectChangeEventDetail, toastController } from "@ionic/vue";
|
||||||
import { toTypedSchema } from "@vee-validate/yup";
|
import { toTypedSchema } from "@vee-validate/zod";
|
||||||
import { informationCircle, shieldCheckmark } from "ionicons/icons";
|
import { informationCircle, shieldCheckmark } from "ionicons/icons";
|
||||||
import { ErrorMessage, Field, Form } from "vee-validate";
|
import { ErrorMessage, Field, Form } from "vee-validate";
|
||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
import { client, safeClient } from "@/api";
|
import { client, safeClient } from "@/api";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -17,12 +17,12 @@ const formInst = useTemplateRef<FormInstance>("formInst");
|
|||||||
|
|
||||||
// 表单验证 Schema
|
// 表单验证 Schema
|
||||||
const schema = toTypedSchema(
|
const schema = toTypedSchema(
|
||||||
yup.object({
|
z.object({
|
||||||
bankName: yup.string().required(t("bankCard.form.validation.bankRequired")),
|
bankName: z.string({ message: t("bankCard.form.validation.bankRequired") }).min(1, t("bankCard.form.validation.bankRequired")),
|
||||||
accountNumber: yup
|
accountNumber: z
|
||||||
.string()
|
.string({ message: t("bankCard.form.validation.accountNumberRequired") })
|
||||||
.required(t("bankCard.form.validation.accountNumberRequired")),
|
.min(1, t("bankCard.form.validation.accountNumberRequired")),
|
||||||
accountName: yup.string().required(t("bankCard.form.validation.accountNameRequired")),
|
accountName: z.string({ message: t("bankCard.form.validation.accountNameRequired") }).min(1, t("bankCard.form.validation.accountNameRequired")),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,130 @@
|
|||||||
<script lang='ts' setup>
|
<script lang='ts' setup>
|
||||||
|
import type { SpotOrderBody } from "@/api/types";
|
||||||
|
import { modalController, toastController } from "@ionic/vue";
|
||||||
|
import { closeOutline } from "ionicons/icons";
|
||||||
|
import { client, safeClient } from "@/api";
|
||||||
|
import { tradeWayConfig } from "../config";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
form: SpotOrderBody & { amount: string };
|
||||||
|
}>();
|
||||||
|
const currentTradeWay = computed(() => {
|
||||||
|
return tradeWayConfig.find(item => item.value === props.form.orderType);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalController.dismiss();
|
||||||
|
}
|
||||||
|
async function onConfirm() {
|
||||||
|
await safeClient(client.api.spot_order.create.post({
|
||||||
|
orderType: props.form.orderType,
|
||||||
|
quantity: props.form.quantity,
|
||||||
|
side: props.form.side,
|
||||||
|
symbol: props.form.symbol,
|
||||||
|
memo: props.form.memo,
|
||||||
|
price: props.form.price,
|
||||||
|
}));
|
||||||
|
const toast = await toastController.create({
|
||||||
|
message: "订单提交成功",
|
||||||
|
duration: 2000,
|
||||||
|
position: "top",
|
||||||
|
color: "success",
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
modalController.dismiss();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
Hello world
|
<div class="ion-padding h-80">
|
||||||
|
<div class="flex justify-between items-center mb-5">
|
||||||
|
<div class="font-semibold">
|
||||||
|
下单确认
|
||||||
|
</div>
|
||||||
|
<ion-icon :icon="closeOutline" class="text-2xl" @click="onClose" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<div class="text-sm">
|
||||||
|
{{ form.symbol }}
|
||||||
|
</div>
|
||||||
|
<ui-tag size="mini" :type="form.side === 'buy' ? 'success' : 'danger'">
|
||||||
|
{{ form.side === 'buy' ? '买入' : '卖出' }}
|
||||||
|
</ui-tag>
|
||||||
|
</div>
|
||||||
|
<template v-if="form.orderType === 'limit'">
|
||||||
|
<div class="cell">
|
||||||
|
<div class="label">
|
||||||
|
委托价格
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ form.price }} USDT
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<div class="label">
|
||||||
|
数量
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ form.quantity }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<div class="label">
|
||||||
|
金额
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ form.amount }} USDT
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<div class="label">
|
||||||
|
类型
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ currentTradeWay?.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="form.orderType === 'market'">
|
||||||
|
<div class="cell">
|
||||||
|
<div class="label">
|
||||||
|
委托价格
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ form.price }} USDT
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<div class="label">
|
||||||
|
数量
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ form.quantity }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<ion-button expand="block" color="success" @click="onConfirm">
|
||||||
|
确认下单
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='css' scoped></style>
|
<style lang='css' scoped>
|
||||||
|
@reference "tailwindcss";
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
@apply flex justify-between items-center py-1;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
@apply text-sm text-(--ion-text-color-step-400);
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
@apply text-sm font-semibold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { caretDownOutline } from "ionicons/icons";
|
|||||||
import { tradeWayConfig } from "../config";
|
import { tradeWayConfig } from "../config";
|
||||||
|
|
||||||
const model = defineModel({ type: Object as PropType<SpotOrderBody>, required: true });
|
const model = defineModel({ type: Object as PropType<SpotOrderBody>, required: true });
|
||||||
|
const currentTradeWay = computed(() => {
|
||||||
|
return tradeWayConfig.find(item => item.value === model.value.orderType);
|
||||||
|
});
|
||||||
|
|
||||||
function onSelectTradeWay(item: TradeWayConfig) {
|
function onSelectTradeWay(item: TradeWayConfig) {
|
||||||
model.value.orderType = item.value;
|
model.value.orderType = item.value;
|
||||||
@@ -17,7 +20,7 @@ function onSelectTradeWay(item: TradeWayConfig) {
|
|||||||
<template>
|
<template>
|
||||||
<div id="open-modal" class="bg-faint flex items-center justify-between px-4 py-2 rounded-md">
|
<div id="open-modal" class="bg-faint flex items-center justify-between px-4 py-2 rounded-md">
|
||||||
<div class="text-xs font-medium text-text-300">
|
<div class="text-xs font-medium text-text-300">
|
||||||
市场
|
{{ currentTradeWay?.name }}
|
||||||
</div>
|
</div>
|
||||||
<ion-icon :icon="caretDownOutline" />
|
<ion-icon :icon="caretDownOutline" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
|
|
||||||
export enum TradeWayValueEnum {
|
export enum TradeWayValueEnum {
|
||||||
LIMIT = "limit",
|
LIMIT = "limit",
|
||||||
@@ -21,28 +21,35 @@ export const tradeWayConfig: TradeWayConfig[] = [
|
|||||||
description: "以指定价格买入或卖出",
|
description: "以指定价格买入或卖出",
|
||||||
icon: "hugeicons:trade-up",
|
icon: "hugeicons:trade-up",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "市价委托",
|
||||||
|
value: "market",
|
||||||
|
description: "以市场价格买入或卖出",
|
||||||
|
icon: "hugeicons:trade-down",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const confirmOrderSchema = yup.object({
|
export const confirmOrderSchema = z.object({
|
||||||
price: yup.number().when("way", {
|
quantity: z.coerce.number({ message: "请输入有效的数量" }).gt(0, "数量必须大于0"),
|
||||||
is: TradeWayValueEnum.LIMIT !== undefined,
|
price: z.coerce.number({ message: "请输入有效的价格" }).gt(0, "价格必须大于0").optional().or(z.coerce.number().optional()),
|
||||||
then: yup
|
orderType: z.enum([TradeWayValueEnum.LIMIT, TradeWayValueEnum.MARKET], {
|
||||||
.number()
|
message: "请选择有效的交易方式",
|
||||||
.typeError("请输入有效的价格")
|
}) as z.ZodType<TradeWayValue>,
|
||||||
.required("价格为必填项")
|
}).refine(
|
||||||
.moreThan(0, "价格必须大于0"),
|
(data) => {
|
||||||
otherwise: yup.number().notRequired(),
|
if (data.orderType === TradeWayValueEnum.LIMIT) {
|
||||||
}),
|
return data.price !== undefined && data.price > 0;
|
||||||
amount: yup
|
}
|
||||||
.number()
|
return true;
|
||||||
.typeError("请输入有效的数量")
|
},
|
||||||
.required("数量为必填项")
|
{
|
||||||
.moreThan(0, "数量必须大于0"),
|
message: "价格为必填项",
|
||||||
way: yup
|
path: ["price"],
|
||||||
.mixed<TradeWayValue>()
|
},
|
||||||
.oneOf(
|
);
|
||||||
Object.values(TradeWayValueEnum),
|
|
||||||
"请选择有效的交易方式",
|
export const confirmOrderSubmitSchema = confirmOrderSchema.transform(data => ({
|
||||||
)
|
...data,
|
||||||
.required("交易方式为必填项"),
|
quantity: data.quantity.toString(),
|
||||||
});
|
price: data.price?.toString() ?? "",
|
||||||
|
}));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { ChartingLibraryWidgetOptions } from "#/charting_library";
|
import type { ChartingLibraryWidgetOptions } from "#/charting_library";
|
||||||
import type { SpotOrderBody } from "@/api/types";
|
import type { SpotOrderBody } from "@/api/types";
|
||||||
import type { TradingViewInst } from "@/tradingview/index";
|
import type { TradingViewInst } from "@/tradingview/index";
|
||||||
|
import type { ModalInstance } from "@/utils";
|
||||||
import { modalController } from "@ionic/vue";
|
import { modalController } from "@ionic/vue";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { caretDownOutline, ellipsisHorizontal } from "ionicons/icons";
|
import { caretDownOutline, ellipsisHorizontal } from "ionicons/icons";
|
||||||
@@ -9,28 +10,32 @@ import MaterialSymbolsCandlestickChartOutline from "~icons/material-symbols/cand
|
|||||||
import { client, safeClient } from "@/api";
|
import { client, safeClient } from "@/api";
|
||||||
import { TradeTypeEnum } from "@/api/enum";
|
import { TradeTypeEnum } from "@/api/enum";
|
||||||
import { TradingViewChart } from "@/tradingview/index";
|
import { TradingViewChart } from "@/tradingview/index";
|
||||||
|
import ConfirmOrder from "./components/confirm-order.vue";
|
||||||
import OrdersPanel from "./components/orders-panel.vue";
|
import OrdersPanel from "./components/orders-panel.vue";
|
||||||
import TradePairsModal from "./components/trade-pairs-modal.vue";
|
import TradePairsModal from "./components/trade-pairs-modal.vue";
|
||||||
import TradeSwitch from "./components/trade-switch.vue";
|
import TradeSwitch from "./components/trade-switch.vue";
|
||||||
import TradeWay from "./components/trade-way.vue";
|
import TradeWay from "./components/trade-way.vue";
|
||||||
import { confirmOrderSchema, TradeWayValueEnum } from "./config";
|
import { confirmOrderSubmitSchema, TradeWayValueEnum } from "./config";
|
||||||
|
|
||||||
|
const { data } = await safeClient(client.api.trading_pairs.get({ query: { limit: 1 } }));
|
||||||
const mode = useRouteQuery<TradeTypeEnum>("mode", TradeTypeEnum.BUY);
|
const mode = useRouteQuery<TradeTypeEnum>("mode", TradeTypeEnum.BUY);
|
||||||
const symbol = useRouteQuery<string>("symbol", "BTCUSD");
|
const symbol = useRouteQuery<string>("symbol", data.value?.data[0].symbol);
|
||||||
|
|
||||||
const tradingviewOptions: Partial<ChartingLibraryWidgetOptions> = {
|
const tradingviewOptions: Partial<ChartingLibraryWidgetOptions> = {
|
||||||
disabled_features: [
|
disabled_features: [
|
||||||
"create_volume_indicator_by_default",
|
"create_volume_indicator_by_default",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const tradingViewInst = useTemplateRef<TradingViewInst>("tradingViewInst");
|
const tradingViewInst = useTemplateRef<TradingViewInst>("tradingViewInst");
|
||||||
const [form] = useResetRef<SpotOrderBody>({
|
const confirmModalInst = useTemplateRef<ModalInstance>("confirmModalInst");
|
||||||
|
|
||||||
|
const [form] = useResetRef<SpotOrderBody & { amount: string }>({
|
||||||
orderType: TradeWayValueEnum.LIMIT,
|
orderType: TradeWayValueEnum.LIMIT,
|
||||||
quantity: "",
|
quantity: "",
|
||||||
side: mode.value,
|
side: mode.value,
|
||||||
symbol: symbol.value,
|
symbol: symbol.value,
|
||||||
memo: "",
|
memo: "",
|
||||||
price: "",
|
price: "",
|
||||||
|
amount: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
async function openTradePairs() {
|
async function openTradePairs() {
|
||||||
@@ -40,22 +45,38 @@ async function openTradePairs() {
|
|||||||
initialBreakpoint: 0.95,
|
initialBreakpoint: 0.95,
|
||||||
handle: true,
|
handle: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await modal.present();
|
await modal.present();
|
||||||
|
|
||||||
const { data: result } = await modal.onWillDismiss<string>();
|
const { data: result } = await modal.onWillDismiss<string>();
|
||||||
|
result && (symbol.value = result);
|
||||||
if (result) {
|
}
|
||||||
symbol.value = result;
|
function handleChangeQuantity(event) {
|
||||||
|
const val = (event.target as HTMLInputElement).value;
|
||||||
|
if (val && form.value.price) {
|
||||||
|
const amount = Number(val) * Number(form.value.price);
|
||||||
|
form.value.amount = amount.toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
form.value.amount = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function handleChangeAmount(event) {
|
||||||
function handleSubmit() {
|
const val = (event.target as HTMLInputElement).value;
|
||||||
confirmOrderSchema.validate(form.value).then(() => {
|
if (val && form.value.price) {
|
||||||
console.log("submit successfully");
|
const quantity = Number(val) / Number(form.value.price);
|
||||||
}).catch((err) => {
|
form.value.quantity = quantity.toString();
|
||||||
console.log("submit failed:", err);
|
}
|
||||||
});
|
else {
|
||||||
|
form.value.quantity = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
await confirmOrderSubmitSchema.parseAsync(form.value);
|
||||||
|
confirmModalInst.value?.$el.present();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error("订单验证失败:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -90,21 +111,37 @@ function handleSubmit() {
|
|||||||
<TradeSwitch v-model:active="mode" @update:active="val => form.side = val" />
|
<TradeSwitch v-model:active="mode" @update:active="val => form.side = val" />
|
||||||
<TradeWay v-model="form" />
|
<TradeWay v-model="form" />
|
||||||
<template v-if="form.orderType === 'limit'">
|
<template v-if="form.orderType === 'limit'">
|
||||||
<ion-input v-model="form.price" label="价格" class="count" inputmode="decimal" type="number" placeholder="请输入价格(USDT)" />
|
<ion-input v-model="form.price" label="价格" class="count" inputmode="decimal" type="number" placeholder="请输入价格">
|
||||||
</template>
|
|
||||||
<ion-input v-model="form.quantity" label="数量" class="count" inputmode="decimal" type="number" placeholder="请输入交易数量">
|
|
||||||
<span slot="end">{{ symbol }}</span>
|
|
||||||
</ion-input>
|
|
||||||
<ion-input v-model="form.price" label="金额" class="count" inputmode="decimal" type="number" placeholder="请输入交易金额">
|
|
||||||
<span slot="end">USDT</span>
|
<span slot="end">USDT</span>
|
||||||
</ion-input>
|
</ion-input>
|
||||||
|
<ion-input v-model="form.quantity" label="数量" class="count" inputmode="decimal" type="number" placeholder="请输入交易数量" @ion-input="handleChangeQuantity">
|
||||||
|
<span slot="end">{{ symbol }}</span>
|
||||||
|
</ion-input>
|
||||||
|
<ion-input v-model="form.amount" label="金额" class="count" inputmode="decimal" type="number" placeholder="请输入交易金额" @ion-input="handleChangeAmount">
|
||||||
|
<span slot="end">USDT</span>
|
||||||
|
</ion-input>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="form.orderType === 'market'">
|
||||||
|
<ion-input v-model="form.price" label="价格" class="count" inputmode="decimal" type="number" placeholder="请输入价格">
|
||||||
|
<span slot="end">USDT</span>
|
||||||
|
</ion-input>
|
||||||
|
<ion-input v-model="form.quantity" label="数量" class="count" inputmode="decimal" type="number" placeholder="请输入交易数量" @ion-input="handleChangeQuantity">
|
||||||
|
<span slot="end">{{ symbol }}</span>
|
||||||
|
</ion-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- <ion-range class="range" aria-label="Range with ticks" :pin="true" :ticks="true" :snaps="true" :min="0" :max="5" /> -->
|
<!-- <ion-range class="range" aria-label="Range with ticks" :pin="true" :ticks="true" :snaps="true" :min="0" :max="5" /> -->
|
||||||
<ion-button expand="block" size="small" shape="round" :color="mode === TradeTypeEnum.BUY ? 'success' : 'danger'" @click="handleSubmit">
|
<ion-button expand="block" size="small" shape="round" :color="mode === TradeTypeEnum.BUY ? 'success' : 'danger'" @click="handleSubmit">
|
||||||
{{ mode === TradeTypeEnum.BUY ? '买入' : '卖出' }}
|
{{ mode === TradeTypeEnum.BUY ? '买入' : '卖出' }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
|
<ion-modal ref="confirmModalInst" class="confirm-modal" :breakpoints="[0, 1]" :initial-breakpoint="1" :handle="false">
|
||||||
|
<ConfirmOrder :form="form" />
|
||||||
|
</ion-modal>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2" />
|
<div class="col-span-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 px-4 pb-4">
|
<div class="mt-6 px-4 pb-4">
|
||||||
<OrdersPanel />
|
<OrdersPanel />
|
||||||
</div>
|
</div>
|
||||||
@@ -145,4 +182,7 @@ ion-range.range::part(tick-active) {
|
|||||||
top: 18px;
|
top: 18px;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
.confirm-modal {
|
||||||
|
--height: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { GenericObject } from "vee-validate";
|
|||||||
import { toastController } from "@ionic/vue";
|
import { toastController } from "@ionic/vue";
|
||||||
import { arrowBackOutline } from "ionicons/icons";
|
import { arrowBackOutline } from "ionicons/icons";
|
||||||
import { Field, Form } from "vee-validate";
|
import { Field, Form } from "vee-validate";
|
||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
import { safeClient } from "@/api";
|
import { safeClient } from "@/api";
|
||||||
import { authClient, emailSchema } from "@/auth";
|
import { authClient, emailSchema } from "@/auth";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const isSending = ref(false);
|
|||||||
const canResend = computed(() => countdown.value === 0 && !isSending.value);
|
const canResend = computed(() => countdown.value === 0 && !isSending.value);
|
||||||
const emailError = ref("");
|
const emailError = ref("");
|
||||||
|
|
||||||
let timer: number | null = null;
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
function startCountdown() {
|
function startCountdown() {
|
||||||
countdown.value = 60;
|
countdown.value = 60;
|
||||||
@@ -40,7 +40,7 @@ async function sendOtp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await yup.string().email().validate(emailValue);
|
await z.string().email().parseAsync(emailValue);
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
emailError.value = t("auth.login.validation.emailInvalid");
|
emailError.value = t("auth.login.validation.emailInvalid");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { FormInstance } from "@/utils";
|
|||||||
import { loadingController, toastController } from "@ionic/vue";
|
import { loadingController, toastController } from "@ionic/vue";
|
||||||
import { swapVerticalOutline } from "ionicons/icons";
|
import { swapVerticalOutline } from "ionicons/icons";
|
||||||
import { ErrorMessage, Field, Form } from "vee-validate";
|
import { ErrorMessage, Field, Form } from "vee-validate";
|
||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
import { client, safeClient } from "@/api";
|
import { client, safeClient } from "@/api";
|
||||||
import { AssetCodeEnum } from "@/api/enum";
|
import { AssetCodeEnum } from "@/api/enum";
|
||||||
import { getCryptoIcon } from "@/config/crypto";
|
import { getCryptoIcon } from "@/config/crypto";
|
||||||
@@ -43,15 +43,15 @@ const availableBalance = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 验证规则
|
// 验证规则
|
||||||
const schema = computed(() => yup.object({
|
const schema = computed(() => z.object({
|
||||||
assetCode: yup.string().required(t("transfer.assetCodeRequired")),
|
assetCode: z.string({ message: t("transfer.assetCodeRequired") }).min(1, t("transfer.assetCodeRequired")),
|
||||||
amount: yup
|
amount: z
|
||||||
.string()
|
.string({ message: t("transfer.amountRequired") })
|
||||||
.required(t("transfer.amountRequired"))
|
.min(1, t("transfer.amountRequired"))
|
||||||
.test("min", t("transfer.amountMinError"), value => Number(value) > 0)
|
.refine(value => Number(value) > 0, t("transfer.amountMinError"))
|
||||||
.test("max", t("transfer.amountMaxError", { amount: availableBalance.value }), value => Number(value) <= Number(availableBalance.value)),
|
.refine(value => Number(value) <= Number(availableBalance.value), t("transfer.amountMaxError", { amount: availableBalance.value })),
|
||||||
fromAccount: yup.string().required(t("transfer.fromAccountRequired")),
|
fromAccount: z.string({ message: t("transfer.fromAccountRequired") }).min(1, t("transfer.fromAccountRequired")),
|
||||||
toAccount: yup.string().required(t("transfer.toAccountRequired")),
|
toAccount: z.string({ message: t("transfer.toAccountRequired") }).min(1, t("transfer.toAccountRequired")),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 交换账户
|
// 交换账户
|
||||||
|
|||||||
@@ -1,43 +1,70 @@
|
|||||||
import { toTypedSchema } from "@vee-validate/yup";
|
import { toTypedSchema } from "@vee-validate/zod";
|
||||||
import * as yup from "yup";
|
import { z } from "zod";
|
||||||
import { WithdrawMethodEnum } from "@/api/enum";
|
import { WithdrawMethodEnum } from "@/api/enum";
|
||||||
|
|
||||||
export function createWithdrawSchema(t: (key: string, params?: any) => string, maxAmount: string) {
|
export function createWithdrawSchema(t: (key: string, params?: any) => string, maxAmount: string) {
|
||||||
return toTypedSchema(
|
return toTypedSchema(
|
||||||
yup.object({
|
z.object({
|
||||||
assetCode: yup.string().required(t("withdraw.validation.assetCodeRequired")),
|
assetCode: z.string({ message: t("withdraw.validation.assetCodeRequired") }).min(1, t("withdraw.validation.assetCodeRequired")),
|
||||||
amount: yup
|
amount: z
|
||||||
.string()
|
.string({ message: t("withdraw.validation.amountRequired") })
|
||||||
.required(t("withdraw.validation.amountRequired"))
|
.min(1, t("withdraw.validation.amountRequired"))
|
||||||
.test("is-number", t("withdraw.validation.amountInvalid"), (value) => {
|
.refine(
|
||||||
return /^\d+(?:\.\d+)?$/.test(value || "");
|
value => /^\d+(?:\.\d+)?$/.test(value),
|
||||||
})
|
t("withdraw.validation.amountInvalid"),
|
||||||
.test("max-amount", t("withdraw.validation.amountExceedsBalance"), (value) => {
|
)
|
||||||
if (!value || maxAmount === "0")
|
.refine(
|
||||||
|
(value) => {
|
||||||
|
if (maxAmount === "0")
|
||||||
return false;
|
return false;
|
||||||
return Number.parseFloat(value) <= Number.parseFloat(maxAmount);
|
return Number.parseFloat(value) <= Number.parseFloat(maxAmount);
|
||||||
|
},
|
||||||
|
t("withdraw.validation.amountExceedsBalance"),
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
value => Number.parseFloat(value) > 0,
|
||||||
|
t("withdraw.validation.amountTooSmall"),
|
||||||
|
),
|
||||||
|
withdrawMethod: z.string({ message: t("withdraw.validation.methodRequired") }).min(1, t("withdraw.validation.methodRequired")),
|
||||||
|
bankAccountId: z.string().optional(),
|
||||||
|
chain: z.string().optional(),
|
||||||
|
toAddress: z.string().optional(),
|
||||||
})
|
})
|
||||||
.test("min-amount", t("withdraw.validation.amountTooSmall"), (value) => {
|
.refine(
|
||||||
if (!value)
|
(data) => {
|
||||||
return false;
|
if (data.withdrawMethod === WithdrawMethodEnum.BANK) {
|
||||||
return Number.parseFloat(value) > 0;
|
return !!data.bankAccountId;
|
||||||
}),
|
}
|
||||||
withdrawMethod: yup.string().required(t("withdraw.validation.methodRequired")),
|
return true;
|
||||||
bankAccountId: yup.string().when("withdrawMethod", {
|
},
|
||||||
is: WithdrawMethodEnum.BANK,
|
{
|
||||||
then: schema => schema.required(t("withdraw.validation.bankAccountRequired")),
|
message: t("withdraw.validation.bankAccountRequired"),
|
||||||
otherwise: schema => schema.optional(),
|
path: ["bankAccountId"],
|
||||||
}),
|
},
|
||||||
chain: yup.string().when("withdrawMethod", {
|
)
|
||||||
is: WithdrawMethodEnum.CRYPTO,
|
.refine(
|
||||||
then: schema => schema.required(t("withdraw.validation.chainRequired")),
|
(data) => {
|
||||||
otherwise: schema => schema.optional(),
|
if (data.withdrawMethod === WithdrawMethodEnum.CRYPTO) {
|
||||||
}),
|
return !!data.chain;
|
||||||
toAddress: yup.string().when("withdrawMethod", {
|
}
|
||||||
is: WithdrawMethodEnum.CRYPTO,
|
return true;
|
||||||
then: schema => schema.required(t("withdraw.validation.addressRequired")).min(10, t("withdraw.validation.addressTooShort")),
|
},
|
||||||
otherwise: schema => schema.optional(),
|
{
|
||||||
}),
|
message: t("withdraw.validation.chainRequired"),
|
||||||
}),
|
path: ["chain"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.withdrawMethod === WithdrawMethodEnum.CRYPTO) {
|
||||||
|
return !!data.toAddress && data.toAddress.length >= 10;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: t("withdraw.validation.addressRequired"),
|
||||||
|
path: ["toAddress"],
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
2187
types/charting_library.d.ts
vendored
2187
types/charting_library.d.ts
vendored
File diff suppressed because it is too large
Load Diff
69
types/datafeeds.d.ts
vendored
69
types/datafeeds.d.ts
vendored
@@ -9,7 +9,7 @@ declare const enum SearchInitiationPoint {
|
|||||||
SymbolSearch = "symbolSearch",
|
SymbolSearch = "symbolSearch",
|
||||||
Watchlist = "watchlist",
|
Watchlist = "watchlist",
|
||||||
Compare = "compare",
|
Compare = "compare",
|
||||||
IndicatorInputs = "indicatorInputs"
|
IndicatorInputs = "indicatorInputs",
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This is the generic type useful for declaring a nominal type,
|
* This is the generic type useful for declaring a nominal type,
|
||||||
@@ -24,14 +24,15 @@ declare const enum SearchInitiationPoint {
|
|||||||
* @example
|
* @example
|
||||||
* type TagName = Nominal<string, 'TagName'>;
|
* type TagName = Nominal<string, 'TagName'>;
|
||||||
*/
|
*/
|
||||||
declare type Nominal<T, Name extends string> = T & { /* eslint-disable-next-line jsdoc/require-jsdoc */
|
declare type Nominal<T, Name extends string> = T & {
|
||||||
[Symbol.species]: Name;
|
[Symbol.species]: Name;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Bar data point
|
* Bar data point
|
||||||
*/
|
*/
|
||||||
interface Bar {
|
interface Bar {
|
||||||
/** Bar time.
|
/**
|
||||||
|
* Bar time.
|
||||||
* Amount of **milliseconds** since Unix epoch start in **UTC** timezone.
|
* Amount of **milliseconds** since Unix epoch start in **UTC** timezone.
|
||||||
* `time` for daily, weekly, and monthly bars is expected to be a trading day (not session start day) at 00:00 UTC.
|
* `time` for daily, weekly, and monthly bars is expected to be a trading day (not session start day) at 00:00 UTC.
|
||||||
* The library adjusts time according to `session` from {@link LibrarySymbolInfo}.
|
* The library adjusts time according to `session` from {@link LibrarySymbolInfo}.
|
||||||
@@ -273,7 +274,7 @@ interface IDatafeedChartApi {
|
|||||||
* @param onDataCallback Callback function containing an array of marks
|
* @param onDataCallback Callback function containing an array of marks
|
||||||
* @param resolution Resolution of the symbol
|
* @param resolution Resolution of the symbol
|
||||||
*/
|
*/
|
||||||
getMarks?(symbolInfo: LibrarySymbolInfo, from: number, to: number, onDataCallback: GetMarksCallback<Mark>, resolution: ResolutionString): void;
|
getMarks?: (symbolInfo: LibrarySymbolInfo, from: number, to: number, onDataCallback: GetMarksCallback<Mark>, resolution: ResolutionString) => void;
|
||||||
/**
|
/**
|
||||||
* The library calls this function to get timescale marks for visible bars range.
|
* The library calls this function to get timescale marks for visible bars range.
|
||||||
* The library assumes that you will call `onDataCallback` only once per `getTimescaleMarks` call.
|
* The library assumes that you will call `onDataCallback` only once per `getTimescaleMarks` call.
|
||||||
@@ -286,7 +287,7 @@ interface IDatafeedChartApi {
|
|||||||
* @param onDataCallback Callback function containing an array of marks
|
* @param onDataCallback Callback function containing an array of marks
|
||||||
* @param resolution Resolution of the symbol
|
* @param resolution Resolution of the symbol
|
||||||
*/
|
*/
|
||||||
getTimescaleMarks?(symbolInfo: LibrarySymbolInfo, from: number, to: number, onDataCallback: GetMarksCallback<TimescaleMark>, resolution: ResolutionString): void;
|
getTimescaleMarks?: (symbolInfo: LibrarySymbolInfo, from: number, to: number, onDataCallback: GetMarksCallback<TimescaleMark>, resolution: ResolutionString) => void;
|
||||||
/**
|
/**
|
||||||
* This function is called if the `supports_time` configuration flag is `true` when the chart needs to know the server time.
|
* This function is called if the `supports_time` configuration flag is `true` when the chart needs to know the server time.
|
||||||
* The library expects a callback to be called once.
|
* The library expects a callback to be called once.
|
||||||
@@ -295,7 +296,7 @@ interface IDatafeedChartApi {
|
|||||||
* `getServerTime` is used to display countdown on the price scale.
|
* `getServerTime` is used to display countdown on the price scale.
|
||||||
* Note that the countdown can be displayed only for [intraday](https://www.tradingview.com/charting-library-docs/latest/core_concepts/Resolution#resolution-in-minutes-intraday) resolutions.
|
* Note that the countdown can be displayed only for [intraday](https://www.tradingview.com/charting-library-docs/latest/core_concepts/Resolution#resolution-in-minutes-intraday) resolutions.
|
||||||
*/
|
*/
|
||||||
getServerTime?(callback: ServerTimeCallback): void;
|
getServerTime?: (callback: ServerTimeCallback) => void;
|
||||||
/**
|
/**
|
||||||
* Provides a list of symbols that match the user's search query.
|
* Provides a list of symbols that match the user's search query.
|
||||||
*
|
*
|
||||||
@@ -305,7 +306,7 @@ interface IDatafeedChartApi {
|
|||||||
* @param onResult Callback function that returns an array of results ({@link SearchSymbolResultItem}) or empty array if no symbols found
|
* @param onResult Callback function that returns an array of results ({@link SearchSymbolResultItem}) or empty array if no symbols found
|
||||||
* @param searchSource The source of the search ({@link SearchInitiationPoint}).
|
* @param searchSource The source of the search ({@link SearchInitiationPoint}).
|
||||||
*/
|
*/
|
||||||
searchSymbols(userInput: string, exchange: string, symbolType: string, onResult: SearchSymbolsCallback, searchSource?: SearchInitiationPoint): void;
|
searchSymbols: (userInput: string, exchange: string, symbolType: string, onResult: SearchSymbolsCallback, searchSource?: SearchInitiationPoint) => void;
|
||||||
/**
|
/**
|
||||||
* The library will call this function when it needs to get SymbolInfo by symbol name.
|
* The library will call this function when it needs to get SymbolInfo by symbol name.
|
||||||
*
|
*
|
||||||
@@ -314,7 +315,7 @@ interface IDatafeedChartApi {
|
|||||||
* @param onError Callback function whose only argument is a text error message
|
* @param onError Callback function whose only argument is a text error message
|
||||||
* @param extension An optional object with additional parameters
|
* @param extension An optional object with additional parameters
|
||||||
*/
|
*/
|
||||||
resolveSymbol(symbolName: string, onResolve: ResolveCallback, onError: DatafeedErrorCallback, extension?: SymbolResolveExtension): void;
|
resolveSymbol: (symbolName: string, onResolve: ResolveCallback, onError: DatafeedErrorCallback, extension?: SymbolResolveExtension) => void;
|
||||||
/**
|
/**
|
||||||
* This function is called when the chart needs a history fragment defined by dates range.
|
* This function is called when the chart needs a history fragment defined by dates range.
|
||||||
*
|
*
|
||||||
@@ -324,7 +325,7 @@ interface IDatafeedChartApi {
|
|||||||
* @param onResult Callback function for historical data
|
* @param onResult Callback function for historical data
|
||||||
* @param onError Callback function whose only argument is a text error message. If using special characters, please consider `encodeURIComponent`.
|
* @param onError Callback function whose only argument is a text error message. If using special characters, please consider `encodeURIComponent`.
|
||||||
*/
|
*/
|
||||||
getBars(symbolInfo: LibrarySymbolInfo, resolution: ResolutionString, periodParams: PeriodParams, onResult: HistoryCallback, onError: DatafeedErrorCallback): void;
|
getBars: (symbolInfo: LibrarySymbolInfo, resolution: ResolutionString, periodParams: PeriodParams, onResult: HistoryCallback, onError: DatafeedErrorCallback) => void;
|
||||||
/**
|
/**
|
||||||
* The library calls this function when it wants to receive real-time updates for a symbol.
|
* The library calls this function when it wants to receive real-time updates for a symbol.
|
||||||
* The library assumes that you will call the callback provided by the `onTick` parameter every time you want to update the most recent bar or to add a new one.
|
* The library assumes that you will call the callback provided by the `onTick` parameter every time you want to update the most recent bar or to add a new one.
|
||||||
@@ -335,13 +336,13 @@ interface IDatafeedChartApi {
|
|||||||
* @param listenerGuid
|
* @param listenerGuid
|
||||||
* @param onResetCacheNeededCallback Function to be executed when bar data has changed
|
* @param onResetCacheNeededCallback Function to be executed when bar data has changed
|
||||||
*/
|
*/
|
||||||
subscribeBars(symbolInfo: LibrarySymbolInfo, resolution: ResolutionString, onTick: SubscribeBarsCallback, listenerGuid: string, onResetCacheNeededCallback: () => void): void;
|
subscribeBars: (symbolInfo: LibrarySymbolInfo, resolution: ResolutionString, onTick: SubscribeBarsCallback, listenerGuid: string, onResetCacheNeededCallback: () => void) => void;
|
||||||
/**
|
/**
|
||||||
* The library calls this function when it doesn't want to receive updates anymore.
|
* The library calls this function when it doesn't want to receive updates anymore.
|
||||||
*
|
*
|
||||||
* @param listenerGuid id to unsubscribe from
|
* @param listenerGuid id to unsubscribe from
|
||||||
*/
|
*/
|
||||||
unsubscribeBars(listenerGuid: string): void;
|
unsubscribeBars: (listenerGuid: string) => void;
|
||||||
/**
|
/**
|
||||||
* The library calls this function when it wants to receive real-time symbol data in the [Depth of Market](https://www.tradingview.com/charting-library-docs/latest/trading_terminal/depth-of-market) (DOM) widget.
|
* The library calls this function when it wants to receive real-time symbol data in the [Depth of Market](https://www.tradingview.com/charting-library-docs/latest/trading_terminal/depth-of-market) (DOM) widget.
|
||||||
* Note that you should set the {@link BrokerConfigFlags.supportLevel2Data} configuration flag to `true`.
|
* Note that you should set the {@link BrokerConfigFlags.supportLevel2Data} configuration flag to `true`.
|
||||||
@@ -350,14 +351,14 @@ interface IDatafeedChartApi {
|
|||||||
* @param callback A function returning an object to update DOM data
|
* @param callback A function returning an object to update DOM data
|
||||||
* @returns A unique identifier that will be used to unsubscribe from the data
|
* @returns A unique identifier that will be used to unsubscribe from the data
|
||||||
*/
|
*/
|
||||||
subscribeDepth?(symbol: string, callback: DOMCallback): string;
|
subscribeDepth?: (symbol: string, callback: DOMCallback) => string;
|
||||||
/**
|
/**
|
||||||
* The library calls this function to stop receiving real-time updates for the [Depth of Market](https://www.tradingview.com/charting-library-docs/latest/trading_terminal/depth-of-market) listener.
|
* The library calls this function to stop receiving real-time updates for the [Depth of Market](https://www.tradingview.com/charting-library-docs/latest/trading_terminal/depth-of-market) listener.
|
||||||
* Note that you should set the {@link BrokerConfigFlags.supportLevel2Data} configuration flag to `true`.
|
* Note that you should set the {@link BrokerConfigFlags.supportLevel2Data} configuration flag to `true`.
|
||||||
*
|
*
|
||||||
* @param subscriberUID A string returned by `subscribeDepth`
|
* @param subscriberUID A string returned by `subscribeDepth`
|
||||||
*/
|
*/
|
||||||
unsubscribeDepth?(subscriberUID: string): void;
|
unsubscribeDepth?: (subscriberUID: string) => void;
|
||||||
/**
|
/**
|
||||||
* The library calls this function to get the resolution that will be used to calculate the Volume Profile Visible Range indicator.
|
* The library calls this function to get the resolution that will be used to calculate the Volume Profile Visible Range indicator.
|
||||||
*
|
*
|
||||||
@@ -371,7 +372,7 @@ interface IDatafeedChartApi {
|
|||||||
* @param symbolInfo A Symbol object
|
* @param symbolInfo A Symbol object
|
||||||
* @returns A resolution
|
* @returns A resolution
|
||||||
*/
|
*/
|
||||||
getVolumeProfileResolutionForPeriod?(currentResolution: ResolutionString, from: number, to: number, symbolInfo: LibrarySymbolInfo): ResolutionString;
|
getVolumeProfileResolutionForPeriod?: (currentResolution: ResolutionString, from: number, to: number, symbolInfo: LibrarySymbolInfo) => ResolutionString;
|
||||||
}
|
}
|
||||||
/** Quotes datafeed API */
|
/** Quotes datafeed API */
|
||||||
interface IDatafeedQuotesApi {
|
interface IDatafeedQuotesApi {
|
||||||
@@ -382,7 +383,7 @@ interface IDatafeedQuotesApi {
|
|||||||
* @param {QuotesCallback} onDataCallback - callback to return the requested data.
|
* @param {QuotesCallback} onDataCallback - callback to return the requested data.
|
||||||
* @param {QuotesErrorCallback} onErrorCallback - callback for responding with an error.
|
* @param {QuotesErrorCallback} onErrorCallback - callback for responding with an error.
|
||||||
*/
|
*/
|
||||||
getQuotes(symbols: string[], onDataCallback: QuotesCallback, onErrorCallback: QuotesErrorCallback): void;
|
getQuotes: (symbols: string[], onDataCallback: QuotesCallback, onErrorCallback: QuotesErrorCallback) => void;
|
||||||
/**
|
/**
|
||||||
* Trading Platform calls this function when it wants to receive real-time quotes for a symbol.
|
* Trading Platform calls this function when it wants to receive real-time quotes for a symbol.
|
||||||
* The library assumes that you will call `onRealtimeCallback` every time you want to update the quotes.
|
* The library assumes that you will call `onRealtimeCallback` every time you want to update the quotes.
|
||||||
@@ -391,13 +392,13 @@ interface IDatafeedQuotesApi {
|
|||||||
* @param {QuotesCallback} onRealtimeCallback - callback to send realtime quote data updates
|
* @param {QuotesCallback} onRealtimeCallback - callback to send realtime quote data updates
|
||||||
* @param {string} listenerGUID - unique identifier of the listener
|
* @param {string} listenerGUID - unique identifier of the listener
|
||||||
*/
|
*/
|
||||||
subscribeQuotes(symbols: string[], fastSymbols: string[], onRealtimeCallback: QuotesCallback, listenerGUID: string): void;
|
subscribeQuotes: (symbols: string[], fastSymbols: string[], onRealtimeCallback: QuotesCallback, listenerGUID: string) => void;
|
||||||
/**
|
/**
|
||||||
* Trading Platform calls this function when it doesn't want to receive updates for this listener anymore.
|
* Trading Platform calls this function when it doesn't want to receive updates for this listener anymore.
|
||||||
* `listenerGUID` will be the same object that the Library passed to `subscribeQuotes` before.
|
* `listenerGUID` will be the same object that the Library passed to `subscribeQuotes` before.
|
||||||
* @param {string} listenerGUID - unique identifier of the listener
|
* @param {string} listenerGUID - unique identifier of the listener
|
||||||
*/
|
*/
|
||||||
unsubscribeQuotes(listenerGUID: string): void;
|
unsubscribeQuotes: (listenerGUID: string) => void;
|
||||||
}
|
}
|
||||||
interface IExternalDatafeed {
|
interface IExternalDatafeed {
|
||||||
/**
|
/**
|
||||||
@@ -406,7 +407,7 @@ interface IExternalDatafeed {
|
|||||||
*
|
*
|
||||||
* @param {OnReadyCallback} callback - callback to return your datafeed configuration ({@link DatafeedConfiguration}) to the library.
|
* @param {OnReadyCallback} callback - callback to return your datafeed configuration ({@link DatafeedConfiguration}) to the library.
|
||||||
*/
|
*/
|
||||||
onReady(callback: OnReadyCallback): void;
|
onReady: (callback: OnReadyCallback) => void;
|
||||||
}
|
}
|
||||||
interface LibrarySubsessionInfo {
|
interface LibrarySubsessionInfo {
|
||||||
/**
|
/**
|
||||||
@@ -414,15 +415,15 @@ interface LibrarySubsessionInfo {
|
|||||||
*
|
*
|
||||||
* @example "Regular Trading Hours"
|
* @example "Regular Trading Hours"
|
||||||
*/
|
*/
|
||||||
description: string;
|
"description": string;
|
||||||
/**
|
/**
|
||||||
* Subsession ID.
|
* Subsession ID.
|
||||||
*/
|
*/
|
||||||
id: LibrarySessionId;
|
"id": LibrarySessionId;
|
||||||
/**
|
/**
|
||||||
* Session string. See {@link LibrarySymbolInfo.session}.
|
* Session string. See {@link LibrarySymbolInfo.session}.
|
||||||
*/
|
*/
|
||||||
session: string;
|
"session": string;
|
||||||
/**
|
/**
|
||||||
* Session corrections string. See {@link LibrarySymbolInfo.corrections}.
|
* Session corrections string. See {@link LibrarySymbolInfo.corrections}.
|
||||||
*/
|
*/
|
||||||
@@ -659,9 +660,9 @@ interface LibrarySymbolInfo {
|
|||||||
/**
|
/**
|
||||||
* The boolean value showing whether or not seconds bars for this symbol can be built from ticks. Only available in Trading Platform.
|
* The boolean value showing whether or not seconds bars for this symbol can be built from ticks. Only available in Trading Platform.
|
||||||
*
|
*
|
||||||
* * {@link LibrarySymbolInfo.has_seconds} must also be `true`
|
* {@link LibrarySymbolInfo.has_seconds} must also be `true`
|
||||||
* * {@link LibrarySymbolInfo.has_ticks} must also be `true`
|
* {@link LibrarySymbolInfo.has_ticks} must also be `true`
|
||||||
* * {@link LibrarySymbolInfo.seconds_multipliers} must be an empty array or only contain multipliers that the datafeed provides itself
|
* {@link LibrarySymbolInfo.seconds_multipliers} must be an empty array or only contain multipliers that the datafeed provides itself
|
||||||
*
|
*
|
||||||
* The library builds resolutions from ticks only if there are no seconds resolutions from the datafeed or the provided resolutions are larger then the required one. For example, the datafeed provides `3S` resolution. In this case, the library can build only `1S` or `2S` resolutions from ticks. Resolutions such as `15S` will be build with seconds bars.
|
* The library builds resolutions from ticks only if there are no seconds resolutions from the datafeed or the provided resolutions are larger then the required one. For example, the datafeed provides `3S` resolution. In this case, the library can build only `1S` or `2S` resolutions from ticks. Resolutions such as `15S` will be build with seconds bars.
|
||||||
* @default false
|
* @default false
|
||||||
@@ -827,10 +828,10 @@ interface LibrarySymbolInfo {
|
|||||||
* - `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4...`
|
* - `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4...`
|
||||||
*/
|
*/
|
||||||
logo_urls?: [
|
logo_urls?: [
|
||||||
string
|
string,
|
||||||
] | [
|
] | [
|
||||||
string,
|
string,
|
||||||
string
|
string,
|
||||||
];
|
];
|
||||||
/**
|
/**
|
||||||
* URL of image to be displayed as the logo for the exchange. The `show_exchange_logos` featureset needs to be enabled for this to be visible in the UI.
|
* URL of image to be displayed as the logo for the exchange. The `show_exchange_logos` featureset needs to be enabled for this to be visible in the UI.
|
||||||
@@ -1000,10 +1001,10 @@ interface SearchSymbolResultItem {
|
|||||||
* - `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4...`
|
* - `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4...`
|
||||||
*/
|
*/
|
||||||
logo_urls?: [
|
logo_urls?: [
|
||||||
string
|
string,
|
||||||
] | [
|
] | [
|
||||||
string,
|
string,
|
||||||
string
|
string,
|
||||||
];
|
];
|
||||||
/**
|
/**
|
||||||
* URL of image to be displayed as the logo for the exchange. The `show_exchange_logos` featureset needs to be enabled for this to be visible in the UI.
|
* URL of image to be displayed as the logo for the exchange. The `show_exchange_logos` featureset needs to be enabled for this to be visible in the UI.
|
||||||
@@ -1145,17 +1146,15 @@ interface UdfResponse {
|
|||||||
s: string;
|
s: string;
|
||||||
}
|
}
|
||||||
interface UdfErrorResponse {
|
interface UdfErrorResponse {
|
||||||
s: 'error';
|
s: "error";
|
||||||
errmsg: string;
|
errmsg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRequester {
|
interface IRequester {
|
||||||
sendRequest<T extends UdfResponse>(datafeedUrl: string, urlPath: string, params?: RequestParams): Promise<T | UdfErrorResponse>;
|
sendRequest: (<T extends UdfResponse>(datafeedUrl: string, urlPath: string, params?: RequestParams) => Promise<T | UdfErrorResponse>) & (<T>(datafeedUrl: string, urlPath: string, params?: RequestParams) => Promise<T>) & (<T>(datafeedUrl: string, urlPath: string, params?: RequestParams) => Promise<T>);
|
||||||
sendRequest<T>(datafeedUrl: string, urlPath: string, params?: RequestParams): Promise<T>;
|
|
||||||
sendRequest<T>(datafeedUrl: string, urlPath: string, params?: RequestParams): Promise<T>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeriodParamsWithOptionalCountback = Omit<PeriodParams, 'countBack'> & {
|
type PeriodParamsWithOptionalCountback = Omit<PeriodParams, "countBack"> & {
|
||||||
countBack?: number;
|
countBack?: number;
|
||||||
};
|
};
|
||||||
interface LimitedResponseConfiguration {
|
interface LimitedResponseConfiguration {
|
||||||
@@ -1172,11 +1171,11 @@ interface LimitedResponseConfiguration {
|
|||||||
* response then `expectedOrder` specifies whether the server
|
* response then `expectedOrder` specifies whether the server
|
||||||
* will send the latest (newest) or earliest (older) data first.
|
* will send the latest (newest) or earliest (older) data first.
|
||||||
*/
|
*/
|
||||||
expectedOrder: 'latestFirst' | 'earliestFirst';
|
expectedOrder: "latestFirst" | "earliestFirst";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IQuotesProvider {
|
interface IQuotesProvider {
|
||||||
getQuotes(symbols: string[]): Promise<QuoteData[]>;
|
getQuotes: (symbols: string[]) => Promise<QuoteData[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UdfCompatibleConfiguration extends DatafeedConfiguration {
|
interface UdfCompatibleConfiguration extends DatafeedConfiguration {
|
||||||
|
|||||||
Reference in New Issue
Block a user