Browse Source

init

master
han\hanst 8 months ago
parent
commit
b48508a5a8
  1. 22
      src/api/production/production-inbound.js
  2. 9
      src/api/sales-return/sales-return.js
  3. 3
      src/i18n/locales/cn.js
  4. 3
      src/i18n/locales/en.js
  5. 8
      src/router/index.js
  6. 305
      src/views/common/login.vue
  7. 56
      src/views/main.vue
  8. 1450
      src/views/modules/production-inbound/index.vue
  9. 394
      src/views/modules/production-inbound/receive.vue
  10. 299
      src/views/modules/sales-return/index.vue
  11. 241
      src/views/modules/sales-return/sales-return-inbound.vue
  12. 133
      src/views/modules/sales-return/sales-return-scrap.vue

22
src/api/production/production-inbound.js

@ -0,0 +1,22 @@
import { createAPI } from "@/utils/httpRequest.js";
// 查询处理单元列表
export const getOrderInfo = data => createAPI(`/production/inbound/getOrderInfo`,'post',data)
// 创建包装箱
export const createPackage = data => createAPI(`/production/inbound/createPackage`,'post',data)
// 打印箱标签
export const printPackageLabel = data => createAPI(`/production/inbound/printPackageLabel`,'post',data)
// 装托盘
export const packToPallet = data => createAPI(`/production/inbound/packToPallet`,'post',data)
// 扫描包装箱获取信息
export const scanPackage = data => createAPI(`/production/inbound/scanPackage`,'post',data)
// 根据包装编码获取包装信息
export const getPackageInfo = data => createAPI(`/production/inbound/getPackageInfo`,'post',data)
// 入库登记
export const inboundRegister = data => createAPI(`/production/inbound/inboundRegister`,'post',data)
// 查询入库记录
export const getInboundRecords = data => createAPI(`/production/inbound/getInboundRecords`,'post',data)
// 生产订单退库
export const returnStock = data => createAPI(`/production/inbound/returnStock`,'post',data)
// 扫描单元获取退库信息
export const scanUnitForReturn = data => createAPI(`/production/inbound/scanUnitForReturn`,'post',data)

9
src/api/sales-return/sales-return.js

@ -0,0 +1,9 @@
import { createAPI } from "@/utils/httpRequest.js";
// 获取采购单信息
export const processReturn = data => createAPI(`salesreturn/processReturn`,'post',data)
// 接收采购单信息
export const getRmaList = data => createAPI(`salesreturn/getRmaList`,'post',data)

3
src/i18n/locales/cn.js

