|
- <template>
- <view class="works">
- <ai-nav-title title="我的作品" />
-
- <scroll-view
- class="page-scroll"
- scroll-y
- refresher-enabled
- :refresher-triggered="refreshing"
- @refresherrefresh="onRefresh"
- @scrolltolower="loadMore"
- >
- <view class="works-content">
- <view class="tabs">
- <view
- v-for="item in tabs"
- :key="item.key"
- class="tab"
- :class="{ active: activeTab === item.key }"
- @click="changeTab(item.key)"
- >
- <text class="tab-name">{{ item.name }}</text>
- <text class="tab-count">{{ counts[item.key] || 0 }}</text>
- </view>
- </view>
-
- <view v-if="list.length" class="works-grid">
- <view
- v-for="item in list"
- :key="item.id"
- class="work-card"
- :class="{ 'name-work': item.work_type === 'ainame' }"
- @click="openWork(item)"
- >
- <view v-if="item.work_type === 'ainame'" class="name-preview">
- <view class="card-top">
- <text class="type-chip name">AI取名</text>
- <text class="status-chip" :class="statusClass(item)">{{ item.status_text }}</text>
- </view>
- <view class="name-list">
- <text v-for="name in item.names" :key="name" class="name-chip">{{ name }}</text>
- <text v-if="!item.names || !item.names.length" class="name-placeholder">名字整理中</text>
- </view>
- <text class="name-summary">{{ item.summary || item.subtitle }}</text>
- </view>
-
- <view v-else class="image-preview">
- <image v-if="item.cover" class="cover" :src="item.cover" mode="aspectFill" />
- <view v-else class="cover-placeholder">
- <view v-if="Number(item.status) === 0" class="spinner"></view>
- <text>{{ Number(item.status) === 0 ? '生成中' : '暂无封面' }}</text>
- </view>
- <view class="image-mask"></view>
- <view class="card-top overlay">
- <text class="type-chip">{{ typeText(item.work_type) }}</text>
- <text class="status-chip" :class="statusClass(item)">{{ item.status_text }}</text>
- </view>
- <view v-if="Number(item.status) === 0" class="pending-mask">
- <view class="spinner light"></view>
- <text>正在创作</text>
- </view>
- </view>
-
- <view class="card-body">
- <text class="work-time">{{ formatTime(item.add_time) }}</text>
- </view>
- </view>
- </view>
-
- <view v-if="!list.length && !loading" class="empty-state">
- <view class="empty-icon">
- <view></view>
- <view></view>
- <view></view>
- <view></view>
- </view>
- <text class="empty-title">{{ userStore.isLogin ? '还没有作品' : '登录后查看作品' }}</text>
- <text class="empty-desc">
- {{ userStore.isLogin ? '去工具箱生成第一张头像、照片或名字方案。' : '登录后会同步你的作品、点数和创作记录。' }}
- </text>
- <view class="empty-action" @click="handleEmptyAction">
- {{ userStore.isLogin ? '去创作' : '去登录' }}
- </view>
- </view>
-
- <view v-if="loading" class="loading">{{ list.length ? '加载更多...' : '加载中...' }}</view>
- <view v-if="list.length && list.length >= total && !loading" class="end-text">没有更多了</view>
- </view>
- </scroll-view>
- </view>
- </template>
-
- <script setup>
- import { computed, ref } from 'vue'
- import { onHide, onShow, onUnload } from '@dcloudio/uni-app'
- import dayjs from 'dayjs'
- import { toolApi } from '@/api/index.js'
- import { useUserStore } from '@/store/user.js'
- import { useApp } from '@/utils/useApp.js'
-
- const userStore = useUserStore()
- const { toast } = useApp()
- const activeTab = ref('all')
- const list = ref([])
- const total = ref(0)
- const page = ref(1)
- const pageSize = 10
- const loading = ref(false)
- const refreshing = ref(false)
- const counts = ref({ all: 0, ainame: 0, anime: 0, image: 0, photo: 0 })
- let timer = 0
-
- const tabs = computed(() => [
- { key: 'all', name: '全部' },
- { key: 'ainame', name: 'AI取名' },
- { key: 'anime', name: '动漫头像' },
- { key: 'photo', name: '证件照' }
- ])
-
- onShow(() => {
- refreshSilent()
- })
-
- onHide(() => clearTimer())
- onUnload(() => clearTimer())
-
- function formatTime(value) {
- if (!value) return ''
- return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
- }
-
- async function fetchList(reset = false, silent = false) {
- if (!userStore.isLogin) {
- list.value = []
- total.value = 0
- refreshing.value = false
- uni.stopPullDownRefresh()
- return
- }
- if (loading.value) {
- if (refreshing.value) uni.stopPullDownRefresh()
- return
- }
- loading.value = true
- if (reset) page.value = 1
- try {
- const res = await toolApi.getMyWorkList({
- type: activeTab.value,
- page_no: page.value,
- page_size: pageSize
- })
- const rows = res.list || []
- const data = res.data || {}
- list.value = reset ? rows : list.value.concat(rows)
- total.value = Number(data.total || rows.length)
- counts.value = normalizeCounts(data.counts || {})
- scheduleRefresh()
- } catch (e) {
- if (!silent) toast(e.msg || '作品加载失败')
- } finally {
- loading.value = false
- refreshing.value = false
- uni.stopPullDownRefresh()
- }
- }
-
- function normalizeCounts(data = {}) {
- return {
- all: Number(data.all || 0),
- ainame: Number(data.ainame || 0),
- anime: Number(data.anime || 0),
- image: Number(data.image || 0),
- photo: Number(data.photo || 0)
- }
- }
-
- function refreshSilent(silent = false) {
- fetchList(true, silent)
- }
-
- function onRefresh() {
- refreshing.value = true
- refreshSilent()
- }
-
- function loadMore() {
- if (loading.value || list.value.length >= total.value) return
- page.value += 1
- fetchList()
- }
-
- function changeTab(key) {
- if (activeTab.value === key) return
- activeTab.value = key
- refreshSilent()
- }
-
- function scheduleRefresh() {
- clearTimer()
- if (list.value.some((item) => Number(item.status) === 0)) {
- timer = setTimeout(() => refreshSilent(true), 3000)
- }
- }
-
- function clearTimer() {
- clearTimeout(timer)
- timer = 0
- }
-
- function openWork(item) {
- if (!item.detail_url) return
- uni.navigateTo({ url: item.detail_url })
- }
-
- function statusClass(item) {
- const status = Number(item.status)
- if (status === 0) return 'pending'
- if (status < 0) return 'failed'
- return 'done'
- }
-
- function typeText(type) {
- const map = {
- ainame: 'AI取名',
- anime: '动漫头像',
- image: 'AI作品',
- photo: '证件照'
- }
- return map[type] || 'AI作品'
- }
-
- function goCreate() {
- uni.switchTab({ url: '/pages/toolbox/toolbox' })
- }
-
- function goLogin() {
- uni.navigateTo({ url: '/pages/login/login' })
- }
-
- function handleEmptyAction() {
- if (userStore.isLogin) {
- goCreate()
- return
- }
- goLogin()
- }
- </script>
-
- <style lang="scss" scoped>
- .works {
- height: 100vh;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- background: #f6f8fb;
- }
-
- .page-scroll {
- flex: 1;
- height: 0;
- }
-
- .works-content {
- min-height: 100%;
- padding: 0 24rpx calc(132rpx + env(safe-area-inset-bottom));
- background: #f6f8fb;
- box-sizing: border-box;
- }
-
- .tabs {
- position: sticky;
- top: 0;
- z-index: 3;
- height: 88rpx;
- margin: 0 -24rpx;
- padding: 0 24rpx;
- border-top: 1rpx solid #eef1f6;
- background: rgba(255, 255, 255, 0.96);
- display: flex;
- align-items: center;
- gap: 14rpx;
- box-sizing: border-box;
- }
-
- .tab {
- height: 56rpx;
- padding: 0 20rpx;
- border-radius: 999rpx;
- display: flex;
- align-items: center;
- gap: 8rpx;
- color: #687386;
- background: #f4f7fb;
- box-sizing: border-box;
- flex-shrink: 0;
-
- &.active {
- color: #0b8cff;
- background: #eaf5ff;
- }
- }
-
- .tab-name {
- font-size: 25rpx;
- font-weight: 800;
- line-height: 1;
- }
-
- .tab-count {
- min-width: 30rpx;
- height: 30rpx;
- padding: 0 8rpx;
- border-radius: 999rpx;
- color: #8a95a8;
- background: #ffffff;
- font-size: 19rpx;
- font-weight: 800;
- line-height: 30rpx;
- text-align: center;
- box-sizing: border-box;
- }
-
- .tab.active .tab-count {
- color: #0b8cff;
- }
-
- .works-grid {
- padding-top: 22rpx;
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 18rpx;
- }
-
- .work-card {
- min-width: 0;
- border-radius: 22rpx;
- background: #ffffff;
- overflow: hidden;
- box-shadow: 0 10rpx 28rpx rgba(34, 68, 112, 0.06);
- }
-
- .image-preview {
- position: relative;
- width: 100%;
- aspect-ratio: 1 / 1;
- background: #eef3f8;
- overflow: hidden;
- }
-
- .cover {
- width: 100%;
- height: 100%;
- display: block;
- }
-
- .cover-placeholder {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: #8a95a8;
- font-size: 24rpx;
- font-weight: 800;
- }
-
- .image-mask {
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- height: 110rpx;
- background: linear-gradient(180deg, rgba(9, 16, 28, 0.36), rgba(9, 16, 28, 0));
- z-index: 1;
- }
-
- .card-top {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12rpx;
- }
-
- .card-top.overlay {
- position: absolute;
- left: 12rpx;
- right: 12rpx;
- top: 12rpx;
- z-index: 3;
- }
-
- .type-chip,
- .status-chip {
- height: 36rpx;
- padding: 0 12rpx;
- border-radius: 999rpx;
- font-size: 19rpx;
- font-weight: 900;
- line-height: 36rpx;
- flex-shrink: 0;
- }
-
- .type-chip {
- color: #172033;
- background: rgba(255, 255, 255, 0.9);
- }
-
- .type-chip.name {
- color: #0b8cff;
- background: #eaf5ff;
- }
-
- .status-chip.done {
- color: #16a15d;
- background: #ecfbf3;
- }
-
- .status-chip.pending {
- color: #ff8a00;
- background: #fff5e8;
- }
-
- .status-chip.failed {
- color: #f04438;
- background: #fff0ef;
- }
-
- .pending-mask {
- position: absolute;
- inset: 0;
- z-index: 2;
- background: rgba(9, 16, 28, 0.42);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: #ffffff;
- font-size: 24rpx;
- font-weight: 800;
- }
-
- .spinner {
- width: 44rpx;
- height: 44rpx;
- margin-bottom: 14rpx;
- border: 6rpx solid rgba(11, 140, 255, 0.18);
- border-top-color: #0b8cff;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
-
- .spinner.light {
- border-color: rgba(255, 255, 255, 0.35);
- border-top-color: #ffffff;
- }
-
- .name-preview {
- min-height: 278rpx;
- padding: 18rpx 18rpx 10rpx;
- background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%);
- box-sizing: border-box;
- }
-
- .name-list {
- min-height: 128rpx;
- margin-top: 22rpx;
- display: flex;
- flex-wrap: wrap;
- gap: 12rpx 10rpx;
- align-content: flex-start;
- }
-
- .name-chip {
- max-width: 100%;
- height: 48rpx;
- padding: 0 14rpx;
- border-radius: 999rpx;
- color: #0b8cff;
- background: #eaf5ff;
- font-size: 26rpx;
- font-weight: 900;
- line-height: 48rpx;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- box-sizing: border-box;
- }
-
- .name-placeholder {
- color: #8a95a8;
- font-size: 24rpx;
- line-height: 44rpx;
- }
-
- .name-summary {
- display: -webkit-box;
- margin-top: 4rpx;
- color: #687386;
- font-size: 23rpx;
- line-height: 1.45;
- overflow: hidden;
- text-overflow: ellipsis;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }
-
- .card-body {
- padding: 12rpx 16rpx 16rpx;
- }
-
- .work-time {
- display: block;
- color: #a0a8b8;
- font-size: 20rpx;
- line-height: 1;
- }
-
- .empty-state {
- min-height: 760rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- text-align: center;
- }
-
- .empty-icon {
- width: 112rpx;
- height: 112rpx;
- padding: 18rpx;
- border-radius: 34rpx;
- background: #ffffff;
- box-shadow: 0 10rpx 28rpx rgba(34, 68, 112, 0.06);
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 10rpx;
- box-sizing: border-box;
-
- view {
- border-radius: 12rpx;
- background: #dceeff;
- }
-
- view:nth-child(2),
- view:nth-child(3) {
- background: #b9ddff;
- }
- }
-
- .empty-title {
- margin-top: 28rpx;
- color: #172033;
- font-size: 32rpx;
- font-weight: 900;
- line-height: 1;
- }
-
- .empty-desc {
- width: 520rpx;
- max-width: 82%;
- margin-top: 16rpx;
- color: #7f8896;
- font-size: 25rpx;
- line-height: 1.5;
- }
-
- .empty-action {
- height: 72rpx;
- min-width: 180rpx;
- margin-top: 34rpx;
- padding: 0 36rpx;
- border-radius: 999rpx;
- color: #ffffff;
- background: #0b8cff;
- font-size: 27rpx;
- font-weight: 900;
- line-height: 72rpx;
- box-sizing: border-box;
- }
-
- .loading,
- .end-text {
- padding: 30rpx 0 8rpx;
- color: #9aa4b2;
- font-size: 24rpx;
- text-align: center;
- }
-
- @keyframes spin {
- to {
- transform: rotate(360deg);
- }
- }
- </style>
|