fix: 调整项目结构,添加 src 目录
This commit is contained in:
37
src/App.vue
Normal file
37
src/App.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<view id="app">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
// 检查登录状态
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 全局样式 */
|
||||
page {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
</style>
|
||||
55
src/api/auth.js
Normal file
55
src/api/auth.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import api from './index'
|
||||
|
||||
/**
|
||||
* 认证相关API
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
sendCode(phone) {
|
||||
return api.request('/auth/send-code', 'POST', { phone })
|
||||
},
|
||||
|
||||
/**
|
||||
* 手机号验证码登录
|
||||
*/
|
||||
phoneLogin(phone, code) {
|
||||
return api.request('/auth/phone-login', 'POST', { phone, code })
|
||||
},
|
||||
|
||||
/**
|
||||
* 微信登录
|
||||
*/
|
||||
wechatLogin(code) {
|
||||
return api.request('/auth/wechat', 'POST', { code })
|
||||
},
|
||||
|
||||
/**
|
||||
* 支付宝登录
|
||||
*/
|
||||
alipayLogin(code) {
|
||||
return api.request('/auth/alipay', 'POST', { code })
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
refreshToken(refreshToken) {
|
||||
return api.request('/auth/refresh', 'POST', { refreshToken })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
getCurrentUser() {
|
||||
return api.request('/auth/me', 'GET')
|
||||
},
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
logout() {
|
||||
return api.request('/auth/logout', 'POST')
|
||||
}
|
||||
}
|
||||
41
src/api/index.js
Normal file
41
src/api/index.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// API基础配置
|
||||
const BASE_URL = 'http://localhost:8080/api/v1'
|
||||
|
||||
// 请求拦截器
|
||||
const request = (url, method, data = {}) => {
|
||||
const token = uni.getStorageSync('token')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: BASE_URL + url,
|
||||
method: method,
|
||||
data: data,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data.code === 0) {
|
||||
resolve(res.data.data)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data.message || '请求失败',
|
||||
icon: 'none'
|
||||
})
|
||||
reject(res.data)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.showToast({
|
||||
title: '网络请求失败',
|
||||
icon: 'none'
|
||||
})
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
request
|
||||
}
|
||||
62
src/api/order.js
Normal file
62
src/api/order.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import api from './index'
|
||||
|
||||
/**
|
||||
* 订单相关API
|
||||
* 核心业务:
|
||||
* - 订单创建:计算原价(total_amount)、优惠金额(discount_amount)、实付金额(actual_amount)
|
||||
* - 订单原价 = 商品标价 × 数量之和
|
||||
* - 实付金额 = 原价 - 优惠金额
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* 创建订单
|
||||
* 请求参数示例:
|
||||
* {
|
||||
* customer_id: "CUS001",
|
||||
* items: [
|
||||
* { product_id: "PROD001", quantity: 10, price: 50.00 }
|
||||
* ],
|
||||
* discount_rate: 90,
|
||||
* remark: "客户要求送货上门",
|
||||
* payment_method: "wechat"
|
||||
* }
|
||||
*/
|
||||
createOrder(data) {
|
||||
return api.request('/orders', 'POST', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
getOrders(params) {
|
||||
return api.request('/orders', 'GET', params)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单详情(含明细)
|
||||
*/
|
||||
getOrderDetail(id) {
|
||||
return api.request(`/orders/${id}`, 'GET')
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
*/
|
||||
cancelOrder(id) {
|
||||
return api.request(`/orders/${id}/cancel`, 'PUT')
|
||||
},
|
||||
|
||||
/**
|
||||
* 退款
|
||||
*/
|
||||
refundOrder(id) {
|
||||
return api.request(`/orders/${id}/refund`, 'PUT')
|
||||
},
|
||||
|
||||
/**
|
||||
* 订单统计
|
||||
*/
|
||||
getStatistics(params) {
|
||||
return api.request('/orders/statistics', 'GET', params)
|
||||
}
|
||||
}
|
||||
76
src/api/product.js
Normal file
76
src/api/product.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import api from './index'
|
||||
|
||||
/**
|
||||
* 商品相关API
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* 获取分类列表
|
||||
*/
|
||||
getCategories() {
|
||||
return api.request('/products/categories', 'GET')
|
||||
},
|
||||
|
||||
/**
|
||||
* 新增分类
|
||||
*/
|
||||
createCategory(data) {
|
||||
return api.request('/products/categories', 'POST', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 修改分类
|
||||
*/
|
||||
updateCategory(id, data) {
|
||||
return api.request(`/products/categories/${id}`, 'PUT', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除分类
|
||||
*/
|
||||
deleteCategory(id) {
|
||||
return api.request(`/products/categories/${id}`, 'DELETE')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
*/
|
||||
getProducts(params) {
|
||||
return api.request('/products', 'GET', params)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商品详情
|
||||
*/
|
||||
getProduct(id) {
|
||||
return api.request(`/products/${id}`, 'GET')
|
||||
},
|
||||
|
||||
/**
|
||||
* 新增商品
|
||||
*/
|
||||
createProduct(data) {
|
||||
return api.request('/products', 'POST', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 修改商品
|
||||
*/
|
||||
updateProduct(id, data) {
|
||||
return api.request(`/products/${id}`, 'PUT', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除商品
|
||||
*/
|
||||
deleteProduct(id) {
|
||||
return api.request(`/products/${id}`, 'DELETE')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取库存预警商品
|
||||
*/
|
||||
getStockAlerts() {
|
||||
return api.request('/products/alerts', 'GET')
|
||||
}
|
||||
}
|
||||
11
src/main.js
Normal file
11
src/main.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
App.mpType = 'page'
|
||||
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
31
src/manifest.json
Normal file
31
src/manifest.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "建材销售管家",
|
||||
"appid": "__UNI__BUILDING",
|
||||
"description": "建材销售管家移动端应用",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
"modules": {}
|
||||
},
|
||||
"h5": {
|
||||
"router": {
|
||||
"mode": "hash",
|
||||
"base": "/"
|
||||
}
|
||||
},
|
||||
"mp-weixin": {
|
||||
"appid": "wx1234567890",
|
||||
"setting": {
|
||||
"urlCheck": false
|
||||
},
|
||||
"usingComponents": true
|
||||
}
|
||||
}
|
||||
60
src/pages.json
Normal file
60
src/pages.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "首页"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/product/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "商品列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/create",
|
||||
"style": {
|
||||
"navigationBarTitleText": "创建订单"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "订单列表"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "建材销售管家",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#7A7E83",
|
||||
"selectedColor": "#3cc51f",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/product/list",
|
||||
"text": "商品"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/order/list",
|
||||
"text": "订单"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
219
src/pages/index/index.vue
Normal file
219
src/pages/index/index.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-info">
|
||||
<view class="avatar">
|
||||
<text class="username">{{ userInfo.username || '用户' }}</text>
|
||||
</view>
|
||||
<view class="role-tag">{{ userInfo.role || '销售员' }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="menu-grid">
|
||||
<view class="menu-item" @click="goTo('/pages/product/list')">
|
||||
<text class="menu-icon">📦</text>
|
||||
<text class="menu-text">商品管理</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goTo('/pages/order/create')">
|
||||
<text class="menu-icon">📝</text>
|
||||
<text class="menu-text">创建订单</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goTo('/pages/order/list')">
|
||||
<text class="menu-icon">📋</text>
|
||||
<text class="menu-text">订单列表</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goStock()">
|
||||
<text class="menu-icon">🏭</text>
|
||||
<text class="menu-text">库存管理</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<view class="section">
|
||||
<view class="section-title">今日概览</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.orderCount || 0 }}</text>
|
||||
<text class="stat-label">今日订单</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">¥{{ stats.actualAmount || 0 }}</text>
|
||||
<text class="stat-label">今日销售额</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.stockAlerts || 0 }}</text>
|
||||
<text class="stat-label">库存预警</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<button class="logout-btn" @click="logout">退出登录</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import authApi from '@/api/auth'
|
||||
import orderApi from '@/api/order'
|
||||
import productApi from '@/api/product'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userInfo: {},
|
||||
stats: {
|
||||
orderCount: 0,
|
||||
actualAmount: 0,
|
||||
stockAlerts: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadUserInfo()
|
||||
this.loadStats()
|
||||
},
|
||||
methods: {
|
||||
async loadUserInfo() {
|
||||
try {
|
||||
const userInfo = await authApi.getCurrentUser()
|
||||
this.userInfo = userInfo
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
async loadStats() {
|
||||
try {
|
||||
// 获取今日订单统计
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const stats = await orderApi.getStatistics({ startDate: today })
|
||||
this.stats.orderCount = stats.orderCount || 0
|
||||
this.stats.actualAmount = stats.actualAmount || 0
|
||||
|
||||
// 获取库存预警
|
||||
const alerts = await productApi.getStockAlerts()
|
||||
this.stats.stockAlerts = alerts ? alerts.length : 0
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
goTo(url) {
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
goStock() {
|
||||
uni.showToast({ title: '库存管理功能开发中', icon: 'none' })
|
||||
},
|
||||
async logout() {
|
||||
try {
|
||||
await authApi.logout()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
uni.removeStorageSync('token')
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: #3cc51f;
|
||||
border-radius: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.role-tag {
|
||||
margin-left: 20rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.menu-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: 25%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 60rpx;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: #fff;
|
||||
color: #ff4d4f;
|
||||
border: none;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
</style>
|
||||
293
src/pages/login/index.vue
Normal file
293
src/pages/login/index.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="logo">
|
||||
<text class="logo-text">🏠</text>
|
||||
<text class="app-name">建材销售管家</text>
|
||||
</view>
|
||||
|
||||
<!-- 手机号登录 -->
|
||||
<view class="login-form">
|
||||
<view class="form-item">
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
v-model="phone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
v-model="code"
|
||||
placeholder="请输入验证码"
|
||||
maxlength="6"
|
||||
/>
|
||||
<button
|
||||
class="code-btn"
|
||||
:disabled="countdown > 0"
|
||||
@click="sendCode"
|
||||
>
|
||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||
</button>
|
||||
</view>
|
||||
<button class="login-btn" @click="phoneLogin">登录</button>
|
||||
</view>
|
||||
|
||||
<!-- 其他登录方式 -->
|
||||
<view class="other-login">
|
||||
<view class="divider">
|
||||
<view class="line"></view>
|
||||
<text class="divider-text">其他登录方式</text>
|
||||
<view class="line"></view>
|
||||
</view>
|
||||
<view class="login-methods">
|
||||
<view class="method-item" @click="wechatLogin">
|
||||
<text class="method-icon">💬</text>
|
||||
<text class="method-text">微信登录</text>
|
||||
</view>
|
||||
<view class="method-item" @click="alipayLogin">
|
||||
<text class="method-icon">💰</text>
|
||||
<text class="method-text">支付宝登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import authApi from '@/api/auth'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
phone: '',
|
||||
code: '',
|
||||
countdown: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 发送验证码
|
||||
async sendCode() {
|
||||
if (!this.phone || this.phone.length !== 11) {
|
||||
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await authApi.sendCode(this.phone)
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
|
||||
// 开始倒计时
|
||||
this.countdown = 60
|
||||
const timer = setInterval(() => {
|
||||
this.countdown--
|
||||
if (this.countdown <= 0) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
|
||||
// 手机号登录
|
||||
async phoneLogin() {
|
||||
if (!this.phone || this.phone.length !== 11) {
|
||||
uni.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.code || this.code.length !== 6) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await authApi.phoneLogin(this.phone, this.code)
|
||||
uni.setStorageSync('token', data.token)
|
||||
uni.setStorageSync('refreshToken', data.refreshToken)
|
||||
uni.setStorageSync('userId', data.userId)
|
||||
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
|
||||
// 微信登录
|
||||
async wechatLogin() {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.getProvider({
|
||||
service: 'oauth',
|
||||
success: (res) => {
|
||||
if (res.provider.includes('weixin')) {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: async (loginRes) => {
|
||||
try {
|
||||
const data = await authApi.wechatLogin(loginRes.code)
|
||||
uni.setStorageSync('token', data.token)
|
||||
uni.setStorageSync('refreshToken', data.refreshToken)
|
||||
uni.setStorageSync('userId', data.userId)
|
||||
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.showToast({ title: '请在微信小程序中使用', icon: 'none' })
|
||||
// #endif
|
||||
},
|
||||
|
||||
// 支付宝登录
|
||||
alipayLogin() {
|
||||
// #ifdef MP-ALIPAY
|
||||
my.getAuthCode({
|
||||
scopes: 'auth_base',
|
||||
success: async (res) => {
|
||||
try {
|
||||
const data = await authApi.alipayLogin(res.authCode)
|
||||
uni.setStorageSync('token', data.token)
|
||||
uni.setStorageSync('refreshToken', data.refreshToken)
|
||||
uni.setStorageSync('userId', data.userId)
|
||||
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-ALIPAY
|
||||
uni.showToast({ title: '请在支付宝小程序中使用', icon: 'none' })
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.login-page {
|
||||
padding: 100rpx 60rpx;
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 120rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
padding: 20rpx 0;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
width: 200rpx;
|
||||
font-size: 24rpx;
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.code-btn[disabled] {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
border: none;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.other-login {
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.line {
|
||||
flex: 1;
|
||||
height: 1rpx;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
padding: 0 20rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.login-methods {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.method-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 40rpx;
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
font-size: 60rpx;
|
||||
}
|
||||
|
||||
.method-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
</style>
|
||||
537
src/pages/order/create.vue
Normal file
537
src/pages/order/create.vue
Normal file
@@ -0,0 +1,537 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 客户信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">客户信息</view>
|
||||
<view class="form-item">
|
||||
<text class="label">客户</text>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="customers"
|
||||
range-key="name"
|
||||
@change="selectCustomer"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ selectedCustomer ? selectedCustomer.name : '请选择客户' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品选择 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">商品明细</text>
|
||||
<text class="add-btn" @click="addProduct">+ 添加商品</text>
|
||||
</view>
|
||||
|
||||
<view v-for="(item, index) in orderItems" :key="index" class="order-item">
|
||||
<view class="item-info">
|
||||
<text class="item-name">{{ item.productName }}</text>
|
||||
<text class="item-spec">{{ item.spec || '-' }}</text>
|
||||
</view>
|
||||
<view class="item-edit">
|
||||
<view class="quantity-edit">
|
||||
<text class="qty-label">数量</text>
|
||||
<input
|
||||
class="qty-input"
|
||||
type="number"
|
||||
v-model="item.quantity"
|
||||
@change="calcAmount"
|
||||
/>
|
||||
</view>
|
||||
<view class="price-edit">
|
||||
<text class="qty-label">单价</text>
|
||||
<input
|
||||
class="price-input"
|
||||
type="digit"
|
||||
v-model="item.price"
|
||||
@change="calcAmount"
|
||||
/>
|
||||
</view>
|
||||
<text class="item-subtotal">¥{{ (item.price * item.quantity).toFixed(2) }}</text>
|
||||
<text class="delete-btn" @click="removeItem(index)">×</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="orderItems.length === 0" class="empty-tip">
|
||||
请添加商品
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 优惠设置 -->
|
||||
<view class="section">
|
||||
<view class="section-title">优惠设置</view>
|
||||
<view class="form-item">
|
||||
<text class="label">折扣率</text>
|
||||
<input
|
||||
class="discount-input"
|
||||
type="digit"
|
||||
v-model="discountRate"
|
||||
@change="calcAmount"
|
||||
/>
|
||||
<text class="unit">%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单金额 -->
|
||||
<view class="section amount-section">
|
||||
<view class="amount-row">
|
||||
<text class="amount-label">原价合计</text>
|
||||
<text class="amount-value">¥{{ totalAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="amount-row">
|
||||
<text class="amount-label">优惠金额</text>
|
||||
<text class="amount-value discount">-¥{{ discountAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="amount-row actual">
|
||||
<text class="amount-label">实付金额</text>
|
||||
<text class="amount-value">¥{{ actualAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付方式 -->
|
||||
<view class="section">
|
||||
<view class="section-title">支付方式</view>
|
||||
<view class="payment-methods">
|
||||
<view
|
||||
class="method-item"
|
||||
:class="{ active: paymentMethod === 'cash' }"
|
||||
@click="paymentMethod = 'cash'"
|
||||
>
|
||||
💵 现金
|
||||
</view>
|
||||
<view
|
||||
class="method-item"
|
||||
:class="{ active: paymentMethod === 'wechat' }"
|
||||
@click="paymentMethod = 'wechat'"
|
||||
>
|
||||
💬 微信
|
||||
</view>
|
||||
<view
|
||||
class="method-item"
|
||||
:class="{ active: paymentMethod === 'alipay' }"
|
||||
@click="paymentMethod = 'alipay'"
|
||||
>
|
||||
💰 支付宝
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="section">
|
||||
<view class="section-title">备注</view>
|
||||
<textarea
|
||||
class="remark-input"
|
||||
v-model="remark"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-bar">
|
||||
<view class="submit-info">
|
||||
<text>实付: </text>
|
||||
<text class="submit-amount">¥{{ actualAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<button class="submit-btn" @click="createOrder">创建订单</button>
|
||||
</view>
|
||||
|
||||
<!-- 商品选择弹窗 -->
|
||||
<uni-popup ref="productPopup" type="bottom">
|
||||
<view class="product-popup">
|
||||
<view class="popup-header">
|
||||
<text>选择商品</text>
|
||||
<text @click="$refs.productPopup.close()">关闭</text>
|
||||
</view>
|
||||
<view class="popup-search">
|
||||
<input v-model="searchKeyword" placeholder="搜索商品" />
|
||||
</view>
|
||||
<scroll-view class="popup-list" scroll-y>
|
||||
<view
|
||||
v-for="p in productList"
|
||||
:key="p.productId"
|
||||
class="popup-item"
|
||||
@click="selectProduct(p)"
|
||||
>
|
||||
<text>{{ p.name }}</text>
|
||||
<text>¥{{ p.price }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import orderApi from '@/api/order'
|
||||
import productApi from '@/api/product'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// 客户相关
|
||||
customers: [],
|
||||
selectedCustomer: null,
|
||||
|
||||
// 商品相关
|
||||
orderItems: [],
|
||||
productList: [],
|
||||
searchKeyword: '',
|
||||
|
||||
// 金额相关
|
||||
discountRate: 100, // 折扣率
|
||||
totalAmount: 0, // 原价
|
||||
discountAmount: 0, // 优惠金额
|
||||
actualAmount: 0, // 实付金额
|
||||
|
||||
// 其他
|
||||
paymentMethod: 'cash',
|
||||
remark: ''
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadProducts()
|
||||
},
|
||||
methods: {
|
||||
async loadProducts() {
|
||||
try {
|
||||
const res = await productApi.getProducts({ page: 1, pageSize: 100 })
|
||||
this.productList = res.records || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
selectCustomer(e) {
|
||||
this.selectedCustomer = this.customers[e.detail.value]
|
||||
},
|
||||
addProduct() {
|
||||
this.$refs.productPopup.open()
|
||||
},
|
||||
selectProduct(product) {
|
||||
// 检查是否已添加
|
||||
const exists = this.orderItems.find(item => item.productId === product.productId)
|
||||
if (exists) {
|
||||
uni.showToast({ title: '商品已添加', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.orderItems.push({
|
||||
productId: product.productId,
|
||||
productName: product.name,
|
||||
spec: product.spec,
|
||||
unit: product.unit,
|
||||
price: product.price,
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
this.calcAmount()
|
||||
this.$refs.productPopup.close()
|
||||
},
|
||||
removeItem(index) {
|
||||
this.orderItems.splice(index, 1)
|
||||
this.calcAmount()
|
||||
},
|
||||
// 核心金额计算逻辑
|
||||
calcAmount() {
|
||||
// 1. 计算原价 = Σ(单价 × 数量)
|
||||
this.totalAmount = this.orderItems.reduce((sum, item) => {
|
||||
return sum + (item.price * item.quantity)
|
||||
}, 0)
|
||||
|
||||
// 2. 计算优惠金额 = 原价 × (100 - 折扣率) / 100
|
||||
this.discountAmount = this.totalAmount * (100 - this.discountRate) / 100
|
||||
|
||||
// 3. 计算实付金额 = 原价 - 优惠金额
|
||||
this.actualAmount = this.totalAmount - this.discountAmount
|
||||
},
|
||||
async createOrder() {
|
||||
if (this.orderItems.length === 0) {
|
||||
uni.showToast({ title: '请添加商品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
customerId: this.selectedCustomer ? this.selectedCustomer.customerId : null,
|
||||
items: this.orderItems.map(item => ({
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
price: item.price
|
||||
})),
|
||||
discountRate: parseFloat(this.discountRate),
|
||||
remark: this.remark,
|
||||
paymentMethod: this.paymentMethod
|
||||
}
|
||||
|
||||
try {
|
||||
const order = await orderApi.createOrder(data)
|
||||
|
||||
uni.showToast({ title: '订单创建成功', icon: 'success' })
|
||||
|
||||
// 跳转到订单详情或列表
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/list`
|
||||
})
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding: 20rpx;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
color: #3cc51f;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 120rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.discount-input {
|
||||
width: 120rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
padding: 20rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-spec {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.item-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quantity-edit, .price-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.qty-label, .qty-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.qty-input, .price-input {
|
||||
width: 100rpx;
|
||||
height: 56rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.item-subtotal {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
margin-left: 20rpx;
|
||||
font-size: 36rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.amount-section {
|
||||
background: #fff7e6;
|
||||
}
|
||||
|
||||
.amount-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.amount-value.discount {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.amount-row.actual {
|
||||
border-top: 1rpx dashed #ddd;
|
||||
padding-top: 20rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.amount-row.actual .amount-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.payment-methods {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.method-item {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.method-item.active {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
border: 1rpx solid #1890ff;
|
||||
}
|
||||
|
||||
.remark-input {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.submit-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.submit-info {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.submit-amount {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 200rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.product-popup {
|
||||
background: #fff;
|
||||
height: 60vh;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.popup-search {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.popup-search input {
|
||||
height: 60rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.popup-list {
|
||||
height: calc(60vh - 140rpx);
|
||||
}
|
||||
|
||||
.popup-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
372
src/pages/order/list.vue
Normal file
372
src/pages/order/list.vue
Normal file
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<view
|
||||
class="filter-item"
|
||||
:class="{ active: status === null }"
|
||||
@click="filterStatus(null)"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-item"
|
||||
:class="{ active: status === 1 }"
|
||||
@click="filterStatus(1)"
|
||||
>
|
||||
已完成
|
||||
</view>
|
||||
<view
|
||||
class="filter-item"
|
||||
:class="{ active: status === 2 }"
|
||||
@click="filterStatus(2)"
|
||||
>
|
||||
已取消
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<view class="order-list">
|
||||
<view
|
||||
v-for="order in orders"
|
||||
:key="order.orderId"
|
||||
class="order-card"
|
||||
@click="viewDetail(order)"
|
||||
>
|
||||
<view class="order-header">
|
||||
<text class="order-no">{{ order.orderNo }}</text>
|
||||
<text class="order-status" :class="getStatusClass(order.status)">
|
||||
{{ getStatusText(order.status) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="order-customer">
|
||||
<text class="customer-name">{{ order.customerName || '散客' }}</text>
|
||||
<text class="customer-phone">{{ order.customerPhone || '-' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="order-items">
|
||||
<text class="items-label">商品明细</text>
|
||||
<text class="items-count">{{ getItemCount(order.orderId) }}种商品</text>
|
||||
</view>
|
||||
|
||||
<view class="order-amount">
|
||||
<view class="amount-row">
|
||||
<text class="amount-label">原价</text>
|
||||
<text class="amount-value">¥{{ order.totalAmount }}</text>
|
||||
</view>
|
||||
<view class="amount-row">
|
||||
<text class="amount-label">优惠</text>
|
||||
<text class="amount-value discount">-¥{{ order.discountAmount }}</text>
|
||||
</view>
|
||||
<view class="amount-row actual">
|
||||
<text class="amount-label">实付</text>
|
||||
<text class="amount-value">¥{{ order.actualAmount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="order-footer">
|
||||
<text class="order-time">{{ formatTime(order.createdAt) }}</text>
|
||||
<text class="operator">{{ order.operatorName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="orders.length === 0" class="empty">
|
||||
<text>暂无订单</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="orders.length > 0" class="load-more">
|
||||
<text>{{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多了') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import orderApi from '@/api/order'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
status: null,
|
||||
orders: [],
|
||||
orderItemsMap: {},
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadOrders()
|
||||
},
|
||||
onReachBottom() {
|
||||
if (this.hasMore && !this.loading) {
|
||||
this.page++
|
||||
this.loadOrders()
|
||||
}
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.page = 1
|
||||
this.loadOrders().then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
async loadOrders() {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const res = await orderApi.getOrders({
|
||||
status: this.status,
|
||||
page: this.page,
|
||||
pageSize: this.pageSize
|
||||
})
|
||||
|
||||
const list = res.records || []
|
||||
|
||||
// 加载每个订单的明细
|
||||
for (const order of list) {
|
||||
try {
|
||||
const detail = await orderApi.getOrderDetail(order.orderId)
|
||||
this.$set(this.orderItemsMap, order.orderId, detail.items || [])
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.page === 1) {
|
||||
this.orders = list
|
||||
} else {
|
||||
this.orders = [...this.orders, ...list]
|
||||
}
|
||||
this.hasMore = list.length >= this.pageSize
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
filterStatus(status) {
|
||||
this.status = status
|
||||
this.page = 1
|
||||
this.loadOrders()
|
||||
},
|
||||
viewDetail(order) {
|
||||
// 跳转到订单详情页或显示详情弹窗
|
||||
uni.showModal({
|
||||
title: '订单详情',
|
||||
content: `订单号: ${order.orderNo}\n原价: ¥${order.totalAmount}\n优惠: ¥${order.discountAmount}\n实付: ¥${order.actualAmount}`,
|
||||
showCancel: false
|
||||
})
|
||||
},
|
||||
getItemCount(orderId) {
|
||||
const items = this.orderItemsMap[orderId]
|
||||
return items ? items.length : 0
|
||||
},
|
||||
getStatusClass(status) {
|
||||
const map = {
|
||||
1: 'status-success',
|
||||
2: 'status-cancel',
|
||||
3: 'status-refunding',
|
||||
4: 'status-refunded'
|
||||
}
|
||||
return map[status] || ''
|
||||
},
|
||||
getStatusText(status) {
|
||||
const map = {
|
||||
1: '已完成',
|
||||
2: '已取消',
|
||||
3: '退款中',
|
||||
4: '已退款'
|
||||
}
|
||||
return map[status] || '未知'
|
||||
},
|
||||
formatTime(time) {
|
||||
if (!time) return '-'
|
||||
return time.substring(0, 16).replace('T', ' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.status-cancel {
|
||||
background: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.status-refunding {
|
||||
background: #fff7e6;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.status-refunded {
|
||||
background: #f9f0ff;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.order-customer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.customer-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.customer-phone {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-items {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.items-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.items-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.amount-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.amount-value.discount {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.amount-row.actual {
|
||||
border-top: 1rpx dashed #ddd;
|
||||
padding-top: 16rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.amount-row.actual .amount-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.amount-row.actual .amount-value {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.order-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.order-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.operator {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 100rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
274
src/pages/product/list.vue
Normal file
274
src/pages/product/list.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
placeholder="搜索商品名称"
|
||||
@confirm="search"
|
||||
/>
|
||||
<button class="search-btn" @click="search">搜索</button>
|
||||
</view>
|
||||
|
||||
<!-- 分类筛选 -->
|
||||
<scroll-view class="category-scroll" scroll-x>
|
||||
<view
|
||||
class="category-item"
|
||||
:class="{ active: !categoryId }"
|
||||
@click="selectCategory('')"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
v-for="cat in categories"
|
||||
:key="cat.categoryId"
|
||||
class="category-item"
|
||||
:class="{ active: categoryId === cat.categoryId }"
|
||||
@click="selectCategory(cat.categoryId)"
|
||||
>
|
||||
{{ cat.name }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="product-list">
|
||||
<view
|
||||
v-for="item in products"
|
||||
:key="item.productId"
|
||||
class="product-item"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ item.name }}</text>
|
||||
<text class="product-spec">{{ item.spec || '-' }}</text>
|
||||
<view class="product-price">
|
||||
<text class="price">¥{{ item.price }}</text>
|
||||
<text class="unit">/{{ item.unit }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="product-stock">
|
||||
<text class="stock-label">库存</text>
|
||||
<text class="stock-value">{{ getStock(item.productId) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="products.length === 0" class="empty">
|
||||
<text>暂无商品</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="products.length > 0" class="load-more">
|
||||
<text>{{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多了') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import productApi from '@/api/product'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
keyword: '',
|
||||
categoryId: '',
|
||||
categories: [],
|
||||
products: [],
|
||||
stocks: {},
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadCategories()
|
||||
this.loadProducts()
|
||||
},
|
||||
onReachBottom() {
|
||||
if (this.hasMore && !this.loading) {
|
||||
this.page++
|
||||
this.loadProducts()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadCategories() {
|
||||
try {
|
||||
const categories = await productApi.getCategories()
|
||||
this.categories = categories || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
async loadProducts() {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const res = await productApi.getProducts({
|
||||
categoryId: this.categoryId,
|
||||
keyword: this.keyword,
|
||||
page: this.page,
|
||||
pageSize: this.pageSize
|
||||
})
|
||||
|
||||
const list = res.records || []
|
||||
if (this.page === 1) {
|
||||
this.products = list
|
||||
} else {
|
||||
this.products = [...this.products, ...list]
|
||||
}
|
||||
this.hasMore = list.length >= this.pageSize
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
selectCategory(id) {
|
||||
this.categoryId = id
|
||||
this.page = 1
|
||||
this.loadProducts()
|
||||
},
|
||||
search() {
|
||||
this.page = 1
|
||||
this.loadProducts()
|
||||
},
|
||||
viewDetail(item) {
|
||||
uni.showToast({ title: '商品详情开发中', icon: 'none' })
|
||||
},
|
||||
getStock(productId) {
|
||||
return this.stocks[productId] || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 70rpx;
|
||||
padding: 0 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
width: 120rpx;
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
margin-left: 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.category-scroll {
|
||||
white-space: nowrap;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: inline-block;
|
||||
padding: 12rpx 24rpx;
|
||||
margin-right: 16rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.category-item.active {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.product-list {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.product-spec {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: #ff4d4f;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.unit {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.product-stock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stock-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.stock-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 100rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user