feat: 移动端兼容优化

This commit is contained in:
Gsh
2026-01-03 22:58:30 +08:00
parent 63aa8d9536
commit 158226601b
7 changed files with 305 additions and 45 deletions

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { Check, CircleCheck, CircleClose, Close, Delete, DocumentCopy, Edit, Files, Hide, Key, Plus, PriceTag, Reading, Refresh, Timer, View } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { computed, onMounted, ref } from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import {
createToken,
@@ -46,6 +46,13 @@ const currentFormData = ref<TokenFormData>({
});
const router = useRouter();
// 移动端检测
const isMobile = ref(false);
function checkMobile() {
isMobile.value = window.innerWidth < 768;
}
// 防抖和节流控制
const operatingTokenId = ref<string>('');
@@ -341,8 +348,14 @@ function isOperating(tokenId: string) {
}
onMounted(async () => {
checkMobile();
window.addEventListener('resize', checkMobile);
await fetchTokenList();
});
onUnmounted(() => {
window.removeEventListener('resize', checkMobile);
});
</script>
<template>
@@ -384,8 +397,10 @@ onMounted(async () => {
</div>
<!-- Token列表 -->
<div v-if="hasTokens" class="token-table-wrapper">
<el-table
<div v-if="hasTokens">
<!-- 桌面端表格 -->
<div v-if="!isMobile" class="token-table-wrapper">
<el-table
:data="tokenList"
stripe
border
@@ -586,6 +601,101 @@ onMounted(async () => {
</template>
</el-table-column>
</el-table>
</div>
<!-- 移动端卡片列表 -->
<div v-else class="mobile-token-list">
<div v-for="token in tokenList" :key="token.id" class="mobile-token-card">
<div class="mobile-card-header">
<div class="mobile-card-title">
<el-icon class="title-icon"><PriceTag /></el-icon>
<span>{{ token.name }}</span>
</div>
<el-tag :type="token.isDisabled ? 'danger' : 'success'" size="small">
{{ token.isDisabled ? '已禁用' : '启用中' }}
</el-tag>
</div>
<div class="mobile-card-body">
<!-- API密钥 -->
<div class="mobile-info-item">
<div class="mobile-info-label">API密钥</div>
<div class="mobile-key-row">
<span class="mobile-key-text">
{{ token.showKey ? token.apiKey : '•••••••••••••••••••••' }}
</span>
<div class="mobile-key-actions">
<el-button
:icon="token.showKey ? Hide : View"
size="small"
text
@click="toggleKeyVisibility(token)"
/>
<el-button
:icon="DocumentCopy"
size="small"
type="primary"
text
@click="copyApiKey(token.apiKey, token.name)"
/>
</div>
</div>
</div>
<!-- 配额信息 -->
<div v-if="token.premiumQuotaLimit" class="mobile-info-item">
<div class="mobile-info-label">配额使用</div>
<div class="mobile-quota-info">
<el-progress
:percentage="getQuotaPercentage(token.premiumUsedQuota, token.premiumQuotaLimit)"
:color="getQuotaColor(getQuotaPercentage(token.premiumUsedQuota, token.premiumQuotaLimit))"
:stroke-width="8"
/>
<div class="mobile-quota-text">
{{ formatQuota(token.premiumUsedQuota) }} / {{ formatQuota(token.premiumQuotaLimit) }}
</div>
</div>
</div>
<!-- 过期时间 -->
<div v-if="token.expireTime" class="mobile-info-item">
<div class="mobile-info-label">过期时间</div>
<div class="mobile-info-value">{{ formatDateTime(token.expireTime) }}</div>
</div>
<!-- 创建时间 -->
<div class="mobile-info-item">
<div class="mobile-info-label">创建时间</div>
<div class="mobile-info-value">{{ formatDateTime(token.creationTime) }}</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="mobile-card-actions">
<el-button size="small" :icon="Edit" @click="showEditDialog(token)">
编辑
</el-button>
<el-button
size="small"
:type="token.isDisabled ? 'success' : 'warning'"
:icon="token.isDisabled ? Check : Close"
:loading="isOperating(token.id)"
@click="handleToggle(token)"
>
{{ token.isDisabled ? '启用' : '禁用' }}
</el-button>
<el-button
size="small"
type="danger"
:icon="Delete"
:loading="isOperating(token.id)"
@click="handleDelete(token)"
>
删除
</el-button>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="totalCount > 0" class="pagination-container">
@@ -1170,5 +1280,114 @@ onMounted(async () => {
font-size: 13px;
}
}
/* 移动端卡片列表 */
.mobile-token-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.mobile-token-card {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
font-weight: 600;
color: #333;
.title-icon {
color: #409eff;
}
}
.mobile-card-body {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-info-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.mobile-info-label {
font-size: 12px;
color: #999;
font-weight: 500;
}
.mobile-info-value {
font-size: 14px;
color: #333;
}
.mobile-key-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
background: #f5f7fa;
padding: 8px;
border-radius: 4px;
}
.mobile-key-text {
flex: 1;
font-size: 12px;
color: #333;
font-family: monospace;
word-break: break-all;
}
.mobile-key-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.mobile-quota-info {
display: flex;
flex-direction: column;
gap: 6px;
}
.mobile-quota-text {
font-size: 13px;
color: #666;
text-align: center;
}
.mobile-card-actions {
display: flex;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
.el-button {
flex: 1;
font-size: 13px;
}
}
}
</style>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { ChatLineRound, List, Refresh, Search } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { computed, onMounted, ref } from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { getRechargeLog } from '@/api/model/index.ts';
import { isUserVip } from '@/utils/user.ts';
@@ -26,6 +26,13 @@ const pageSize = ref(10);
const showWechatFullscreen = ref(false);
const showWxGroupFullscreen = ref(false);
// 移动端检测
const isMobile = ref(false);
function checkMobile() {
isMobile.value = window.innerWidth < 768;
}
const wxSrc = computed(
() => `/src/assets/images/wx.png`,
);
@@ -148,8 +155,14 @@ const showPagination = computed(() => {
});
onMounted(() => {
checkMobile();
window.addEventListener('resize', checkMobile);
fetchRechargeLog();
});
onUnmounted(() => {
window.removeEventListener('resize', checkMobile);
});
</script>
<template>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { ElMessage } from 'element-plus';
import { computed, ref, watch } from 'vue';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
interface TokenFormData {
id?: string;
@@ -43,6 +43,22 @@ const submitting = ref(false);
const neverExpire = ref(false); // 永不过期开关
const unlimitedQuota = ref(false); // 无限制额度开关
// 移动端检测
const isMobile = ref(false);
function checkMobile() {
isMobile.value = window.innerWidth < 768;
}
onMounted(() => {
checkMobile();
window.addEventListener('resize', checkMobile);
});
onUnmounted(() => {
window.removeEventListener('resize', checkMobile);
});
const quotaUnitOptions = [
{ label: '个', value: '个', multiplier: 1 },
{ label: '十', value: '十', multiplier: 10 },
@@ -180,12 +196,13 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
<el-dialog
:model-value="visible"
:title="dialogTitle"
width="540px"
:width="isMobile ? '95%' : '540px'"
:fullscreen="isMobile"
:close-on-click-modal="false"
:show-close="!submitting"
@close="handleClose"
>
<el-form :model="localFormData" label-width="110px" label-position="right">
<el-form :model="localFormData" :label-width="isMobile ? '100%' : '110px'" :label-position="isMobile ? 'top' : 'right'">
<el-form-item label="API密钥名称" required>
<el-input
v-model="localFormData.name"

View File

@@ -194,7 +194,7 @@ function toggleMobileMenu() {
v-model="mobileMenuVisible"
direction="rtl"
:size="280"
:show-close="false"
:close-on-click-modal="true"
class="mobile-drawer"
>
<template #header>

View File

@@ -489,11 +489,19 @@ watch(dateRange, () => {
</div>
</div>
<!-- Download Button Overlay -->
<div v-if="currentTask.storeUrl" class="absolute bottom-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<!-- Desktop Button Overlay -->
<div v-if="currentTask.storeUrl" class="hidden md:flex absolute bottom-4 right-4 gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<el-button circle type="primary" :icon="ZoomIn" @click="handlePreview(currentTask.storeUrl)" />
<el-button circle type="primary" :icon="Download" @click="downloadImage(currentTask.storeUrl)" />
</div>
<!-- Mobile Button Overlay -->
<div v-if="currentTask.storeUrl" class="md:hidden absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 z-10">
<div class="flex items-center justify-center gap-2">
<el-button circle type="primary" :icon="ZoomIn" @click="handlePreview(currentTask.storeUrl)" />
<el-button circle type="primary" :icon="Download" @click="downloadImage(currentTask.storeUrl)" />
</div>
</div>
</div>
<!-- Right Info -->

View File

@@ -555,11 +555,19 @@ watch([() => searchForm.TaskStatus, () => searchForm.PublishStatus, dateRange],
</div>
</div>
<!-- Download Button Overlay -->
<div v-if="currentTask.storeUrl" class="absolute bottom-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<!-- Desktop Button Overlay -->
<div v-if="currentTask.storeUrl" class="hidden md:flex absolute bottom-4 right-4 gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<el-button circle type="primary" :icon="ZoomIn" @click="handlePreview(currentTask.storeUrl)" />
<el-button circle type="primary" :icon="Download" @click="downloadImage(currentTask.storeUrl)" />
</div>
<!-- Mobile Button Overlay -->
<div v-if="currentTask.storeUrl" class="md:hidden absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 z-10">
<div class="flex items-center justify-center gap-2">
<el-button circle type="primary" :icon="ZoomIn" @click="handlePreview(currentTask.storeUrl)" />
<el-button circle type="primary" :icon="Download" @click="downloadImage(currentTask.storeUrl)" />
</div>
</div>
</div>
<!-- Right Info -->
@@ -637,28 +645,21 @@ watch([() => searchForm.TaskStatus, () => searchForm.PublishStatus, dateRange],
<el-tag size="small" type="info" effect="plain">未发布</el-tag>
</div>
<div v-if="currentTask.taskStatus === 'Success'" class="pt-2 space-y-2">
<div class="grid grid-cols-1 gap-1">
<el-button
type="primary"
plain
:icon="MagicStick"
@click="$emit('use-prompt', currentTask.prompt)"
>
使用提示词
</el-button>
<el-button
v-if="false"
type="primary"
plain
:icon="Picture"
@click="$emit('use-reference', currentTask.storeUrl)"
>
做参考图
</el-button>
</div>
<!-- 操作按钮 - 所有状态都可以使用提示词 -->
<div class="pt-2 space-y-2">
<el-button
v-if="currentTask.publishStatus === 'Unpublished'"
type="primary"
plain
:icon="MagicStick"
class="w-full"
@click="$emit('use-prompt', currentTask.prompt); dialogVisible = false"
>
使用提示词
</el-button>
<!-- 成功状态才显示发布按钮 -->
<el-button
v-if="currentTask.taskStatus === 'Success' && currentTask.publishStatus === 'Unpublished'"
type="success"
class="w-full"
:icon="Share"
@@ -666,7 +667,7 @@ watch([() => searchForm.TaskStatus, () => searchForm.PublishStatus, dateRange],
>
发布到广场
</el-button>
<el-button v-else type="info" disabled class="w-full">
<el-button v-else-if="currentTask.taskStatus === 'Success'" type="info" disabled class="w-full">
已发布
</el-button>
</div>

View File

@@ -129,7 +129,7 @@ async function handleDownload() {
</div>
<!-- Mobile Actions Bar -->
<div v-if="task.taskStatus === 'Success'" class="absolute bottom-0 left-0 right-0 md:hidden bg-gradient-to-t from-black/70 to-transparent p-2 z-20">
<div class="absolute bottom-0 left-0 right-0 md:hidden bg-gradient-to-t from-black/70 to-transparent p-2 z-20">
<div class="flex items-center justify-between gap-2">
<el-button
type="primary"
@@ -140,16 +140,18 @@ async function handleDownload() {
>
查看详情
</el-button>
<el-button circle size="small" :icon="ZoomIn" @click.stop="handlePreview" />
<el-button circle size="small" :icon="Download" @click.stop="handleDownload" />
<el-button
v-if="showPublishStatus && task.publishStatus === 'Unpublished'"
circle
size="small"
type="success"
:icon="Share"
@click.stop="$emit('publish', task)"
/>
<template v-if="task.taskStatus === 'Success'">
<el-button circle size="small" :icon="ZoomIn" @click.stop="handlePreview" />
<el-button circle size="small" :icon="Download" @click.stop="handleDownload" />
<el-button
v-if="showPublishStatus && task.publishStatus === 'Unpublished'"
circle
size="small"
type="success"
:icon="Share"
@click.stop="$emit('publish', task)"
/>
</template>
</div>
</div>