feat: 添加分页组件和地址选择组件,优化样式和类型定义

refactor: 重构路由配置,移除订单页面路由

style: 统一使用CSS变量替换硬编码颜色值

docs: 添加前端数据需求分析文档

fix: 修复登录页面记住我功能,更新用户信息页面链接

perf: 优化搜索组件动画效果和响应式设计

chore: 更新TypeScript类型定义,添加订单和地址相关类型
This commit is contained in:
qingfeng1121
2026-01-19 11:35:50 +08:00
parent c287650fbb
commit 73cf25e586
20 changed files with 4618 additions and 422 deletions

278
Api.text Normal file
View File

@@ -0,0 +1,278 @@
# 前端数据需求分析
## 组件数据需求
### 1. Address.vue (地址选择组件)
- 数据需求:省市区级联选择数据
- 数据结构:
```
address {
province: string, // 省份名称
city: string, // 城市名称
district: string // 区县名称
}
```
### 2. Search.vue (搜索组件)
- 数据需求:搜索关键词、搜索类型、搜索历史
- 数据结构:
```
search {
keyword: string, // 搜索关键词
type: string, // 搜索类型(如"宝贝"、"店铺"
history: string[] // 搜索历史记录
}
```
### 3. page.vue (分页组件)
- 数据需求:总页数
- 数据结构:
```
pagination {
totalPages: number // 总页数
}
```
## 页面数据需求
### 1. Login.vue (登录页面)
- 数据需求:用户登录信息
- 数据结构:
```
login {
username: string, // 用户名
password: string, // 密码
rememberMe: boolean // 是否记住密码
}
```
### 2. Userinfo.vue (用户信息页面)
- 数据需求:用户信息、订单状态数量
- 数据结构:
```
user {
id: number, // 用户ID
username: string, // 用户名
avatar: string, // 头像URL
level: number, // 用户等级
followCount: number, // 关注数
fansCount: number, // 粉丝数
collectionCount: number, // 收藏数
couponCount: number, // 红包数
discountCount: number, // 优惠券数
taoCoin: number, // 淘币数
points: number // 积分
}
orderCounts {
pending_payment: number, // 待付款
pending_shipment: number, // 待发货
pending_receipt: number, // 待收货
pending_review: number, // 待评价
completed: number // 已完成
}
```
### 3. productdetil.vue (商品详情页面)
- 数据需求:商品信息、商品图片、商品型号、评论、推荐商品
- 数据结构:
```
product {
id: number, // 商品ID
title: string, // 商品标题
images: string[], // 商品图片URL
price: number, // 价格
originalPrice: number, // 原价
models: [ // 商品型号
{
id: string,
name: string,
price: string,
originalPrice: string,
img: string
}
],
description: string, // 商品描述
features: string[], // 商品特点
specs: [ // 商品参数
{
category: string, // 参数类别
value: string // 参数值
}
],
reviews: [ // 用户评论
{
id: number,
reviewerName: string, // 评论者姓名
rating: number, // 评分
time: string, // 评论时间
content: string, // 评论内容
images: string[] // 评论图片
}
],
recommendations: [ // 推荐商品
{
id: number,
name: string, // 商品名称
price: number, // 价格
rating: number, // 评分
image: string // 商品图片
}
]
}
```
### 4. SettlementLite.vue (结算页面)
- 数据需求:地址列表、默认地址、商品信息、价格计算
- 数据结构:
```
address {
id: number, // 地址ID
name: string, // 收货人姓名
phone: string, // 联系电话
province: string, // 省份
city: string, // 城市
district: string, // 区县
detail: string, // 详细地址
isDefault: boolean // 是否默认地址
}[]
order {
products: [ // 订单商品
{
id: number, // 商品ID
name: string, // 商品名称
price: number, // 商品价格
quantity: number, // 购买数量
image: string // 商品图片
}
],
totalPrice: number, // 总价格
shippingFee: number, // 运费
finalPrice: number // 最终价格
}
```
### 5. User/UpdataAddress.vue (地址修改页面)
- 数据需求:地址信息
- 数据结构:
```
address {
id: number, // 地址ID
name: string, // 收货人姓名
phone: string, // 联系电话
province: string, // 省份
city: string, // 城市
district: string, // 区县
detail: string, // 详细地址
isDefault: boolean // 是否默认地址
}
```
### 6. User/User.vue (用户中心页面)
- 数据需求:用户信息、订单列表、地址列表、收藏列表
- 数据结构:
```
user {
id: number, // 用户ID
username: string, // 用户名
avatar: string // 头像URL
}
orders {
id: number, // 订单ID
status: string, // 订单状态
totalPrice: number, // 订单总价
createTime: string, // 创建时间
products: [ // 订单商品
{
id: number,
name: string,
price: number,
quantity: number,
image: string
}
]
}[]
addresses: address[] // 地址列表结构同SettlementLite.vue
collections: [ // 收藏列表
{
id: number, // 收藏ID
productId: number, // 商品ID
productName: string, // 商品名称
productPrice: number, // 商品价格
productImage: string // 商品图片
}
]
```
### 7. Cart.vue (购物车页面)
- 数据需求:购物车商品列表
- 数据结构:
```
cart {
items: [ // 购物车商品
{
id: number, // 商品ID
name: string, // 商品名称
price: number, // 商品价格
quantity: number, // 购买数量
image: string, // 商品图片
selected: boolean // 是否选中
}
],
totalPrice: number, // 总价格
selectedCount: number // 选中数量
}
```
### 8. product/productList.vue (商品列表页面)
- 数据需求:商品列表、分页信息
- 数据结构:
```
products {
items: [ // 商品列表
{
id: number, // 商品ID
name: string, // 商品名称
price: number, // 商品价格
originalPrice: number, // 原价
image: string, // 商品图片
rating: number, // 评分
sales: number // 销量
}
],
total: number, // 总商品数
page: number, // 当前页码
pageSize: number, // 每页数量
totalPages: number // 总页数
}
```
### 9. Home.vue (首页)
- 数据需求:轮播图、推荐商品、分类信息
- 数据结构:
```
home {
banners: [ // 轮播图
{
id: number,
image: string, // 轮播图图片
link: string // 跳转链接
}
],
recommendedProducts: [ // 推荐商品
{
id: number,
name: string,
price: number,
image: string,
rating: number
}
],
categories: [ // 商品分类
{
id: number,
name: string, // 分类名称
icon: string // 分类图标
}
]
}
```

View File

@@ -8,5 +8,6 @@
import { RouterView } from "vue-router"; import { RouterView } from "vue-router";
import Herde from "@/Views/Herde.vue"; import Herde from "@/Views/Herde.vue";
import './Style/App.css' import './Style/App.css'
</script> </script>

View File

