Signed-off-by: 吴文峰 <kevin@lmve.net>
This commit is contained in:
@@ -0,0 +1,762 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header">
|
||||
<text class="page-title">编辑订单</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="form-content" v-if="!loading">
|
||||
<!-- 基本信息 -->
|
||||
<view class="card">
|
||||
<view class="card-title">基本信息</view>
|
||||
|
||||
<!-- 标题(必填) -->
|
||||
<view class="form-item">
|
||||
<text class="form-label required">配件名称</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.title"
|
||||
placeholder="请输入配件名称"
|
||||
maxlength="50"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">备注</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="form.remark"
|
||||
placeholder="请输入备注信息"
|
||||
maxlength="256"
|
||||
/>
|
||||
<text class="char-count">{{ form.remark.length }}/256</text>
|
||||
</view>
|
||||
|
||||
<!-- 采购链接 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">采购链接</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="form.link"
|
||||
placeholder="请输入采购链接"
|
||||
rows="2"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 款式标签 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">款式标签</text>
|
||||
<view class="tags-input">
|
||||
<view class="tag-list">
|
||||
<view
|
||||
class="tag"
|
||||
v-for="(tag, index) in tags"
|
||||
:key="index"
|
||||
>
|
||||
{{ tag }}
|
||||
<text class="tag-remove" @click="removeTag(index)">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<input
|
||||
class="tag-input"
|
||||
v-model="newTag"
|
||||
placeholder="输入标签后回车添加"
|
||||
@confirm="addTag"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 费用明细 -->
|
||||
<view class="card">
|
||||
<view class="card-title">费用明细</view>
|
||||
|
||||
<!-- 已添加的费用列表 -->
|
||||
<view class="cost-table" v-if="costEntries.length > 0">
|
||||
<view class="cost-header">
|
||||
<text class="cost-col">类型</text>
|
||||
<text class="cost-col">数量</text>
|
||||
<text class="cost-col">单价</text>
|
||||
<text class="cost-col">总计</text>
|
||||
<text class="cost-col">货币</text>
|
||||
<text class="cost-col">操作</text>
|
||||
</view>
|
||||
<view
|
||||
class="cost-row"
|
||||
v-for="(item, index) in costEntries"
|
||||
:key="index"
|
||||
>
|
||||
<text class="cost-col">{{ costType[item.type] || item.type }}</text>
|
||||
<text class="cost-col">{{ item.int }}</text>
|
||||
<text class="cost-col">{{ item.cost }}</text>
|
||||
<text class="cost-col">{{ item.costt }}</text>
|
||||
<text class="cost-col">{{ currencyOptions[item.currencytype] || item.currencytype }}</text>
|
||||
<text class="cost-col delete" @click="removeCostEntry(index)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加费用表单 -->
|
||||
<view class="cost-form">
|
||||
<view class="cost-form-row">
|
||||
<view class="cost-field">
|
||||
<text class="cost-label">费用类型</text>
|
||||
<picker mode="selector" :range="costTypeOptions" range-key="label" @change="onCostTypeChange">
|
||||
<view class="cost-picker">
|
||||
{{ costTypeOptions[costTypeIndex].label }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="cost-field">
|
||||
<text class="cost-label">数量</text>
|
||||
<input class="cost-input" type="number" v-model="newCost.int" min="1" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="cost-form-row">
|
||||
<view class="cost-field">
|
||||
<text class="cost-label">单价</text>
|
||||
<input class="cost-input" type="digit" v-model="newCost.cost" step="0.01" />
|
||||
</view>
|
||||
<view class="cost-field">
|
||||
<text class="cost-label">货币</text>
|
||||
<picker mode="selector" :range="currencyOptionsList" range-key="label" @change="onCurrencyChange">
|
||||
<view class="cost-picker">
|
||||
{{ currencyOptionsList[currencyIndex].label }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cost-total" v-if="newCostCost > 0">
|
||||
总计:{{ currencyOptionsList[currencyIndex].symbol }}{{ newCostTotal }}
|
||||
</view>
|
||||
<view class="cost-error" v-if="costError">单价必须大于0</view>
|
||||
<button class="add-cost-btn" @click="addCostEntry">添加费用</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<view class="card">
|
||||
<view class="card-title">图片</view>
|
||||
<view class="photo-upload">
|
||||
<view
|
||||
v-for="(photo, index) in photos"
|
||||
:key="index"
|
||||
class="photo-item"
|
||||
@click="previewImage(index)"
|
||||
>
|
||||
<image
|
||||
class="photo-img"
|
||||
:src="photo.url"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="photo-remove" @click.stop="removePhoto(index)">×</view>
|
||||
</view>
|
||||
<view v-if="photos.length < 9" class="photo-add" @click="chooseImage">
|
||||
<text class="photo-add-icon">+</text>
|
||||
<text class="photo-add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-area">
|
||||
<button class="submit-btn" :disabled="submitting" @click="handleSubmit">
|
||||
{{ submitting ? '提交中...' : '保存修改' }}
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-else class="loading-view"><text>加载中...</text></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { purchaseApi } from '@/api/purchase.js'
|
||||
import { api } from '@/api/index.js'
|
||||
import { useConfigStore } from '@/stores/config.js'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// 订单ID
|
||||
const orderId = ref(null)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
|
||||
// 费用类型选项
|
||||
const costTypeOptions = [
|
||||
{ value: 1, label: '单价' },
|
||||
{ value: 2, label: '运费' }
|
||||
]
|
||||
const costTypeIndex = ref(0)
|
||||
const costType = { 1: '单价', 2: '运费' }
|
||||
|
||||
// 货币选项
|
||||
const currencyOptionsList = [
|
||||
{ value: 1, label: 'CNY', symbol: '¥' },
|
||||
{ value: 2, label: 'MOP', symbol: 'MOP' },
|
||||
{ value: 3, label: 'HKD', symbol: 'HK$' },
|
||||
{ value: 4, label: 'USD', symbol: '$' }
|
||||
]
|
||||
const currencyOptions = { 1: 'CNY', 2: 'MOP', 3: 'HKD', 4: 'USD' }
|
||||
const currencyIndex = ref(0)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
title: '',
|
||||
remark: '',
|
||||
link: '',
|
||||
styles: '',
|
||||
photos: []
|
||||
})
|
||||
|
||||
// 标签
|
||||
const tags = ref([])
|
||||
const newTag = ref('')
|
||||
|
||||
// 费用明细
|
||||
const costEntries = reactive([])
|
||||
const newCost = reactive({
|
||||
type: 1,
|
||||
int: 1,
|
||||
cost: 0,
|
||||
currencyType: 1
|
||||
})
|
||||
const costError = ref(false)
|
||||
|
||||
// 图片
|
||||
const photos = ref([])
|
||||
const newCostCost = computed(() => parseFloat(newCost.cost) || 0)
|
||||
const newCostTotal = computed(() => (newCost.int * newCostCost.value).toFixed(2))
|
||||
|
||||
// 提交状态
|
||||
const submitting = ref(false)
|
||||
|
||||
// 初始化页面
|
||||
onLoad((options) => {
|
||||
if (options?.id) {
|
||||
orderId.value = parseInt(options.id)
|
||||
fetchOrderDetail()
|
||||
} else {
|
||||
uni.showToast({ title: '缺少订单ID', icon: 'none' })
|
||||
setTimeout(() => uni.navigateBack(), 1500)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取订单详情
|
||||
async function fetchOrderDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await purchaseApi.getOrder({ id: orderId.value })
|
||||
if (res.errCode === 0 && res.data?.order) {
|
||||
const order = res.data.order
|
||||
|
||||
// 填充表单 (注意:Go 结构体字段首字母大写)
|
||||
form.title = order.Title || ''
|
||||
form.remark = order.Remark || ''
|
||||
form.link = order.Link || ''
|
||||
form.styles = order.Styles || ''
|
||||
|
||||
// 解析标签
|
||||
if (order.Styles) {
|
||||
tags.value = order.Styles.split(',').filter(t => t.trim())
|
||||
}
|
||||
|
||||
// 解析费用 (API返回的是数据库模型 Price/Quantity/CurrencyType/CostType)
|
||||
costEntries.length = 0
|
||||
if (res.data.costs && res.data.costs.length > 0) {
|
||||
res.data.costs.forEach(c => {
|
||||
costEntries.push({
|
||||
type: c.CostType,
|
||||
int: c.Quantity,
|
||||
cost: (c.Price / 100).toFixed(2),
|
||||
costt: ((c.Price * c.Quantity) / 100).toFixed(2),
|
||||
currencytype: c.CurrencyType
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 解析图片
|
||||
photos.value = []
|
||||
if (res.data.photos && res.data.photos.length > 0) {
|
||||
res.data.photos.forEach(p => {
|
||||
photos.value.push({
|
||||
hash: p.Sha256,
|
||||
url: `${configStore.getFileBaseUrl()}/api/files/get/${p.Sha256}`
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: '获取订单信息失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取订单详情失败', e)
|
||||
uni.showToast({ title: '获取订单信息失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加标签
|
||||
function addTag() {
|
||||
if (newTag.value.trim()) {
|
||||
tags.value.push(newTag.value.trim())
|
||||
newTag.value = ''
|
||||
form.styles = tags.value.join(',')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除标签
|
||||
function removeTag(index) {
|
||||
tags.value.splice(index, 1)
|
||||
form.styles = tags.value.join(',')
|
||||
}
|
||||
|
||||
// 费用类型选择
|
||||
function onCostTypeChange(e) {
|
||||
costTypeIndex.value = e.detail.value
|
||||
newCost.type = costTypeOptions[e.detail.value].value
|
||||
}
|
||||
|
||||
// 货币类型选择
|
||||
function onCurrencyChange(e) {
|
||||
currencyIndex.value = e.detail.value
|
||||
newCost.currencyType = currencyOptionsList[e.detail.value].value
|
||||
}
|
||||
|
||||
// 添加费用
|
||||
function addCostEntry() {
|
||||
if (newCost.cost <= 0) {
|
||||
costError.value = true
|
||||
return
|
||||
}
|
||||
costError.value = false
|
||||
costEntries.push({
|
||||
type: newCost.type,
|
||||
int: newCost.int,
|
||||
cost: parseFloat(newCost.cost).toFixed(2),
|
||||
costt: newCostTotal.value,
|
||||
currencytype: newCost.currencyType
|
||||
})
|
||||
// 重置
|
||||
newCost.type = 1
|
||||
newCost.int = 1
|
||||
newCost.cost = 0
|
||||
newCost.currencyType = 1
|
||||
costTypeIndex.value = 0
|
||||
currencyIndex.value = 0
|
||||
}
|
||||
|
||||
// 删除费用
|
||||
function removeCostEntry(index) {
|
||||
costEntries.splice(index, 1)
|
||||
}
|
||||
|
||||
// 选择图片
|
||||
function chooseImage() {
|
||||
uni.chooseImage({
|
||||
count: 9 - photos.value.length,
|
||||
success: (res) => {
|
||||
res.tempFiles.forEach(file => {
|
||||
uploadImage(file.path)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
async function uploadImage(filePath) {
|
||||
uni.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
try {
|
||||
const res = await api.upload('/files/upload/image', { uri: filePath, name: 'file' })
|
||||
if (res.errCode === 0 && res.data?.hash) {
|
||||
photos.value.push({
|
||||
hash: res.data.hash,
|
||||
url: `${configStore.getFileBaseUrl()}/api/files/get/${res.data.hash}`
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('上传失败', e)
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
function previewImage(index) {
|
||||
const urls = photos.value.map(p => p.url)
|
||||
uni.previewImage({
|
||||
urls,
|
||||
current: index
|
||||
})
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
function removePhoto(index) {
|
||||
photos.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
// 验证必填项
|
||||
if (!form.title.trim()) {
|
||||
uni.showToast({ title: '请输入配件名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
// 转换费用数据(元转分)
|
||||
const costs = costEntries.map(h => ({
|
||||
type: h.type,
|
||||
int: h.int,
|
||||
cost: Math.round(parseFloat(h.cost) * 100),
|
||||
costt: Math.round(parseFloat(h.costt) * 100),
|
||||
currencytype: h.currencytype
|
||||
}))
|
||||
|
||||
// 转换图片
|
||||
const photoHashes = photos.value.map(p => p.hash)
|
||||
|
||||
const res = await purchaseApi.updateOrder({
|
||||
id: orderId.value,
|
||||
title: form.title,
|
||||
remark: form.remark,
|
||||
link: form.link,
|
||||
styles: form.styles,
|
||||
costs,
|
||||
photos: photoHashes
|
||||
})
|
||||
|
||||
if (res.errCode === 0) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
uni.$emit('purchase-refresh')
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交失败', e)
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.form-label.required::after {
|
||||
content: ' *';
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 20rpx 24rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
min-height: 120rpx;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 标签输入 */
|
||||
.tags-input {
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #e6f0ff;
|
||||
color: #1890ff;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
margin-left: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 费用明细 */
|
||||
.cost-table {
|
||||
margin-bottom: 30rpx;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cost-header,
|
||||
.cost-row {
|
||||
display: flex;
|
||||
padding: 16rpx 12rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.cost-header {
|
||||
background-color: #f8f8f8;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cost-row {
|
||||
border-top: 1px solid #eee;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cost-col {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cost-col.delete {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.cost-form {
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.cost-form-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.cost-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cost-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.cost-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
padding: 0 16rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cost-picker {
|
||||
height: 72rpx;
|
||||
padding: 0 16rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 8rpx;
|
||||
line-height: 72rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cost-total {
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
color: #1890ff;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.cost-error {
|
||||
font-size: 24rpx;
|
||||
color: #f56c6c;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.add-cost-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 12rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 图片上传 */
|
||||
.photo-upload {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
position: relative;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.photo-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.photo-remove {
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
right: -12rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background-color: #f56c6c;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 28rpx;
|
||||
text-align: center;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.photo-add {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
border: 2rpx dashed #ddd;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.photo-add-icon {
|
||||
font-size: 60rpx;
|
||||
color: #ccc;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.photo-add-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit-area {
|
||||
padding: 20rpx 0 40rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.submit-btn[disabled] {
|
||||
background-color: #a0cfff;
|
||||
}
|
||||
|
||||
.loading-view {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,679 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header">
|
||||
<text class="page-title">新增订单</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="form-content">
|
||||
<!-- 基本信息 -->
|
||||
<view class="card">
|
||||
<view class="card-title">基本信息</view>
|
||||
|
||||
<!-- 标题(必填) -->
|
||||
<view class="form-item">
|
||||
<text class="form-label required">配件名称</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.title"
|
||||
placeholder="请输入配件名称"
|
||||
maxlength="50"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">备注</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="form.remark"
|
||||
placeholder="请输入备注信息"
|
||||
maxlength="256"
|
||||
/>
|
||||
<text class="char-count">{{ form.remark.length }}/256</text>
|
||||
</view>
|
||||
|
||||
<!-- 采购链接 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">采购链接</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="form.link"
|
||||
placeholder="请输入采购链接"
|
||||
rows="2"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 款式标签 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">款式标签</text>
|
||||
<view class="tags-input">
|
||||
<view class="tag-list">
|
||||
<view
|
||||
class="tag"
|
||||
v-for="(tag, index) in tags"
|
||||
:key="index"
|
||||
>
|
||||
{{ tag }}
|
||||
<text class="tag-remove" @click="removeTag(index)">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<input
|
||||
class="tag-input"
|
||||
v-model="newTag"
|
||||
placeholder="输入标签后回车添加"
|
||||
@confirm="addTag"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 费用明细 -->
|
||||
<view class="card">
|
||||
<view class="card-title">费用明细</view>
|
||||
|
||||
<!-- 已添加的费用列表 -->
|
||||
<view class="cost-table" v-if="costEntries.length > 0">
|
||||
<view class="cost-header">
|
||||
<text class="cost-col">类型</text>
|
||||
<text class="cost-col">数量</text>
|
||||
<text class="cost-col">单价</text>
|
||||
<text class="cost-col">总计</text>
|
||||
<text class="cost-col">货币</text>
|
||||
<text class="cost-col">操作</text>
|
||||
</view>
|
||||
<view
|
||||
class="cost-row"
|
||||
v-for="(item, index) in costEntries"
|
||||
:key="index"
|
||||
>
|
||||
<text class="cost-col">{{ costType[item.type] || item.type }}</text>
|
||||
<text class="cost-col">{{ item.int }}</text>
|
||||
<text class="cost-col">{{ item.cost }}</text>
|
||||
<text class="cost-col">{{ item.costt }}</text>
|
||||
<text class="cost-col">{{ currencyOptions[item.currencytype] || item.currencytype }}</text>
|
||||
<text class="cost-col delete" @click="removeCostEntry(index)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加费用表单 -->
|
||||
<view class="cost-form">
|
||||
<view class="cost-form-row">
|
||||
<view class="cost-field">
|
||||
<text class="cost-label">费用类型</text>
|
||||
<picker mode="selector" :range="costTypeOptions" range-key="label" @change="onCostTypeChange">
|
||||
<view class="cost-picker">
|
||||
{{ costTypeOptions[costTypeIndex].label }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="cost-field">
|
||||
<text class="cost-label">数量</text>
|
||||
<input class="cost-input" type="number" v-model="newCost.int" min="1" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="cost-form-row">
|
||||
<view class="cost-field">
|
||||
<text class="cost-label">单价</text>
|
||||
<input class="cost-input" type="digit" v-model="newCost.cost" step="0.01" />
|
||||
</view>
|
||||
<view class="cost-field">
|
||||
<text class="cost-label">货币</text>
|
||||
<picker mode="selector" :range="currencyOptionsList" range-key="label" @change="onCurrencyChange">
|
||||
<view class="cost-picker">
|
||||
{{ currencyOptionsList[currencyIndex].label }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cost-total" v-if="newCostCost > 0">
|
||||
总计:{{ currencyOptionsList[currencyIndex].symbol }}{{ newCostTotal }}
|
||||
</view>
|
||||
<view class="cost-error" v-if="costError">单价必须大于0</view>
|
||||
<button class="add-cost-btn" @click="addCostEntry">添加费用</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<view class="card">
|
||||
<view class="card-title">图片</view>
|
||||
<view class="photo-upload">
|
||||
<view
|
||||
v-for="(photo, index) in photos"
|
||||
:key="index"
|
||||
class="photo-item"
|
||||
@click="previewImage(index)"
|
||||
>
|
||||
<image
|
||||
class="photo-img"
|
||||
:src="photo.url"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="photo-remove" @click.stop="removePhoto(index)">×</view>
|
||||
</view>
|
||||
<view v-if="photos.length < 9" class="photo-add" @click="chooseImage">
|
||||
<text class="photo-add-icon">+</text>
|
||||
<text class="photo-add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-area">
|
||||
<button class="submit-btn" :disabled="submitting" @click="handleSubmit">
|
||||
{{ submitting ? '提交中...' : '提交订单' }}
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { purchaseApi } from '@/api/purchase.js'
|
||||
import { api } from '@/api/index.js'
|
||||
import { useConfigStore } from '@/stores/config.js'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// 费用类型选项
|
||||
const costTypeOptions = [
|
||||
{ value: 1, label: '单价' },
|
||||
{ value: 2, label: '运费' }
|
||||
]
|
||||
const costTypeIndex = ref(0)
|
||||
const costType = { 1: '单价', 2: '运费' }
|
||||
|
||||
// 货币选项
|
||||
const currencyOptionsList = [
|
||||
{ value: 1, label: 'CNY', symbol: '¥' },
|
||||
{ value: 2, label: 'MOP', symbol: 'MOP' },
|
||||
{ value: 3, label: 'HKD', symbol: 'HK$' },
|
||||
{ value: 4, label: 'USD', symbol: '$' }
|
||||
]
|
||||
const currencyOptions = { 1: 'CNY', 2: 'MOP', 3: 'HKD', 4: 'USD' }
|
||||
const currencyIndex = ref(0)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
title: '',
|
||||
remark: '',
|
||||
link: '',
|
||||
styles: '',
|
||||
photos: []
|
||||
})
|
||||
|
||||
// 标签
|
||||
const tags = ref([])
|
||||
const newTag = ref('')
|
||||
|
||||
// 费用明细
|
||||
const costEntries = reactive([])
|
||||
const newCost = reactive({
|
||||
type: 1,
|
||||
int: 1,
|
||||
cost: 0,
|
||||
currencyType: 1
|
||||
})
|
||||
const costError = ref(false)
|
||||
|
||||
// 图片
|
||||
const photos = ref([])
|
||||
const newCostCost = computed(() => parseFloat(newCost.cost) || 0)
|
||||
const newCostTotal = computed(() => (newCost.int * newCostCost.value).toFixed(2))
|
||||
|
||||
// 提交状态
|
||||
const submitting = ref(false)
|
||||
|
||||
// 添加标签
|
||||
function addTag() {
|
||||
if (newTag.value.trim()) {
|
||||
tags.value.push(newTag.value.trim())
|
||||
newTag.value = ''
|
||||
form.styles = tags.value.join(',')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除标签
|
||||
function removeTag(index) {
|
||||
tags.value.splice(index, 1)
|
||||
form.styles = tags.value.join(',')
|
||||
}
|
||||
|
||||
// 费用类型选择
|
||||
function onCostTypeChange(e) {
|
||||
costTypeIndex.value = e.detail.value
|
||||
newCost.type = costTypeOptions[e.detail.value].value
|
||||
}
|
||||
|
||||
// 货币类型选择
|
||||
function onCurrencyChange(e) {
|
||||
currencyIndex.value = e.detail.value
|
||||
newCost.currencyType = currencyOptionsList[e.detail.value].value
|
||||
}
|
||||
|
||||
// 添加费用
|
||||
function addCostEntry() {
|
||||
if (newCost.cost <= 0) {
|
||||
costError.value = true
|
||||
return
|
||||
}
|
||||
costError.value = false
|
||||
costEntries.push({
|
||||
type: newCost.type,
|
||||
int: newCost.int,
|
||||
cost: parseFloat(newCost.cost).toFixed(2),
|
||||
costt: newCostTotal.value,
|
||||
currencytype: newCost.currencyType
|
||||
})
|
||||
// 重置
|
||||
newCost.type = 1
|
||||
newCost.int = 1
|
||||
newCost.cost = 0
|
||||
newCost.currencyType = 1
|
||||
costTypeIndex.value = 0
|
||||
currencyIndex.value = 0
|
||||
}
|
||||
|
||||
// 删除费用
|
||||
function removeCostEntry(index) {
|
||||
costEntries.splice(index, 1)
|
||||
}
|
||||
|
||||
// 选择图片
|
||||
function chooseImage() {
|
||||
uni.chooseImage({
|
||||
count: 9,
|
||||
success: (res) => {
|
||||
res.tempFiles.forEach(file => {
|
||||
uploadImage(file.path)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
async function uploadImage(filePath) {
|
||||
uni.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
try {
|
||||
const res = await api.upload('/files/upload/image', { uri: filePath, name: 'file' })
|
||||
if (res.errCode === 0 && res.data?.hash) {
|
||||
photos.value.push({
|
||||
hash: res.data.hash,
|
||||
url: `${configStore.getFileBaseUrl()}/api/files/get/${res.data.hash}`
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('上传失败', e)
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
function previewImage(index) {
|
||||
const urls = photos.value.map(p => p.url)
|
||||
uni.previewImage({
|
||||
urls,
|
||||
current: index
|
||||
})
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
function removePhoto(index) {
|
||||
photos.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
// 验证必填项
|
||||
if (!form.title.trim()) {
|
||||
uni.showToast({ title: '请输入配件名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
// 转换费用数据(元转分)
|
||||
const costs = costEntries.map(h => ({
|
||||
type: h.type,
|
||||
int: h.int,
|
||||
cost: Math.round(parseFloat(h.cost) * 100),
|
||||
costt: Math.round(parseFloat(h.costt) * 100),
|
||||
currencytype: h.currencytype
|
||||
}))
|
||||
|
||||
// 转换图片
|
||||
const photoHashes = photos.value.map(p => p.hash)
|
||||
|
||||
const res = await purchaseApi.addOrder({
|
||||
title: form.title,
|
||||
remark: form.remark,
|
||||
link: form.link,
|
||||
styles: form.styles,
|
||||
costs,
|
||||
photos: photoHashes
|
||||
})
|
||||
|
||||
if (res.errCode === 0) {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
uni.$emit('purchase-refresh')
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交失败', e)
|
||||
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.form-label.required::after {
|
||||
content: ' *';
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 20rpx 24rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
min-height: 120rpx;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 标签输入 */
|
||||
.tags-input {
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #e6f0ff;
|
||||
color: #1890ff;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
margin-left: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 费用明细 */
|
||||
.cost-table {
|
||||
margin-bottom: 30rpx;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cost-header,
|
||||
.cost-row {
|
||||
display: flex;
|
||||
padding: 16rpx 12rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.cost-header {
|
||||
background-color: #f8f8f8;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cost-row {
|
||||
border-top: 1px solid #eee;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cost-col {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cost-col.delete {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.cost-form {
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.cost-form-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.cost-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cost-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.cost-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
padding: 0 16rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cost-picker {
|
||||
height: 72rpx;
|
||||
padding: 0 16rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 8rpx;
|
||||
line-height: 72rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cost-total {
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
color: #1890ff;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.cost-error {
|
||||
font-size: 24rpx;
|
||||
color: #f56c6c;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.add-cost-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 12rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 图片上传 */
|
||||
.photo-upload {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
position: relative;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.photo-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.photo-remove {
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
right: -12rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background-color: #f56c6c;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 28rpx;
|
||||
text-align: center;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.photo-add {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
border: 2rpx dashed #ddd;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.photo-add-icon {
|
||||
font-size: 60rpx;
|
||||
color: #ccc;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.photo-add-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit-area {
|
||||
padding: 20rpx 0 40rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.submit-btn[disabled] {
|
||||
background-color: #a0cfff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,510 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="back-btn" @click="goBack">‹ 返回</text>
|
||||
<text class="title">订单详情</text>
|
||||
<view class="header-actions">
|
||||
<text v-if="order" class="print-btn" @click="printOrder">🖨</text>
|
||||
<text v-if="canModify" class="edit-btn" @click="goEdit">编辑</text>
|
||||
<view v-else class="header-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="content" refresher-enabled @refresherrefresh="onRefresh" :refresher-triggered="refreshing">
|
||||
<view v-if="loading" class="loading"><text>加载中...</text></view>
|
||||
<view v-else-if="order">
|
||||
<!-- 基本信息 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">订单信息</text>
|
||||
<text class="order-status" :class="order.OrderStatus">{{ getStatusText(order.OrderStatus) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">标题</text>
|
||||
<text class="info-value">{{ order.Title || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="order.Link">
|
||||
<text class="info-label">链接</text>
|
||||
<text class="info-value link" @click="openLink">{{ order.Link }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="order.Styles">
|
||||
<text class="info-label">样式</text>
|
||||
<text class="info-value">{{ order.Styles }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="order.Remark">
|
||||
<text class="info-label">备注</text>
|
||||
<text class="info-value">{{ order.Remark }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">创建时间</text>
|
||||
<text class="info-value">{{ formatDate(order.CreatedAt) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态切换 -->
|
||||
<view class="card">
|
||||
<view class="card-header"><text class="card-title">变更状态</text></view>
|
||||
<view class="status-buttons">
|
||||
<view v-for="opt in statusOptions" :key="opt.value" class="status-btn" :class="order.OrderStatus === opt.value ? 'active ' + opt.value : ''" @click="openStatusDialog(opt.value)">
|
||||
<text>{{ opt.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 费用明细 -->
|
||||
<view class="card" v-if="costs.length > 0">
|
||||
<view class="card-header"><text class="card-title">费用明细</text></view>
|
||||
<view class="cost-table">
|
||||
<view class="cost-header">
|
||||
<text class="cost-th">类型</text>
|
||||
<text class="cost-th">数量</text>
|
||||
<text class="cost-th">单价</text>
|
||||
<text class="cost-th">小计</text>
|
||||
</view>
|
||||
<view class="cost-row" v-for="(cost, idx) in costs" :key="idx">
|
||||
<text class="cost-td">{{ getCostTypeText(cost.CostType) }}</text>
|
||||
<text class="cost-td">{{ cost.Quantity }}</text>
|
||||
<text class="cost-td">{{ formatPrice(cost.Price) }}</text>
|
||||
<text class="cost-td">{{ formatPrice(cost.Price * cost.Quantity) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cost-total">
|
||||
<text v-for="g in costsByCurrency" :key="g.currency" class="total-tag">{{ g.currency }} {{ g.total }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片 -->
|
||||
<view class="card" v-if="photos.length > 0">
|
||||
<view class="card-header"><text class="card-title">图片 ({{ photos.length }})</text></view>
|
||||
<view class="photo-grid">
|
||||
<view v-for="photo in photos" :key="photo.ID" class="photo-item">
|
||||
<image :src="getImageUrl(photo.Sha256)" mode="aspectFill" @click="previewImages(photo.Sha256)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 关联工单 -->
|
||||
<view class="card" v-if="workOrders.length > 0">
|
||||
<view class="card-header"><text class="card-title">关联工单</text></view>
|
||||
<view v-for="wo in workOrders" :key="wo.id" class="workorder-item" @click="goToWorkOrder(wo.id)">
|
||||
<text class="wo-id">#{{ wo.id }}</text>
|
||||
<text class="wo-title">{{ wo.title }}</text>
|
||||
<text class="wo-status" :class="wo.status">{{ getWOStatusText(wo.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态记录 -->
|
||||
<view class="card" v-if="commits.length > 0">
|
||||
<view class="card-header"><text class="card-title">状态记录</text></view>
|
||||
<view class="timeline">
|
||||
<view v-for="commit in commits" :key="commit.id" class="timeline-item">
|
||||
<view class="timeline-dot" :class="commit.status"></view>
|
||||
<view class="timeline-content">
|
||||
<view class="timeline-header">
|
||||
<text class="timeline-status" :class="commit.status">{{ getStatusText(commit.status) }}</text>
|
||||
<text class="timeline-date">{{ formatDate(commit.createdAt) }}</text>
|
||||
</view>
|
||||
<text class="timeline-user">by {{ getUsernameById(commit.userId) }}</text>
|
||||
<text class="timeline-comment" v-if="commit.comment">{{ commit.comment }}</text>
|
||||
<view class="timeline-photos" v-if="commit.photos && commit.photos.length">
|
||||
<image v-for="(hash, idx) in commit.photos" :key="hash" class="timeline-photo" :src="getImageUrl(hash)" mode="aspectFill" @click="previewCommitImages(commit.photos, idx)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty"><text>未找到订单信息</text></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 状态变更弹窗 -->
|
||||
<view class="modal" v-if="statusDialogVisible" @click="closeStatusDialog">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">变更状态</text>
|
||||
<text class="modal-close" @click="closeStatusDialog">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="form-label">新状态</text>
|
||||
<view class="status-display">{{ getStatusText(pendingStatus) }}</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">备注</text>
|
||||
<textarea v-model="pendingComment" class="form-textarea" placeholder="可选" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">图片</text>
|
||||
<view class="photo-upload-area">
|
||||
<view v-for="(photo, idx) in pendingPhotos" :key="idx" class="uploaded-photo">
|
||||
<image :src="photo.url" mode="aspectFill" />
|
||||
<view class="photo-remove" @click="pendingPhotos.splice(idx, 1)">×</view>
|
||||
</view>
|
||||
<view class="add-photo-btn" @click="chooseImage" v-if="pendingPhotos.length < 9">
|
||||
<text>+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<text class="btn-cancel" @click="closeStatusDialog">取消</text>
|
||||
<text class="btn-confirm" @click="confirmStatusChange">确认</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { purchaseApi } from '@/api/purchase.js'
|
||||
import { useConfigStore } from '@/stores/config.js'
|
||||
import { fetchUserInfo, getUsername } from '@/stores/users.js'
|
||||
import { api } from '@/api/index.js'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const orderId = ref(0)
|
||||
const loading = ref(false)
|
||||
const order = ref(null)
|
||||
const costs = ref([])
|
||||
const photos = ref([])
|
||||
const commits = ref([])
|
||||
const workOrders = ref([])
|
||||
const canModify = ref(false)
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'ordered', label: '已下单' },
|
||||
{ value: 'arrived', label: '已到达' },
|
||||
{ value: 'received', label: '已收件' },
|
||||
{ value: 'lost', label: '丢件' },
|
||||
{ value: 'returned', label: '退件' }
|
||||
]
|
||||
|
||||
const currencyOptions = { 1: 'CNY', 2: 'MOP', 3: 'HKD', 4: 'USD' }
|
||||
const costTypeOptions = { 1: '单价', 2: '运费' }
|
||||
|
||||
const costsByCurrency = computed(() => {
|
||||
const groups = {}
|
||||
costs.value.forEach(c => {
|
||||
const cur = currencyOptions[c.CurrencyType] || 'Unknown'
|
||||
const amount = c.Price && c.Quantity ? ((c.Price * c.Quantity) / 100).toFixed(2) : '0.00'
|
||||
if (!groups[cur]) groups[cur] = 0
|
||||
groups[cur] += parseFloat(amount)
|
||||
})
|
||||
return Object.entries(groups).map(([currency, total]) => ({ currency, total: total.toFixed(2) }))
|
||||
})
|
||||
|
||||
const statusDialogVisible = ref(false)
|
||||
const pendingStatus = ref('')
|
||||
const pendingComment = ref('')
|
||||
const pendingPhotos = ref([])
|
||||
const submitting = ref(false)
|
||||
const refreshing = ref(false)
|
||||
|
||||
function getStatusText(status) {
|
||||
return statusOptions.find(s => s.value === status)?.label || status
|
||||
}
|
||||
|
||||
function getCurrencyText(type) { return currencyOptions[type] || '-' }
|
||||
function getCostTypeText(type) { return costTypeOptions[type] || type }
|
||||
function getWOStatusText(status) {
|
||||
return { pending: '待处理', checked: '已检查', parts_ordered: '已下单零件', repaired: '已维修', returned: '已送还', unrepairable: '无法维修' }[status] || status
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
const d = new Date(dateStr)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function formatPrice(priceInCents) {
|
||||
return priceInCents ? (priceInCents / 100).toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
function getImageUrl(sha256) {
|
||||
return configStore.getFileBaseUrl() + '/api/files/get/' + sha256
|
||||
}
|
||||
|
||||
function previewImages(currentSha) {
|
||||
const allPhotos = photos.value || []
|
||||
const urls = allPhotos.map(p => getImageUrl(p.Sha256))
|
||||
const idx = allPhotos.findIndex(p => p.Sha256 === currentSha)
|
||||
uni.previewImage({ urls, current: idx >= 0 ? idx : 0 })
|
||||
}
|
||||
|
||||
function previewCommitImages(photoHashes, currentIndex) {
|
||||
const urls = photoHashes.map(h => getImageUrl(h))
|
||||
uni.previewImage({ urls, current: currentIndex })
|
||||
}
|
||||
|
||||
function getUsernameById(userId) {
|
||||
return getUsername(userId) || `用户${userId}`
|
||||
}
|
||||
|
||||
async function fetchOrderDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await purchaseApi.getOrder({ id: orderId.value })
|
||||
if (res.errCode === 0 && res.data) {
|
||||
order.value = res.data.order || null
|
||||
canModify.value = res.data.canModify || false
|
||||
costs.value = res.data.costs || []
|
||||
photos.value = res.data.photos || []
|
||||
commits.value = res.data.commits || []
|
||||
workOrders.value = res.data.workOrders || []
|
||||
|
||||
// 预取用户信息
|
||||
if (order.value?.UserID) fetchUserInfo(order.value.UserID)
|
||||
commits.value.forEach(c => {
|
||||
if (c.userId) fetchUserInfo(c.userId)
|
||||
})
|
||||
} else {
|
||||
order.value = null
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取订单详情失败', e)
|
||||
order.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openStatusDialog(status) {
|
||||
pendingStatus.value = status
|
||||
pendingComment.value = ''
|
||||
pendingPhotos.value = []
|
||||
statusDialogVisible.value = true
|
||||
}
|
||||
|
||||
function closeStatusDialog() {
|
||||
statusDialogVisible.value = false
|
||||
pendingStatus.value = ''
|
||||
pendingComment.value = ''
|
||||
pendingPhotos.value = []
|
||||
}
|
||||
|
||||
async function chooseImage() {
|
||||
uni.chooseImage({
|
||||
count: 9 - pendingPhotos.value.length,
|
||||
success: (res) => res.tempFilePaths.forEach(path => uploadImage(path))
|
||||
})
|
||||
}
|
||||
|
||||
async function uploadImage(filePath) {
|
||||
try {
|
||||
const res = await api.upload('/files/upload/image', { uri: filePath, name: 'file' })
|
||||
if (res.errCode === 0 && res.data?.hash) {
|
||||
pendingPhotos.value.push({ hash: res.data.hash, url: getImageUrl(res.data.hash) })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('上传失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmStatusChange() {
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await api.post('/purchase/updatestatus', {
|
||||
id: orderId.value,
|
||||
status: pendingStatus.value,
|
||||
comment: pendingComment.value,
|
||||
photos: pendingPhotos.value.map(p => p.hash)
|
||||
})
|
||||
if (res.errCode === 0) {
|
||||
uni.showToast({ title: '状态更新成功', icon: 'success' })
|
||||
closeStatusDialog()
|
||||
fetchOrderDetail()
|
||||
uni.$emit('purchase-refresh')
|
||||
} else {
|
||||
uni.showToast({ title: res.errMsg || '更新失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() { uni.navigateBack() }
|
||||
|
||||
function printOrder() {
|
||||
if (!order.value) return
|
||||
|
||||
// #ifndef APP-PLUS
|
||||
uni.showToast({ title: '打印功能仅在 App 端可用', icon: 'none' })
|
||||
return
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
const printer = uni.requireNativePlugin('LcPrinter')
|
||||
|
||||
// 初始化打印机
|
||||
printer.initPrinter({})
|
||||
printer.setConcentration({ level: 39 })
|
||||
printer.setLineSpacing({ spacing: 1 })
|
||||
|
||||
// 标签打印模式(使用黑标定位)
|
||||
printer.printEnableMark({ enable: true })
|
||||
|
||||
// 第一行:标题(加粗大字)
|
||||
printer.setFontSize({ fontSize: 1 })
|
||||
printer.setTextBold({ bold: true })
|
||||
printer.printText({ content: (order.value.Title || '(无标题)')+'\n' })
|
||||
//printer.printLine({ line_length: 1 })
|
||||
|
||||
// 第二行:备注
|
||||
printer.setFontSize({ fontSize: 0 })
|
||||
printer.setTextBold({ bold: false })
|
||||
printer.printText({ content: '备注: ' + (order.value.Remark || '(无备注)')+'\n' })
|
||||
//printer.printLine({ line_length: 1 })
|
||||
|
||||
// 第三行:样式
|
||||
printer.printText({ content: '样式: ' + (order.value.Styles || '(无样式)')+'\n' })
|
||||
//printer.printLine({ line_length: 1 })
|
||||
|
||||
// 第四行:创建日期
|
||||
printer.printText({ content: '日期: ' + formatDate(order.value.CreatedAt) })
|
||||
//printer.printLine({ line_length: 1 })
|
||||
|
||||
// 条形码:内容 po:ID,高度 4
|
||||
printer.printBarcode({
|
||||
text: 'po:' + orderId.value,
|
||||
height: 40,
|
||||
barcodeType: 73
|
||||
})
|
||||
|
||||
printer.printGoToNextMark()
|
||||
// #endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
function goEdit() {
|
||||
uni.navigateTo({ url: `/pages/order/edit-order?id=${orderId.value}` })
|
||||
}
|
||||
|
||||
function goToWorkOrder(id) {
|
||||
uni.navigateTo({ url: `/pages/workorder/show-workorder?id=${id}` })
|
||||
}
|
||||
|
||||
function openLink() {
|
||||
if (!order.value?.Link) return
|
||||
let url = order.value.Link.trim()
|
||||
if (!/^https?:\/\//i.test(url)) url = 'https://' + url
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
success: () => uni.showToast({ title: '链接已复制', icon: 'success' })
|
||||
})
|
||||
}
|
||||
|
||||
async function onRefresh() {
|
||||
refreshing.value = true
|
||||
await fetchOrderDetail()
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.id) {
|
||||
orderId.value = parseInt(options.id)
|
||||
fetchOrderDetail()
|
||||
}
|
||||
})
|
||||
|
||||
// 每次页面显示时刷新数据
|
||||
onShow(() => {
|
||||
if (orderId.value) {
|
||||
fetchOrderDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container { min-height: 100vh; background-color: #f5f5f5; padding-bottom: 20rpx; }
|
||||
.header { background-color: #fff; padding: 30rpx; display: flex; align-items: center; }
|
||||
.back-btn { font-size: 32rpx; color: #007AFF; margin-right: 20rpx; }
|
||||
.title { font-size: 36rpx; font-weight: bold; color: #333; flex: 1; text-align: center; }
|
||||
.header-actions { display: flex; align-items: center; gap: 20rpx; }
|
||||
.print-btn { font-size: 36rpx; color: #007AFF; }
|
||||
.edit-btn { font-size: 28rpx; color: #007AFF; }
|
||||
.header-right { width: 60rpx; }
|
||||
.content { padding: 20rpx; height: calc(100vh - 120rpx); }
|
||||
.loading, .empty { display: flex; justify-content: center; align-items: center; height: 300rpx; color: #999; }
|
||||
.card { background-color: #fff; border-radius: 16rpx; margin-bottom: 20rpx; overflow: hidden; }
|
||||
.card-header { padding: 24rpx 30rpx; border-bottom: 1rpx solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-title { font-size: 30rpx; font-weight: bold; color: #333; }
|
||||
.order-status { font-size: 24rpx; padding: 8rpx 20rpx; border-radius: 20rpx; }
|
||||
.order-status.pending, .timeline-status.pending, .timeline-dot.pending { background-color: #fff7e6; color: #faad14; }
|
||||
.order-status.ordered, .timeline-status.ordered, .timeline-dot.ordered { background-color: #e6f7ff; color: #1890ff; }
|
||||
.order-status.arrived, .timeline-status.arrived, .timeline-dot.arrived { background-color: #f9f0ff; color: #722ed1; }
|
||||
.order-status.received, .timeline-status.received, .timeline-dot.received { background-color: #f6ffed; color: #52c41a; }
|
||||
.order-status.lost, .timeline-status.lost, .timeline-dot.lost { background-color: #fff1f0; color: #ff4d4f; }
|
||||
.order-status.returned, .timeline-status.returned, .timeline-dot.returned { background-color: #f5f5f5; color: #999; }
|
||||
.info-row { padding: 20rpx 30rpx; display: flex; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.info-label { width: 160rpx; color: #999; font-size: 28rpx; flex-shrink: 0; }
|
||||
.info-value { flex: 1; color: #333; font-size: 28rpx; word-break: break-all; }
|
||||
.info-value.link { color: #007AFF; }
|
||||
.status-buttons { display: flex; flex-wrap: wrap; padding: 20rpx; gap: 20rpx; }
|
||||
.status-btn { padding: 16rpx 30rpx; border-radius: 30rpx; background-color: #f5f5f5; color: #666; font-size: 26rpx; }
|
||||
.status-btn.active { color: #fff; }
|
||||
.status-btn.active.pending { background-color: #faad14; }
|
||||
.status-btn.active.ordered { background-color: #1890ff; }
|
||||
.status-btn.active.arrived { background-color: #722ed1; }
|
||||
.status-btn.active.received { background-color: #52c41a; }
|
||||
.status-btn.active.lost { background-color: #ff4d4f; }
|
||||
.status-btn.active.returned { background-color: #999; }
|
||||
.cost-table { padding: 0 20rpx 20rpx; }
|
||||
.cost-header, .cost-row { display: flex; padding: 20rpx 10rpx; }
|
||||
.cost-header { background-color: #fafafa; border-radius: 8rpx; }
|
||||
.cost-th, .cost-td { flex: 1; text-align: center; font-size: 24rpx; }
|
||||
.cost-th { color: #999; font-weight: 500; }
|
||||
.cost-total { padding: 20rpx; border-top: 1rpx solid #f0f0f0; display: flex; gap: 20rpx; }
|
||||
.total-tag { background-color: #e6f7ff; color: #1890ff; padding: 8rpx 20rpx; border-radius: 8rpx; font-size: 24rpx; }
|
||||
.photo-grid { display: flex; flex-wrap: wrap; padding: 20rpx; gap: 20rpx; }
|
||||
.photo-item { width: 200rpx; height: 200rpx; border-radius: 12rpx; overflow: hidden; }
|
||||
.photo-item image { width: 100%; height: 100%; }
|
||||
.workorder-item { padding: 24rpx 30rpx; border-bottom: 1rpx solid #f0f0f0; display: flex; align-items: center; }
|
||||
.wo-id { color: #999; font-size: 26rpx; margin-right: 16rpx; }
|
||||
.wo-title { flex: 1; color: #333; font-size: 28rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wo-status { font-size: 24rpx; padding: 8rpx 16rpx; border-radius: 16rpx; margin-left: 16rpx; }
|
||||
.wo-status.pending { background-color: #fff7e6; color: #faad14; }
|
||||
.wo-status.checked { background-color: #e6f7ff; color: #1890ff; }
|
||||
.wo-status.parts_ordered { background-color: #f9f0ff; color: #722ed1; }
|
||||
.wo-status.repaired { background-color: #f6ffed; color: #52c41a; }
|
||||
.wo-status.returned { background-color: #f5f5f5; color: #999; }
|
||||
.wo-status.unrepairable { background-color: #fff1f0; color: #ff4d4f; }
|
||||
.timeline { padding: 20rpx 30rpx; }
|
||||
.timeline-item { display: flex; padding-bottom: 30rpx; position: relative; }
|
||||
.timeline-item:not(:last-child)::before { content: ''; position: absolute; left: 10rpx; top: 30rpx; bottom: 0; width: 2rpx; background-color: #e5e5e5; }
|
||||
.timeline-dot { width: 20rpx; height: 20rpx; border-radius: 50%; margin-top: 8rpx; flex-shrink: 0; margin-right: 20rpx; }
|
||||
.timeline-content { flex: 1; }
|
||||
.timeline-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.timeline-date { font-size: 24rpx; color: #999; }
|
||||
.timeline-user { display: block; font-size: 24rpx; color: #999; margin-top: 8rpx; }
|
||||
.timeline-comment { display: block; font-size: 28rpx; color: #333; margin-top: 12rpx; }
|
||||
.timeline-photos { display: flex; flex-wrap: wrap; gap: 16rpx; margin-top: 16rpx; }
|
||||
.timeline-photo { width: 100rpx; height: 100rpx; border-radius: 8rpx; }
|
||||
.modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: flex-end; z-index: 999; }
|
||||
.modal-content { width: 100%; background-color: #fff; border-radius: 24rpx 24rpx 0 0; max-height: 80vh; }
|
||||
.modal-header { padding: 30rpx; display: flex; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.modal-close { font-size: 48rpx; color: #999; line-height: 1; }
|
||||
.modal-body { padding: 30rpx; max-height: 60vh; overflow-y: auto; }
|
||||
.form-item { margin-bottom: 30rpx; }
|
||||
.form-label { display: block; font-size: 28rpx; color: #666; margin-bottom: 16rpx; }
|
||||
.status-display { font-size: 32rpx; color: #333; font-weight: 500; }
|
||||
.form-textarea { width: 100%; height: 160rpx; border: 1rpx solid #e5e5e5; border-radius: 12rpx; padding: 20rpx; font-size: 28rpx; box-sizing: border-box; }
|
||||
.photo-upload-area { display: flex; flex-wrap: wrap; gap: 20rpx; }
|
||||
.uploaded-photo { width: 150rpx; height: 150rpx; border-radius: 12rpx; overflow: hidden; position: relative; }
|
||||
.uploaded-photo image { width: 100%; height: 100%; }
|
||||
.photo-remove { position: absolute; top: -10rpx; right: -10rpx; width: 40rpx; height: 40rpx; background-color: rgba(0, 0, 0, 0.5); color: #fff; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 28rpx; }
|
||||
.add-photo-btn { width: 150rpx; height: 150rpx; border-radius: 12rpx; border: 2rpx dashed #ddd; display: flex; justify-content: center; align-items: center; }
|
||||
.add-photo-btn text { font-size: 60rpx; color: #999; }
|
||||
.modal-footer { padding: 30rpx; display: flex; gap: 20rpx; border-top: 1rpx solid #f0f0f0; }
|
||||
.btn-cancel, .btn-confirm { flex: 1; text-align: center; padding: 24rpx; border-radius: 12rpx; font-size: 30rpx; }
|
||||
.btn-cancel { background-color: #f5f5f5; color: #666; }
|
||||
.btn-confirm { background-color: #007AFF; color: #fff; }
|
||||
</style>
|
||||
@@ -0,0 +1,433 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">采购订单</text>
|
||||
<view class="add-btn" @click="goToAdd">
|
||||
<text>+ 新增</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-item" @click="filterStatus('pending')">
|
||||
<text class="stat-num">{{ stats.pending || 0 }}</text>
|
||||
<text class="stat-label">待处理</text>
|
||||
</view>
|
||||
<view class="stat-item" @click="filterStatus('ordered')">
|
||||
<text class="stat-num">{{ stats.ordered || 0 }}</text>
|
||||
<text class="stat-label">已下单</text>
|
||||
</view>
|
||||
<view class="stat-item" @click="filterStatus('arrived')">
|
||||
<text class="stat-num">{{ stats.arrived || 0 }}</text>
|
||||
<text class="stat-label">已到达</text>
|
||||
</view>
|
||||
<view class="stat-item" @click="filterStatus('received')">
|
||||
<text class="stat-num">{{ stats.received || 0 }}</text>
|
||||
<text class="stat-label">已收件</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-bar">
|
||||
<view class="search-box">
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索订单..."
|
||||
v-model="searchText"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
<text class="search-btn" @click="onSearch">搜索</text>
|
||||
</view>
|
||||
<picker mode="selector" :range="statusOptions" range-key="label" @change="onStatusChange">
|
||||
<view class="picker">
|
||||
{{ currentFilter.label || '全部状态' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="order-list" refresher-enabled @refresherrefresh="onRefresh" :refresher-triggered="refreshing" @scrolltolower="loadMore">
|
||||
<view v-if="loading && orders.length === 0" class="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="orders.length === 0" class="empty">
|
||||
<text>暂无订单</text>
|
||||
</view>
|
||||
<view v-else>
|
||||
<view
|
||||
v-for="item in orders"
|
||||
:key="item.ID"
|
||||
class="order-card"
|
||||
@click="goDetail(item.ID)"
|
||||
>
|
||||
<view class="order-header">
|
||||
<text class="order-id">#{{ item.ID }}</text>
|
||||
<text class="order-status" :class="item.OrderStatus">{{ getStatusText(item.OrderStatus) }}</text>
|
||||
</view>
|
||||
<view class="order-content">
|
||||
<text class="order-title">{{ item.Title || '无标题' }}</text>
|
||||
<text class="order-remark" v-if="item.Remark">{{ item.Remark }}</text>
|
||||
</view>
|
||||
<view class="order-footer">
|
||||
<text class="order-date">{{ formatDate(item.CreatedAt) }}</text>
|
||||
<text class="order-link" v-if="item.Link">有链接</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loadingMore" class="loading-more">
|
||||
<text>加载更多...</text>
|
||||
</view>
|
||||
<view v-else-if="hasMore" class="load-more-btn" @click="loadMore">
|
||||
<text>加载更多</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { purchaseApi } from '@/api/purchase.js'
|
||||
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const orders = ref([])
|
||||
const stats = ref({})
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const hasMore = ref(false)
|
||||
const currentFilter = ref({})
|
||||
const searchText = ref('')
|
||||
const refreshing = ref(false)
|
||||
const statusOptions = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'ordered', label: '已下单' },
|
||||
{ value: 'arrived', label: '已到达' },
|
||||
{ value: 'received', label: '已收件' },
|
||||
{ value: 'lost', label: '丢件' },
|
||||
{ value: 'returned', label: '退件' }
|
||||
]
|
||||
|
||||
function getStatusText(status) {
|
||||
const option = statusOptions.find(s => s.value === status)
|
||||
return option ? option.label : status
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const res = await purchaseApi.getOrderCount()
|
||||
if (res.errCode === 0 && res.data) {
|
||||
stats.value = res.data
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取订单统计失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOrders(reset = false) {
|
||||
if (reset) {
|
||||
page.value = 1
|
||||
orders.value = []
|
||||
}
|
||||
|
||||
if (loading.value || loadingMore.value) return
|
||||
if (!reset && !hasMore.value) return
|
||||
|
||||
if (reset) {
|
||||
loading.value = true
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
entries: pageSize.value,
|
||||
page: page.value
|
||||
}
|
||||
if (currentFilter.value.value) {
|
||||
params.status = currentFilter.value.value
|
||||
}
|
||||
if (searchText.value) {
|
||||
params.search = searchText.value
|
||||
}
|
||||
|
||||
const res = await purchaseApi.getOrders(params)
|
||||
if (res.errCode === 0 && res.data) {
|
||||
const list = res.data.all_orders || []
|
||||
if (reset) {
|
||||
orders.value = list
|
||||
} else {
|
||||
orders.value = [...orders.value, ...list]
|
||||
}
|
||||
hasMore.value = list.length >= pageSize.value
|
||||
page.value++
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取订单列表失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onStatusChange(e) {
|
||||
currentFilter.value = statusOptions[e.detail.value]
|
||||
fetchOrders(true)
|
||||
}
|
||||
|
||||
function filterStatus(status) {
|
||||
currentFilter.value = statusOptions.find(s => s.value === status) || statusOptions[0]
|
||||
fetchOrders(true)
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
fetchOrders(true)
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (hasMore.value && !loadingMore.value) {
|
||||
fetchOrders(false)
|
||||
}
|
||||
}
|
||||
|
||||
function goDetail(id) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/order-detail?id=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
fetchOrders(true)
|
||||
})
|
||||
|
||||
// 监听状态变更后刷新
|
||||
uni.$on('purchase-refresh', () => {
|
||||
fetchStats()
|
||||
fetchOrders(true)
|
||||
})
|
||||
|
||||
function goToAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/order/order-add'
|
||||
})
|
||||
}
|
||||
|
||||
async function onRefresh() {
|
||||
refreshing.value = true
|
||||
await Promise.all([fetchStats(), fetchOrders(true)])
|
||||
refreshing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
padding: 20rpx 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 15rpx 0;
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background-color: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background-color: #f0f0f0;
|
||||
padding: 15rpx 25rpx;
|
||||
border-radius: 10rpx 0 0 10rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
padding: 15rpx 35rpx;
|
||||
border-radius: 0 10rpx 10rpx 0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker {
|
||||
background-color: #f0f0f0;
|
||||
padding: 20rpx 30rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
height: calc(100vh - 480rpx);
|
||||
}
|
||||
|
||||
.loading, .empty {
|
||||
text-align: center;
|
||||
padding: 100rpx 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background-color: #fff;
|
||||
margin: 0 20rpx 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.order-id {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.order-status.pending {
|
||||
background-color: #fff3e0;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.order-status.ordered {
|
||||
background-color: #e3f2fd;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.order-status.arrived {
|
||||
background-color: #e8f5e9;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.order-status.received {
|
||||
background-color: #e0f2f1;
|
||||
color: #009688;
|
||||
}
|
||||
|
||||
.order-status.lost {
|
||||
background-color: #ffebee;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.order-status.returned {
|
||||
background-color: #f3e5f5;
|
||||
color: #9c27b0;
|
||||
}
|
||||
|
||||
.order-content {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.order-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.order-remark {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
padding-top: 15rpx;
|
||||
}
|
||||
|
||||
.order-date {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-link {
|
||||
font-size: 24rpx;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.loading-more, .load-more-btn {
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.load-more-btn {
|
||||
background-color: #fff;
|
||||
margin: 0 20rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user