Skip to content

Commit fb30687

Browse files
committed
feat: support AIHubMix
1 parent 6a40015 commit fb30687

File tree

9 files changed

+454
-34
lines changed

9 files changed

+454
-34
lines changed

ui/src/components/ProviderCard.vue

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { useI18n } from 'vue-i18n'
66
import { toast } from 'vue-sonner'
77
import { Button } from '@/components/ui/button'
88
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
9-
import ProviderPaymentDialog from './ProviderPaymentDialog.vue'
109
import ProviderRealNameDialog from './ProviderRealNameDialog.vue'
10+
import QRCPaymentDialog from './QRCPaymentDialog.vue'
1111
1212
const props = defineProps<{
1313
provider: Provider
@@ -29,12 +29,12 @@ onMounted(async () => {
2929
})
3030
3131
function handleRecharge() {
32-
if ('websiteURL' in props.provider.payment!) {
33-
window.open(props.provider.payment!.websiteURL, '_blank', 'width=600,height=600,noopener=yes,noreferrer=yes')
32+
if (props.provider.payment?.type === 'website') {
33+
window.open(props.provider.payment.websiteURL, '_blank', 'width=600,height=600,noopener=yes,noreferrer=yes')
3434
return
3535
}
3636
37-
if (!userInfo.value?.verified) {
37+
if (props.provider.verification && !userInfo.value?.verified) {
3838
toast.error(t('realNameRequired'))
3939
openRealNameDialog.value = true
4040
}
@@ -94,15 +94,15 @@ function handleRecharge() {
9494
<div class="flex-grow" />
9595
<Button v-if="!!provider.payment" variant="secondary" size="sm" class="h-8" @click="handleRecharge">
9696
{{ t('recharge') }}
97-
<ExternalLinkIcon v-if="'websiteURL' in provider.payment" />
97+
<ExternalLinkIcon v-if="provider.payment?.type === 'website'" />
9898
</Button>
9999
<Button variant="secondary" size="sm" class="h-8" @click="provider.logout">
100100
{{ t('logout') }}
101101
</Button>
102102
</CardFooter>
103103

104104
<ProviderRealNameDialog v-if="!!provider.verification" v-model="openRealNameDialog" :provider="provider" />
105-
<ProviderPaymentDialog v-if="!!provider.payment" v-model="openPaymentDialog" :provider="provider" />
105+
<QRCPaymentDialog v-if="!!provider.payment" v-model="openPaymentDialog" :provider="provider" />
106106
</Card>
107107
</template>
108108

ui/src/components/ProviderPaymentDialog.vue renamed to ui/src/components/QRCPaymentDialog.vue

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import type { Provider, ProviderPaymentWeChat } from '@/lib/providers'
2+
import type { Provider, ProviderPaymentQRC } from '@/lib/providers'
33
import { CircleQuestionMarkIcon } from 'lucide-vue-next'
44
import { renderSVG } from 'uqr'
55
import { computed, onUnmounted, ref } from 'vue'
@@ -30,7 +30,7 @@ const open = defineModel<boolean>()
3030
3131
const { t } = useI18n()
3232
33-
const payment = computed(() => props.provider.payment as ProviderPaymentWeChat)
33+
const payment = computed(() => props.provider.payment as ProviderPaymentQRC)
3434
const quickAmounts = [10, 50, 100, 200, 500, 1000]
3535
const formAmount = ref('')
3636
const creating = ref(false)
@@ -53,7 +53,7 @@ async function createPayment() {
5353
creating.value = true
5454
5555
try {
56-
const { orderId: newOrderId, qrcUrl, interval, timeout } = await payment.value.createWeChatPay({
56+
const { orderId: newOrderId, qrcUrl, interval, timeout } = await payment.value.create({
5757
amount: formAmount.value,
5858
})
5959
qrcSvg.value = renderSVG(qrcUrl, {})
@@ -74,6 +74,7 @@ async function createPayment() {
7474
}
7575
}
7676
catch (error) {
77+
toast.error(t('paymentCreationFailed'))
7778
console.error(error)
7879
}
7980
finally {
@@ -86,15 +87,17 @@ async function checkPayment(manual?: boolean) {
8687
checking.value = true
8788
}
8889
try {
89-
const result = await payment.value.checkWeChatPay({ orderId: orderId.value })
90+
const result = await payment.value.check({ orderId: orderId.value })
9091
9192
if (result === 'success') {
9293
toast.success(t('paymentCompleted'))
9394
open.value = false
95+
clearTimers()
9496
}
9597
else if (result === 'canceled') {
9698
toast.error(t('paymentCanceled'))
9799
open.value = false
100+
clearTimers()
98101
}
99102
else if (result === 'wait') {
100103
if (manual) {
@@ -156,7 +159,7 @@ function clear() {
156159
</TooltipProvider>
157160
<div class="flex-grow" />
158161
<div v-if="qrcSvg" class="text-sm text-muted-foreground">
159-
{{ formAmount }} {{ t('currency') }}
162+
{{ payment.currency === 'CNY' ? formAmount + t('yuan') : `$${formAmount}` }}
160163
</div>
161164
</DialogDescription>
162165
<DialogClose />
@@ -174,7 +177,7 @@ function clear() {
174177
min="0.01"
175178
class="flex-1"
176179
/>
177-
<span class="text-sm text-muted-foreground">{{ t('currency') }}</span>
180+
<span class="text-sm text-muted-foreground">{{ payment.currency === 'CNY' ? t('yuan') : t('usd') }}</span>
178181
</div>
179182
</div>
180183

@@ -188,7 +191,7 @@ function clear() {
188191
class="h-8"
189192
@click="formAmount = amount.toString()"
190193
>
191-
{{ amount }}{{ t('currency') }}
194+
{{ amount }}{{ payment.currency === 'CNY' ? t('yuan') : t('usd') }}
192195
</Button>
193196
</div>
194197

@@ -201,7 +204,7 @@ function clear() {
201204
</div>
202205
<div class="text-center space-y-2">
203206
<p class="text-sm text-muted-foreground">
204-
{{ t('scanQRCodeMessage') }}
207+
{{ t('scanQRCodeMessage', [payment.platform]) }}
205208
</p>
206209
<div v-if="expired" class="text-center text-sm text-red-500">
207210
{{ t('qrCodeExpired') }}
@@ -241,12 +244,14 @@ en-US:
241244
accountRechargeDescription: Recharge to your {0} account
242245
rechargeAmount: Recharge Amount
243246
rechargeAmountPlaceholder: Please enter recharge amount
244-
currency: Yuan
245-
scanQRCodeMessage: Please use your phone to open WeChat and scan the QR code to complete payment
247+
yuan: Yuan
248+
usd: USD
249+
scanQRCodeMessage: Please use your phone to open {0} and scan the QR code to complete payment
246250
qrCodeExpired: QR code has expired, please regenerate
247251
regenerateQRCode: Regenerate QR Code
248252
checking: Checking...
249253
check: Complete
254+
paymentCreationFailed: Payment creation failed
250255
paymentCompleted: Payment Completed
251256
paymentCanceled: Payment Canceled
252257
paymentPending: Payment Not Completed
@@ -259,12 +264,14 @@ zh-CN:
259264
accountRechargeDescription: 向{0}账户充值
260265
rechargeAmount: 充值金额
261266
rechargeAmountPlaceholder: 请输入充值金额
262-
currency:
263-
scanQRCodeMessage: 请使用手机打开微信扫描二维码完成支付
267+
yuan:
268+
usd: 美元
269+
scanQRCodeMessage: 请使用手机打开{0}扫描二维码完成支付
264270
qrCodeExpired: 二维码已过期,请重新生成
265271
regenerateQRCode: 重新生成二维码
266272
checking: 检查中...
267273
check: 我已支付
274+
paymentCreationFailed: 支付创建失败
268275
paymentCompleted: 已完成
269276
paymentCanceled: 已取消支付
270277
paymentPending: 支付未完成
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<script setup lang="ts">
2+
import { computed, ref } from 'vue'
3+
import { useI18n } from 'vue-i18n'
4+
import { toast } from 'vue-sonner'
5+
import { Button } from '@/components/ui/button'
6+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
7+
import { Input } from '@/components/ui/input'
8+
import { useKeysStore } from '@/stores'
9+
import { useAIHubMixProvider } from '.'
10+
11+
const { t } = useI18n()
12+
const keysStore = useKeysStore()
13+
const provider = useAIHubMixProvider()
14+
15+
const isRegisterMode = ref(false)
16+
const username = ref('')
17+
const password = ref('')
18+
const confirmPassword = ref('')
19+
const isLoading = ref(false)
20+
21+
const passwordError = computed(() => {
22+
if (isRegisterMode.value && password.value && confirmPassword.value) {
23+
if (password.value !== confirmPassword.value)
24+
return t('passwordsDoNotMatch')
25+
}
26+
if (isRegisterMode.value && password.value) {
27+
if (password.value.length < 8 || password.value.length > 20)
28+
return t('passwordLengthError')
29+
}
30+
return ''
31+
})
32+
33+
const canSubmit = computed(() => {
34+
if (isLoading.value)
35+
return false
36+
if (isRegisterMode.value)
37+
return username.value && password.value && confirmPassword.value && !passwordError.value
38+
else
39+
return username.value && password.value
40+
})
41+
42+
async function submitForm() {
43+
if (!canSubmit.value)
44+
return
45+
46+
try {
47+
isLoading.value = true
48+
if (isRegisterMode.value) {
49+
// Register logic
50+
await provider.apis.register(username.value, password.value)
51+
toast.success(t('registerSuccess'))
52+
// Switch to login mode after successful registration
53+
isRegisterMode.value = false
54+
}
55+
else {
56+
// Login logic
57+
await provider.apis.login(username.value, password.value)
58+
toast.success(t('loginSuccess'))
59+
}
60+
61+
await provider.refreshUser()
62+
63+
// Automatically create API key after successful login
64+
if (!isRegisterMode.value)
65+
await keysStore.createAndAddKey(provider)
66+
67+
// Clear form
68+
username.value = ''
69+
password.value = ''
70+
confirmPassword.value = ''
71+
}
72+
catch (error) {
73+
console.error('Auth error:', error)
74+
toast.error(isRegisterMode.value ? t('registerFailed') : t('loginFailed'))
75+
}
76+
finally {
77+
isLoading.value = false
78+
}
79+
}
80+
</script>
81+
82+
<template>
83+
<Card>
84+
<CardHeader class="pb-4">
85+
<CardTitle class="text-lg">
86+
{{ isRegisterMode ? t('registerTitle') : t('loginTitle') }}
87+
</CardTitle>
88+
<CardDescription class="text-sm">
89+
{{ isRegisterMode ? t('registerDescription') : t('loginDescription') }}
90+
</CardDescription>
91+
</CardHeader>
92+
<CardContent class="space-y-4">
93+
<!-- Username Input -->
94+
<div class="flex rounded-md border border-input bg-background">
95+
<Input
96+
id="username" v-model="username" :placeholder="t('usernamePlaceholder')" type="text"
97+
class="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-10"
98+
/>
99+
</div>
100+
101+
<!-- Password Input -->
102+
<div class="flex rounded-md border border-input bg-background">
103+
<Input
104+
id="password" v-model="password" :placeholder="t('passwordPlaceholder')" type="password"
105+
class="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-10"
106+
/>
107+
</div>
108+
109+
<!-- Confirm Password Input (Register Mode) -->
110+
<div v-if="isRegisterMode" class="flex flex-col">
111+
<div class="flex rounded-md border border-input bg-background">
112+
<Input
113+
id="confirmPassword" v-model="confirmPassword" :placeholder="t('confirmPasswordPlaceholder')"
114+
type="password" class="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-10"
115+
/>
116+
</div>
117+
<p v-if="passwordError" class="text-xs text-red-500 mt-1.5">
118+
{{ passwordError }}
119+
</p>
120+
</div>
121+
</CardContent>
122+
<CardFooter class="flex flex-col space-y-3 pt-3 items-start">
123+
<Button class="w-full h-10" :disabled="!canSubmit" @click="submitForm()">
124+
<span v-if="isLoading">{{ isRegisterMode ? t('registering') : t('loggingIn') }}</span>
125+
<span v-else>{{ isRegisterMode ? t('register') : t('login') }}</span>
126+
</Button>
127+
<div class="w-full grid grid-cols-1">
128+
<Button variant="link" class="w-full h-10 text-sm" @click="isRegisterMode = !isRegisterMode">
129+
{{ isRegisterMode ? t('switchToLogin') : t('switchToRegister') }}
130+
</Button>
131+
</div>
132+
</CardFooter>
133+
</Card>
134+
</template>
135+
136+
<i18n lang="yaml">
137+
en-US:
138+
loginTitle: Login to AIHubMix
139+
loginDescription: Enter your username and password to log in
140+
registerTitle: Create an Account
141+
registerDescription: Create a new AIHubMix account
142+
usernamePlaceholder: Username
143+
passwordPlaceholder: Password
144+
confirmPasswordPlaceholder: Confirm Password
145+
passwordsDoNotMatch: Passwords do not match
146+
passwordLengthError: Password must be 8-20 characters long
147+
loggingIn: Logging in...
148+
registering: Creating account...
149+
login: Login
150+
register: Register
151+
switchToLogin: Already have an account? Login
152+
switchToRegister: Don't have an account? Register
153+
loginSuccess: Login successful!
154+
registerSuccess: Registration successful! Please log in.
155+
loginFailed: Login failed. Please check your credentials.
156+
registerFailed: Registration failed. The username may already exist.
157+
158+
zh-CN:
159+
loginTitle: 登录 AIHubMix
160+
loginDescription: 输入您的账户名和密码进行登录
161+
registerTitle: 创建新账户
162+
registerDescription: 创建一个新的 AIHubMix 账户
163+
usernamePlaceholder: 账户名
164+
passwordPlaceholder: 密码
165+
confirmPasswordPlaceholder: 确认密码
166+
passwordsDoNotMatch: 两次输入的密码不一致
167+
passwordLengthError: 密码长度必须为 8-20 个字符
168+
loggingIn: 登录中...
169+
registering: 注册中...
170+
login: 登录
171+
register: 注册
172+
switchToLogin: 已有账户?前往登录
173+
switchToRegister: 没有账户?前往注册
174+
loginSuccess: 登录成功!
175+
registerSuccess: 注册成功!请登录。
176+
loginFailed: 登录失败,请检查您的账户名和密码。
177+
registerFailed: 注册失败,账户名可能已被占用。
178+
</i18n>

0 commit comments

Comments
 (0)