@@ -0,0 +1,927 @@
<!-- 结算页 用于展示购物车中的商品信息 并确认结算 -->
<template>
<a-modal v-model:visible="visible" width="800px" @ok="afterClose" @cancel="cancel" class="settlement-modal"
:footer-extra="null" :footer="null">
<!-- 结算界面 左右结构 -->
<div class="settlement-container">
<!-- 左侧 结算列表 -->
<div class="settlement-left">
<!-- 地址信息 -->
<div class="address-info">
<div class="address-item" @click="showAddressModal = true">
<div class="address-header">
<span class="address-name">{{ selectedAddress.name }}</span>
<span class="address-phone">{{ selectedAddress.phone }}</span>
<span v-if="selectedAddress.isDefault" class="address-tag default">默认</span>
</div>
<div class="address-detail">
{{ selectedAddress.province }}{{ selectedAddress.city }}{{ selectedAddress.district }}{{
selectedAddress.detail }}
</div>
</div>
<a-modal v-model:visible="showAddressModal" width="600px" @ok="afterCloseAddressModal"
:after-close="afterCloseAddressModal" :footer-extra="null" :footer="null">
<!-- 顶部导航 地址列表 添加地址 -->
<div class="address-nav">
<span class="nav-item" :class="{ active: activeAddressTab === 'list' }"
@click="switchAddressTab('list')">
地址列表
</span>
<span class="nav-item" :class="{ active: activeAddressTab === 'add' }"
@click="switchAddressTab('add')">
添加地址
</span>
</div>
<div>
<!-- 地址列表 -->
<div class="address-list" v-if="activeAddressTab === 'list'">
<div class="address-item" :class="{ active: selectedAddress.id === address.id }"
v-for="address in addresses" :key="address.id" @click="selectAddress(address)">
<div class="address-header">
<span class="address-name">{{ address.name }}</span>
<span class="address-phone">{{ address.phone }}</span>
<span v-if="address.isDefault" class="address-tag default">默认</span>
</div>
<div class="address-detail">
{{ address.province }}{{ address.city }}{{ address.district }}{{ address.detail
}}
</div>
</div>
<div v-if="addresses.length === 0" class="empty-address">
<p>暂无地址请添加新地址</p>
</div>
</div>
<!-- 添加地址表单 -->
<div class="add-address-form" v-if="activeAddressTab === 'add'">
<div class="form-item">
<label>收货人</label>
<input type="text" v-model="newAddressForm.name" placeholder="请输入收货人姓名">
</div>
<div class="form-item">
<label>手机号</label>
<input type="tel" v-model="newAddressForm.phone" placeholder="请输入手机号码">
</div>
<div class="form-item">
<label>所在地区</label>
<!-- 省份选择 -->
<input type="text" :readonly="true" @click="showAddressComponent = true"
v-model="address" placeholder="省市县">
</div>
<!-- 所在省份被选择后显示城市选择器 -->
<AddressComponent @address="handleAddressChange"
v-model:visible="showAddressComponent" />
<div class="form-item">
<label>详细地址</label>
<textarea v-model="newAddressForm.detail" placeholder="请输入详细地址信息"
rows="3"></textarea>
</div>
<div class="form-item checkbox-item">
<label>
<input type="checkbox" v-model="newAddressForm.isDefault">
设为默认地址
</label>
</div>
</div>
</div>
</a-modal>
</div>
<!-- 商品信息 -->
<div class="settlement-section goods-section">
<h3 class="section-title">商品信息</h3>
<div class="goods-list">
<div v-for="product in products" :key="product.id" class="goods-item">
<div class="goods-image">
<img :src="product.image" :alt="product.name">
</div>
<div class="goods-info">
<h4 class="goods-name">{{ product.name }}</h4>
<p class="goods-desc">{{ product.description }}</p>
<div class="goods-price-quantity">
<span class="goods-price">¥{{ product.price.toFixed(2) }}</span>
<span class="goods-quantity">x{{ product.quantity }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 价格信息 -->
<div class="settlement-section price-section">
<h3 class="section-title">价格明细</h3>
<div class="price-details">
<div class="price-item">
<span class="price-label">商品总价</span>
<span class="price-value">¥{{ totalPrice.toFixed(2) }}</span>
</div>
<div class="price-item">
<span class="price-label">运费</span>
<span class="price-value">¥{{ shippingFee.toFixed(2) }}</span>
</div>
<div class="price-item discount">
<span class="price-label">优惠</span>
<span class="price-value discount">-¥{{ discountAmount.toFixed(2) }}</span>
</div>
<div class="price-item total">
<span class="price-label">实付金额</span>
<span class="price-value total">¥{{ actualPrice.toFixed(2) }}</span>
</div>
</div>
</div>
<!-- 发票信息 -->
<div class="settlement-section invoice-section">
<h3 class="section-title">发票信息</h3>
<div class="invoice-info">
<div class="invoice-type">
<label>
<input type="radio" v-model="invoiceType" value="personal"> 个人发票
</label>
<label>
<input type="radio" v-model="invoiceType" value="company"> 企业发票
</label>
</div>
<div v-if="invoiceType === 'company'" class="company-info">
<input type="text" v-model="companyName" placeholder="请输入企业名称">
<input type="text" v-model="taxNumber" placeholder="请输入税号">
</div>
</div>
</div>
</div>
<!-- 右侧 结算信息 -->
<div class="settlement-right">
<div class="payment-section">
<h3 class="section-title">支付方式</h3>
<div class="payment-methods">
<div class="payment-item" :class="{ active: selectedPayment === 'balance' }"
@click="selectedPayment = 'balance'">
<div class="payment-icon balance-icon"></div>
<span>余额支付</span>
</div>
<div class="payment-item" :class="{ active: selectedPayment === 'bank' }"
@click="selectedPayment = 'bank'">
<div class="payment-icon bank-icon"></div>
<span>银行卡</span>
</div>
<div class="payment-item" :class="{ active: selectedPayment === 'wechat' }"
@click="selectedPayment = 'wechat'">
<div class="payment-icon wechat-icon"></div>
<span>微信支付</span>
</div>
<div class="payment-item" :class="{ active: selectedPayment === 'alipay' }"
@click="selectedPayment = 'alipay'">
<div class="payment-icon alipay-icon"></div>
<span>支付宝</span>
</div>
</div>
</div>
<!-- 最终价格 -->
<div class="settlement-section final-price-section">
<div class="settlement-footer">
<div class="footer-price">
<span class="footer-price-value">¥{{ actualPrice.toFixed(2) }}</span>
<span class="footer-price-value discount">共减¥{{ discountAmount.toFixed(2) }}</span>
</div>
<div class="footer-buttons">
<a-button type="primary" @click="submit" class="submit-btn">确认订单并支付</a-button>
</div>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import type { Address, Product } from '@/Util/Type'
import AddressComponent from '@/Component/common/Address.vue';
// Props
const props = defineProps({
products: {
type: Array as () => Product[],
default: () => []
}
})
const emit = defineEmits(['submit', 'update:visible'])
// 响应式数据
const visible = ref(false)
const addresses = ref<Address[]>([
{
id: 1,
name: '张三',
phone: '138****8888',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '三里屯SOHO A座 2001室',
isDefault: true
},
{
id: 1,
name: '张三',
phone: '138****8888',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '三里屯SOHO A座 2001室',
isDefault: false
}
])
const invoiceType = ref('personal')
const companyName = ref('')
const taxNumber = ref('')
const selectedPayment = ref('wechat')
const orderRemark = ref('')
// 地址弹窗显示状态
const showAddressModal = ref(false)
// 地址组件显示状态
const showAddressComponent = ref(false)
//省市区拼接
const address = computed(() => {
if (newAddressForm.value.province && newAddressForm.value.city && newAddressForm.value.district) {
return newAddressForm.value.province + ' / ' + newAddressForm.value.city + ' / ' + newAddressForm.value.district
}
return ''
})
// 地址弹窗相关数据
const activeAddressTab = ref('list') // 'list' 或 'add'
const newAddressForm = ref({
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: false
})
// 方法 选择isDefault为true的数据
const selectedAddress = ref<Address>(addresses.value.find(addr => addr.isDefault) as Address)
// 地址组件选择地址后,更新表单数据
const handleAddressChange = (address: Address) => {
newAddressForm.value.province = address.province
newAddressForm.value.city = address.city
newAddressForm.value.district = address.district
console.log('选择的地址:', address)
}
const switchAddressTab = (tab: string) => {
activeAddressTab.value = tab
if (tab === 'add') {
// 重置表单
newAddressForm.value = {
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: false
}
}
}
const selectAddress = (address: Address) => {
showAddressModal.value = false
}
const afterCloseAddressModal = () => {
// 重置状态
activeAddressTab.value = 'list'
newAddressForm.value = {
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: false
}
}
// 计算属性
const totalPrice = computed(() => {
return props.products.reduce((sum, product) => sum + product.price * product.quantity, 0)
})
const shippingFee = computed(() => {
return totalPrice.value >= 99 ? 0 : 10
})
const discountAmount = computed(() => {
return Math.min(totalPrice.value * 0.1, 50)
})
const actualPrice = computed(() => {
return totalPrice.value + shippingFee.value - discountAmount.value
})
const submit = () => {
console.log('确认结算')
console.log('选中的地址:', addresses.value)
console.log('选中的支付方式:', selectedPayment.value)
console.log('订单备注:', orderRemark.value)
console.log('实付金额:', actualPrice.value)
// 发送结算事件
emit('submit', {
address: addresses.value,
payment: selectedPayment.value,
remark: orderRemark.value,
totalAmount: actualPrice.value
})
// 关闭弹窗
emit('update:visible', false)
}
const cancel = () => {
emit('update:visible', false)
}
const afterClose = () => {
// 重置表单
companyName.value = ''
taxNumber.value = ''
orderRemark.value = ''
console.log('afterClose')
console.log('visible.value:', visible.value)
emit('update:visible', false)
}
// 监听商品变化
watch(() => props.products, (newProducts) => {
if (newProducts.length > 0) {
visible.value = true
}
}, { deep: true })
// 监听可见性变化
watch(() => visible.value, (newVal) => {
visible.value = newVal
})
</script>
<style scoped>
.settlement-modal {
.ant-modal-content {
border-radius: var(--border-radius-md);
}
.ant-modal-title {
font-size: 18px;
font-weight: 600;
color: var(--text-dark);
}
}
.settlement-container {
display: flex;
gap: var(--spacing-lg);
background-color: var(--bg-white);
}
/* 左侧结算列表 */
.settlement-left {
flex: 1;
min-width: 0;
max-height: 550px;
overflow-y: auto;
}
/* 右侧结算信息 */
.settlement-right {
width: 350px;
min-width: 300px;
}
/* 通用区块样式 */
.settlement-section {
margin: var(--spacing-md) 0;
padding: var(--spacing-md);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--text-dark);
margin: 0 0 var(--spacing-md);
padding-bottom: var(--spacing-xs);
border-bottom: 1px solid var(--border-light);
}
/* 地址区块 */
.address-item {
padding: var(--spacing-md);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-sm);
margin-bottom: var(--spacing-sm);
cursor: pointer;
transition: all var(--transition-normal) ease;
}
.address-item:hover {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.address-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.address-name {
font-weight: 600;
color: var(--text-dark);
}
.address-phone {
color: var(--text-secondary);
}
.address-tag {
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-full);
font-size: 12px;
font-weight: 500;
}
.address-tag.default {
background-color: var(--primary-color);
color: var(--bg-white);
}
.address-detail {
color: var(--text-secondary);
font-size: 14px;
line-height: 1.4;
}
.add-address-btn {
padding: var(--spacing-md);
border: 1px dashed var(--border-light);
border-radius: var(--border-radius-sm);
text-align: center;
color: var(--primary-color);
cursor: pointer;
transition: all var(--transition-normal) ease;
}
.add-address-btn:hover {
border-color: var(--primary-color);
background-color: var(--primary-light);
}
/* 地址弹窗样式 */
.address-nav {
display: flex;
margin-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border-light);
}
.nav-item {
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
font-size: 14px;
color: var(--text-secondary);
border-bottom: 2px solid transparent;
transition: all var(--transition-normal) ease;
}
.nav-item:hover {
color: var(--primary-color);
}
.nav-item.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.address-list {
max-height: 400px;
overflow-y: auto;
}
.address-list .address-item {
margin-bottom: var(--spacing-sm);
}
.empty-address {
padding: var(--spacing-xl) 0;
text-align: center;
color: var(--text-tertiary);
}
/* 添加地址表单样式 */
.add-address-form {
padding: var(--spacing-sm) 0;
}
.form-item {
margin-bottom: var(--spacing-md);
}
.form-item label {
display: block;
margin-bottom: var(--spacing-xs);
font-size: 14px;
font-weight: 500;
color: var(--text-dark);
}
.form-item input[type="text"],
.form-item input[type="tel"],
.form-item select,
.form-item textarea {
width: 100%;
padding: var(--spacing-sm);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-sm);
font-size: 14px;
transition: all var(--transition-normal) ease;
background-color: var(--bg-white);
}
.form-item input[type="text"]:focus,
.form-item input[type="tel"]:focus,
.form-item select:focus,
.form-item textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.form-item.checkbox-item {
display: flex;
align-items: center;
}
.form-item.checkbox-item label {
display: flex;
align-items: center;
gap: var(--spacing-xs);
cursor: pointer;
font-weight: 400;
}
.form-item.checkbox-item input[type="checkbox"] {
width: auto;
}
.form-actions {
display: flex;
gap: var(--spacing-sm);
justify-content: flex-end;
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-light);
}
.form-actions button {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-sm);
font-size: 14px;
cursor: pointer;
transition: all var(--transition-normal) ease;
}
.cancel-btn {
background-color: var(--bg-white);
color: var(--text-secondary);
}
.cancel-btn:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
.save-btn {
background-color: var(--primary-color);
color: var(--bg-white);
border-color: var(--primary-color);
}
.save-btn:hover {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
}
/* 商品区块 */
.goods-item {
display: flex;
gap: var(--spacing-md);
padding: var(--spacing-md);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-sm);
margin-bottom: var(--spacing-sm);
transition: all var(--transition-normal) ease;
}
.goods-item:hover {
box-shadow: var(--shadow-sm);
}
.goods-image {
width: 80px;
height: 80px;
border-radius: var(--border-radius-sm);
overflow: hidden;
flex-shrink: 0;
}
.goods-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.goods-info {
flex: 1;
min-width: 0;
}
.goods-name {
font-size: 14px;
font-weight: 600;
color: var(--text-dark);
margin: 0 0 var(--spacing-xs);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.goods-desc {
font-size: 12px;
color: var(--text-secondary);
margin: 0 0 var(--spacing-xs);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.goods-price-quantity {
display: flex;
justify-content: space-between;
align-items: center;
}
.goods-price {
font-size: 14px;
font-weight: 600;
color: var(--error-color);
}
.goods-quantity {
font-size: 14px;
color: var(--text-secondary);
}
/* 价格区块 */
.price-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xs);
font-size: 14px;
}
.price-label {
color: var(--text-secondary);
}
.price-value {
color: var(--text-dark);
}
.price-item.discount .price-value {
color: var(--success-color);
}
.price-item.total {
margin-top: var(--spacing-sm);
padding-top: var(--spacing-sm);
border-top: 1px solid var(--border-light);
font-weight: 600;
}
.price-item.total .price-value {
color: var(--error-color);
font-size: 16px;
}
/* 发票区块 */
.invoice-type {
display: flex;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-md);
}
.invoice-type label {
display: flex;
align-items: center;
gap: var(--spacing-xs);
cursor: pointer;
}
.company-info {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.company-info input {
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-sm);
font-size: 14px;
}
.company-info input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
/* 支付方式区块 */
.payment-methods {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.payment-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-sm);
cursor: pointer;
transition: all var(--transition-normal) ease;
}
.payment-item:hover {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.payment-item.active {
border-color: var(--primary-color);
background-color: var(--primary-light);
}
.payment-icon {
width: 24px;
height: 24px;
border-radius: var(--border-radius-sm);
background-color: var(--bg-light);
}
.balance-icon {
background-color: var(--primary-color);
}
.bank-icon {
background-color: var(--info-color);
}
.wechat-icon {
background-color: var(--success-color);
}
.alipay-icon {
background-color: var(--warning-color);
}
/* 订单备注区块 */
.remark-section textarea {
width: 100%;
padding: var(--spacing-sm);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-sm);
font-size: 14px;
resize: vertical;
}
.remark-section textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
/* 最终价格区块 */
.final-price {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md);
background-color: var(--bg-light);
border-radius: var(--border-radius-sm);
}
.final-price-label {
font-weight: 600;
color: var(--text-dark);
}
.final-price-value {
font-size: 18px;
font-weight: 700;
color: var(--error-color);
}
/* 最终价格区块样式 */
.settlement-section.final-price-section {
padding: 0;
margin: 8px 0;
position: absolute;
bottom: var(--spacing-md);
}
/* 底部区块 */
.settlement-footer {
display: flex;
flex-direction: column;
width: 100%;
padding: 0 25px;
}
.footer-price {
display: flex;
align-items: flex-end;
gap: var(--spacing-xs);
}
.footer-price-value {
margin-left: 10px;
font-size: 34px;
font-weight: 700;
color: var(--error-color);
}
.footer-price-value.discount {
margin-left: 0;
font-size: 18px;
padding: var(--spacing-xs);
color: var(--success-color);
}
.footer-buttons {
display: flex;
gap: var(--spacing-sm);
width: 100%;
}
.submit-btn {
min-width: 300px;
height: 48px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.settlement-container {
flex-direction: column;
}
.settlement-right {
width: 100%;
min-width: unset;
}
.settlement-footer {
flex-direction: column;
gap: var(--spacing-sm);
align-items: stretch;
}
.footer-buttons {
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,379 @@
<!-- 地址组件 当visible为true时 才会显示类似于弹窗 >> 只有选择前一个后一个选项才会显示 -->
<!-- 省市县选择器 返回选中的省市县信息 -->
<template>
<a-modal :visible.sync="visible" title="选择地址" width="300px" :close-on-click-modal="false" :after-close="close" :footer-extra="null" :footer="null">
<div class="address-form-content">
<!-- 上下结构 -->
<div class="form-tab">
<!-- 选项卡 >>县只有选择前一个后一个选项才会显示 -->
<div class="tab-item" :class="{ active: activeTab === 'province' }">
省份
</div>
<div class="tab-item" :class="{ active: activeTab === 'city', disabled: !selectedProvince }">
城市
</div>
<div class="tab-item" :class="{ active: activeTab === 'district', disabled: !selectedCity }">
区县
</div>
</div>
<div class="form-content">
<!-- 选项卡内容 根据选项卡显示不同的内容 -->
<div v-if="activeTab === 'province'" class="tab-content">
<div class="region-item" v-for="province in provinces" :key="province.value"
@click="selectProvince(province)">
{{ province.label }}
</div>
</div>
<div v-else-if="activeTab === 'city'" class="tab-content">
<div class="region-item" v-for="city in cities" :key="city.value" @click="selectCity(city)">
{{ city.label }}
</div>
</div>
<div v-else-if="activeTab === 'district'" class="tab-content">
<div class="region-item" v-for="district in districts" :key="district.value"
@click="selectDistrict(district)">
{{ district.label }}
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible', 'address'])
// 响应式数据
const activeTab = ref('province')
const selectedProvince = ref('')
const selectedCity = ref('')
const selectedDistrict = ref('')
// 省份数据
const provinces = ref([
{ value: '北京市', label: '北京市' },
{ value: '上海市', label: '上海市' },
{ value: '广东省', label: '广东省' },
{ value: '江苏省', label: '江苏省' },
{ value: '浙江省', label: '浙江省' },
{ value: '四川省', label: '四川省' },
{ value: '湖北省', label: '湖北省' },
{ value: '湖南省', label: '湖南省' },
{ value: '山东省', label: '山东省' },
{ value: '河南省', label: '河南省' }
])
// 城市数据(根据省份动态生成)
const cities = computed(() => {
const cityMap: Record<string, Array<{ value: string; label: string }>> = {
'北京市': [{ value: '北京市', label: '北京市' }],
'上海市': [{ value: '上海市', label: '上海市' }],
'广东省': [
{ value: '广州市', label: '广州市' },
{ value: '深圳市', label: '深圳市' },
{ value: '东莞市', label: '东莞市' },
{ value: '佛山市', label: '佛山市' },
{ value: '珠海市', label: '珠海市' }
],
'江苏省': [
{ value: '南京市', label: '南京市' },
{ value: '苏州市', label: '苏州市' },
{ value: '无锡市', label: '无锡市' },
{ value: '常州市', label: '常州市' },
{ value: '南通市', label: '南通市' }
],
'浙江省': [
{ value: '杭州市', label: '杭州市' },
{ value: '宁波市', label: '宁波市' },
{ value: '温州市', label: '温州市' },
{ value: '嘉兴市', label: '嘉兴市' },
{ value: '湖州市', label: '湖州市' }
],
'四川省': [
{ value: '成都市', label: '成都市' },
{ value: '绵阳市', label: '绵阳市' },
{ value: '德阳市', label: '德阳市' },
{ value: '自贡市', label: '自贡市' },
{ value: '泸州市', label: '泸州市' }
],
'湖北省': [
{ value: '武汉市', label: '武汉市' },
{ value: '黄石市', label: '黄石市' },
{ value: '十堰市', label: '十堰市' },
{ value: '宜昌市', label: '宜昌市' },
{ value: '襄阳市', label: '襄阳市' }
],
'湖南省': [
{ value: '长沙市', label: '长沙市' },
{ value: '株洲市', label: '株洲市' },
{ value: '湘潭市', label: '湘潭市' },
{ value: '衡阳市', label: '衡阳市' },
{ value: '邵阳市', label: '邵阳市' }
],
'山东省': [
{ value: '济南市', label: '济南市' },
{ value: '青岛市', label: '青岛市' },
{ value: '烟台市', label: '烟台市' },
{ value: '潍坊市', label: '潍坊市' },
{ value: '临沂市', label: '临沂市' }
],
'河南省': [
{ value: '郑州市', label: '郑州市' },
{ value: '开封市', label: '开封市' },
{ value: '洛阳市', label: '洛阳市' },
{ value: '平顶山市', label: '平顶山市' },
{ value: '安阳市', label: '安阳市' }
]
}
return cityMap[selectedProvince.value] || []
})
// 区县数据(根据城市动态生成)
const districts = computed(() => {
const districtMap: Record<string, Array<{ value: string; label: string }>> = {
'北京市': [
{ value: '东城区', label: '东城区' },
{ value: '西城区', label: '西城区' },
{ value: '朝阳区', label: '朝阳区' },
{ value: '海淀区', label: '海淀区' },
{ value: '丰台区', label: '丰台区' }
],
'上海市': [
{ value: '黄浦区', label: '黄浦区' },
{ value: '徐汇区', label: '徐汇区' },
{ value: '长宁区', label: '长宁区' },
{ value: '静安区', label: '静安区' },
{ value: '普陀区', label: '普陀区' }
],
'广州市': [
{ value: '天河区', label: '天河区' },
{ value: '越秀区', label: '越秀区' },
{ value: '海珠区', label: '海珠区' },
{ value: '荔湾区', label: '荔湾区' },
{ value: '白云区', label: '白云区' }
],
'深圳市': [
{ value: '福田区', label: '福田区' },
{ value: '罗湖区', label: '罗湖区' },
{ value: '南山区', label: '南山区' },
{ value: '宝安区', label: '宝安区' },
{ value: '龙岗区', label: '龙岗区' }
],
'南京市': [
{ value: '玄武区', label: '玄武区' },
{ value: '秦淮区', label: '秦淮区' },
{ value: '建邺区', label: '建邺区' },
{ value: '鼓楼区', label: '鼓楼区' },
{ value: '浦口区', label: '浦口区' }
],
'苏州市': [
{ value: '姑苏区', label: '姑苏区' },
{ value: '虎丘区', label: '虎丘区' },
{ value: '吴中区', label: '吴中区' },
{ value: '相城区', label: '相城区' },
{ value: '吴江区', label: '吴江区' }
],
'杭州市': [
{ value: '上城区', label: '上城区' },
{ value: '下城区', label: '下城区' },
{ value: '江干区', label: '江干区' },
{ value: '拱墅区', label: '拱墅区' },
{ value: '西湖区', label: '西湖区' }
],
'宁波市': [
{ value: '海曙区', label: '海曙区' },
{ value: '江北区', label: '江北区' },
{ value: '北仑区', label: '北仑区' },
{ value: '镇海区', label: '镇海区' },
{ value: '鄞州区', label: '鄞州区' }
],
'成都市': [
{ value: '锦江区', label: '锦江区' },
{ value: '青羊区', label: '青羊区' },
{ value: '金牛区', label: '金牛区' },
{ value: '武侯区', label: '武侯区' },
{ value: '成华区', label: '成华区' }
],
'武汉市': [
{ value: '江岸区', label: '江岸区' },
{ value: '江汉区', label: '江汉区' },
{ value: '硚口区', label: '硚口区' },
{ value: '汉阳区', label: '汉阳区' },
{ value: '武昌区', label: '武昌区' }
],
'长沙市': [
{ value: '芙蓉区', label: '芙蓉区' },
{ value: '天心区', label: '天心区' },
{ value: '岳麓区', label: '岳麓区' },
{ value: '开福区', label: '开福区' },
{ value: '雨花区', label: '雨花区' }
],
'济南市': [
{ value: '历下区', label: '历下区' },
{ value: '市中区', label: '市中区' },
{ value: '槐荫区', label: '槐荫区' },
{ value: '天桥区', label: '天桥区' },
{ value: '历城区', label: '历城区' }
],
'郑州市': [
{ value: '中原区', label: '中原区' },
{ value: '二七区', label: '二七区' },
{ value: '管城回族区', label: '管城回族区' },
{ value: '金水区', label: '金水区' },
{ value: '上街区', label: '上街区' }
]
}
return districtMap[selectedCity.value] || []
})
// 方法
const selectProvince = (province: { value: string; label: string }) => {
selectedProvince.value = province.value
selectedCity.value = ''
selectedDistrict.value = ''
activeTab.value = 'city'
}
const selectCity = (city: { value: string; label: string }) => {
selectedCity.value = city.value
selectedDistrict.value = ''
activeTab.value = 'district'
}
const selectDistrict = (district: { value: string; label: string }) => {
selectedDistrict.value = district.value
// 完成选择,提交结果
emit('address', {
province: selectedProvince.value,
city: selectedCity.value,
district: selectedDistrict.value
})
close()
}
const close = () => {
// 重置状态
activeTab.value = 'province'
selectedProvince.value = ''
selectedCity.value = ''
selectedDistrict.value = ''
emit('update:visible', false)
}
</script>
<style scoped>
.address-form-content {
background-color: white;
border-radius: 16px 16px 0 0;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #333;
}
.form-tab {
display: flex;
border-bottom: 1px solid #e8e8e8;
}
.tab-item {
flex: 1;
padding: 14px 0;
text-align: center;
font-size: 14px;
color: #666;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.3s ease;
}
.tab-item.active {
color: #1890ff;
border-bottom-color: #1890ff;
}
.tab-item.disabled {
color: #ccc;
cursor: not-allowed;
}
.form-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.tab-content {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.region-item {
padding: 12px;
text-align: center;
border: 1px solid #e8e8e8;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.region-item:hover {
border-color: #1890ff;
color: #1890ff;
}
/* 响应式设计 */
@media (max-width: 768px) {
.tab-content {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.tab-content {
grid-template-columns: 1fr;
}
.form-content {
padding: 12px;
}
.region-item {
padding: 10px;
}
}
</style>

View File

@@ -0,0 +1,444 @@
<!-- 搜索组件 -->
<template>
<div class="search-component">
<!-- 搜索类型选择 -->
<div v-if="false" class="search-type-selector">
<ul>
<li class="search-type-item" :class="{ active: currentSearchType === '宝贝' }"
@click="setSearchType('宝贝', $event)">
宝贝
</li>
<li class="search-type-item" :class="{ active: currentSearchType === '店铺' }"
@click="setSearchType('店铺', $event)">
店铺
</li>
</ul>
</div>
<!-- 搜索输入区域 -->
<div class="search-input-wrapper">
<div class="search-input-prefix">
<i class="iconfont icon-sousuo prefix-icon"></i>
</div>
<Input v-model:value="searchValue" placeholder="商品昵称/商品编号/订单编号" style="width: 100%" @search="onSearch"
@focus="onInputFocus" @blur="onInputBlur" class="custom-search-input" />
<div class="search-input-suffix" v-if="searchValue">
<i class="iconfont icon-guanbi" @click="clearSearch"></i>
</div>
</div>
<!-- 搜索按钮 -->
<div class="search-button-container" @click="onSearch()">
<span class="search-button-text">搜索</span>
<div class="search-button-icon">
<i class="iconfont icon-sousuo"></i>
</div>
</div>
<!-- 搜索历史记录 -->
<div class="search-history-container" v-if="showHistory">
<div class="search-history-header">
<span class="history-title">搜索历史</span>
<a class="clear-history" @click="clearHistory" href="javascript:void(0)">
清空历史
</a>
</div>
<div class="search-history-tags" v-if="historyList.length > 0">
<Tag v-for="(item, index) in historyList" :key="index" class="history-tag"
@click="onClickHistory(item)">
{{ item }}
</Tag>
</div>
<div class="no-history" v-else>
暂无搜索历史
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Input, Tag } from 'ant-design-vue'
const router = useRouter()
const emit = defineEmits(['search'])
// 搜索关键词
const searchValue = ref('')
// 控制历史记录显示状态
const showHistory = ref(false)
// 标志:是否正在清空历史记录
const isClearingHistory = ref(false)
// 当前搜索类型
const currentSearchType = ref('宝贝')
// 搜索历史记录
const historyList = ref(['手机', '电脑', '耳机', '键盘', '鼠标'])
// 搜索事件
const onSearch = (value?: string) => {
const keyword = value || searchValue.value
if (!keyword.trim()) return
console.log('搜索关键词:', keyword)
console.log('搜索类型:', currentSearchType.value)
// 添加到历史记录
// if (!historyList.value.includes(keyword)) {
// historyList.value.unshift(keyword)
// // 限制历史记录数量
// if (historyList.value.length > 10) {
// historyList.value.pop()
// }
// }
// 跳转到搜索结果页面
// router.push({
// name: 'search',
// query: {
// keyword: keyword,
// type: currentSearchType.value
// }
// })
emit('search', keyword)
}
// 搜索框获得焦点
const onInputFocus = () => {
showHistory.value = true
}
// 搜索框失去焦点
const onInputBlur = () => {
// 使用setTimeout确保点击历史记录的事件能先执行
setTimeout(() => {
// 如果正在清空历史记录,则不隐藏历史记录容器
if (!isClearingHistory.value) {
showHistory.value = false
}
}, 200)
}
// 设置搜索类型
const setSearchType = (type: string, event: MouseEvent) => {
const typeItems = document.querySelectorAll('.search-type-item')
// 找到当前活跃的元素
const currentActiveItem = document.querySelector('.search-type-item.active')
// 保存点击的元素引用
const clickedTarget = event.currentTarget as HTMLElement
// 为当前活跃的元素添加消失动画
if (currentActiveItem && currentActiveItem !== clickedTarget) {
if (currentSearchType.value === '宝贝' && type === '店铺') {
// 宝贝 -> 店铺:宝贝元素从左往右消失
currentActiveItem.classList.add('animate-right-out')
} else if (currentSearchType.value === '店铺' && type === '宝贝') {
// 店铺 -> 宝贝:店铺元素从右往左消失
currentActiveItem.classList.add('animate-left-out')
}
// 等待消失动画完成后再继续
setTimeout(() => {
// 移除所有元素的类
typeItems.forEach(item => {
item.classList.remove('active', 'animate-left', 'animate-right', 'animate-left-out', 'animate-right-out')
})
// 添加active类到点击的元素
if (clickedTarget) {
// 判断切换方向
if (currentSearchType.value === '宝贝' && type === '店铺') {
// 宝贝 -> 店铺:从左往右出现
clickedTarget.classList.add('active', 'animate-right')
} else if (currentSearchType.value === '店铺' && type === '宝贝') {
// 店铺 -> 宝贝:从右往左出现
clickedTarget.classList.add('active', 'animate-left')
} else {
// 默认动画
clickedTarget.classList.add('active')
}
currentSearchType.value = type
}
console.log('搜索类型:', type)
}, 300)
} else {
// 如果点击的是当前活跃的元素,直接返回
return
}
}
// 清除搜索输入
const clearSearch = () => {
searchValue.value = ''
console.log('已清除搜索输入')
}
// 点击历史记录
const onClickHistory = (item: string) => {
searchValue.value = item
onSearch(item)
}
// 清空历史记录
const clearHistory = () => {
isClearingHistory.value = true
historyList.value = []
// 300ms后重置标志确保onInputBlur的200ms延迟执行完毕
setTimeout(() => {
isClearingHistory.value = false
}, 300)
console.log('已清空搜索历史')
}
</script>
<style scoped>
.search-component {
display: flex;
width: 100%;
}
/* 搜索类型选择 */
.search-type-selector {
margin-bottom: 10px;
}
.search-type-selector ul {
display: flex;
list-style: none;
padding: 0;
margin: 0;
}
.search-type-item {
padding: 8px 16px;
cursor: pointer;
position: relative;
transition: all 0.3s ease;
border-radius: var(--border-radius-md);
margin-right: 10px;
}
.search-type-item:hover {
background-color: var(--bg-light);
}
.search-type-item.active {
color: var(--primary-color);
font-weight: 600;
background-color: var(--primary-light);
}
/* 动画效果 */
.animate-left {
animation: slideInLeft 0.3s ease-in-out;
}
.animate-right {
animation: slideInRight 0.3s ease-in-out;
}
.animate-left-out {
animation: slideOutLeft 0.3s ease-in-out;
}
.animate-right-out {
animation: slideOutRight 0.3s ease-in-out;
}
@keyframes slideInLeft {
from {
transform: translateX(-20px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideInRight {
from {
transform: translateX(20px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutLeft {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(-20px);
opacity: 0;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(20px);
opacity: 0;
}
}
/* 搜索输入区域 */
.search-input-wrapper {
position: relative;
display: flex;
align-items: center;
background-color: var(--bg-white);
border: 2px solid var(--border-light);
border-right: none;
border-radius: var(--border-radius-md) 0 0 var(--border-radius-md);
padding: 0 12px 0 5px;
transition: all 0.3s ease;
}
.search-input-wrapper:focus-within {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.search-input-prefix {
margin-right: 8px;
color: var(--text-secondary);
}
.prefix-icon {
font-size: 16px;
}
.custom-search-input {
border: none !important;
box-shadow: none !important;
}
.custom-search-input:focus {
border: none !important;
box-shadow: none !important;
}
.search-input-suffix {
margin-left: 8px;
color: var(--text-secondary);
cursor: pointer;
transition: color 0.3s ease;
}
.search-input-suffix:hover {
color: var(--primary-color);
}
/* 搜索按钮 */
.search-button-container {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
color: var(--bg-white);
border: none;
border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0;
padding: 12px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.search-button-container:hover {
color: white;
}
.search-button-text {
}
.search-button-icon {
font-size: 16px;
}
/* 搜索历史记录 */
.search-history-container {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--bg-white);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-md);
padding: 16px;
margin-top: 4px;
z-index: 100;
}
.search-history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.history-title {
font-weight: 600;
color: var(--text-dark);
}
.clear-history {
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
transition: color 0.3s ease;
}
.clear-history:hover {
color: var(--primary-color);
}
.search-history-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.history-tag {
cursor: pointer;
transition: all 0.3s ease;
}
.history-tag:hover {
background-color: var(--primary-light) !important;
color: var(--primary-color) !important;
}
.no-history {
text-align: center;
color: var(--text-secondary);
padding: 20px 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.search-type-selector {
flex-wrap: wrap;
}
.search-type-item {
margin-bottom: 8px;
}
.search-button-container {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,63 @@
<!-- 分页组件 -->
<template>
<div class="pagination">
<ul>
<li v-for="page in totalPages" :key="page">
<a href="#" @click="changePage(page)">{{ page }}</a>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps({
totalPages: {
type: Number,
default: 1
}
})
const emit = defineEmits(['changePage'])
const changePage = (page: number) => {
emit('changePage', page)
}
</script>
<style scoped>
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: var(--spacing-md);
}
.pagination ul {
display: flex;
justify-content: center;
align-items: center;
list-style: none;
}
.pagination ul li {
margin: 0 var(--spacing-xs);
}
.pagination ul li a {
display: block;
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--border-light);
border-radius: var(--border-radius);
font-size: 14px;
font-weight: 600;
color: var(--primary-color);
transition: all var(--transition-normal) ease;
}
.pagination ul li a:hover {
background-color: var(--primary-color);
color: var(--text-color);
}
</style>

View File

@@ -6,7 +6,6 @@ import Login from '../Views/Login.vue'
import Search from '../Views/Search.vue' import Search from '../Views/Search.vue'
import ProductDetail from '../Views/product/productdetil.vue' import ProductDetail from '../Views/product/productdetil.vue'
import Cart from '../Views/Cart.vue' import Cart from '../Views/Cart.vue'
import Order from '../Views/Order.vue'
import User from '../Views/User/User.vue' import User from '../Views/User/User.vue'
import Chat from '../Views/Chat.vue' import Chat from '../Views/Chat.vue'
@@ -38,11 +37,6 @@ const routes = [
name: 'cart', name: 'cart',
component: Cart component: Cart
}, },
{
path: '/order',
name: 'order',
component: Order
},
{ {
path: '/user', path: '/user',
name: 'user', name: 'user',

View File

@@ -25,19 +25,31 @@ export type Cart = {
// 商品类型 // 商品类型
export type Product = { export type Product = {
id: string id: string
name: string name: string; // 商品名称
price: string description: string; // 商品描述
img: string price: number; // 商品价格
model: string quantity: number; // 商品数量
image: string; // 商品图片
model: string; // 商品型号
} }
// 订单商品类型
// 订单详情类型 export type OrderProduct = {
productId: string | number // 商品 ID
quantity: number // 商品数量
// 其他待补充字段,如型号、颜色等
}
// 订单类型
export type OrderDetail = { export type OrderDetail = {
id: string id: string // 订单 ID
name: string shopName: string // 店铺名称
price: string receiverName: string // 收货人
img: string orderId: string // 订单 ID
count: number createTime: string // 创建时间
status: string // 订单状态
statusText: string // 订单状态文本
totalAmount: number // 订单总金额
paymentMethod: string // 支付方式
goods: Product[] // 商品列表
} }
// 注册类型 // 注册类型
export type Register = { export type Register = {
@@ -49,4 +61,16 @@ export type Register = {
export type Login = { export type Login = {
username: string username: string
password: string password: string
rememberMe: boolean
}
// 地址类型
export type Address = {
id: number; // 修改时有 ID新增时无
name: string; // 收货人
phone: string; // 手机号
province: string; // 省
city: string; // 市
district: string; // 区
detail: string; // 详细地址(街道、门牌号等)
isDefault: boolean; // 是否默认地址
} }

View File

@@ -580,17 +580,17 @@ const removeItem = (id: number) => {
} }
.cart-item-model { .cart-item-model {
font-size: 16px; font-size: var(--font-size-base);
padding: 18px 32px; padding: var(--spacing-lg) var(--spacing-xl);
color: #666; color: var(--text-secondary);
border-radius: 18px; border-radius: var(--border-radius-lg);
cursor: pointer; cursor: pointer;
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background: linear-gradient(135deg, #fafafa 0%, #ffffff 100%); background: linear-gradient(135deg, var(--bg-light) 0%, var(--card-bg) 100%);
border: 2px solid #f0f0f0; border: 2px solid var(--border-color);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: var(--shadow-md);
font-weight: 500; font-weight: 500;
} }
@@ -601,8 +601,8 @@ const removeItem = (id: number) => {
left: -100%; left: -100%;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 80, 0, 0.15), transparent); background: linear-gradient(90deg, transparent, rgba(var(--primary-light-rgb), 0.15), transparent);
transition: left 0.6s ease; transition: left var(--transition-slow);
z-index: 0; z-index: 0;
} }
@@ -613,18 +613,18 @@ const removeItem = (id: number) => {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: linear-gradient(135deg, rgba(255, 80, 0, 0.08) 0%, rgba(255, 80, 0, 0.04) 100%); background: linear-gradient(135deg, rgba(var(--primary-light-rgb), 0.08) 0%, rgba(var(--primary-light-rgb), 0.04) 100%);
opacity: 0; opacity: 0;
transition: opacity 0.4s ease; transition: opacity var(--transition-transform);
z-index: -1; z-index: -1;
border-radius: 18px; border-radius: var(--border-radius-lg);
} }
.cart-item-model:hover { .cart-item-model:hover {
border: 2px dashed #ff5000; border: 2px dashed var(--primary-color);
color: #ff5000; color: var(--primary-color);
transform: translateY(-5px) scale(1.05); transform: translateY(-5px) scale(1.05);
box-shadow: 0 10px 24px rgba(255, 80, 0, 0.2); box-shadow: 0 10px 24px rgba(var(--primary-light-rgb), 0.2);
} }
.cart-item-model:hover::before { .cart-item-model:hover::before {
@@ -659,13 +659,13 @@ const removeItem = (id: number) => {
width: 200px; width: 200px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: var(--spacing-md);
padding: 15px; padding: var(--spacing-md);
background: linear-gradient(135deg, #fafafa 0%, #ffffff 100%); background: linear-gradient(135deg, var(--bg-light) 0%, var(--card-bg) 100%);
border-radius: 20px; border-radius: var(--border-radius-xl);
border: 2px solid #f0f0f0; border: 2px solid var(--border-color);
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: var(--shadow-md);
position: relative; position: relative;
} }
@@ -676,16 +676,16 @@ const removeItem = (id: number) => {
left: -2px; left: -2px;
right: -2px; right: -2px;
bottom: -2px; bottom: -2px;
border-radius: 20px; border-radius: var(--border-radius-xl);
background: linear-gradient(135deg, #ff5000, #ff8500, #ff5000); background: linear-gradient(135deg, var(--primary-color), var(--primary-hover), var(--primary-color));
z-index: -1; z-index: -1;
opacity: 0; opacity: 0;
transition: opacity 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: opacity var(--transition-transform);
} }
.cart-item:hover .cart-item-price { .cart-item:hover .cart-item-price {
border-color: #ff5000; border-color: var(--primary-color);
box-shadow: 0 10px 30px rgba(255, 80, 0, 0.2); box-shadow: 0 10px 30px rgba(var(--primary-light-rgb), 0.2);
transform: translateY(-4px); transform: translateY(-4px);
} }
@@ -718,25 +718,25 @@ const removeItem = (id: number) => {
} }
.price-label { .price-label {
font-size: 14px; font-size: var(--font-size-sm);
color: #666; color: var(--text-secondary);
font-weight: 600; font-weight: 600;
background: linear-gradient(135deg, #fff5f5 0%, #fff0f0 100%); background: linear-gradient(135deg, var(--bg-primary-light) 0%, var(--bg-primary-lighter) 100%);
padding: 8px 20px; padding: var(--spacing-xs) var(--spacing-lg);
border-radius: 20px; border-radius: var(--border-radius-xl);
align-self: flex-start; align-self: flex-start;
border: 1px solid #ffece5; border: 1px solid var(--border-color);
transition: all 0.3s ease; transition: all var(--transition-normal);
box-shadow: 0 3px 12px rgba(255, 80, 0, 0.1); box-shadow: 0 3px 12px rgba(var(--primary-light-rgb), 0.1);
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.cart-item:hover .price-label { .cart-item:hover .price-label {
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%); background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
color: #ffffff; color: #ffffff;
border-color: #ff5000; border-color: var(--primary-color);
box-shadow: 0 6px 20px rgba(255, 80, 0, 0.35); box-shadow: 0 6px 20px rgba(var(--primary-light-rgb), 0.35);
transform: scale(1.08); transform: scale(1.08);
animation: label-pulse 0.6s ease; animation: label-pulse 0.6s ease;
} }
@@ -751,10 +751,10 @@ const removeItem = (id: number) => {
} }
.price-symbol { .price-symbol {
font-size: 16px; font-size: var(--font-size-base);
color: #ff5000; color: var(--primary-color);
font-weight: 600; font-weight: 600;
transition: all 0.3s ease; transition: all var(--transition-normal);
} }
.cart-item:hover .price-symbol { .cart-item:hover .price-symbol {
@@ -762,42 +762,42 @@ const removeItem = (id: number) => {
} }
.price-value { .price-value {
font-size: 26px; font-size: var(--font-size-2xl);
color: #ff5000; color: var(--primary-color);
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
transition: all 0.3s ease; transition: all var(--transition-normal);
position: relative; position: relative;
text-shadow: 0 2px 4px rgba(255, 80, 0, 0.2); text-shadow: 0 2px 4px rgba(var(--primary-light-rgb), 0.2);
} }
.cart-item:hover .price-value { .cart-item:hover .price-value {
transform: scale(1.05); transform: scale(1.05);
text-shadow: 0 4px 8px rgba(255, 80, 0, 0.35); text-shadow: 0 4px 8px rgba(var(--primary-light-rgb), 0.35);
} }
.original-price .price-symbol { .original-price .price-symbol {
font-size: 13px; font-size: var(--font-size-xs);
color: #999; color: var(--text-tertiary);
} }
.original-price .price-value { .original-price .price-value {
font-size: 16px; font-size: var(--font-size-base);
color: #999; color: var(--text-tertiary);
text-decoration: line-through; text-decoration: line-through;
text-shadow: none; text-shadow: none;
} }
.discount-badge { .discount-badge {
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%); background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
color: #ffffff; color: #ffffff;
font-size: 11px; font-size: var(--font-size-xs);
font-weight: 600; font-weight: 600;
padding: 4px 12px; padding: var(--spacing-xs) var(--spacing-sm);
border-radius: 12px; border-radius: var(--border-radius-sm);
margin-left: 6px; margin-left: var(--spacing-xs);
box-shadow: 0 3px 6px rgba(255, 80, 0, 0.25); box-shadow: 0 3px 6px rgba(var(--primary-light-rgb), 0.25);
transition: all 0.3s ease; transition: all var(--transition-normal);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@@ -1006,12 +1006,12 @@ const removeItem = (id: number) => {
.cart-item-quantity { .cart-item-quantity {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: var(--spacing-sm);
background: linear-gradient(135deg, #fafafa 0%, #ffffff 100%); background: linear-gradient(135deg, var(--bg-light) 0%, var(--card-bg) 100%);
padding: 10px 20px; padding: var(--spacing-sm) var(--spacing-lg);
border-radius: 32px; border-radius: var(--border-radius-2xl);
border: 2px solid #f0f0f0; border: 2px solid var(--border-color);
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@@ -1023,13 +1023,13 @@ const removeItem = (id: number) => {
left: -100%; left: -100%;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 80, 0, 0.08), transparent); background: linear-gradient(90deg, transparent, rgba(var(--primary-light-rgb), 0.08), transparent);
transition: left 0.6s ease; transition: left var(--transition-slow);
} }
.cart-item:hover .cart-item-quantity { .cart-item:hover .cart-item-quantity {
border-color: #ff5000; border-color: var(--primary-color);
box-shadow: 0 6px 16px rgba(255, 80, 0, 0.15); box-shadow: 0 6px 16px rgba(var(--primary-light-rgb), 0.15);
transform: translateY(-2px); transform: translateY(-2px);
} }
@@ -1040,14 +1040,14 @@ const removeItem = (id: number) => {
.quantity-btn { .quantity-btn {
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 2px solid #f0f0f0; border: 2px solid var(--border-color);
background: linear-gradient(135deg, #ffffff 0%, #f8f8f8 100%); background: linear-gradient(135deg, var(--card-bg) 0%, var(--bg-light) 100%);
border-radius: 50%; border-radius: var(--border-radius-full);
cursor: pointer; cursor: pointer;
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
color: #666; color: var(--text-secondary);
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1063,9 +1063,9 @@ const removeItem = (id: number) => {
left: 50%; left: 50%;
width: 0; width: 0;
height: 0; height: 0;
background: linear-gradient(135deg, #ff5000 0%, #ff7300 100%); background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
border-radius: 50%; border-radius: var(--border-radius-full);
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 0; z-index: 0;
} }
@@ -1076,10 +1076,10 @@ const removeItem = (id: number) => {
} }
.quantity-btn:hover { .quantity-btn:hover {
border-color: #ff5000; border-color: var(--primary-color);
color: #ffffff; color: #ffffff;
transform: scale(1.15); transform: scale(1.15);
box-shadow: 0 6px 16px rgba(255, 80, 0, 0.35); box-shadow: 0 6px 16px rgba(var(--primary-light-rgb), 0.35);
} }
.quantity-btn:active { .quantity-btn:active {
@@ -1124,22 +1124,22 @@ const removeItem = (id: number) => {
width: 80px; width: 80px;
height: 40px; height: 40px;
text-align: center; text-align: center;
border: 2px solid #f0f0f0; border: 2px solid var(--border-color);
border-radius: 12px; border-radius: var(--border-radius-md);
font-size: 18px; font-size: var(--font-size-lg);
color: #333; color: var(--text-primary);
font-weight: 600; font-weight: 600;
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); background: linear-gradient(135deg, var(--card-bg) 0%, var(--bg-light) 100%);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05);
} }
.quantity-input:focus { .quantity-input:focus {
outline: none; outline: none;
border-color: #ff5000; border-color: var(--primary-color);
box-shadow: 0 0 0 4px rgba(255, 80, 0, 0.2), inset 0 2px 4px rgba(0, 0, 0, 0.08); box-shadow: 0 0 0 4px rgba(var(--primary-light-rgb), 0.2), inset 0 2px 4px rgba(0, 0, 0, 0.08);
transform: scale(1.1); transform: scale(1.1);
background: linear-gradient(135deg, #ffffff 0%, #fff5f5 100%); background: linear-gradient(135deg, var(--card-bg) 0%, var(--bg-primary-light) 100%);
} }
.quantity-input::-webkit-inner-spin-button, .quantity-input::-webkit-inner-spin-button,
@@ -1174,28 +1174,28 @@ const removeItem = (id: number) => {
width: 160px; width: 160px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: var(--spacing-md);
align-items: center; align-items: center;
padding: 10px 0; padding: var(--spacing-sm) 0;
} }
.cart-action-btn { .cart-action-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: var(--spacing-sm);
padding: 12px 30px; padding: var(--spacing-sm) var(--spacing-xl);
border-radius: 32px; border-radius: var(--border-radius-2xl);
font-size: 15px; font-size: var(--font-size-base);
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
border: 2px solid transparent; border: 2px solid transparent;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
text-decoration: none; text-decoration: none;
min-width: 120px; min-width: 120px;
justify-content: center; justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); box-shadow: var(--shadow-sm);
} }
.cart-action-btn::before { .cart-action-btn::before {
@@ -1205,8 +1205,8 @@ const removeItem = (id: number) => {
left: 50%; left: 50%;
width: 0; width: 0;
height: 0; height: 0;
border-radius: 50%; border-radius: var(--border-radius-full);
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-slow);
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 0; z-index: 0;
} }
@@ -1218,7 +1218,7 @@ const removeItem = (id: number) => {
.cart-action-btn:hover { .cart-action-btn:hover {
transform: translateY(-3px) scale(1.05); transform: translateY(-3px) scale(1.05);
box-shadow: 0 8px 24px rgba(255, 80, 0, 0.25); box-shadow: 0 8px 24px rgba(var(--primary-light-rgb), 0.25);
} }
.cart-action-btn:active { .cart-action-btn:active {
@@ -1242,43 +1242,43 @@ const removeItem = (id: number) => {
} }
.delete-btn { .delete-btn {
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); background: linear-gradient(135deg, var(--card-bg) 0%, var(--bg-light) 100%);
border-color: #ff4d4f; border-color: var(--error-color);
color: #ff4d4f; color: var(--error-color);
} }
.delete-btn::before { .delete-btn::before {
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%); background: linear-gradient(135deg, var(--error-color) 0%, #ff7875 100%);
} }
.delete-btn:hover { .delete-btn:hover {
color: #ffffff; color: #ffffff;
border-color: #ff4d4f; border-color: var(--error-color);
} }
.favorite-btn { .favorite-btn {
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); background: linear-gradient(135deg, var(--card-bg) 0%, var(--bg-light) 100%);
border-color: #ff5000; border-color: var(--primary-color);
color: #ff5000; color: var(--primary-color);
} }
.favorite-btn::before { .favorite-btn::before {
background: linear-gradient(135deg, #ff5000 0%, #ff7300 100%); background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
} }
.favorite-btn:hover { .favorite-btn:hover {
color: #ffffff; color: #ffffff;
border-color: #ff5000; border-color: var(--primary-color);
} }
.favorite-btn.favorited { .favorite-btn.favorited {
background: linear-gradient(135deg, #ff5000 0%, #ff7300 100%); background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
color: #ffffff; color: #ffffff;
border-color: #ff5000; border-color: var(--primary-color);
} }
.favorite-btn.favorited:hover { .favorite-btn.favorited:hover {
background: linear-gradient(135deg, #ff7300 0%, #ff5000 100%); background: linear-gradient(135deg, var(--primary-hover) 0%, var(--primary-color) 100%);
} }
.favorite-btn.favorited i::before { .favorite-btn.favorited i::before {
@@ -1290,9 +1290,9 @@ const removeItem = (id: number) => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 25px 40px; padding: var(--spacing-lg) var(--spacing-xxl);
border-top: 2px solid #f0f0f0; border-top: 2px solid var(--border-color);
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); background: linear-gradient(135deg, var(--card-bg) 0%, var(--bg-light) 100%);
position: sticky; position: sticky;
bottom: 0; bottom: 0;
z-index: 100; z-index: 100;
@@ -1301,77 +1301,77 @@ const removeItem = (id: number) => {
.footer-left { .footer-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 30px; gap: var(--spacing-xl);
} }
.select-all { .select-all {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: var(--spacing-xs);
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: var(--font-size-sm);
color: #666; color: var(--text-secondary);
transition: all 0.3s ease; transition: all var(--transition-normal);
} }
.select-all:hover { .select-all:hover {
color: #ff5000; color: var(--primary-color);
} }
.selected-count { .selected-count {
font-size: 14px; font-size: var(--font-size-sm);
color: #666; color: var(--text-secondary);
} }
.count { .count {
font-size: 18px; font-size: var(--font-size-lg);
font-weight: 700; font-weight: 700;
color: #ff5000; color: var(--primary-color);
} }
.footer-right { .footer-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 30px; gap: var(--spacing-xl);
} }
.total-price { .total-price {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 5px; gap: var(--spacing-xs);
} }
.total-label { .total-label {
font-size: 16px; font-size: var(--font-size-base);
color: #666; color: var(--text-secondary);
font-weight: 500; font-weight: 500;
} }
.total-symbol { .total-symbol {
font-size: 20px; font-size: var(--font-size-lg);
color: #ff5000; color: var(--primary-color);
font-weight: 600; font-weight: 600;
} }
.total-value { .total-value {
font-size: 32px; font-size: var(--font-size-3xl);
color: #ff5000; color: var(--primary-color);
font-weight: 700; font-weight: 700;
} }
.checkout-btn { .checkout-btn {
padding: 12px 40px; padding: var(--spacing-sm) var(--spacing-xxl);
font-size: 16px; font-size: var(--font-size-base);
font-weight: 600; font-weight: 600;
border-radius: 24px; border-radius: var(--border-radius-xl);
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%); background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
border: none; border: none;
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3); box-shadow: 0 4px 12px rgba(var(--primary-light-rgb), 0.3);
transition: all 0.3s ease; transition: all var(--transition-normal);
} }
.checkout-btn:hover { .checkout-btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 80, 0, 0.4); box-shadow: 0 6px 20px rgba(var(--primary-light-rgb), 0.4);
} }
</style> </style>

View File

@@ -467,52 +467,52 @@ const sendMessage = () => {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: 10px; padding: var(--spacing-sm);
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid var(--border-color);
} }
.chat-user-info h3 { .chat-user-info h3 {
margin: 0 0 2px; margin: 0 0 2px;
font-size: 16px; font-size: var(--font-size-base);
font-weight: 600; font-weight: 600;
color: #333333; color: var(--text-primary);
} }
.user-status { .user-status {
font-size: 12px; font-size: var(--font-size-xs);
color: #4caf50; color: var(--success-color);
margin: 0; margin: 0;
} }
.chat-actions { .chat-actions {
display: flex; display: flex;
gap: 10px; gap: var(--spacing-sm);
} }
.action-btn { .action-btn {
width: 36px; width: 36px;
height: 36px; height: 36px;
border-radius: 50%; border-radius: var(--border-radius-full);
background-color: #f0f0f0; background-color: var(--bg-light);
border: none; border: none;
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: var(--font-size-base);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.3s ease; transition: all var(--transition-normal);
} }
.action-btn:hover { .action-btn:hover {
background-color: #e0e0e0; background-color: var(--border-color);
} }
/* 聊天历史 */ /* 聊天历史 */
.chat-history { .chat-history {
flex: 1; flex: 1;
padding: 20px; padding: var(--spacing-xl);
overflow-y: auto; overflow-y: auto;
background-color: #f5f5f5; background-color: var(--bg-color);
background-image: url('?prompt=chat%20background%20pattern%20subtle%20light&image_size=landscape_16_9'); background-image: url('?prompt=chat%20background%20pattern%20subtle%20light&image_size=landscape_16_9');
background-size: cover; background-size: cover;
background-position: center; background-position: center;
@@ -520,7 +520,7 @@ const sendMessage = () => {
.chat-message { .chat-message {
display: flex; display: flex;
margin-bottom: 15px; margin-bottom: var(--spacing-md);
align-items: flex-end; align-items: flex-end;
} }
@@ -530,7 +530,7 @@ const sendMessage = () => {
.chat-message.sent .user-avatar { .chat-message.sent .user-avatar {
margin-right: 0; margin-right: 0;
margin-left: 10px; margin-left: var(--spacing-sm);
} }
.message-content { .message-content {
@@ -544,55 +544,55 @@ const sendMessage = () => {
} }
.message-text { .message-text {
padding: 10px 15px; padding: var(--spacing-sm) var(--spacing-md);
border-radius: 18px; border-radius: var(--message-radius);
margin-bottom: 4px; margin-bottom: 4px;
line-height: 1.4; line-height: 1.4;
} }
.chat-message.received .message-text { .chat-message.received .message-text {
background-color: #ffffff; background-color: var(--card-bg);
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
} }
.chat-message.sent .message-text { .chat-message.sent .message-text {
background-color: #ff5000; background-color: var(--primary-color);
color: #ffffff; color: #ffffff;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
.message-time { .message-time {
font-size: 11px; font-size: var(--font-size-xs);
color: #999999; color: var(--text-tertiary);
} }
/* 消息输入区 */ /* 消息输入区 */
.chat-input-area { .chat-input-area {
background-color: #f5f5f5; background-color: var(--bg-color);
padding: 15px 20px; padding: var(--spacing-md) var(--spacing-xl);
border-top: 1px solid #e0e0e0; border-top: 1px solid var(--border-color);
border-radius: 0 0 10px 0; border-radius: 0 0 var(--border-radius-lg) 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: var(--spacing-sm);
} }
.input-container { .input-container {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
background-color: #f0f0f0; background-color: var(--bg-light);
border-radius: 24px; border-radius: var(--border-radius-xl);
padding: 8px 15px; padding: var(--spacing-xs) var(--spacing-md);
gap: 10px; gap: var(--spacing-sm);
} }
.input-btn { .input-btn {
background: none; background: none;
border: none; border: none;
font-size: 16px; font-size: var(--font-size-base);
cursor: pointer; cursor: pointer;
padding: 4px; padding: var(--spacing-xs);
} }
.message-input { .message-input {
@@ -600,29 +600,29 @@ const sendMessage = () => {
border: none; border: none;
background: none; background: none;
outline: none; outline: none;
font-size: 14px; font-size: var(--font-size-sm);
color: #333333; color: var(--text-primary);
min-width: 0; min-width: 0;
} }
.message-input::placeholder { .message-input::placeholder {
color: #999999; color: var(--text-tertiary);
} }
.send-btn { .send-btn {
padding: 8px 20px; padding: var(--spacing-xs) var(--spacing-xl);
background-color: #ff5000; background-color: var(--primary-color);
color: #ffffff; color: #ffffff;
border: none; border: none;
border-radius: 20px; border-radius: var(--border-radius-xl);
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: var(--font-size-sm);
font-weight: 600; font-weight: 600;
transition: all 0.3s ease; transition: all var(--transition-normal);
} }
.send-btn:hover { .send-btn:hover {
background-color: #ff6a00; background-color: var(--primary-hover);
} }
/* 空状态 */ /* 空状态 */
@@ -632,29 +632,29 @@ const sendMessage = () => {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: #f5f5f5; background-color: var(--bg-color);
padding: 40px; padding: var(--spacing-xxl);
text-align: center; text-align: center;
} }
.empty-state img { .empty-state img {
width: 120px; width: 120px;
height: 120px; height: 120px;
margin-bottom: 20px; margin-bottom: var(--spacing-xl);
opacity: 0.6; opacity: 0.6;
} }
.empty-state h3 { .empty-state h3 {
margin: 0 0 10px; margin: 0 0 var(--spacing-md);
font-size: 18px; font-size: var(--font-size-lg);
font-weight: 600; font-weight: 600;
color: #333333; color: var(--text-primary);
} }
.empty-state p { .empty-state p {
margin: 0; margin: 0;
font-size: 14px; font-size: var(--font-size-sm);
color: #999999; color: var(--text-tertiary);
} }
/* 响应式设计 */ /* 响应式设计 */

View File

@@ -24,12 +24,20 @@
<div v-if="errorMessage" class="error-message"> <div v-if="errorMessage" class="error-message">
{{ errorMessage }} {{ errorMessage }}
</div> </div>
<!-- 记住我 如果用户登录成功记住用户信息5天内无需重新登录 -->
<div class="form-group remember-me">
<label class="checkbox-label">
<input type="checkbox" v-model="loginform.rememberMe" class="checkbox-input">
<span class="checkbox-text">记住我</span>
</label>
</div>
<!-- 登录按钮 -->
<button type="submit" class="login-button" :disabled="isLoading"> <button type="submit" class="login-button" :disabled="isLoading">
<span v-if="isLoading" class="loading-spinner"></span> <span v-if="isLoading" class="loading-spinner"></span>
{{ isLoading ? '登录中...' : '登录' }} {{ isLoading ? '登录中...' : '登录' }}
</button> </button>
<!-- 注册链接 -->
<div class="login-footer"> <div class="login-footer">
<p class="register-link"> <p class="register-link">
还没有账号 <a href="#" @click.prevent="router.push({ name: 'register' })">立即注册</a> 还没有账号 <a href="#" @click.prevent="router.push({ name: 'register' })">立即注册</a>
@@ -51,7 +59,8 @@ const router = useRouter()
const loginform = ref<Login>({ const loginform = ref<Login>({
username: '', username: '',
password: '' password: '',
rememberMe: false
}) })
const errorMessage = ref('') const errorMessage = ref('')
@@ -140,6 +149,37 @@ const login = async () => {
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.15); box-shadow: 0 15px 50px rgba(0, 0, 0, 0.15);
} }
/* 记住我复选框样式 */
.remember-me {
display: flex;
align-items: center;
margin: 15px 0;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.checkbox-input {
margin-right: 8px;
width: 16px;
height: 16px;
cursor: pointer;
}
.checkbox-text {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
}
.checkbox-text:hover {
color: #fff;
}
/* 登录头部 */ /* 登录头部 */
.login-header { .login-header {
text-align: center; text-align: center;

View File

@@ -0,0 +1,446 @@
<!-- 更新地址弹窗 上下结构 上方有一个输入框可以识别地址更新到下方 下方有可以更新地址信息省份城市区县详细地址 -->
<!-- 父组件会传递一个地址对象 子组件需要根据这个地址对象更新弹窗的地址信息 -->
<template>
<a-modal :open="visible" title="更新地址" :footer="null" width="500px">
<div class="modal-content">
<!-- 地址识别输入框 -->
<div class="form-group">
<label for="address-input">地址识别</label>
<a-input v-model:value="addressInput" placeholder="请输入完整地址,系统会自动识别省份、城市、区县"
@change="handleAddressRecognition" />
<div v-if="recognitionError" class="error-message">{{ recognitionError }}</div>
</div>
<!-- 地址信息表单 -->
<div class="form-group">
<label for="province">省份 <span class="required">*</span></label>
<a-select v-model:value="form.province" :options="provinceOptions" placeholder="请选择省份"
@change="handleProvinceChange" />
<div v-if="errors.province" class="error-message">{{ errors.province }}</div>
</div>
<div class="form-group">
<label for="city">城市 <span class="required">*</span></label>
<a-select v-model:value="form.city" :options="cityOptions" placeholder="请选择城市"
@change="handleCityChange" />
<div v-if="errors.city" class="error-message">{{ errors.city }}</div>
</div>
<div class="form-group">
<label for="district">区县 <span class="required">*</span></label>
<a-select v-model:value="form.district" :options="districtOptions" placeholder="请选择区县" />
<div v-if="errors.district" class="error-message">{{ errors.district }}</div>
</div>
<div class="form-group">
<label for="detailAddress">详细地址 <span class="required">*</span></label>
<a-input v-model:value="form.detail" placeholder="请输入详细地址(街道、楼栋、门牌号等)" :rows="3" type="textarea" />
<div v-if="errors.detail" class="error-message">{{ errors.detail }}</div>
</div>
</div>
<div class="modal-footer">
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleOk">确认更新</a-button>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import type { Address } from '@/Util/Type'
const props = defineProps({
// 设置弹窗显示状态控制
visible: {
type: Boolean,
default: false,
},
// 传递的地址对象
address: {
type: Object as () => Address,
default: () => ({
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: false
}) as Address,
}
})
// 定义事件
const emit = defineEmits(['update:visible', 'update:updateaddress'])
// 弹窗显示状态控制
const visible = ref(props.visible)
// 表单数据
const form = ref<Address>({
id:props.address.id ,
name: props.address.name,
phone: props.address.phone,
province: props.address.province,
city: props.address.city,
district: props.address.district,
detail: props.address.detail,
isDefault: props.address.isDefault
})
// 地址识别输入框
const addressInput = ref('')
const recognitionError = ref('')
// 表单验证错误
const errors = ref({
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: ''
})
// 定义类型
interface Data {
[key: string]: { label: string; value: string }[] | undefined;
}
// 城市选项(根据省份动态生成)
const cityOptions = ref<any[]>([])
// 区县选项(根据城市动态生成)
const districtOptions = ref<any[]>([])
// 省份选项
const provinceOptions = ref([
{ label: '北京市', value: '北京市' },
{ label: '上海市', value: '上海市' },
{ label: '广东省', value: '广东省' },
{ label: '浙江省', value: '浙江省' },
{ label: '江苏省', value: '江苏省' },
{ label: '四川省', value: '四川省' },
{ label: '湖北省', value: '湖北省' },
{ label: '湖南省', value: '湖南省' },
{ label: '山东省', value: '山东省' },
{ label: '河南省', value: '河南省' }
])
// 城市数据
const cityData: Data = {
'北京市': [{ label: '北京市', value: '北京市' }],
'上海市': [{ label: '上海市', value: '上海市' }],
'广东省': [
{ label: '广州市', value: '广州市' },
{ label: '深圳市', value: '深圳市' },
{ label: '东莞市', value: '东莞市' },
{ label: '佛山市', value: '佛山市' }
],
'浙江省': [
{ label: '杭州市', value: '杭州市' },
{ label: '宁波市', value: '宁波市' },
{ label: '温州市', value: '温州市' },
{ label: '嘉兴市', value: '嘉兴市' }
],
'江苏省': [
{ label: '南京市', value: '南京市' },
{ label: '苏州市', value: '苏州市' },
{ label: '无锡市', value: '无锡市' },
{ label: '常州市', value: '常州市' }
]
}
// 区县数据
const districtData: Data = {
'北京市-北京市': [
{ label: '东城区', value: '东城区' },
{ label: '西城区', value: '西城区' },
{ label: '朝阳区', value: '朝阳区' },
{ label: '海淀区', value: '海淀区' },
{ label: '丰台区', value: '丰台区' },
{ label: '石景山区', value: '石景山区' },
{ label: '门头沟区', value: '门头沟区' },
{ label: '房山区', value: '房山区' },
{ label: '通州区', value: '通州区' },
{ label: '顺义区', value: '顺义区' }
],
'上海市-上海市': [
{ label: '黄浦区', value: '黄浦区' },
{ label: '徐汇区', value: '徐汇区' },
{ label: '长宁区', value: '长宁区' },
{ label: '静安区', value: '静安区' },
{ label: '普陀区', value: '普陀区' },
{ label: '虹口区', value: '虹口区' },
{ label: '杨浦区', value: '杨浦区' },
{ label: '浦东新区', value: '浦东新区' }
],
'广东省-广州市': [
{ label: '越秀区', value: '越秀区' },
{ label: '海珠区', value: '海珠区' },
{ label: '荔湾区', value: '荔湾区' },
{ label: '天河区', value: '天河区' },
{ label: '白云区', value: '白云区' },
{ label: '黄埔区', value: '黄埔区' },
{ label: '番禺区', value: '番禺区' },
{ label: '花都区', value: '花都区' }
],
'浙江省-杭州市': [
{ label: '上城区', value: '上城区' },
{ label: '下城区', value: '下城区' },
{ label: '江干区', value: '江干区' },
{ label: '拱墅区', value: '拱墅区' },
{ label: '西湖区', value: '西湖区' },
{ label: '滨江区', value: '滨江区' },
{ label: '萧山区', value: '萧山区' },
{ label: '余杭区', value: '余杭区' }
]
}
// 监听 address 变化 用于更新弹窗的地址信息
watch(() => props.address, (newVal) => {
if (newVal) {
form.value = { ...newVal }
// 更新城市和区县选项
handleProvinceChange(newVal.province)
if (newVal.city) {
handleCityChange(newVal.city)
}
}
}, { deep: true })
// 监听 visible 变化 用于更新弹窗的显示状态
watch(() => props.visible, (newVal) => {
visible.value = newVal
})
// 监听弹窗关闭事件
watch(() => props.visible, (newVal) => {
if (!newVal) {
// 重置表单
form.value = {
id:0,
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: false
}
addressInput.value = ''
recognitionError.value = ''
errors.value = {
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: ''
}
cityOptions.value = []
districtOptions.value = []
}
})
// 地址识别处理
const handleAddressRecognition = () => {
if (!addressInput.value) {
recognitionError.value = ''
return
}
// 简单的地址识别逻辑实际项目中可以使用更复杂的算法或API
const address = addressInput.value
let province = ''
let city = ''
let district = ''
let detail = ''
// 识别省份
const provinces = provinceOptions.value.map(p => p.value)
for (const p of provinces) {
if (address.includes(p)) {
province = p
break
}
}
// 识别城市
// if (province && (cityData as Record<string, { label: string; value: string }[]>)[province]) {
// const cities = cityData[province].map((c: { value: any }) => c.value)
// for (const c of cities) {
// if (address.includes(c)) {
// city = c
// break
// }
// }
// }
// 识别区县
// if (province && city && districtData[`${province}-${city}`]) {
// const districts = districtData[`${province}-${city}`].map((d: { value: any }) => d.value)
// for (const d of districts) {
// if (address.includes(d)) {
// district = d
// break
// }
// }
// }
// 提取详细地址
if (province && city && district) {
detail = address.replace(province, '').replace(city, '').replace(district, '').trim()
} else if (province && city) {
detail = address.replace(province, '').replace(city, '').trim()
} else if (province) {
detail = address.replace(province, '').trim()
} else {
detail = address
}
// 更新表单
if (province) {
form.value.province = province
handleProvinceChange(province)
if (city) {
form.value.city = city
handleCityChange(city)
if (district) {
form.value.district = district
}
}
form.value.detail = detail
recognitionError.value = ''
} else {
recognitionError.value = '未能识别出省份,请手动选择'
}
}
// 省份变化处理
const handleProvinceChange = (province: string) => {
form.value.province = province
form.value.city = ''
form.value.district = ''
// 更新城市选项
if (province && cityData[province]) {
cityOptions.value = cityData[province]
} else {
cityOptions.value = []
}
districtOptions.value = []
// 清除验证错误
errors.value.province = ''
errors.value.city = ''
errors.value.district = ''
}
// 城市变化处理
const handleCityChange = (city: string) => {
form.value.city = city
form.value.district = ''
// 更新区县选项
if (form.value.province && city && districtData[`${form.value.province}-${city}`]) {
districtOptions.value = districtData[`${form.value.province}-${city}`] ?? []
} else {
districtOptions.value = []
}
// 清除验证错误
errors.value.city = ''
errors.value.district = ''
}
// 表单验证
const validateForm = (): boolean => {
let isValid = true
// 重置错误信息
errors.value = {
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: ''
}
// 验证省份
if (!form.value.province) {
errors.value.province = '请选择省份'
isValid = false
}
// 验证城市
if (!form.value.city) {
errors.value.city = '请选择城市'
isValid = false
}
// 验证区县
if (!form.value.district) {
errors.value.district = '请选择区县'
isValid = false
}
// 验证详细地址
if (!form.value.detail) {
errors.value.detail = '请输入详细地址'
isValid = false
} else if (form.value.detail.length < 5) {
errors.value.detail = '详细地址至少需要5个字符'
isValid = false
}
return isValid
}
// 点击确认按钮 触发事件 用于更新父组件的 address
const handleOk = () => {
// 表单验证
if (!validateForm()) {
return
}
emit('update:updateaddress', form.value)
emit('update:visible', false)
}
// 点击取消按钮
const handleCancel = () => {
emit('update:visible', false)
}
</script>
<style scoped>
.modal-content {
padding: var(--spacing-md);
}
.form-group {
margin-bottom: var(--spacing-md);
}
label {
display: block;
margin-bottom: var(--spacing-sm);
font-weight: 500;
}
.required {
color: var(--error-color, #ff4d4f);
}
.error-message {
color: var(--error-color, #ff4d4f);
font-size: 12px;
margin-top: 4px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
padding: var(--spacing-md);
border-top: 1px solid var(--border-light, #e8e8e8);
margin-top: var(--spacing-lg);
}
/* 为textarea添加最小高度 */
:deep(.ant-input-textarea) {
min-height: 80px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -33,8 +33,8 @@
</div> </div>
</div> </div>
<div class="user-actions"> <div class="user-actions">
<a href="/user/address" class="action-link">收货地址</a> <a href="/user?nav=address" class="action-link">收货地址</a>
<a href="/user/follow" class="action-link">关注店铺</a> <a href="/user?nav=follow" class="action-link">关注店铺</a>
</div> </div>
</div> </div>
</div> </div>
@@ -265,7 +265,7 @@ const navigateToOrder = (status: string) => {
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 50%, #45b7d1 100%); background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 50%, #45b7d1 100%);
border-radius: 16px; border-radius: 16px;
padding: 10px; padding: 10px;
margin-bottom: 30px; margin-bottom: 10px;
color: #ffffff; color: #ffffff;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
position: relative; position: relative;
@@ -429,7 +429,6 @@ const navigateToOrder = (status: string) => {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
margin-top: 45px;
} }
.order-status-card:hover { .order-status-card:hover {

View File

@@ -74,7 +74,7 @@ const productLists = ref([
padding: 0 var(--spacing-md); padding: 0 var(--spacing-md);
} }
h1 { h1 {
color: #42b983; color: var(--success-color);
} }
#footer-contact { #footer-contact {
@@ -116,31 +116,31 @@ h1 {
} }
.copyright-links a:hover { .copyright-links a:hover {
color: #ff5000; color: var(--primary-color);
} }
.copyright-separator { .copyright-separator {
color: #999; color: var(--text-tertiary);
margin: 0 4px; margin: 0 var(--spacing-xs);
} }
.copyright-icp { .copyright-icp {
margin: 0; margin: 0;
font-size: 12px; font-size: var(--font-size-xs);
color: #999; color: var(--text-tertiary);
} }
/* 响应式调整 */ /* 响应式调整 */
@media (max-width: 768px) { @media (max-width: 768px) {
.copyright-content { .copyright-content {
padding: 0 20px; padding: 0 var(--spacing-xl);
} }
.copyright-links { .copyright-links {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 10px; gap: var(--spacing-sm);
} }
.copyright-links a { .copyright-links a {

View File

@@ -1,5 +1,5 @@
<template> <template>
<div id="header"> <div id="header" v-if="booleanHeader">
<div id="header-profile"> <div id="header-profile">
<div id="header-profile-left"> <div id="header-profile-left">
<!-- 用户信息悬浮菜单 --> <!-- 用户信息悬浮菜单 -->
@@ -15,6 +15,19 @@
</div> </div>
<span class="user-arrow"></span> <span class="user-arrow"></span>
</div> </div>
<template #overlay>
<div class="user-info-menu">
<div class="user-info-menu-item">
<a href="javascript:;">1st menu item</a>
</div>
<div class="user-info-menu-item">
<a href="javascript:;">2nd menu item</a>
</div>
<div class="user-info-menu-item">
<a href="javascript:;">3rd menu item</a>
</div>
</div>
</template>
</Dropdown> </Dropdown>
</div> </div>
<div id="header-profile-right"> <div id="header-profile-right">
@@ -93,9 +106,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Button, Col, Row, Space, Dropdown, Avatar, Menu, Divider, Tag } from 'ant-design-vue'; import { Button, Col, Row, Space, Dropdown, Avatar, Menu, Divider, Tag } from 'ant-design-vue';
import Input from 'ant-design-vue/es/input';
const router = useRouter() const router = useRouter()
// 定义搜索框绑定的变量 // 定义搜索框绑定的变量
const searchValue = ref('') const searchValue = ref('')
@@ -105,19 +119,23 @@ const showHistory = ref(false)
const isClearingHistory = ref(false) const isClearingHistory = ref(false)
// 控制搜索框是否显示 // 控制搜索框是否显示
const booleanSearch = ref(true) const booleanSearch = ref(true)
// 路由事件 // 控制Header是否显示
router.beforeEach((to, from, next) => { const booleanHeader = ref(true)
if (to.name === 'search' && to.query.keyword) { // 监听路由变化 当路由变化时 更新搜索框的显示状态
searchValue.value = to.query.keyword as string watch(() => router.currentRoute.value.name, (newName) => {
// 如果是搜索页面 则更新查询参数
if (newName === 'search') {
searchValue.value = router.currentRoute.value.query.keyword as string
} }
console.log(to.name) // 如果是商品页面 则显示搜索框
// 如果在商品页面 隐藏搜索框 if (newName === 'productDetail' || newName === 'chat') {
if (to.name === 'productDetail' || to.name === 'chat') {
booleanSearch.value = false
} else {
booleanSearch.value = true booleanSearch.value = true
} }
next() // 如果在登录页面 则隐藏搜索框
if (newName === 'login') {
booleanHeader.value = false
}
console.log(newName)
}) })
// 定义搜索框的搜索事件 // 定义搜索框的搜索事件
const onSearch = (value: string) => { const onSearch = (value: string) => {
@@ -270,9 +288,7 @@ const rightMenu = [
<style scoped> <style scoped>
#header { #header {
width: 100%; width: 100%;
background-color: var(--card-bg);
padding: var(--spacing-sm) var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md);
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
position: relative; position: relative;
z-index: 1000; z-index: 1000;
} }
@@ -310,8 +326,6 @@ const rightMenu = [
border-radius: var(--border-radius-3xl); border-radius: var(--border-radius-3xl);
cursor: pointer; cursor: pointer;
transition: all var(--transition-transform); transition: all var(--transition-transform);
background-color: var(--card-bg);
box-shadow: var(--shadow-sm);
} }
.user-info-container:hover { .user-info-container:hover {
@@ -399,6 +413,35 @@ const rightMenu = [
transform: rotate(180deg) scale(1.1); transform: rotate(180deg) scale(1.1);
color: var(--text-secondary); color: var(--text-secondary);
} }
/* 用户信息菜单样式 */
.user-info-menu {
display: flex;
flex-direction: column;
padding: var(--spacing-md);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
width: 100%;
z-index: 1000;
transition: all var(--transition-transform);
background-color: var(--card-bg);
}
.user-info-menu-item {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius-2xl);
transition: all var(--transition-transform);
}
.user-info-menu-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.user-info-menu-item:hover {
background-color: var(--bg-color);
color: var(--text-primary);
}
/* 右侧功能菜单样式 */ /* 右侧功能菜单样式 */
.header-more-btn { .header-more-btn {
@@ -715,6 +758,7 @@ const rightMenu = [
.search-type-item.animate-left-out::after { .search-type-item.animate-left-out::after {
animation: slideOutLeft 0.3s ease-out; animation: slideOutLeft 0.3s ease-out;
} }
/* 搜索类型选择器从左往右动画 */ /* 搜索类型选择器从左往右动画 */
@keyframes slideInRight { @keyframes slideInRight {
from { from {
@@ -727,6 +771,7 @@ const rightMenu = [
left: -10%; left: -10%;
} }
} }
/* 搜索类型选择器从右往左动画 */ /* 搜索类型选择器从右往左动画 */
@keyframes slideInLeft { @keyframes slideInLeft {
from { from {
@@ -750,20 +795,20 @@ const rightMenu = [
to { to {
width: 0; width: 0;
left: 100%; left: 200%;
} }
} }
/* 从右往左消失动画 */ /* 从右往左消失动画 */
@keyframes slideOutLeft { @keyframes slideOutLeft {
from { from {
width: 10%; width: 100%;
right: 0; right: -10%;
} }
to { to {
width: 0%; width: 0%;
right: 100%; right: -200%;
} }
} }

View File

@@ -87,7 +87,7 @@ const adList = [
} }
h1 { h1 {
color: #42b983; color: var(--success-color);
} }
#main-header { #main-header {
@@ -104,25 +104,25 @@ h1 {
#main-header .ant-btn { #main-header .ant-btn {
width: 120px; width: 120px;
height: 45px; height: 45px;
font-size: 14px; font-size: var(--font-size-sm);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
padding: 0; padding: 0;
border-radius: 8px; border-radius: var(--border-radius-md);
font-weight: 500; font-weight: 500;
} }
#main-header .ant-btn-primary { #main-header .ant-btn-primary {
background-color: #1890ff; background-color: var(--secondary-color);
border-color: #1890ff; border-color: var(--secondary-color);
transition: all 0.3s ease; transition: all var(--transition-normal);
} }
#main-header .ant-btn-primary:hover { #main-header .ant-btn-primary:hover {
background-color: #40a9ff; background-color: var(--secondary-hover);
border-color: #40a9ff; border-color: var(--secondary-hover);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3); box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
} }
@@ -228,16 +228,16 @@ h1 {
.category-detail-item { .category-detail-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: var(--spacing-sm);
padding: 12px 0; padding: var(--spacing-sm) 0;
border-bottom: 1px solid #f5f5f5; border-bottom: 1px solid var(--bg-light);
transition: all 0.2s ease; transition: all var(--transition-fast);
} }
.category-detail-item:hover { .category-detail-item:hover {
background-color: #f9f9f9; background-color: var(--bg-light);
padding-left: 8px; padding-left: var(--spacing-xs);
border-radius: 4px; border-radius: var(--border-radius-sm);
} }
.category-detail-item:last-child { .category-detail-item:last-child {
@@ -245,16 +245,16 @@ h1 {
} }
.category-detail-item a { .category-detail-item a {
color: #333; color: var(--text-primary);
text-decoration: none; text-decoration: none;
font-size: 14px; font-size: var(--font-size-sm);
font-weight: 500; font-weight: 500;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
position: relative; position: relative;
} }
.category-detail-item a:hover { .category-detail-item a:hover {
color: #ff5000; color: var(--primary-color);
transform: translateX(4px); transform: translateX(4px);
} }
@@ -265,8 +265,8 @@ h1 {
left: 0; left: 0;
width: 0; width: 0;
height: 1px; height: 1px;
background-color: #ff5000; background-color: var(--primary-color);
transition: width 0.3s ease; transition: width var(--transition-normal);
} }
.category-detail-item a:hover::after { .category-detail-item a:hover::after {
@@ -274,98 +274,98 @@ h1 {
} }
.category-detail-item span { .category-detail-item span {
color: #999; color: var(--text-tertiary);
font-size: 14px; font-size: var(--font-size-sm);
transition: color 0.3s ease; transition: color var(--transition-normal);
} }
.main-content-col-top .iconfont { .main-content-col-top .iconfont {
font-size: 22px; font-size: 22px;
color: #ff5000; color: var(--primary-color);
flex-shrink: 0; flex-shrink: 0;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
} }
.main-content-col-top li:hover .iconfont { .main-content-col-top li:hover .iconfont {
transform: scale(1.1) rotate(5deg); transform: scale(1.1) rotate(5deg);
color: #ff5000; color: var(--primary-color);
} }
.main-content-col-top a { .main-content-col-top a {
text-decoration: none; text-decoration: none;
color: #333; color: var(--text-primary);
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all var(--transition-transform);
position: relative; position: relative;
border-radius: 4px; border-radius: var(--border-radius-sm);
} }
.main-content-col-top a:hover { .main-content-col-top a:hover {
color: #ff5000; color: var(--primary-color);
background-color: rgba(255, 80, 0, 0.1); background-color: var(--primary-light);
transform: translateY(-1px); transform: translateY(-1px);
} }
.main-content-col-top span { .main-content-col-top span {
color: #999; color: var(--text-tertiary);
font-size: 14px; font-size: var(--font-size-sm);
transition: color 0.3s ease; transition: color var(--transition-normal);
} }
.main-content-col-top li:hover span { .main-content-col-top li:hover span {
color: #ff5000; color: var(--primary-color);
opacity: 0.8; opacity: 0.8;
} }
/* 主内容区域样式 */ /* 主内容区域样式 */
#main-content { #main-content {
margin-top: 20px; margin-top: var(--spacing-xl);
gap: 32px; gap: var(--spacing-xl);
} }
.main-content-col-center{ .main-content-col-center{
background-color: #ffffff; background-color: var(--card-bg);
border-radius: 8px; border-radius: var(--border-radius-md);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: var(--shadow-md);
} }
.main-content-col-center h2{ .main-content-col-center h2{
font-size: 18px; font-size: var(--font-size-lg);
font-weight: 600; font-weight: 600;
color: #333; color: var(--text-primary);
margin: 0; margin: 0;
padding: 16px; padding: var(--spacing-md);
background-color: #fafafa; background-color: var(--bg-light);
border-radius: 6px; border-radius: var(--border-radius-sm);
text-align: center; text-align: center;
} }
#main-content-ad, #main-content-ad,
#main-content-hot-goods { #main-content-hot-goods {
padding: 0 20px; padding: 0 var(--spacing-xl);
margin-bottom: 16px; margin-bottom: var(--spacing-md);
} }
#main-content-ad .ant-col, #main-content-ad .ant-col,
#main-content-hot-goods .ant-col { #main-content-hot-goods .ant-col {
background-color: #fafafa; background-color: var(--bg-light);
border-radius: 8px; border-radius: var(--border-radius-md);
transition: all 0.3s ease; transition: all var(--transition-normal);
} }
#main-content-ad .ant-col:hover, #main-content-ad .ant-col:hover,
#main-content-hot-goods .ant-col:hover { #main-content-hot-goods .ant-col:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: var(--shadow-lg);
transform: translateY(-2px); transform: translateY(-2px);
} }
/* 轮播广告样式 */ /* 轮播广告样式 */
#carousel-container { #carousel-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 8px; border-radius: var(--border-radius-md);
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: var(--shadow-md);
} }
.carousel-item { .carousel-item {
@@ -379,7 +379,7 @@ h1 {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
transition: transform 0.5s ease; transition: transform var(--transition-slow);
} }
.carousel-item:hover .carousel-image { .carousel-item:hover .carousel-image {
@@ -391,22 +391,22 @@ h1 {
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
padding: 20px; padding: var(--spacing-xl);
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent); background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
color: #ffffff; color: #ffffff;
font-size: 24px; font-size: var(--font-size-2xl);
font-weight: 600; font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
} }
.ant-carousel .slick-dots { .ant-carousel .slick-dots {
bottom: 20px; bottom: var(--spacing-xl);
z-index: 10; z-index: 10;
} }
.ant-carousel .slick-dots li button { .ant-carousel .slick-dots li button {
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%; border-radius: var(--border-radius-full);
width: 12px; width: 12px;
height: 12px; height: 12px;
} }

View File

@@ -40,7 +40,7 @@
</div> </div>
<div class="tab-container"> <div class="tab-container">
<div class="tab-header"> <div class="tab-header">
<a-anchor direction="horizontal" :items="tabs" class="tab-item" /> <a-anchor direction="horizontal" :bounds="150" :getCurrentAnchor="activeTab" :setAnchor="setActiveTab" :items="tabs" class="tab-item" />
</div> </div>
<div class="tab-content"> <div class="tab-content">
<!-- 评论 --> <!-- 评论 -->
@@ -52,7 +52,74 @@
<div class="reviews-content"> <div class="reviews-content">
<!-- 评论内容 --> <!-- 评论内容 -->
<h3>用户评论</h3> <h3>用户评论</h3>
<!-- 更多评论... --> <!-- 评论统计 -->
<div class="reviews-stats">
<div class="reviews-rating">
<span class="rating-label">综合评分</span>
<span class="rating-value">4.8</span>
<div class="rating-stars">
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star-half"></i>
</div>
</div>
<div class="reviews-count"> 128 条评论</div>
</div>
<!-- 评论列表 -->
<div class="reviews-list">
<div class="review-item">
<div class="review-header">
<span class="reviewer-name">用户123</span>
<div class="review-rating">
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
</div>
<span class="review-time">2026-01-15</span>
</div>
<div class="review-content">投影仪质量很好画面清晰操作简单物流也很快非常满意的一次购物</div>
<div class="review-images">
<!-- <img src="https://neeko-copilot.bytedance.net/api/text2image?prompt=projector%20in%20office%20room&size=800x600" alt="评论图片" width="100">
<img src="https://neeko-copilot.bytedance.net/api/text2image?prompt=projector%20screen%20display&size=800x600" alt="评论图片" width="100"> -->
</div>
</div>
<div class="review-item">
<div class="review-header">
<span class="reviewer-name">科技达人</span>
<div class="review-rating">
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
</div>
<span class="review-time">2026-01-10</span>
</div>
<div class="review-content">亮度很高白天也能看清楚音响效果也不错接口丰富值得购买</div>
</div>
<div class="review-item">
<div class="review-header">
<span class="reviewer-name">办公室采购</span>
<div class="review-rating">
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star-o"></i>
</div>
<span class="review-time">2026-01-05</span>
</div>
<div class="review-content">性价比很高适合办公室使用客服态度也很好有问必答</div>
</div>
</div>
<!-- 查看更多评论 -->
<div class="reviews-more">
<a href="#" class="btn btn-secondary">查看更多评论</a>
</div>
</div> </div>
</div> </div>
<!-- 文图详情 --> <!-- 文图详情 -->
@@ -61,6 +128,30 @@
文图详情 文图详情
<a href="#description"></a> <a href="#description"></a>
</h2> </h2>
<div class="description-content">
<div class="product-intro">
<h3>产品介绍</h3>
<p>BenQ 明基投影仪采用先进的DLP投影技术提供清晰锐利的画面效果无论是商务会议教育培训还是家庭娱乐都能满足您的需求</p>
</div>
<div class="product-features">
<h3>产品特点</h3>
<ul>
<li>1080P全高清分辨率画质清晰细腻</li>
<li>3500ANSI流明高亮度白天也能清晰观看</li>
<li>智能系统支持无线投屏</li>
<li>多种接口满足不同设备连接需求</li>
<li>长寿命灯泡节能环保</li>
</ul>
</div>
<div class="product-images">
<h3>产品展示</h3>
<div class="image-gallery">
<!-- <img src="https://neeko-copilot.bytedance.net/api/text2image?prompt=modern%20projector%20front%20view&size=1024x768" alt="投影仪正面" class="gallery-image">
<img src="https://neeko-copilot.bytedance.net/api/text2image?prompt=projector%20rear%20view%20with%20ports&size=1024x768" alt="投影仪背面" class="gallery-image">
<img src="https://neeko-copilot.bytedance.net/api/text2image?prompt=projector%20in%20meeting%20room&size=1024x768" alt="会议室使用场景" class="gallery-image"> -->
</div>
</div>
</div>
</div> </div>
<!-- 参数信息 --> <!-- 参数信息 -->
<div id="specs"> <div id="specs">
@@ -68,6 +159,62 @@
参数信息 参数信息
<a href="#specs"></a> <a href="#specs"></a>
</h2> </h2>
<div class="specs-content">
<table class="specs-table">
<thead>
<tr>
<th>参数类别</th>
<th>参数值</th>
</tr>
</thead>
<tbody>
<tr>
<td>投影技术</td>
<td>DLP</td>
</tr>
<tr>
<td>分辨率</td>
<td>1920 x 1080 (1080P)</td>
</tr>
<tr>
<td>亮度</td>
<td>3500 ANSI流明</td>
</tr>
<tr>
<td>对比度</td>
<td>15000:1</td>
</tr>
<tr>
<td>投影尺寸</td>
<td>30-300英寸</td>
</tr>
<tr>
<td>投影距离</td>
<td>1.0-10.0</td>
</tr>
<tr>
<td>灯泡寿命</td>
<td>正常模式4000小时经济模式6000小时</td>
</tr>
<tr>
<td>接口</td>
<td>HDMI x 2, VGA x 1, USB x 2, Audio x 1</td>
</tr>
<tr>
<td>功耗</td>
<td>正常模式280W经济模式200W</td>
</tr>
<tr>
<td>尺寸</td>
<td>310 x 220 x 100 mm</td>
</tr>
<tr>
<td>重量</td>
<td>2.8 kg</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
<!-- 本店推荐 --> <!-- 本店推荐 -->
<div id="recommendations"> <div id="recommendations">
@@ -75,12 +222,84 @@
本店推荐 本店推荐
<a href="#recommendations"></a> <a href="#recommendations"></a>
</h2> </h2>
<div class="recommendations-content">
<div class="recommended-products">
<div class="recommended-product">
<div class="product-image">
<!-- <img src="https://neeko-copilot.bytedance.net/api/text2image?prompt=mini%20portable%20projector&size=800x800" alt="迷你投影仪"> -->
</div>
<div class="product-info">
<h4 class="product-name">BenQ 明基 迷你便携投影仪</h4>
<div class="product-price">¥2999</div>
<div class="product-rating">
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star-half"></i>
<span>(45)</span>
</div>
</div>
</div>
<div class="recommended-product">
<div class="product-image">
<!-- <img src="https://neeko-copilot.bytedance.net/api/text2image?prompt=4k%20ultra%20hd%20projector&size=800x800" alt="4K投影仪"> -->
</div>
<div class="product-info">
<h4 class="product-name">BenQ 明基 4K超高清投影仪</h4>
<div class="product-price">¥8999</div>
<div class="product-rating">
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<span>(28)</span>
</div>
</div>
</div>
<div class="recommended-product">
<div class="product-image">
<!-- <img src="https://neeko-copilot.bytedance.net/api/text2image?prompt=projector%20screen%20white&size=800x800" alt="投影幕布"> -->
</div>
<div class="product-info">
<h4 class="product-name">100英寸投影幕布</h4>
<div class="product-price">¥599</div>
<div class="product-rating">
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star-o"></i>
<span>(67)</span>
</div>
</div>
</div>
<div class="recommended-product">
<div class="product-image">
<!-- <img src="https://neeko-copilot.bytedance.net/api/text2image?prompt=projector%20mount%20ceiling&size=800x800" alt="投影仪吊架"> -->
</div>
<div class="product-info">
<h4 class="product-name">投影仪吊架</h4>
<div class="product-price">¥199</div>
<div class="product-rating">
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star"></i>
<i class="iconfont icon-star-half"></i>
<span>(32)</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="right-panel"> </div>
</div>
</div>
</div>
<!-- 右侧内容 -->
<div class="right-panel" >
<div class="product-info"> <div class="product-info">
<h1 class="product-title">BenQ 明基投影仪 商务办公会议培训用 1080P高清智能投影机</h1> <h1 class="product-title">BenQ 明基投影仪 商务办公会议培训用 1080P高清智能投影机</h1>
<div class="price-section"> <div class="price-section">
@@ -124,6 +343,13 @@
<div class="model-name">{{ model.name }}</div> <div class="model-name">{{ model.name }}</div>
</div> </div>
</div> </div>
<!-- 数量 -->
<div class="quantity-selector">
<div class="quantity-label">数量</div>
<div class="quantity-input">
<a-input-number v-model:value="formOrderProduct.quantity" :min="1" :max="10" />
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -139,13 +365,16 @@
</div> </div>
</div> </div>
</div> </div>
<SettlementLite ref="settlementLite" v-model:visible="visible" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import SettlementLite from '../../Component/business/SettlementLite.vue'
import type { OrderProduct } from '@/Util/Type';
const router = useRouter() const router = useRouter()
const productId = ref(router.currentRoute.value.params.productId) const productId = ref(router.currentRoute.value.params.productId)
const visible = ref(false)
const currentIndex = ref(0) const currentIndex = ref(0)
const images = ref([ const images = ref([
@@ -161,7 +390,11 @@ const currentImage = computed(() => images.value[currentIndex.value])
const selectImage = (index: number) => { const selectImage = (index: number) => {
currentIndex.value = index currentIndex.value = index
} }
// 提交表单 商品基础信息 型号 数量
const formOrderProduct = ref<OrderProduct>({
productId: 0,
quantity: 1,
})
const selectedModel = ref('x500') const selectedModel = ref('x500')
const models = ref([ const models = ref([
{ id: 'x500', name: 'X500', price: '999', originalPrice: '1999', img: '/0.png' }, { id: 'x500', name: 'X500', price: '999', originalPrice: '1999', img: '/0.png' },
@@ -175,6 +408,9 @@ const selectModel = (id: string) => {
const selectedModelInfo = computed(() => models.value.find(model => model.id === selectedModel.value)) const selectedModelInfo = computed(() => models.value.find(model => model.id === selectedModel.value))
const activeTab = ref('reviews') const activeTab = ref('reviews')
const setActiveTab = (tab: string) => {
activeTab.value = tab
}
const tabs = ref([ const tabs = ref([
{ {
key: 'reviews', key: 'reviews',
@@ -209,24 +445,37 @@ const collectProduct = () => {
} }
const addToCart = () => { const addToCart = () => {
// visible.value = true
console.log('加入购物车') console.log('加入购物车')
} }
const buyProduct = () => { const buyProduct = () => {
visible.value = true
console.log('领券购买') console.log('领券购买')
} }
// 屏幕滚动 超过300px 右侧内容根据屏幕固定位置
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
const rightPanel = document.querySelector('.right-panel') as HTMLElement;
console.log("scrollTop:", scrollTop)
console.log("rightPanel:", rightPanel)
if (scrollTop > 150) {
rightPanel.classList.add('fixed');
} else {
rightPanel.classList.remove('fixed');
}
});
</script> </script>
<style scoped> <style scoped>
#product-detail { #product-detail {
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
background-color: #f5f5f5; background-color: #f5f5f5;
padding: 20px;
} }
.product-detail-container { .product-detail-container {
display: flex; display: flex;
border-radius: 16px;
margin-bottom: 20px; margin-bottom: 20px;
overflow: hidden; overflow: hidden;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
@@ -235,7 +484,7 @@ const buyProduct = () => {
} }
.left-panel { .left-panel {
width: 50%; width: 761px;
padding: 25px; padding: 25px;
border-right: 1px solid #f0f0f0; border-right: 1px solid #f0f0f0;
overflow-y: auto; overflow-y: auto;
@@ -270,7 +519,7 @@ const buyProduct = () => {
.shop-info-detail { .shop-info-detail {
position: absolute; position: absolute;
top: 100%; top: 50px;
left: 0; left: 0;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
@@ -297,6 +546,7 @@ const buyProduct = () => {
color: #333; color: #333;
font-weight: 600; font-weight: 600;
} }
.shop-btn-group { .shop-btn-group {
display: flex; display: flex;
gap: 10px; gap: 10px;
@@ -346,6 +596,7 @@ const buyProduct = () => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transition: all 0.3s ease; transition: all 0.3s ease;
height: 500px;
position: relative; position: relative;
} }
@@ -354,7 +605,7 @@ const buyProduct = () => {
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
overflow-y: auto; overflow-y: auto;
max-height: 400px; max-height: 500px;
padding: 5px; padding: 5px;
} }
@@ -388,12 +639,15 @@ const buyProduct = () => {
.right-panel { .right-panel {
flex: 1; flex: 1;
padding: 35px; padding: 35px;
overflow-y: auto;
overflow-x: auto;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
} }
.right-panel.fixed {
position: fixed;
top: 0;
right: 24px;
}
.product-info { .product-info {
width: 765px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 25px; gap: 25px;
@@ -486,6 +740,7 @@ const buyProduct = () => {
.selection-container { .selection-container {
display: flex; display: flex;
} }
/* 切换视图 */ /* 切换视图 */
.view-switch { .view-switch {
/* 靠右对齐 */ /* 靠右对齐 */
@@ -500,7 +755,9 @@ const buyProduct = () => {
.view-switch:hover { .view-switch:hover {
color: #ff5000; color: #ff5000;
} }
.model-options-small, .model-options-big {
.model-options-small,
.model-options-big {
display: flex; display: flex;
gap: 15px; gap: 15px;
flex-wrap: wrap; flex-wrap: wrap;
@@ -530,21 +787,25 @@ const buyProduct = () => {
box-shadow: 0 4px 16px rgba(255, 80, 0, 0.25); box-shadow: 0 4px 16px rgba(255, 80, 0, 0.25);
transform: translateY(-3px); transform: translateY(-3px);
} }
.model-image { .model-image {
border-radius: 8px; border-radius: 8px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.model-image img { .model-image img {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
} }
.model-name { .model-name {
font-size: 16px; font-size: 16px;
color: #333; color: #333;
font-weight: 600; font-weight: 600;
margin-bottom: 5px; margin-bottom: 5px;
} }
.model-small-item { .model-small-item {
text-align: left; text-align: left;
border-radius: 8px; border-radius: 8px;
@@ -552,6 +813,7 @@ const buyProduct = () => {
transition: all 0.3s ease; transition: all 0.3s ease;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
} }
.model-small-item img { .model-small-item img {
margin: 5px; margin: 5px;
height: 30px; height: 30px;
@@ -578,48 +840,52 @@ const buyProduct = () => {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.action-buttons { .action-buttons {
width: 400px;
height: 48px;
display: flex; display: flex;
gap: 2px; overflow: hidden;
} }
.btn-cart, .btn-cart,
.btn-buy { .btn-buy {
text-align: center; text-align: center;
width: 200px;
line-height: 48px; line-height: 48px;
flex: 1;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
border-radius: 24px;
margin-right: 10px;
} }
.btn-cart { .btn-cart {
width: 100px;
border-radius: 24px 0 0 24px;
background: linear-gradient(135deg, #ffffff 0%, #fff5f5 100%); background: linear-gradient(135deg, #ffffff 0%, #fff5f5 100%);
color: #ff5000; color: #ff5000;
border: 2px solid #ff5000; border: 2px solid #ff5000;
} }
.btn-cart:hover { .btn-buy {
width: 300px;
border-radius: 0 24px 24px 0;
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%); background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%);
color: #ffffff; color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3); box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
} }
.btn-buy { .btn-cart:hover {
width: 300px;
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%); background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%);
color: #ffffff; color: #ffffff;
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3); box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
} }
.btn-cart:hover .btn-buy {
width: 100px;
}
.btn-buy:hover { .btn-buy:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 80, 0, 0.4); box-shadow: 0 6px 20px rgba(255, 80, 0, 0.4);
} }
@@ -671,38 +937,54 @@ const buyProduct = () => {
.tab-header { .tab-header {
display: flex; display: flex;
background: #ffffff; background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.tab-header .ant-anchor-link-title a {
font-size: 30px;
} }
.tab-item { .tab-item {
padding: 18px 35px; padding: 18px 35px;
font-size: 16px; font-size: 20px;
color: #666; color: #666;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
font-weight: 500; font-weight: 500;
} border-bottom: 3px solid transparent;
background-color: #ffffff;
.tab-item:hover {
color: #ff5000;
background-color: #fff5f5;
} }
.tab-item.active { .tab-item.active {
color: #ff5000; color: #ff5000;
font-weight: 600; font-weight: 600;
background-color: #fff5f5; background-color: #fff5f5;
box-shadow: 0 2px 12px rgba(255, 80, 0, 0.15);
} }
.tab-item.active::after { .tab-item.active::after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: -2px; bottom: 0;
left: 0; left: 10%;
right: 0; right: 10%;
height: 3px; height: 3px;
background: linear-gradient(90deg, #ff5000 0%, #ff6b00 100%); background: linear-gradient(90deg, #ff5000 0%, #ff6b00 100%);
box-shadow: 0 2px 8px rgba(255, 80, 0, 0.3); box-shadow: 0 2px 12px rgba(255, 80, 0, 0.4);
border-radius: 3px 3px 0 0;
animation: tabSlideIn 0.3s ease-out;
}
@keyframes tabSlideIn {
from {
left: 50%;
right: 50%;
opacity: 0;
}
to {
left: 10%;
right: 10%;
opacity: 1;
}
} }
.tab-content { .tab-content {
@@ -748,6 +1030,313 @@ const buyProduct = () => {
margin: 0; margin: 0;
} }
/* 评论部分样式 */
.reviews-stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.reviews-rating {
display: flex;
align-items: center;
gap: 10px;
}
.rating-label {
font-size: 14px;
color: #666;
}
.rating-value {
font-size: 20px;
color: #ff5000;
font-weight: 600;
}
.rating-stars {
color: #ffc107;
font-size: 16px;
}
.reviews-count {
font-size: 14px;
color: #999;
}
.reviews-list {
margin-bottom: 20px;
}
.review-item {
padding: 20px;
border-bottom: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.review-item:hover {
background-color: #fafafa;
transform: translateX(5px);
}
.review-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.reviewer-name {
font-size: 14px;
color: #333;
font-weight: 600;
}
.review-time {
font-size: 12px;
color: #999;
}
.review-content {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: 10px;
}
.review-images {
display: flex;
gap: 10px;
}
.review-images img {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
transition: all 0.3s ease;
}
.review-images img:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.reviews-more {
text-align: center;
margin-top: 20px;
}
.btn-secondary {
display: inline-block;
padding: 10px 24px;
border: 1px solid #ff5000;
border-radius: 20px;
font-size: 14px;
color: #ff5000;
text-decoration: none;
transition: all 0.3s ease;
background-color: #ffffff;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%);
color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
}
/* 文图详情部分样式 */
.description-content {
display: flex;
flex-direction: column;
gap: 30px;
}
.product-intro h3,
.product-features h3,
.product-images h3 {
font-size: 18px;
color: #333;
margin: 0 0 15px 0;
font-weight: 600;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.product-intro p {
font-size: 14px;
color: #666;
line-height: 1.6;
margin: 0;
}
.product-features ul {
list-style: none;
padding: 0;
margin: 0;
}
.product-features li {
font-size: 14px;
color: #666;
line-height: 1.8;
position: relative;
padding-left: 20px;
}
.product-features li::before {
content: '•';
color: #ff5000;
font-size: 16px;
font-weight: bold;
position: absolute;
left: 0;
top: 0;
}
.image-gallery {
display: flex;
flex-direction: column;
gap: 20px;
}
.gallery-image {
width: 100%;
max-width: 600px;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.gallery-image:hover {
transform: scale(1.02);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
/* 参数信息部分样式 */
.specs-content {
overflow-x: auto;
}
.specs-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.specs-table th,
.specs-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
}
.specs-table th {
background-color: #fafafa;
font-weight: 600;
color: #333;
width: 30%;
}
.specs-table td {
color: #666;
width: 70%;
}
.specs-table tr:hover {
background-color: #f5f5f5;
}
/* 本店推荐部分样式 */
.recommendations-content {
padding-top: 10px;
}
.recommended-products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.recommended-product {
background-color: #fafafa;
border-radius: 8px;
padding: 15px;
transition: all 0.3s ease;
cursor: pointer;
}
.recommended-product:hover {
background-color: #ffffff;
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.recommended-product .product-image {
width: 100%;
height: 150px;
overflow: hidden;
border-radius: 4px;
margin-bottom: 10px;
}
.recommended-product .product-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 0.3s ease;
}
.recommended-product:hover .product-image img {
transform: scale(1.1);
}
.recommended-product .product-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.recommended-product .product-name {
font-size: 14px;
color: #333;
font-weight: 500;
margin: 0;
line-height: 1.4;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.recommended-product .product-price {
font-size: 16px;
color: #ff5000;
font-weight: 600;
margin: 0;
}
.recommended-product .product-rating {
font-size: 12px;
color: #ffc107;
display: flex;
align-items: center;
gap: 5px;
}
.recommended-product .product-rating span {
color: #999;
font-size: 12px;
margin-left: 5px;
}
.params-table { .params-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;

View File

@@ -5,7 +5,7 @@
<div class="product-modal-container"> <div class="product-modal-container">
<div class="product-modal-right"> <div class="product-modal-right">
<div class="product-modal-img"> <div class="product-modal-img">
<img :src="currentProduct.img" alt="产品图片" class="product-modal-image"> <img :src="currentProduct.image" alt="产品图片" class="product-modal-image">
</div> </div>
</div> </div>
<div class="product-modal-left"> <div class="product-modal-left">
@@ -21,7 +21,7 @@
<div v-for="item in currentItems" :key="item.id" class="model-item" <div v-for="item in currentItems" :key="item.id" class="model-item"
:class="{ active: currentProduct.id === item.id }" @click="selectProduct(item)"> :class="{ active: currentProduct.id === item.id }" @click="selectProduct(item)">
<div class="model-img"> <div class="model-img">
<img :src="item.img" alt="型号图片" class="model-image"> <img :src="item.image" alt="型号图片" class="model-image">
</div> </div>
<div class="model-name">{{ item.model }}</div> <div class="model-name">{{ item.model }}</div>
</div> </div>

View File

@@ -1,6 +1,6 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "../admin_frontend/src/shims-vue.d.ts"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",