225 lines
9.3 KiB
HTML
225 lines
9.3 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
|
/>
|
|
<meta name="format-detection" content="telephone=no" />
|
|
<title>Recharge Center</title>
|
|
<link rel="stylesheet" href="./style.css?v=20260601-0400" />
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
</head>
|
|
<body>
|
|
<div id="app" v-cloak>
|
|
<div class="page" :dir="isRtl ? 'rtl' : 'ltr'">
|
|
<header class="topbar">
|
|
<button class="icon-button" type="button" :aria-label="text.back" @click="closePage">
|
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
<path d="M15 5 8 12l7 7" />
|
|
</svg>
|
|
</button>
|
|
<h1>{{ text.title }}</h1>
|
|
<div class="language-switcher" @click.stop>
|
|
<button
|
|
class="language-button"
|
|
type="button"
|
|
:aria-label="text.languageButtonAria"
|
|
:aria-expanded="languageMenuOpen ? 'true' : 'false'"
|
|
@click="toggleLanguageMenu"
|
|
>
|
|
{{ languageLabel }}
|
|
</button>
|
|
<div class="language-menu" v-show="languageMenuOpen">
|
|
<button
|
|
v-for="language in languages"
|
|
:key="language.type"
|
|
type="button"
|
|
:class="{ 'is-active': language.type === lang }"
|
|
:aria-pressed="language.type === lang ? 'true' : 'false'"
|
|
@click="applyLanguage(language.type)"
|
|
>
|
|
{{ language.label }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="content" @click="closeLanguageMenu">
|
|
<section v-if="paymentReturn" class="notice" :class="paymentReturn.type">
|
|
<strong>{{ paymentReturn.title }}</strong>
|
|
<span>{{ paymentReturn.message }}</span>
|
|
</section>
|
|
|
|
<section v-if="!authenticated && !isMock" class="notice warning">
|
|
<strong>{{ text.authMissingTitle }}</strong>
|
|
<span>{{ text.authMissingDesc }}</span>
|
|
</section>
|
|
|
|
<section v-if="selfMode && !targetProfile" class="panel">
|
|
<div class="empty-state">{{ searching ? text.loadingAccount : (statusText || text.loadingAccount) }}</div>
|
|
</section>
|
|
|
|
<section v-if="!selfMode" class="panel search-panel">
|
|
<div class="section-head">
|
|
<h3>{{ text.searchTitle }}</h3>
|
|
</div>
|
|
<form class="search-form" @submit.prevent="searchAccount">
|
|
<input
|
|
v-model.trim="account"
|
|
type="text"
|
|
inputmode="text"
|
|
autocomplete="off"
|
|
:placeholder="text.accountPlaceholder"
|
|
/>
|
|
<button type="submit" :disabled="searching || !account">
|
|
{{ searching ? text.searching : text.search }}
|
|
</button>
|
|
</form>
|
|
<p class="guide-text">{{ text.searchGuide }}</p>
|
|
<p v-if="statusText" class="status" :class="statusType">{{ statusText }}</p>
|
|
</section>
|
|
|
|
<section v-if="targetProfile" class="panel account-panel">
|
|
<div class="avatar-wrap">
|
|
<img v-if="avatarUrl(targetProfile)" :src="avatarUrl(targetProfile)" alt="" @error="hideBrokenImage" />
|
|
<span v-else>{{ fallbackInitial(targetProfile) }}</span>
|
|
</div>
|
|
<div class="account-copy">
|
|
<div class="role-row">
|
|
<span class="role-pill" :class="{ dealer: isDealer }">{{ roleLabel }}</span>
|
|
<span v-if="isDealer" class="role-pill fee">{{ text.lowFee }}</span>
|
|
</div>
|
|
<h3>{{ profileName(targetProfile) }}</h3>
|
|
<p>{{ text.userId }} {{ profileAccount(targetProfile) }}</p>
|
|
<p v-if="selectedCountry">{{ text.country }} {{ countryName(selectedCountry) }}</p>
|
|
</div>
|
|
<button v-if="!selfMode" class="mini-button" type="button" @click="resetTarget">{{ text.change }}</button>
|
|
</section>
|
|
|
|
<section v-if="targetProfile" class="panel methods-panel">
|
|
<div class="section-head">
|
|
<h3>{{ text.packageTitle }}</h3>
|
|
</div>
|
|
|
|
<div v-if="loadingOptions" class="empty-state">{{ text.loading }}</div>
|
|
|
|
<template v-else>
|
|
<div v-if="commodity.length" class="package-grid">
|
|
<button
|
|
v-for="goods in commodity"
|
|
:key="goods.id"
|
|
class="package-button"
|
|
:class="{ active: selectedGoodsId === String(goods.id) }"
|
|
type="button"
|
|
:disabled="ordering"
|
|
@click="selectGoods(goods)"
|
|
>
|
|
<span class="coin-mark"></span>
|
|
<strong>{{ goods.content || goods.name || '-' }}</strong>
|
|
<span>{{ formatPrice(goods) }}</span>
|
|
<small v-if="goods.awardContent">{{ goods.awardContent }}</small>
|
|
</button>
|
|
</div>
|
|
<div v-else class="empty-state">{{ text.noPackage }}</div>
|
|
|
|
<div class="section-head payment-head">
|
|
<h3>{{ text.methodTitle }}</h3>
|
|
</div>
|
|
|
|
<div v-if="visibleChannels.length" class="channel-list">
|
|
<button
|
|
v-for="channel in visibleChannels"
|
|
:key="channelKey(channel)"
|
|
class="channel-button"
|
|
:class="{ usdt: isUsdtTrc20Channel(channel), active: selectedChannelKey === channelKey(channel) }"
|
|
type="button"
|
|
:disabled="ordering"
|
|
@click="selectChannel(channel)"
|
|
>
|
|
<div class="method-icon">
|
|
<img v-if="channelIcon(channel)" :src="channelIcon(channel)" alt="" />
|
|
<span v-else>{{ channelName(channel).slice(0, 1) }}</span>
|
|
</div>
|
|
<div class="channel-copy">
|
|
<h4>{{ channelName(channel) }}</h4>
|
|
</div>
|
|
<span v-if="isUsdtTrc20Channel(channel)" class="method-badge">USDT TRC20</span>
|
|
</button>
|
|
</div>
|
|
<div v-else class="empty-state">{{ text.noOptions }}</div>
|
|
|
|
<button
|
|
class="create-order-button"
|
|
type="button"
|
|
:disabled="ordering || !selectedChannel || !selectedGoods"
|
|
@click="createOrder"
|
|
>
|
|
{{ ordering ? text.creatingOrder : text.createOrder }}
|
|
</button>
|
|
</template>
|
|
</section>
|
|
</main>
|
|
|
|
<div v-if="orderResult" class="sheet-mask" @click="orderResult = null">
|
|
<section class="result-sheet" @click.stop>
|
|
<button class="sheet-close" type="button" @click="orderResult = null">×</button>
|
|
<div class="result-icon" :class="orderResult.type"></div>
|
|
<h3>{{ orderResult.title }}</h3>
|
|
<p>{{ orderResult.message }}</p>
|
|
<dl v-if="orderResult.orderNo">
|
|
<dt>{{ text.orderNo }}</dt>
|
|
<dd>{{ orderResult.orderNo }}</dd>
|
|
</dl>
|
|
<button class="primary-wide" type="button" @click="orderResult = null">{{ text.done }}</button>
|
|
</section>
|
|
</div>
|
|
|
|
<div v-if="usdtModalOpen" class="sheet-mask usdt-mask">
|
|
<section class="usdt-dialog" @click.stop>
|
|
<button class="sheet-close" type="button" :aria-label="text.done" @click="closeUsdtModal">×</button>
|
|
<h3>{{ text.usdtOrderTitle }}</h3>
|
|
|
|
<div class="field-block">
|
|
<label>{{ text.trc20Address }}</label>
|
|
<div class="copy-row">
|
|
<button class="address-value" type="button" @click="copyUsdtAddress">{{ usdtAddressText }}</button>
|
|
<button class="copy-button" type="button" @click="copyUsdtAddress">{{ text.copy }}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-block">
|
|
<label>{{ text.amount }}</label>
|
|
<div class="readonly-amount">{{ formatPrice(selectedGoods) }}</div>
|
|
</div>
|
|
|
|
<form class="usdt-form" @submit.prevent="queryUsdtPaymentStatus">
|
|
<div class="field-block">
|
|
<label for="usdtOrderInput">{{ text.orderInputLabel }}</label>
|
|
<input
|
|
id="usdtOrderInput"
|
|
v-model.trim="usdtOrderInput"
|
|
class="usdt-input"
|
|
type="text"
|
|
inputmode="text"
|
|
autocomplete="off"
|
|
:placeholder="text.orderInputPlaceholder"
|
|
/>
|
|
</div>
|
|
<p v-if="usdtOrderError" class="status error">{{ usdtOrderError }}</p>
|
|
<button class="primary-wide" type="submit" :disabled="queryingPayment || !usdtOrderInput">
|
|
{{ queryingPayment ? text.queryingStatus : text.queryPaymentStatus }}
|
|
</button>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
|
|
<div v-if="toastText" class="toast">{{ toastText }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="./script.js?v=20260601-0400"></script>
|
|
</body>
|
|
</html>
|