fix: 全面修复项目问题
后端修复: - 验证码存储与校验机制 - 订单创建事务+库存扣减 - GetStats字段名错误 - VerifyEmail改为POST - 供应商更新字段白名单 - 文件删除安全检查 - 抽奖安全随机数 - 用户管理CRUD - 订单取消/确认收货 - 工单回复 - Toggle返回新数据 - 移除死代码 前端修复: - 404兜底路由 - 401软跳转 - API层统一 - 面包屑补充banners - 国际化完善 - 购物车并行删除 - 退出清理购物车 - 供应商Dashboard数据 - 工单详情页 - 订单取消/确认收货
This commit is contained in:
@@ -445,8 +445,6 @@ onMounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cart-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
|
||||
+737
-165
@@ -1,218 +1,790 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<h1>Discover Premium Products</h1>
|
||||
<p>Find the best products from trusted suppliers worldwide</p>
|
||||
<el-button type="primary" size="large" @click="$router.push('/products')">
|
||||
{{ $t('common.products') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">{{ $t('product.category') }}</h2>
|
||||
</div>
|
||||
<div class="category-grid">
|
||||
<div v-for="cat in categories" :key="cat.id" class="category-card" @click="goCategory(cat.id)">
|
||||
<div class="category-icon">
|
||||
<el-icon size="32"><Folder /></el-icon>
|
||||
</div>
|
||||
<span class="category-name">{{ cat.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">{{ $t('common.products') }}</h2>
|
||||
<el-button link @click="$router.push('/products')">{{ $t('common.search') }} →</el-button>
|
||||
</div>
|
||||
<div class="product-grid">
|
||||
<div v-for="p in products" :key="p.id" class="product-card" @click="$router.push(`/products/${p.id}`)">
|
||||
<div class="product-image">
|
||||
<el-image v-if="getFirstImage(p.images)" :src="getFirstImage(p.images)" fit="cover" lazy>
|
||||
<template #error>
|
||||
<div class="image-placeholder">
|
||||
<el-icon size="48"><Goods /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="image-placeholder">
|
||||
<el-icon size="48"><Goods /></el-icon>
|
||||
<div class="banner-section">
|
||||
<el-carousel height="280px" :interval="5000" arrow="hover" trigger="click">
|
||||
<el-carousel-item v-for="(banner, index) in banners" :key="index">
|
||||
<div class="banner-slide" :style="{ background: banner.bg_color || banner.bg }">
|
||||
<img v-if="banner.image" :src="banner.image" class="banner-bg-image" />
|
||||
<div class="banner-content">
|
||||
<h2>{{ banner.title }}</h2>
|
||||
<p>{{ banner.desc }}</p>
|
||||
<button class="banner-btn" @click="$router.push(banner.link)">{{ banner.button || banner.btn }}</button>
|
||||
</div>
|
||||
<div class="banner-icon" v-if="banner.icon">
|
||||
<el-icon :size="80"><component :is="banner.icon" /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<h3 class="product-name">{{ p.name }}</h3>
|
||||
<p class="product-price">¥{{ p.price }}</p>
|
||||
<div class="product-tags">
|
||||
<el-tag v-if="p.require_credit" size="small" type="warning">{{ $t('product.requireCredit') }}</el-tag>
|
||||
<el-tag v-if="p.credit_reward > 0" size="small" type="success">+{{ p.credit_reward }} {{ $t('product.creditReward') }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
|
||||
<section class="section" v-if="lotteries.length">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">{{ $t('lottery.title') }}</h2>
|
||||
<el-button link @click="$router.push('/lotteries')">View All →</el-button>
|
||||
<div class="quick-actions">
|
||||
<div class="action-item" @click="$router.push('/products')">
|
||||
<div class="action-icon" style="background: rgba(78, 110, 242, 0.15); color: #4e6ef2;">
|
||||
<el-icon size="22"><Goods /></el-icon>
|
||||
</div>
|
||||
<span>商品</span>
|
||||
</div>
|
||||
<div class="lottery-grid">
|
||||
<div class="action-item" @click="$router.push('/lotteries')">
|
||||
<div class="action-icon" style="background: rgba(245, 158, 11, 0.15); color: #f59e0b;">
|
||||
<el-icon size="22"><Trophy /></el-icon>
|
||||
</div>
|
||||
<span>抽奖</span>
|
||||
</div>
|
||||
<div class="action-item" @click="$router.push('/articles')">
|
||||
<div class="action-icon" style="background: rgba(16, 185, 129, 0.15); color: #10b981;">
|
||||
<el-icon size="22"><Document /></el-icon>
|
||||
</div>
|
||||
<span>资讯</span>
|
||||
</div>
|
||||
<div class="action-item" @click="$router.push('/cart')">
|
||||
<div class="action-icon" style="background: rgba(124, 92, 252, 0.15); color: #7c5cfc;">
|
||||
<el-icon size="22"><ShoppingCart /></el-icon>
|
||||
</div>
|
||||
<span>购物车</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" v-if="lotteries.length">
|
||||
<div class="section-head">
|
||||
<h2>热门活动</h2>
|
||||
</div>
|
||||
<div class="lottery-cards">
|
||||
<div v-for="l in lotteries" :key="l.id" class="lottery-card" @click="$router.push(`/lotteries/${l.id}`)">
|
||||
<h3>{{ l.name }}</h3>
|
||||
<p>{{ l.description }}</p>
|
||||
<el-button type="primary" size="small">{{ $t('lottery.register') }}</el-button>
|
||||
<div class="lottery-icon">
|
||||
<el-icon size="20"><Trophy /></el-icon>
|
||||
</div>
|
||||
<div class="lottery-info">
|
||||
<h3>{{ l.name }}</h3>
|
||||
<p>{{ l.description }}</p>
|
||||
</div>
|
||||
<div class="lottery-right">
|
||||
<span class="lottery-status" :class="l.status === 'active' ? 'active' : ''">
|
||||
{{ l.status === 'active' ? '进行中' : '已结束' }}
|
||||
</span>
|
||||
<button class="lottery-btn">参与</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="section" v-if="articles.length">
|
||||
<div class="section-head">
|
||||
<h2>最新资讯</h2>
|
||||
<span class="section-more" @click="$router.push('/articles')">查看全部 →</span>
|
||||
</div>
|
||||
<div class="article-list">
|
||||
<div v-for="a in articles" :key="a.id" class="article-item" @click="$router.push(`/articles/${a.id}`)">
|
||||
<div class="article-icon">
|
||||
<el-icon size="18"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="article-info">
|
||||
<h3>{{ a.title }}</h3>
|
||||
<p>{{ a.summary || a.content?.slice(0, 60) }}...</p>
|
||||
</div>
|
||||
<span class="article-date">{{ formatDate(a.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" v-if="categories.length">
|
||||
<div class="section-head">
|
||||
<h2>热门分类</h2>
|
||||
<span class="section-more" @click="$router.push('/products')">查看全部 →</span>
|
||||
</div>
|
||||
<div class="category-list">
|
||||
<div v-for="cat in categories" :key="cat.id" class="category-item" @click="goCategory(cat.id)">
|
||||
<div class="category-icon">
|
||||
<el-icon size="18"><Folder /></el-icon>
|
||||
</div>
|
||||
<div class="category-info">
|
||||
<h3>{{ cat.name }}</h3>
|
||||
<p>{{ cat.product_count || 0 }} 件商品</p>
|
||||
</div>
|
||||
<el-icon class="category-arrow"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, shallowRef } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Folder, Goods } from '@element-plus/icons-vue'
|
||||
import { categoryApi, productApi, lotteryApi } from '../../api'
|
||||
import { getFirstImage } from '../../utils/image'
|
||||
import { Goods, Trophy, Document, ShoppingCart, Folder, Present, Star, Timer, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { categoryApi, lotteryApi, articleApi, bannerApi } from '../../api'
|
||||
|
||||
const router = useRouter()
|
||||
const categories = ref<any[]>([])
|
||||
const products = ref<any[]>([])
|
||||
const lotteries = ref<any[]>([])
|
||||
const articles = ref<any[]>([])
|
||||
const banners = ref<any[]>([])
|
||||
|
||||
const defaultBanners = [
|
||||
{ title: '新品上市', desc: '精选优质商品,限时特惠', btn: '立即选购', link: '/products', icon: shallowRef(Present), bg: 'linear-gradient(135deg, #4e6ef2 0%, #7c5cfc 100%)' },
|
||||
{ title: '幸运抽奖', desc: '参与抽奖赢取好礼', btn: '参与活动', link: '/lotteries', icon: shallowRef(Star), bg: 'linear-gradient(135deg, #f59e0b 0%, #f97316 100%)' },
|
||||
{ title: '限时秒杀', desc: '每日精选,超值优惠', btn: '查看详情', link: '/products', icon: shallowRef(Timer), bg: 'linear-gradient(135deg, #10b981 0%, #059669 100%)' },
|
||||
]
|
||||
|
||||
function goCategory(id: number) {
|
||||
router.push({ path: '/products', query: { category_id: String(id) } })
|
||||
}
|
||||
|
||||
function formatDate(date: string) {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [catRes, prodRes, lotRes]: any[] = await Promise.all([
|
||||
const [catRes, lotRes, artRes, banRes]: any[] = await Promise.all([
|
||||
categoryApi.list(),
|
||||
productApi.list({ page: 1, page_size: 8 }),
|
||||
lotteryApi.list(),
|
||||
articleApi.list({ page: 1, page_size: 5 }),
|
||||
bannerApi.list(),
|
||||
])
|
||||
categories.value = catRes.data || []
|
||||
products.value = prodRes.data || []
|
||||
categories.value = (catRes.data || []).slice(0, 6)
|
||||
lotteries.value = (lotRes.data || []).slice(0, 3)
|
||||
} catch {}
|
||||
articles.value = (artRes.data || []).slice(0, 5)
|
||||
|
||||
if (banRes.data && banRes.data.length > 0) {
|
||||
banners.value = banRes.data
|
||||
} else {
|
||||
banners.value = defaultBanners as any
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('加载失败:', e)
|
||||
banners.value = defaultBanners as any
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.hero {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: #fff;
|
||||
padding: 80px 24px;
|
||||
text-align: center;
|
||||
|
||||
h1 { font-size: 42px; font-weight: 700; margin-bottom: 16px; }
|
||||
p { font-size: 18px; opacity: 0.8; margin-bottom: 32px; }
|
||||
.home-page {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.banner-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
background: #2d2d44;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover { transform: translateY(-4px); border-color: rgba(78, 110, 242, 0.3); }
|
||||
}
|
||||
|
||||
.category-icon { margin-bottom: 8px; color: #4e6ef2; }
|
||||
.category-name { font-size: 14px; font-weight: 500; color: rgba(255, 255, 255, 0.85); }
|
||||
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background: #2d2d44;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover { transform: translateY(-4px); border-color: rgba(78, 110, 242, 0.3); }
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
height: 200px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
.banner-slide {
|
||||
height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 60px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.banner-bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.banner-content h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 12px;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.banner-content p {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin-bottom: 24px;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.banner-btn {
|
||||
padding: 12px 32px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.banner-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 28px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
background: #2d2d44;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
background: #35355a;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
.el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.product-info { padding: 16px; }
|
||||
.product-name { font-size: 16px; font-weight: 500; margin-bottom: 8px; color: rgba(255, 255, 255, 0.9); }
|
||||
.product-price { font-size: 20px; font-weight: 700; color: #4e6ef2; margin-bottom: 8px; }
|
||||
.product-tags { display: flex; gap: 8px; }
|
||||
.action-item span {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.lottery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
.section {
|
||||
margin-bottom: 28px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.section-head h2 {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.section-more:hover {
|
||||
color: #4e6ef2;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
background: #2d2d44;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.category-item:hover {
|
||||
background: #35355a;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #f59e0b, #f97316);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.category-info h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.category-info p {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.category-arrow {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lottery-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lottery-card {
|
||||
background: linear-gradient(135deg, #4e6ef2, #7c5cfc);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
background: #2d2d44;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
transition: all 0.15s;
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
h3 { margin-bottom: 8px; }
|
||||
p { opacity: 0.8; margin-bottom: 16px; font-size: 14px; }
|
||||
.lottery-card:hover {
|
||||
background: #35355a;
|
||||
}
|
||||
|
||||
.lottery-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #4e6ef2, #7c5cfc);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lottery-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lottery-info h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.lottery-info p {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lottery-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lottery-status {
|
||||
font-size: 12px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.lottery-status.active {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.lottery-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: #4e6ef2;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.lottery-btn:hover {
|
||||
background: #5a7af5;
|
||||
}
|
||||
|
||||
.article-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.article-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #2d2d44;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.article-item:hover {
|
||||
background: #35355a;
|
||||
}
|
||||
|
||||
.article-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #10b981;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.article-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.article-info h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.article-info p {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.article-date {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.el-carousel) {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__container) {
|
||||
position: relative !important;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__arrow) {
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
font-size: 16px !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__arrow:hover) {
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__arrow--left) {
|
||||
left: 16px !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__arrow--right) {
|
||||
right: 16px !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__indicators) {
|
||||
position: absolute !important;
|
||||
bottom: 16px !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
z-index: 10 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__indicator--horizontal .el-carousel__button) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
width: 24px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__indicator--horizontal.is-active .el-carousel__button) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.home-page {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.banner-section {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.banner-slide {
|
||||
height: 200px;
|
||||
padding: 0 20px;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.banner-content h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.banner-content p {
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.banner-btn {
|
||||
padding: 8px 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
flex: 1 1 45%;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.action-item span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
padding: 0 12px;
|
||||
width: 100% !important;
|
||||
max-width: 100vw !important;
|
||||
box-sizing: border-box !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-head h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.category-info h3 {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.category-info p {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.lottery-cards {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lottery-card {
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lottery-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.lottery-info h3 {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.lottery-info p {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.lottery-status {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.lottery-btn {
|
||||
padding: 5px 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.article-list {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.article-item {
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.article-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.article-info h3 {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.article-info p {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.article-date {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
:deep(.el-carousel) {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__container) {
|
||||
height: 200px !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__arrow) {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
font-size: 14px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__arrow--left) {
|
||||
left: 10px !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__arrow--right) {
|
||||
right: 10px !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__indicators) {
|
||||
bottom: 12px !important;
|
||||
position: absolute !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__indicator--horizontal .el-carousel__button) {
|
||||
width: 20px;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">{{ formatDate(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.edit')" width="120">
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="$router.push(`/orders/${row.id}`)">查看</el-button>
|
||||
<el-button v-if="row.status === 'pending_payment' || row.status === 'pending_confirm'" link type="warning" size="small" @click="cancelOrder(row.id)">取消</el-button>
|
||||
<el-button v-if="row.status === 'shipped'" link type="success" size="small" @click="confirmReceipt(row.id)">确认收货</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -27,6 +29,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { orderApi } from '../../api'
|
||||
import BackNav from '../../components/BackNav.vue'
|
||||
|
||||
@@ -54,10 +57,34 @@ function formatDate(date: string) {
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const res: any = await orderApi.list()
|
||||
orders.value = res.data || []
|
||||
})
|
||||
async function fetchOrders() {
|
||||
try {
|
||||
const res: any = await orderApi.list()
|
||||
orders.value = res.data || []
|
||||
} catch {
|
||||
ElMessage.error('获取订单失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelOrder(id: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要取消该订单吗?', '提示', { type: 'warning' })
|
||||
await orderApi.cancel(id)
|
||||
ElMessage.success('订单已取消')
|
||||
fetchOrders()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function confirmReceipt(id: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认已收到商品?', '提示', { type: 'info' })
|
||||
await orderApi.confirmReceipt(id)
|
||||
ElMessage.success('已确认收货')
|
||||
fetchOrders()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(fetchOrders)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -94,8 +94,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.products-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="ticket-detail-page">
|
||||
<BackNav />
|
||||
<div v-if="ticket" class="ticket-card">
|
||||
<div class="ticket-header">
|
||||
<h2>{{ ticket.subject }}</h2>
|
||||
<el-tag :type="statusType(ticket.status)">{{ statusText(ticket.status) }}</el-tag>
|
||||
</div>
|
||||
<div class="ticket-meta">
|
||||
<span>分类:{{ ticket.category }}</span>
|
||||
<span>创建时间:{{ formatDate(ticket.created_at) }}</span>
|
||||
</div>
|
||||
<div class="ticket-content">
|
||||
<p>{{ ticket.content }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="ticket.reply" class="reply-section">
|
||||
<h3>回复</h3>
|
||||
<div class="reply-content" v-html="formatReply(ticket.reply)"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="ticket.status !== 'resolved' && ticket.status !== 'closed'" class="reply-form">
|
||||
<h3>追加回复</h3>
|
||||
<el-input v-model="replyContent" type="textarea" :rows="4" placeholder="请输入回复内容" />
|
||||
<el-button type="primary" @click="submitReply" :loading="submitting" style="margin-top: 12px">提交回复</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ticketApi } from '../../api'
|
||||
import BackNav from '../../components/BackNav.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const ticket = ref<any>(null)
|
||||
const replyContent = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待处理', processing: '处理中', resolved: '已解决', closed: '已关闭',
|
||||
}
|
||||
const statusTypeMap: Record<string, string> = {
|
||||
pending: 'warning', processing: 'primary', resolved: 'success', closed: 'info',
|
||||
}
|
||||
function statusText(s: string) { return statusMap[s] || s }
|
||||
function statusType(s: string) { return statusTypeMap[s] || 'info' }
|
||||
|
||||
function formatDate(date: string) {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function formatReply(reply: string) {
|
||||
return reply.replace(/\n/g, '<br>').replace(/---/g, '<hr style="border-color: rgba(255,255,255,0.1); margin: 12px 0;">')
|
||||
}
|
||||
|
||||
async function fetchTicket() {
|
||||
try {
|
||||
const id = Number(route.params.id)
|
||||
const res: any = await ticketApi.getById(id)
|
||||
ticket.value = res.data
|
||||
} catch {
|
||||
ElMessage.error('获取工单失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function submitReply() {
|
||||
if (!replyContent.value.trim()) {
|
||||
ElMessage.warning('请输入回复内容')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await ticketApi.reply(Number(route.params.id), { content: replyContent.value })
|
||||
ElMessage.success('回复成功')
|
||||
replyContent.value = ''
|
||||
fetchTicket()
|
||||
} catch {
|
||||
ElMessage.error('回复失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchTicket)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ticket-detail-page { padding: 0; }
|
||||
|
||||
.ticket-card {
|
||||
background: #2d2d44;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ticket-content {
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.reply-section {
|
||||
margin-bottom: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
padding: 16px;
|
||||
background: rgba(78, 110, 242, 0.08);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.reply-form {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="table-card">
|
||||
<el-table :data="tickets">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" :label="$t('ticket.subject')" />
|
||||
<el-table-column prop="subject" :label="$t('ticket.subject')" />
|
||||
<el-table-column prop="status" :label="$t('ticket.status')" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
@@ -16,6 +16,11 @@
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">{{ formatDate(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="$router.push(`/tickets/${row.id}`)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user