@ -12,7 +12,8 @@ export default {
logout: "退出",
locale: "语言",
stockIn : "采购入库",
greeting: "{name}!"
greeting: "{name}!",
pdaName:"仓储PDA系统"
},
button: {
login: "登录"

3
src/i18n/locales/en.js

@ -12,7 +12,8 @@ export default {
logout: "Logout",
locale: "Language",
stockIn: "Stock In",
greeting: "{name}!" // 支持动态插值
greeting: "{name}!" ,
pdaName:"Storage PDA System"
},
button: {
login: "Login"

8
src/router/index.js

@ -39,10 +39,8 @@ const globalRoutes = [
name: 'ProductionIssuePick',component: resolve => require(["@/views/modules/production-issue/pick.vue"], resolve),
meta: { transition: 'instant' ,preload: true,keepAlive: true}},
// 生产入库
{path: "/productionInbound",name: "productionwhse", component: resolve => require(["@/views/modules/production-inbound/index.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
{path: '/production-inbound/receive/:orderNo',
name: 'ProductionIssuePick',component: resolve => require(["@/views/modules/production-inbound/receive.vue"], resolve),
meta: { transition: 'instant' ,preload: true,keepAlive: true}},
{path: "/productionInbound",name: "productionInbound", component: resolve => require(["@/views/modules/production-inbound/index.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
// 委外
{path: "/outsource",name: "outsource", component: resolve => require(["@/views/modules/outsourcing-issue/index.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
@ -51,6 +49,8 @@ const globalRoutes = [
// 销售退货
{path: "/salereturn",name: "salereturn", component: resolve => require(["@/views/modules/sales-return/index.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
{path: "/salereturn-inbound",name: "salereturn-inbound", component: resolve => require(["@/views/modules/sales-return/sales-return-inbound.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
{path: "/salereturn-scrap",name: "salereturn-scrap", component: resolve => require(["@/views/modules/sales-return/sales-return-scrap.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
// 其他出入库
{path: "/otherinout",name: "otherinout", component: resolve => require(["@/views/modules/other-transaction/index.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},

305
src/views/common/login.vue

@ -1,51 +1,69 @@
<template>
<div class="pda-login">
<div class="login-card">
<!-- 语言切换 -->
<div class="lang-switch">
<el-dropdown trigger="click">
<span class="lang-text">
{{ $t('home.locale') }}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="lang in languageList"
:key="lang.languageCode"
@click.native="switch_the_language(lang.languageCode)"
>
{{ lang.languageName }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="logo-area">
<svg viewBox="0 0 48 48">
<path fill="#2196F3" d="M24 6L6 24l18 18 18-18z"/>
<path fill="#1976D2" d="M24 6v36l18-18z"/>
</svg>
<h1>Storage PDA system</h1>
<h1>{{ $t('home.pdaName') }}</h1>
</div>
<el-form class="login-form" @keyup.enter.native="dataFormSubmit()">
<!-- 用户名 -->
<div class="input-group" :class="{focus: usernameFocus}">
<input placeholder="username"
<input placeholder="Username"
type="text"
v-model="dataForm.userName"
@focus="usernameFocus=true"
@blur="usernameFocus=false"
required
>
v-model="dataForm.userName"
@focus="usernameFocus=true"
@blur="handleUsernameBlur"
required />
</div>
<!-- 密码 -->
<div class="input-group" :class="{focus: passwordFocus}">
<input placeholder="password"
:type="showPassword?'text':'password'"
v-model="dataForm.password"
@focus="passwordFocus=true"
@blur="passwordFocus=false"
required
>
<button
type="button"
class="pwd-toggle"
@click="showPassword=!showPassword"
>
{{ showPassword ? 'hide' : 'view' }}
</button>
<input placeholder="Password"
:type="showPassword ? 'text' : 'password'"
v-model="dataForm.password"
@focus="passwordFocus=true"
@blur="passwordFocus=false"
required />
</div>
<button
type="submit"
class="login-btn"
:disabled="loading"
@click.prevent="dataFormSubmit"
>
<!-- 工厂下拉框site -->
<el-select
v-if="siteList.length"
v-model="selectedSite"
placeholder="请选择工厂"
style="width: 100%; margin-bottom: 1.5rem"
:popper-append-to-body="false">
<el-option
v-for="item in siteList"
:key="item.siteCode"
:label="item.siteName"
:value="item.siteCode" />
</el-select>
<!-- 登录按钮 -->
<button type="submit" class="login-btn" :disabled="loading" @click.prevent="dataFormSubmit">
<span v-if="!loading">Login</span>
<span v-else class="loading-dots"></span>
</button>
@ -54,131 +72,69 @@
</div>
</template>
<script>
export default {
data() {
return {
form: { username: '', password: '' },
usernameFocus: false,
passwordFocus: false,
showPassword: false,
loading: false,
src: 'http://127.0.0.1/upload/ifs.png',
dataForm: {
userName: '',
password: '',
uuid: '',
captcha: ''
},
dataRule: {
userName: [
{ required: true, message: '帐号不能为空', trigger: 'blur' }
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' }
]
},
captchaPath: ''
}
},
computed: {
multiLanguage: {
get() {
return this.$store.state.user.multiLanguage
},
set(val) {
this.$store.commit('user/updateMultiLanguage', val)
}
},
authControl: {
get() {
return this.$store.state.user.authControl
uuid: ''
},
set(val) {
this.$store.commit('user/updateAuthControl', val)
}
}
},
mounted() {
// redirectPath
const redirectPath = this.$route.query.redirect || '/home';
if (!localStorage.getItem('redirectPath')) {
localStorage.setItem('redirectPath', redirectPath);
}
},
created () {
this.userName();
// redirectPath
const currentPath = localStorage.getItem('redirectPath');
if (!currentPath) {
localStorage.setItem('redirectPath', this.$route.fullPath);
}
usernameFocus: false,
passwordFocus: false,
showPassword: false,
loading: false,
siteList: [{ "siteCode": "1", "siteName": "南京工厂" },
{ "siteCode": "2", "siteName": "苏州工厂" }],
languageList: [
{ languageName: '中文', languageCode: 'cn' },
{ languageName: 'English', languageCode: 'en' }
],
selectedSite: ''
};
},
methods: {
//
userName(){
this.dataForm.userName = localStorage.getItem('userName')
switch_the_language(val) {
localStorage.setItem('locale', val)
this.$i18n.locale = val; //
location.reload()
},
//
dataFormSubmit () {
this.$http({
url: this.$http.adornUrl('/sys/login'),
method: 'post',
data: this.$http.adornData({
'username': this.dataForm.userName,
'password': this.dataForm.password,
'uuid': this.dataForm.uuid
})
}).then(({data}) => {
if (data && data.code === 0) {
console.log('跳转前路径:', localStorage.getItem('redirectPath')); //
this.$cookie.set('token', data.token)
//
const redirectPath = localStorage.getItem('redirectPath') || '';
handleUsernameBlur() {
//
const pathsToRedirect = ['/auth-authQuote', '/auth-authInquiry'];
if (pathsToRedirect.some(path => redirectPath.indexOf(path) !== -1)) {
this.$router.replace(redirectPath);
} else {
this.$router.replace({ name: 'home' });
}
console.log('Redirecting to:', redirectPath); //
this.$i18n.locale=data.language
localStorage.setItem('locale', data.language)
this.$i18n.locale = data.language; //
localStorage.setItem('refresh', "0")
localStorage.setItem('userName', this.dataForm.userName)
//this.getConfigParams()
} else {
this.$alert("用户名或密码错误", '错误', {
confirmButtonText: '确定'
})
//this.$message.error("")
}
})
},
//
getConfigParams() {
getConfigParams().then(({data}) => {
if (data && data.code == 0) {
localStorage.setItem('configParams', JSON.stringify(data.data))
// this.multiLanguage = JSON.parse(localStorage.getItem('configParams')).multiLanguage
// this.authControl = JSON.parse(localStorage.getItem('configParams')).authControl
dataFormSubmit() {
if (!this.selectedSite) {
this.$message.error('请选择工厂');
return;
}
this.loading = true;
this.$http({
url: this.$http.adornUrl('/sys/login'),
method: 'post',
data: this.$http.adornData({
username: this.dataForm.userName,
password: this.dataForm.password,
uuid: this.dataForm.uuid,
site: this.selectedSite
})
}).then(({ data }) => {
this.loading = false;
if (data && data.code === 0) {
this.$cookie.set('token', data.token);
localStorage.setItem('userName', this.dataForm.userName);
localStorage.setItem('site', this.selectedSite);
this.$router.replace({ name: 'home' });
} else {
this.$alert("用户名或密码错误", '错误', {
confirmButtonText: '确定'
});
}
})
},
handleSubmit() {
this.loading = true
// API
setTimeout(() => {
this.$router.push('/dashboard')
this.loading = false
}, 1500)
});
}
}
}
</script>
@ -188,34 +144,43 @@ export default {
display: flex;
justify-content: center;
align-items: center;
background: #f0f2f5;
background: linear-gradient(to right, #e0f7fa, #f1f8e9);
font-family: 'Segoe UI', sans-serif;
}
.login-card {
position: relative;
width: 90%;
max-width: 400px;
padding: 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border-radius: 16px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1);
}
.lang-switch {
position: absolute;
top: 18px;
right: 20px;
}
.lang-text {
cursor: pointer;
font-size: 14px;
color: #1890ff;
}
.logo-area {
text-align: center;
margin-bottom: 2rem;
margin-bottom: 1.5rem;
}
.logo-area svg {
width: 64px;
height: 64px;
filter: drop-shadow(0 2px 4px rgba(33, 150, 243, 0.3));
}
.logo-icon {
width: 60px;
height: 60px;
fill: #1890ff;
}
h1 {
color: #333;
font-size: 1.5rem;
@ -224,35 +189,26 @@ h1 {
.input-group {
position: relative;
margin-bottom: 1.5rem;
border-bottom: 1px solid #ddd;
transition: all 0.3s;
margin-bottom: 1.2rem;
border-bottom: 1px solid #ccc;
transition: all 0.3s ease;
}
.input-group.focus {
border-color: #1890ff;
}
.input-group label {
position: absolute;
left: 0;
top: 8px;
color: #999;
transition: all 0.3s;
}
.input-group.focus label {
top: -18px;
font-size: 12px;
color: #1890ff;
}
.input-group input {
width: 100%;
padding: 8px 0;
padding: 10px 0;
border: none;
outline: none;
font-size: 16px;
font-size: 15px;
}
.input-group.site-select {
border: none;
padding-top: 8px;
}
.pwd-toggle {
@ -264,6 +220,7 @@ h1 {
border: none;
color: #1890ff;
font-size: 12px;
cursor: pointer;
}
.login-btn {
@ -272,19 +229,23 @@ h1 {
background: #1890ff;
color: white;
border: none;
border-radius: 6px;
border-radius: 8px;
font-size: 16px;
margin-top: 1rem;
margin-top: 1.2rem;
transition: all 0.3s;
}
.login-btn:hover {
background: #40a9ff;
}
.login-btn:disabled {
background: #bae7ff;
background: #a0d4ff;
}
.loading-dots {
letter-spacing: 2px;
animation: blink 1.5s infinite;
animation: blink 1.2s infinite;
}
@keyframes blink {

56
src/views/main.vue

@ -12,7 +12,11 @@
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div class="logout-button" @click="logoutHandle()">{{ $t('home.logout') }}</div>
<div class="logout-button" @click="logoutHandle()">
<i class="el-icon-close"></i>
<span class="logout-text">{{ $t('home.logout') }}</span>
</div>
</div>
<!-- 功能菜单 -->
@ -140,7 +144,7 @@ export default {
flex-direction: column;
background: #f5f5f5;
font-family: 'Arial', sans-serif;
overflow: auto;
overflow-y: auto;
}
.status-bar {
@ -157,19 +161,55 @@ export default {
}
.logout-button {
color: #ffffff;
background: linear-gradient(135deg, #ff4d4f, #ff7875);
color: #fff;
font-size: 15px;
padding: 5px 10px;
background: #ff4d4f;
border-radius: 5px;
font-weight: bold;
padding: 6px 16px;
border-radius: 20px;
cursor: pointer;
transition: background 0.3s;
box-shadow: 0 2px 6px rgba(255, 77, 79, 0.4);
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
height: 44px; /* 保证可点击性 */
}
.logout-button:hover {
background: #ff7875;
background: linear-gradient(135deg, #ff7875, #ff9c91);
box-shadow: 0 4px 12px rgba(255, 77, 79, 0.5);
transform: scale(1.05);
}
.logout-button i {
font-size: 18px;
margin-right: 8px;
display: flex;
align-items: center;
}
.logout-text {
display: inline-block;
line-height: 1;
}
/* === 小屏优化 === */
@media screen and (max-width: 480px) {
.logout-button {
font-size: 13px;
padding: 4px 12px;
border-radius: 16px;
height: 40px;
}
.logout-button i {
font-size: 16px;
margin-right: 6px;
}
}
.menu-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);

1450
src/views/modules/production-inbound/index.vue
File diff suppressed because it is too large
View File

394
src/views/modules/production-inbound/receive.vue

@ -1,394 +0,0 @@
<template>
<div class="receive-container">
<van-nav-bar title="生产入库" left-arrow @click-left="$router.back()" />
<!-- 订单信息 -->
<div class="order-info">
<div class="info-header">
<div class="order-no">{{ orderInfo.orderNo }}</div>
<div class="order-status">{{ getStatusText(orderInfo.status) }}</div>
</div>
<van-cell-group>
<van-cell title="产品编码" :value="orderInfo.productCode" />
<van-cell title="产品名称" :value="orderInfo.productName" />
<van-cell title="计划数量" :value="orderInfo.planQuantity" />
<van-cell title="已完工" :value="orderInfo.completedQuantity" />
<van-cell title="已入库" :value="orderInfo.inboundQuantity" />
</van-cell-group>
</div>
<!-- 入库明细 -->
<div class="inbound-section">
<div class="section-title">入库明细</div>
<div class="inbound-item">
<div class="product-info">
<div class="product-name">{{ orderInfo.productCode }} - {{ orderInfo.productName }}</div>
<div class="product-spec">规格{{ orderInfo.specification }}</div>
<div class="product-quantity">
可入库{{ orderInfo.completedQuantity - orderInfo.inboundQuantity }}
</div>
</div>
<div class="inbound-input">
<van-stepper
v-model="inboundQuantity"
:min="0"
:max="orderInfo.completedQuantity - orderInfo.inboundQuantity"
integer
/>
</div>
</div>
</div>
<!-- 质量检验 -->
<div class="quality-section">
<div class="section-title">质量检验</div>
<van-field
v-model="qualityInfo.inspector"
label="检验员"
placeholder="请输入检验员"
/>
<van-field
v-model="qualityInfo.qualityStatus"
label="质量状态"
placeholder="请选择质量状态"
readonly
is-link
@click="showQualityPicker = true"
/>
<van-field
v-model="qualityInfo.qualityRemark"
label="检验备注"
type="textarea"
placeholder="请输入检验备注"
rows="2"
/>
</div>
<!-- 批次信息 -->
<div class="batch-section">
<div class="section-title">批次信息</div>
<van-field
v-model="batchInfo.batchNo"
label="批次号"
placeholder="系统自动生成"
disabled
/>
<van-field
v-model="batchInfo.productionDate"
label="生产日期"
placeholder="请选择生产日期"
readonly
is-link
@click="showDatePicker = true"
/>
<van-field
v-model="batchInfo.expiryDate"
label="有效期至"
placeholder="请选择有效期"
readonly
is-link
@click="showExpiryPicker = true"
/>
</div>
<!-- 库位分配 -->
<div class="location-section">
<div class="section-title">库位分配</div>
<van-field
v-model="selectedLocation"
label="入库库位"
placeholder="请选择或扫描库位"
readonly
is-link
@click="showLocationPicker = true"
>
<template #right-icon>
<van-icon name="scan" @click.stop="handleScanLocation" />
</template>
</van-field>
</div>
<!-- 备注 -->
<div class="remark-section">
<van-field
v-model="remark"
label="备注"
type="textarea"
placeholder="请输入入库备注"
rows="3"
autosize
/>
</div>
<!-- 底部按钮 -->
<div class="bottom-actions">
<van-button
type="primary"
block
:loading="submitting"
@click="handleSubmit"
>
确认入库
</van-button>
</div>
<!-- 选择器 -->
<van-popup v-model="showQualityPicker" position="bottom">
<van-picker
:columns="qualityColumns"
@confirm="onQualityConfirm"
@cancel="showQualityPicker = false"
/>
</van-popup>
<van-popup v-model="showDatePicker" position="bottom">
<van-datetime-picker
v-model="productionDate"
type="date"
title="选择生产日期"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
/>
</van-popup>
<van-popup v-model="showExpiryPicker" position="bottom">
<van-datetime-picker
v-model="expiryDate"
type="date"
title="选择有效期"
@confirm="onExpiryConfirm"
@cancel="showExpiryPicker = false"
/>
</van-popup>
<van-popup v-model="showLocationPicker" position="bottom">
<van-picker
:columns="locationColumns"
@confirm="onLocationConfirm"
@cancel="showLocationPicker = false"
/>
</van-popup>
</div>
</template>
<script>
export default {
name: 'ProductionReceive',
data() {
return {
orderInfo: {
orderNo: 'MO202401001',
productCode: 'PROD001',
productName: '成品A',
specification: '标准规格',
planQuantity: 100,
completedQuantity: 80,
inboundQuantity: 0,
status: 0
},
inboundQuantity: 0,
qualityInfo: {
inspector: '',
qualityStatus: '',
qualityRemark: ''
},
batchInfo: {
batchNo: 'BATCH' + Date.now(),
productionDate: '',
expiryDate: ''
},
selectedLocation: '',
remark: '',
submitting: false,
showQualityPicker: false,
showDatePicker: false,
showExpiryPicker: false,
showLocationPicker: false,
productionDate: new Date(),
expiryDate: new Date(),
qualityColumns: [
'合格',
'待检',
'不合格',
'返工'
],
locationColumns: [
'FG01-01-01',
'FG01-01-02',
'FG01-02-01',
'FG01-02-02',
'FG02-01-01'
]
}
},
mounted() {
this.loadOrderData()
},
methods: {
loadOrderData() {
const orderNo = this.$route.params.orderNo
console.log('加载生产订单数据:', orderNo)
},
handleScanLocation() {
this.$toast('扫描库位功能开发中...')
},
onQualityConfirm(value) {
this.qualityInfo.qualityStatus = value
this.showQualityPicker = false
},
onDateConfirm(value) {
this.batchInfo.productionDate = this.formatDate(value)
this.showDatePicker = false
},
onExpiryConfirm(value) {
this.batchInfo.expiryDate = this.formatDate(value)
this.showExpiryPicker = false
},
onLocationConfirm(value) {
this.selectedLocation = value
this.showLocationPicker = false
},
formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
async handleSubmit() {
//
if (this.inboundQuantity <= 0) {
this.$toast('请输入入库数量')
return
}
//
if (!this.qualityInfo.qualityStatus) {
this.$toast('请选择质量状态')
return
}
//
if (!this.selectedLocation) {
this.$toast('请选择入库库位')
return
}
this.submitting = true
try {
await new Promise(resolve => setTimeout(resolve, 2000))
this.$toast.success('入库成功')
this.$router.back()
} catch (error) {
this.$toast.fail('入库失败')
} finally {
this.submitting = false
}
},
getStatusText(status) {
const statusMap = {
0: '待入库',
1: '部分入库',
2: '已完成'
}
return statusMap[status] || '未知'
}
}
}
</script>
<style scoped>
.receive-container {
min-height: 100vh;
background-color: #f7f8fa;
padding-bottom: 80px;
}
.order-info {
background: white;
margin-bottom: 10px;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #ebedf0;
}
.order-no {
font-size: 18px;
font-weight: bold;
color: #323233;
}
.order-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
color: white;
background-color: #ff976a;
}
.inbound-section,
.quality-section,
.batch-section,
.location-section,
.remark-section {
background: white;
margin-bottom: 10px;
}
.section-title {
padding: 16px;
font-size: 16px;
font-weight: bold;
color: #323233;
border-bottom: 1px solid #ebedf0;
}
.inbound-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 16px;
font-weight: bold;
color: #323233;
margin-bottom: 4px;
}
.product-spec {
font-size: 12px;
color: #969799;
margin-bottom: 4px;
}
.product-quantity {
font-size: 14px;
color: #646566;
}
.inbound-input {
margin-left: 16px;
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: white;
border-top: 1px solid #ebedf0;
}
</style>

299
src/views/modules/sales-return/index.vue

@ -1,266 +1,119 @@
<template>
<div class="sales-return-container">
<van-nav-bar title="销售退货" left-arrow @click-left="$router.back()" />
<!-- 搜索栏 -->
<div class="search-section">
<van-search
v-model="searchValue"
placeholder="请输入退货单号"
@search="handleSearch"
@clear="handleClear"
/>
<div class="pda-container">
<!-- 状态栏 -->
<div class="status-bar">
<div class="goBack" @click="$router.back()"><i class="el-icon-arrow-left"></i>上一页</div>
<div class="time">销售退货</div>
<div class="network" style="color: #fff" @click="$router.push({path: '/'})">🏠首页</div>
</div>
<!-- 状态筛选 -->
<div class="filter-section">
<van-tabs v-model="activeTab" @change="handleTabChange">
<van-tab title="全部" name="all" />
<van-tab title="待收货" name="0" />
<van-tab title="质检中" name="1" />
<van-tab title="已完成" name="2" />
</van-tabs>
</div>
<!-- 退货单列表 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
<!-- 功能菜单 -->
<div class="menu-grid">
<div
class="menu-item"
v-for="(btn, index) in buttons"
:key="index"
@click="$router.push(btn.to)"
>
<div
v-for="item in returnList"
:key="item.id"
class="return-item"
@click="handleReturnClick(item)"
>
<div class="return-header">
<div class="return-no">{{ item.returnNo }}</div>
<div class="return-status" :class="getStatusClass(item.status)">
{{ getStatusText(item.status) }}
</div>
</div>
<div class="return-info">
<div class="info-row">
<span class="label">原订单号</span>
<span class="value">{{ item.originalOrderNo }}</span>
</div>
<div class="info-row">
<span class="label">客户</span>
<span class="value">{{ item.customerName }}</span>
</div>
<div class="info-row">
<span class="label">退货原因</span>
<span class="value">{{ item.returnReason }}</span>
</div>
<div class="info-row">
<span class="label">退货数量</span>
<span class="value">{{ item.returnQuantity }}</span>
</div>
<div class="info-row">
<span class="label">退货金额</span>
<span class="value amount">¥{{ item.returnAmount }}</span>
</div>
<div class="info-row">
<span class="label">申请时间</span>
<span class="value">{{ item.applyTime }}</span>
</div>
</div>
<div class="menu-icon" :class="btn.iconClass">
<van-icon :name="btn.icon" size="24" />
</div>
</van-list>
</van-pull-refresh>
<div class="menu-text">{{ btn.label }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SalesReturn',
data() {
return {
searchValue: '',
activeTab: 'all',
refreshing: false,
loading: false,
finished: false,
returnList: [
{
id: 1,
returnNo: 'RT202401001',
originalOrderNo: 'SO202401001',
customerName: '客户A有限公司',
returnReason: '质量问题',
returnQuantity: 10,
returnAmount: 25000.00,
applyTime: '2024-01-15 10:30',
status: 0
},
{
id: 2,
returnNo: 'RT202401002',
originalOrderNo: 'SO202401002',
customerName: '客户B有限公司',
returnReason: '规格不符',
returnQuantity: 5,
returnAmount: 12500.00,
applyTime: '2024-01-14 09:20',
status: 1
},
{
id: 3,
returnNo: 'RT202401003',
originalOrderNo: 'SO202401003',
customerName: '客户C有限公司',
returnReason: '数量错误',
returnQuantity: 8,
returnAmount: 18000.00,
applyTime: '2024-01-13 14:15',
status: 2
}
buttons: [
{ icon: 'shopping-cart-o', label: '入库', iconClass: 'purchase', to: 'salereturn-inbound' },
{ icon: 'orders-o', label: '报废', iconClass: 'inspection', to: 'salereturn-scrap' }
]
}
},
mounted() {
this.loadData()
},
methods: {
loadData() {
this.loading = true
setTimeout(() => {
this.loading = false
this.finished = true
}, 1000)
},
onRefresh() {
this.refreshing = true
setTimeout(() => {
this.refreshing = false
this.$toast.success('刷新成功')
}, 1000)
},
onLoad() {
this.loadData()
},
handleSearch() {
this.loadData()
},
handleClear() {
this.searchValue = ''
this.loadData()
},
handleTabChange() {
this.loadData()
},
handleReturnClick(item) {
if (item.status === 0 || item.status === 1) {
this.$router.push(`/sales-return/receive/${item.returnNo}`)
} else {
this.$toast('该退货单已完成处理')
}
},
getStatusText(status) {
const statusMap = {
0: '待收货',
1: '质检中',
2: '已完成'
}
return statusMap[status] || '未知'
},
getStatusClass(status) {
const classMap = {
0: 'status-pending',
1: 'status-checking',
2: 'status-completed'
}
return classMap[status] || ''
}
}
}
</script>
<style scoped>
.sales-return-container {
min-height: 100vh;
background-color: #f7f8fa;
}
.search-section {
padding: 10px 16px;
background: white;
}
.filter-section {
background: white;
border-bottom: 1px solid #ebedf0;
<style>
/* filepath: D:\xjcode\wms-pda\src\views\modules\recv\po-recv.vue */
:root {
--columns: 3;
--button-size: calc(100vw / var(--columns) - 20px);
}
.return-item {
background: white;
margin: 10px 16px;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.pda-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
font-family: 'Arial', sans-serif;
overflow: auto;
}
.return-header {
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.return-no {
font-size: 16px;
font-weight: bold;
color: #323233;
}
.return-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
color: white;
padding: 10px 20px;
background: #17B3A3;
}
.status-pending {
background-color: #ff976a;
.menu-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
padding: 20px;
justify-content: center; /* 水平居中 */
align-content: center; /* 垂直居中 */
width: 100%; /* 确保占满容器宽度 */
}
.status-checking {
background-color: #1989fa;
}
.status-completed {
background-color: #07c160;
.menu-item {
background: white;
border-radius: 12px;
padding: 20px 10px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.return-info {
font-size: 14px;
.menu-item:active {
transform: scale(0.95);
}
.info-row {
.menu-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
margin-bottom: 6px;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
color: white;
}
.info-row:last-child {
margin-bottom: 0;
.menu-icon.purchase {
background: linear-gradient(135deg, #17B3A3 0%, #1dc5ef 100%);
}
.label {
color: #969799;
width: 80px;
flex-shrink: 0;
.menu-icon.inspection {
background: linear-gradient(135deg, #17B3A3 0%, #1dc5ef 100%);
}
.value {
color: #323233;
flex: 1;
.menu-icon.qualified {
background: linear-gradient(135deg, #17B3A3 0%, #1dc5ef 100%);
}
.value.amount {
color: #ee0a24;
font-weight: bold;
.menu-text {
font-size: 13px;
color: #333;
font-weight: bold; /* 加粗字体 */
white-space: nowrap; /* 防止文字换行 */
overflow: hidden;
text-overflow: ellipsis;
}
</style>
</style>

241
src/views/modules/sales-return/sales-return-inbound.vue

@ -0,0 +1,241 @@
<template>
<div>
<div class="pda-container">
<div class="status-bar">
<div class="goBack" @click="handleBack"><i class="el-icon-arrow-left"></i>上一页</div>
<div class="goBack">销售退货-入库</div>
<div class="network" style="color: #fff" @click="$router.push({ path: '/' })">🏠首页</div>
</div>
<div class="main-content">
<!-- RMA号输入/扫描 -->
<el-input v-model="scanRma" placeholder="扫描RMA条码或输入RMA号" @keyup.enter.native="searchRmaList" ref="scanRmaRef" class="rma-input" />
<!-- RMA明细选择 -->
<div v-if="rmaList.length > 0" class="rma-list">
<el-form>
<el-row v-for="(rmaDetail, index) in rmaList[0].detailList" :key="index" :class="index < rmaList.length - 1 ? 'bottom-line-row' : ''">
<el-col :span="8">
<el-form-item label="物料"><span>{{ rmaDetail.partNo }}</span></el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="批号"><span>{{ rmaDetail.batchNo }}</span></el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="数量"><span>{{ rmaDetail.processQty }}</span></el-form-item>
</el-col>
<el-col :span="24">
<el-button type="primary" size="mini" @click="addToReturnList(rmaDetail)" :disabled="isInReturnList(rmaDetail)">添加到退货明细</el-button>
</el-col>
</el-row>
</el-form>
</div>
<!-- 退货明细列表 -->
<div v-if="returnList.length > 0">
<div class="return-list-title">退货明细列表</div>
<div v-for="(detail, dIdx) in returnList" :key="dIdx" class="detail-block">
<el-card shadow="hover" class="detail-card">
<div slot="header" class="detail-header">
<span>明细{{ dIdx + 1 }}</span>
<el-button type="danger" icon="el-icon-delete" circle size="mini" @click="removeDetail(dIdx)" v-if="returnList.length > 1" style="float:right;"></el-button>
</div>
<el-form label-position="top" class="detail-form">
<el-row :gutter="10">
<el-col :span="12"><el-form-item label="物料"><el-input v-model="detail.partNo" size="small" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="数量"><el-input v-model="detail.processQty" type="number" size="small" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="批号"><el-input v-model="detail.batchNo" size="small" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="库位"><el-input v-model="detail.locationNo" size="small" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="仓库"><el-input v-model="detail.warehouseId" size="small" /></el-form-item></el-col>
</el-row>
<!-- 包装单元 -->
<div class="pack-unit-area">
<div class="pack-title">包装单元</div>
<div v-for="(pack, pIdx) in detail.packUnitList" :key="pIdx" class="pack-block">
<el-row :gutter="10">
<el-col :span="6"><el-input v-model="pack.code" placeholder="编码" size="small" /></el-col>
<el-col :span="6"><el-input v-model="pack.qty" placeholder="数量" type="number" size="small" /></el-col>
<el-col :span="6"><el-input v-model="pack.perQty" placeholder="单包数量" type="number" size="small" /></el-col>
<el-col :span="4"><el-input v-model="pack.packageQty" placeholder="包数" type="number" size="small" /></el-col>
<el-col :span="2">
<el-button type="danger" icon="el-icon-delete" circle size="mini" @click="removePackUnit(detail, pIdx)"></el-button>
</el-col>
</el-row>
</div>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="addPackUnit(detail)" class="add-pack-btn">添加包装单元</el-button>
</div>
</el-form>
</el-card>
</div>
<el-button type="success" class="submit-btn" @click="submitReturn">提交</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getRmaList, processReturn } from "@/api/sales-return/sales-return.js";
export default {
data() {
return {
scanRma: "",
rmaList: [],
returnList: [] // 退
};
},
methods: {
handleBack() {
this.$router.back();
},
searchRmaList() {
if (!this.scanRma) return (this.rmaList = []);
getRmaList({ rmaNo: this.scanRma }).then(({ data }) => {
if (data.code === 0) this.rmaList = data.rows;
});
},
addToReturnList(rmaDetail) {
//
if (this.isInReturnList(rmaDetail)) return;
this.returnList.push({
site: "1",
partNo: rmaDetail.partNo,
processQty: rmaDetail.processQty,
batchNo: rmaDetail.batchNo,
locationNo: "",
warehouseId: "",
packUnitList: []
});
},
isInReturnList(rmaDetail) {
return this.returnList.some(
d => d.partNo === rmaDetail.partNo && d.batchNo === rmaDetail.batchNo
);
},
removeDetail(idx) {
this.returnList.splice(idx, 1);
},
addPackUnit(detail) {
detail.packUnitList.push({code: "", qty: '', perQty: '', packageQty: ''});
},
removePackUnit(detail, idx) {
detail.packUnitList.splice(idx, 1);
},
submitReturn() {
if (!this.scanRma || this.returnList.length === 0) {
this.$message.error("请先扫描RMA号并选择退货明细");
return;
}
processReturn({
rmaNo: this.scanRma,
processType: "inbound",
detailList: this.returnList
}).then(({data}) => {
if (data.code === 0) {
this.$message.success("操作成功");
this.scanRma = "";
this.rmaList = [];
this.returnList = [];
} else {
this.$message.error(data.msg);
}
});
}
},
mounted() {
this.$nextTick(() => this.$refs.scanRmaRef && this.$refs.scanRmaRef.focus());
}
};
</script>
<style scoped>
.pda-container {
background: #f5f5f5;
min-height: 100vh;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: #409EFF;
color: #fff;
padding: 8px 12px;
}
.goBack {
cursor: pointer;
}
.main-content {
padding: 10px 6px 30px 6px;
}
.rma-input {
margin-bottom: 12px;
}
.rma-list {
margin-bottom: 18px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px #e6e6e6;
padding: 10px;
}
.return-list-title {
font-size: 16px;
font-weight: bold;
color: #409EFF;
margin: 12px 0 8px 0;
}
.detail-block {
margin-bottom: 18px;
}
.detail-card {
border-radius: 10px;
box-shadow: 0 2px 12px #e0e7ef;
}
.detail-header {
font-weight: bold;
font-size: 15px;
color: #333;
border-left: 4px solid #409EFF;
padding-left: 8px;
}
.detail-form {
padding: 6px 0 0 0;
}
.pack-unit-area {
background: #f8fafd;
border-radius: 6px;
padding: 8px 6px 6px 6px;
margin-top: 8px;
}
.pack-title {
font-weight: bold;
color: #666;
margin-bottom: 4px;
}
.pack-block {
margin-bottom: 6px;
}
.add-pack-btn {
margin-top: 4px;
}
.submit-btn {
width: 100%;
font-size: 16px;
margin-top: 18px;
}
.bottom-line-row {
border-bottom: 1px solid #eee;
}
</style>

133
src/views/modules/sales-return/sales-return-scrap.vue

@ -0,0 +1,133 @@
<template>
<div>
<div class="pda-container">
<div class="status-bar">
<div class="goBack" @click="handleBack"><i class="el-icon-arrow-left"></i>上一页</div>
<div class="goBack">销售退货-报废</div>
<div class="network" style="color: #fff" @click="$router.push({ path: '/' })">🏠首页</div>
</div>
<div class="main-content">
<!-- RMA号输入/扫描 -->
<el-input v-model="scanRma" placeholder="扫描RMA条码或输入RMA号" @keyup.enter.native="searchRmaList" ref="scanRmaRef" class="rma-input" />
<!-- RMA明细选择 -->
<div v-if="rmaList.length > 0" class="rma-list">
<el-form>
<el-row v-for="(rmaDetail, index) in rmaList[0].detailList" :key="index" :class="index < rmaList.length - 1 ? 'bottom-line-row' : ''">
<el-col :span="8"><el-form-item label="物料"><span>{{ rmaDetail.partNo }}</span></el-form-item></el-col>
<el-col :span="8"><el-form-item label="批号"><span>{{ rmaDetail.batchNo }}</span></el-form-item></el-col>
<el-col :span="8"><el-form-item label="数量"><span>{{ rmaDetail.processQty }}</span></el-form-item></el-col>
<el-col :span="24"><el-form-item label="描述"><span>{{ rmaDetail.desc }}</span></el-form-item></el-col>
<el-col :span="24">
<el-button type="primary" size="mini" @click="addToReturnList(rmaDetail)" :disabled="isInReturnList(rmaDetail)">添加到退货明细</el-button>
</el-col>
</el-row>
</el-form>
</div>
<!-- 退货明细列表 -->
<div v-if="returnList.length > 0">
<div class="return-list-title">退货明细列表</div>
<div v-for="(detail, dIdx) in returnList" :key="dIdx" class="detail-block">
<el-card shadow="hover" class="detail-card">
<div slot="header" class="detail-header">
<span>明细{{ dIdx + 1 }}</span>
<el-button type="danger" icon="el-icon-delete" circle size="mini" @click="removeDetail(dIdx)" v-if="returnList.length > 1" style="float:right;"></el-button>
</div>
<el-form label-position="top" class="detail-form">
<el-row :gutter="10">
<el-col :span="12"><el-form-item label="物料"><el-input v-model="detail.partNo" size="small" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="数量"><el-input v-model="detail.processQty" type="number" size="small" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="批号"><el-input v-model="detail.batchNo" size="small" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="库位"><el-input v-model="detail.locationNo" size="small" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="仓库"><el-input v-model="detail.warehouseId" size="small" /></el-form-item></el-col>
</el-row>
</el-form>
</el-card>
</div>
<el-button type="success" class="submit-btn" @click="submitReturn">提交</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getRmaList, processReturn } from "@/api/sales-return/sales-return.js";
export default {
data() {
return {
scanRma: "",
rmaList: [],
returnList: []
};
},
methods: {
handleBack() {
this.$router.back();
},
searchRmaList() {
if (!this.scanRma) return (this.rmaList = []);
getRmaList({ rmaNo: this.scanRma }).then(({ data }) => {
if (data.code === 0) this.rmaList = data.rows;
});
},
addToReturnList(rmaDetail) {
if (this.isInReturnList(rmaDetail)) return;
this.returnList.push({
site: "1",
partNo: rmaDetail.partNo,
processQty: rmaDetail.processQty,
batchNo: rmaDetail.batchNo,
locationNo: "",
warehouseId: ""
});
},
isInReturnList(rmaDetail) {
return this.returnList.some(
d => d.partNo === rmaDetail.partNo && d.batchNo === rmaDetail.batchNo
);
},
removeDetail(idx) {
this.returnList.splice(idx, 1);
},
submitReturn() {
if (!this.scanRma || this.returnList.length === 0) {
this.$message.error("请先扫描RMA号并选择退货明细");
return;
}
processReturn({
rmaNo: this.scanRma,
processType: "scrap",
detailList: this.returnList
}).then(({ data }) => {
if (data.code === 0) {
this.$message.success("操作成功");
this.scanRma = "";
this.rmaList = [];
this.returnList = [];
} else {
this.$message.error(data.msg);
}
});
}
},
mounted() {
this.$nextTick(() => this.$refs.scanRmaRef && this.$refs.scanRmaRef.focus());
}
};
</script>
<style scoped>
.pda-container { background: #f5f5f5; min-height: 100vh; }
.status-bar { display: flex; justify-content: space-between; align-items: center; background: #409EFF; color: #fff; padding: 8px 12px; }
.goBack { cursor: pointer; }
.main-content { padding: 10px 6px 30px 6px; }
.rma-input { margin-bottom: 12px; }
.rma-list { margin-bottom: 18px; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px #e6e6e6; padding: 10px; }
.return-list-title { font-size: 16px; font-weight: bold; color: #409EFF; margin: 12px 0 8px 0; }
.detail-block { margin-bottom: 18px; }
.detail-card { border-radius: 10px; box-shadow: 0 2px 12px #e0e7ef; }
.detail-header { font-weight: bold; font-size: 15px; color: #333; border-left: 4px solid #409EFF; padding-left: 8px; }
.detail-form { padding: 6px 0 0 0; }
.submit-btn { width: 100%; font-size: 16px; margin-top: 18px; }
.bottom-line-row { border-bottom: 1px solid #eee; }
</style>
Loading…
Cancel
Save