Compare commits

..

8 Commits

Author SHA1 Message Date
一休哥哥6666
721543f423 feat:uniapp模块 2026-07-14 14:53:45 +08:00
一休哥哥6666
324679964c feat:配片单验光参数调整优化 2026-07-14 14:53:12 +08:00
一休哥哥6666
9be6fd6ec9 feat:优化 2026-06-27 23:49:24 +08:00
一休哥哥6666
98aa066ae4 feat:前端列表隐藏镜片厂和品牌商列,价格合并,不显示明细 2026-06-24 19:05:20 +08:00
一休哥哥6666
4d833d7882 feat:名称及logo修改 2026-06-18 18:45:46 +08:00
一休哥哥6666
ddae147aef feat:换货发货,发送短信提醒 2026-06-15 14:41:04 +08:00
一休哥哥6666
2991dd88bb feat:增加换货单的逻辑,已发货短信模板更改 2026-06-15 13:42:52 +08:00
一休哥哥6666
dfef98814b feat:新增配片单重复打开的缓存问题,已发货短信 2026-06-14 16:31:41 +08:00
106 changed files with 17827 additions and 886 deletions

View File

@@ -1,10 +1,10 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 觉远创智管理系统 VUE_APP_TITLE = 觉远创智功能镜片配镜系统
# 开发环境配置 # 开发环境配置
ENV = 'development' ENV = 'development'
# 觉远创智管理系统/开发环境 # 觉远创智功能镜片配镜系统/开发环境
VUE_APP_BASE_API = '/dev-api' VUE_APP_BASE_API = '/dev-api'
# 路由懒加载 # 路由懒加载

View File

@@ -1,10 +1,10 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 觉远创智管理系统 VUE_APP_TITLE = 觉远创智功能镜片配镜系统
# 开发环境配置 # 开发环境配置
ENV = 'development' ENV = 'development'
# 觉远创智管理系统/开发环境 # 觉远创智功能镜片配镜系统/开发环境
VUE_APP_BASE_API = '/dev-api' VUE_APP_BASE_API = '/dev-api'
# 路由懒加载 # 路由懒加载

View File

@@ -1,8 +1,8 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 觉远创智管理系统 VUE_APP_TITLE = 觉远创智功能镜片配镜系统
# 生产环境配置 # 生产环境配置
ENV = 'production' ENV = 'production'
# 觉远创智管理系统/生产环境 # 觉远创智功能镜片配镜系统/生产环境
VUE_APP_BASE_API = '/prod-api' VUE_APP_BASE_API = '/prod-api'

View File

@@ -1,10 +1,10 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 觉远创智管理系统 VUE_APP_TITLE = 觉远创智功能镜片配镜系统
NODE_ENV = production NODE_ENV = production
# 测试环境配置 # 测试环境配置
ENV = 'staging' ENV = 'staging'
# 觉远创智管理系统/测试环境 # 觉远创智功能镜片配镜系统/测试环境
VUE_APP_BASE_API = '/stage-api' VUE_APP_BASE_API = '/stage-api'

View File

@@ -1,7 +1,7 @@
{ {
"name": "ruoyi", "name": "ruoyi",
"version": "3.8.8", "version": "3.8.8",
"description": "觉远创智管理系统", "description": "觉远创智功能镜片配镜系统",
"author": "觉远创智", "author": "觉远创智",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View File

@@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!-- <title>11<%= webpackConfig.name %></title> --> <!-- <title>11<%= webpackConfig.name %></title> -->
<title>觉远创智管理系统</title> <title>觉远创智功能镜片配镜系统</title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]--> <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style> <style>
html, html,

View File

@@ -114,23 +114,6 @@ export function rejectExchangeOrder(data) {
}) })
} }
// 拒绝换货申请订单完成
export function completeExchangeRejectedOrder(data) {
return request({
url: '/jysystem/order/exchangeRejectedComplete',
method: 'put',
data: data
})
}
// 批量将拒绝换货申请订单更新为已完成
export function completeAllExchangeRejectedOrder() {
return request({
url: '/jysystem/order/exchangeRejectedCompleteAll',
method: 'put'
})
}
// 变更订单状态 // 变更订单状态
export function changeOrderStatus(data) { export function changeOrderStatus(data) {
return request({ return request({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -2,11 +2,9 @@
<div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"> <div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<transition name="sidebarLogoFade"> <transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" /> <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
</router-link> </router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/"> <router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1> <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
</router-link> </router-link>
</transition> </transition>
@@ -14,7 +12,6 @@
</template> </template>
<script> <script>
import logoImg from '@/assets/logo/logo.png'
import variables from '@/assets/styles/variables.scss' import variables from '@/assets/styles/variables.scss'
export default { export default {
@@ -35,9 +32,7 @@ export default {
}, },
data() { data() {
return { return {
// title: process.env.VUE_APP_TITLE, title: '觉远创智功能镜片配镜系统'
title: '觉远创智管理系统',
logo: logoImg
} }
} }
} }
@@ -66,13 +61,6 @@ export default {
height: 100%; height: 100%;
width: 100%; width: 100%;
& .sidebar-logo {
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: 12px;
}
& .sidebar-title { & .sidebar-title {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
@@ -84,11 +72,5 @@ export default {
vertical-align: middle; vertical-align: middle;
} }
} }
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
} }
</style> </style>

View File

@@ -172,15 +172,6 @@
@click="handleQuickFilter('exchangeRejected')" @click="handleQuickFilter('exchangeRejected')"
>拒绝换货申请</el-button> >拒绝换货申请</el-button>
</el-col> </el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
size="mini"
@click="handleBatchCompleteExchangeRejected"
v-hasPermi="['jysystem:order:edit']"
>更新拒绝换货申请为已完成</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
@@ -200,17 +191,22 @@
</el-table-column> </el-table-column>
<el-table-column label="B价格" align="center" width="90"> <el-table-column label="B价格" align="center" width="90">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatPriceB(scope.row) }}</span> <span>{{ formatPriceAmount(formatPriceB(scope.row)) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="C价格" align="center" width="90"> <el-table-column label="C价格" align="center" width="90">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatPriceC(scope.row) }}</span> <span>{{ formatPriceAmount(formatPriceC(scope.row)) }}</span>
</template>
</el-table-column>
<el-table-column label="物流费用" align="center" width="90">
<template slot-scope="scope">
<span>{{ formatPriceAmount(formatLogisticsFee(scope.row)) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="渐进多焦点价格" align="center" width="120"> <el-table-column label="渐进多焦点价格" align="center" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatProgressiveMultifocalPrice(scope.row) }}</span> <span>{{ formatPriceAmount(formatProgressiveMultifocalPrice(scope.row)) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="订单状态" align="center" prop="orderStatus" width="130"> <el-table-column label="订单状态" align="center" prop="orderStatus" width="130">
@@ -539,17 +535,26 @@
</el-table-column> </el-table-column>
<el-table-column label="球镜S" align="center" min-width="90"> <el-table-column label="球镜S" align="center" min-width="90">
<template slot-scope="scope"> <template slot-scope="scope">
<el-input-number v-model="scope.row.sphereS" :precision="2" :step="0.25" controls-position="right" style="width: 100%" /> <el-input-number v-model="scope.row.sphereS" :precision="2" :step="0.25" controls-position="right" style="width: 100%" @change="val => onSphereSChange(val, scope.row)" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="柱镜C" align="center" min-width="90"> <el-table-column label="柱镜C" align="center" min-width="90">
<template slot-scope="scope"> <template slot-scope="scope">
<el-input-number v-model="scope.row.cylinderC" :precision="2" :step="0.25" controls-position="right" style="width: 100%" /> <el-input-number
v-model="scope.row.cylinderC"
:precision="2"
:step="0.25"
:min="getCylinderCMin(scope.row)"
:max="getCylinderCMax(scope.row)"
controls-position="right"
style="width: 100%"
@change="val => onCylinderCChange(val, scope.row)"
/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="轴位A" align="center" min-width="80"> <el-table-column label="轴位A" align="center" min-width="80">
<template slot-scope="scope"> <template slot-scope="scope">
<el-input-number v-model="scope.row.axisA" :min="0" :max="180" controls-position="right" style="width: 100%" /> <el-input-number v-model="scope.row.axisA" :min="0" :max="180" :disabled="!isAxisAEditable(scope.row)" controls-position="right" style="width: 100%" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="瞳距(mm)" align="center" min-width="90"> <el-table-column label="瞳距(mm)" align="center" min-width="90">
@@ -557,7 +562,7 @@
<el-input-number v-model="scope.row.pd" :precision="2" :min="0" controls-position="right" style="width: 100%" /> <el-input-number v-model="scope.row.pd" :precision="2" :min="0" controls-position="right" style="width: 100%" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="瞳高(mm)" align="center" min-width="80"> <el-table-column v-if="form.progressiveMultifocal === 1" label="瞳高(mm)" align="center" min-width="80">
<template slot-scope="scope"> <template slot-scope="scope">
<el-input-number v-model="scope.row.pupilHeight" :precision="2" :min="0" controls-position="right" style="width: 100%" /> <el-input-number v-model="scope.row.pupilHeight" :precision="2" :min="0" controls-position="right" style="width: 100%" />
</template> </template>
@@ -567,9 +572,9 @@
<el-input v-model="scope.row.distanceVa" placeholder="远用VA" /> <el-input v-model="scope.row.distanceVa" placeholder="远用VA" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="近用ADD(屈光度)" align="center" min-width="100"> <el-table-column v-if="form.progressiveMultifocal === 1" label="近用ADD(屈光度)" align="center" min-width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<el-input-number v-model="scope.row.nearAdd" :precision="2" :min="0" controls-position="right" style="width: 100%" /> <el-input-number v-model="scope.row.nearAdd" :precision="2" :step="0.25" :min="0.75" :max="4" controls-position="right" style="width: 100%" />
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -850,21 +855,21 @@
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="B合计"> <el-descriptions-item label="B合计">
<el-tag <el-tag
v-if="isPendingTagText(formatPriceAmount(formatPriceTotal(detail, 'priceB')))" v-if="isPendingTagText(formatPriceAmount(formatDetailPriceTotal(detail, 'priceB')))"
type="danger" type="danger"
size="small" size="small"
:disable-transitions="true" :disable-transitions="true"
>{{ formatPriceAmount(formatPriceTotal(detail, 'priceB')) }}</el-tag> >{{ formatPriceAmount(formatDetailPriceTotal(detail, 'priceB')) }}</el-tag>
<span v-else class="price-total-value">{{ formatPriceAmount(formatPriceTotal(detail, 'priceB')) }}</span> <span v-else class="price-total-value">{{ formatPriceAmount(formatDetailPriceTotal(detail, 'priceB')) }}</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="C合计"> <el-descriptions-item label="C合计">
<el-tag <el-tag
v-if="isPendingTagText(formatPriceAmount(formatPriceTotal(detail, 'priceC')))" v-if="isPendingTagText(formatPriceAmount(formatDetailPriceTotal(detail, 'priceC')))"
type="danger" type="danger"
size="small" size="small"
:disable-transitions="true" :disable-transitions="true"
>{{ formatPriceAmount(formatPriceTotal(detail, 'priceC')) }}</el-tag> >{{ formatPriceAmount(formatDetailPriceTotal(detail, 'priceC')) }}</el-tag>
<span v-else class="price-total-value">{{ formatPriceAmount(formatPriceTotal(detail, 'priceC')) }}</span> <span v-else class="price-total-value">{{ formatPriceAmount(formatDetailPriceTotal(detail, 'priceC')) }}</span>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
<div class="detail-logistics-fee-block"> <div class="detail-logistics-fee-block">
@@ -876,26 +881,6 @@
<span>{{ formatPriceAmount(formatLogisticsLogFee(scope.row, detail)) }}</span> <span>{{ formatPriceAmount(formatLogisticsLogFee(scope.row, detail)) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="应收结算" align="center" width="100">
<template slot-scope="scope">
<dict-tag
v-if="formatLogSaleSettleStatus(scope.row) != null"
:options="dict.type.jy_sale_settle_status"
:value="formatLogSaleSettleStatus(scope.row)"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="应付结算" align="center" width="100">
<template slot-scope="scope">
<dict-tag
v-if="formatLogPurchaseSettleStatus(scope.row) != null"
:options="dict.type.jy_purchase_settle_status"
:value="formatLogPurchaseSettleStatus(scope.row)"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="发货日期" align="center" width="120"> <el-table-column label="发货日期" align="center" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatShipDate(scope.row.operateTime) }}</span> <span>{{ formatShipDate(scope.row.operateTime) }}</span>
@@ -987,7 +972,7 @@
<span>{{ formatDetailPdValue(scope.row, scope.$index) }}</span> <span>{{ formatDetailPdValue(scope.row, scope.$index) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="瞳高(mm)" align="center" min-width="80"> <el-table-column v-if="detail.progressiveMultifocal === 1" label="瞳高(mm)" align="center" min-width="80">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ scope.row.pupilHeight != null ? scope.row.pupilHeight : '-' }}</span> <span>{{ scope.row.pupilHeight != null ? scope.row.pupilHeight : '-' }}</span>
</template> </template>
@@ -997,7 +982,7 @@
<span>{{ scope.row.distanceVa != null && scope.row.distanceVa !== '' ? scope.row.distanceVa : '-' }}</span> <span>{{ scope.row.distanceVa != null && scope.row.distanceVa !== '' ? scope.row.distanceVa : '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="近用ADD(屈光度)" align="center" min-width="100"> <el-table-column v-if="detail.progressiveMultifocal === 1" label="近用ADD(屈光度)" align="center" min-width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatDetailNearAdd(scope.row.nearAdd) }}</span> <span>{{ formatDetailNearAdd(scope.row.nearAdd) }}</span>
</template> </template>
@@ -1021,26 +1006,6 @@
<dict-tag :options="dict.type.jy_order_status" :value="scope.row.orderStatus" /> <dict-tag :options="dict.type.jy_order_status" :value="scope.row.orderStatus" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="应收结算" align="center" width="100">
<template slot-scope="scope">
<dict-tag
v-if="formatLogSaleSettleStatus(scope.row) != null"
:options="dict.type.jy_sale_settle_status"
:value="formatLogSaleSettleStatus(scope.row)"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="应付结算" align="center" width="100">
<template slot-scope="scope">
<dict-tag
v-if="formatLogPurchaseSettleStatus(scope.row) != null"
:options="dict.type.jy_purchase_settle_status"
:value="formatLogPurchaseSettleStatus(scope.row)"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip /> <el-table-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip />
<el-table-column label="操作人" align="center" prop="operator" width="100" /> <el-table-column label="操作人" align="center" prop="operator" width="100" />
<el-table-column label="操作时间" align="center" prop="operateTime" width="160" /> <el-table-column label="操作时间" align="center" prop="operateTime" width="160" />
@@ -1124,7 +1089,7 @@
/> />
</el-form-item> </el-form-item>
<el-alert <el-alert
title="仅汇总所选年月内已完成状态的订单品牌方生成应收(AR)镜片厂生成应付(AP)" title="仅汇总所选年月内已完成或换货完成状态的订单品牌方生成应收(AR)镜片厂生成应付(AP)"
type="info" type="info"
:closable="false" :closable="false"
show-icon show-icon
@@ -1142,7 +1107,7 @@
import axios from 'axios' import axios from 'axios'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import { isExternal } from '@/utils/validate' import { isExternal } from '@/utils/validate'
import { listOrder, getOrder, getOrderLogistics, delOrder, addOrder, updateOrder, previewAssignPrice, assignOrder, rejectOrder, closeOrder, completeOrder, agreeExchangeOrder, rejectExchangeOrder, completeExchangeRejectedOrder, completeAllExchangeRejectedOrder, changeOrderStatus } from "@/api/jysystem/order"; import { listOrder, getOrder, getOrderLogistics, delOrder, addOrder, updateOrder, previewAssignPrice, assignOrder, rejectOrder, closeOrder, completeOrder, agreeExchangeOrder, rejectExchangeOrder, changeOrderStatus } from "@/api/jysystem/order";
import { checkPermi } from "@/utils/permission"; import { checkPermi } from "@/utils/permission";
import { listBrandTenant, listLensFactoryTenant } from "@/api/jysystem/tenant"; import { listBrandTenant, listLensFactoryTenant } from "@/api/jysystem/tenant";
import { listLensOptions } from "@/api/jysystem/lens"; import { listLensOptions } from "@/api/jysystem/lens";
@@ -1216,6 +1181,9 @@ export default {
ORDER_STATUS_SHIPPED: '8', ORDER_STATUS_SHIPPED: '8',
ORDER_STATUS_COMPLETED: '9', ORDER_STATUS_COMPLETED: '9',
ORDER_STATUS_EXCHANGE_APPLY: '12', ORDER_STATUS_EXCHANGE_APPLY: '12',
ORDER_STATUS_EXCHANGE_AGREED: '13',
ORDER_STATUS_EXCHANGE_LOGISTICS: '15',
ORDER_STATUS_EXCHANGE_COMPLETED: '17',
ORDER_STATUS_EXCHANGE_REJECTED: '21', ORDER_STATUS_EXCHANGE_REJECTED: '21',
logisticsDialogVisible: false, logisticsDialogVisible: false,
logisticsBasic: null, logisticsBasic: null,
@@ -1368,20 +1336,42 @@ export default {
}, },
formatPriceB(row) { formatPriceB(row) {
if (!row) return '-'; if (!row) return '-';
if (this.isExchangeOrder(row)) return '-';
if (this.isPendingAssign(row)) return '未核算'; if (this.isPendingAssign(row)) return '未核算';
return row.priceB != null ? row.priceB : '-'; return row.priceB != null ? row.priceB : '-';
}, },
formatPriceC(row) { formatPriceC(row) {
if (!row) return '-'; if (!row) return '-';
if (this.isExchangeOrder(row)) return '-';
if (this.isPendingAssign(row)) return '未核算'; if (this.isPendingAssign(row)) return '未核算';
return row.priceC != null ? row.priceC : '-'; return row.priceC != null ? row.priceC : '-';
}, },
formatProgressiveMultifocalPrice(row) { formatProgressiveMultifocalPrice(row) {
if (!row) return '-'; if (!row) return '-';
if (this.isExchangeOrder(row)) return '-';
if (Number(row.progressiveMultifocal) !== 1) return '-'; if (Number(row.progressiveMultifocal) !== 1) return '-';
if (this.isPendingAssign(row)) return '未核算'; if (this.isPendingAssign(row)) return '未核算';
return row.progressiveMultifocalPrice != null ? row.progressiveMultifocalPrice : '-'; return row.progressiveMultifocalPrice != null ? row.progressiveMultifocalPrice : '-';
}, },
isExchangeOrder(row) {
if (!row) return false;
if (row.orderNo && String(row.orderNo).startsWith('HH')) return true;
const status = row.orderStatus != null ? String(row.orderStatus) : '';
return status === this.ORDER_STATUS_EXCHANGE_APPLY
|| status === this.ORDER_STATUS_EXCHANGE_AGREED
|| status === this.ORDER_STATUS_EXCHANGE_LOGISTICS
|| status === this.ORDER_STATUS_EXCHANGE_COMPLETED;
},
formatDetailPriceTotal(detail, priceKey) {
if (this.isExchangeOrder(detail)) return this.formatDetailLogisticsFee(detail);
return this.formatPriceTotal(detail, priceKey);
},
formatLogisticsFee(row) {
if (!row) return '-';
if (!this.isExchangeOrder(row)) return '-';
const fee = this.parseLogisticsFeeValue(row.logisticsFee);
return fee != null ? fee : '-';
},
formatPriceBC(row) { formatPriceBC(row) {
return `${this.formatPriceB(row)} / ${this.formatPriceC(row)}`; return `${this.formatPriceB(row)} / ${this.formatPriceC(row)}`;
}, },
@@ -1430,7 +1420,7 @@ export default {
hasFee = true; hasFee = true;
} }
}); });
if (!hasFee && logs.length > 0 && detail.logisticsFee != null) { if (!hasFee && detail.logisticsFee != null) {
const legacyFee = this.parseLogisticsFeeValue(detail.logisticsFee); const legacyFee = this.parseLogisticsFeeValue(detail.logisticsFee);
if (legacyFee != null) return legacyFee; if (legacyFee != null) return legacyFee;
} }
@@ -1440,24 +1430,6 @@ export default {
formatDetailLogisticsFee(detail) { formatDetailLogisticsFee(detail) {
return this.sumDetailLogisticsFee(detail); return this.sumDetailLogisticsFee(detail);
}, },
formatLogSaleSettleStatus(log) {
if (!log || log.expressNo == null || String(log.expressNo).trim() === '') {
return null;
}
if (log.saleSettleStatus != null && log.saleSettleStatus !== '') {
return log.saleSettleStatus;
}
return '0';
},
formatLogPurchaseSettleStatus(log) {
if (!log || log.expressNo == null || String(log.expressNo).trim() === '') {
return null;
}
if (log.purchaseSettleStatus != null && log.purchaseSettleStatus !== '') {
return log.purchaseSettleStatus;
}
return '0';
},
formatShipDate(time) { formatShipDate(time) {
if (!time) { if (!time) {
return '-'; return '-';
@@ -1508,8 +1480,8 @@ export default {
}, },
createDefaultOptometryList() { createDefaultOptometryList() {
return [ return [
{ eyeSide: 'OS', sphereS: null, cylinderC: null, axisA: null, pd: null, dualPd: null, pupilHeight: null, distanceVa: null, nearAdd: null }, { eyeSide: 'OS', sphereS: 0, cylinderC: null, axisA: null, pd: null, dualPd: null, pupilHeight: null, distanceVa: null, nearAdd: null },
{ eyeSide: 'OD', sphereS: null, cylinderC: null, axisA: null, pd: null, dualPd: null, pupilHeight: null, distanceVa: null, nearAdd: null } { eyeSide: 'OD', sphereS: 0, cylinderC: null, axisA: null, pd: null, dualPd: null, pupilHeight: null, distanceVa: null, nearAdd: null }
]; ];
}, },
detectPdMode(optometryList) { detectPdMode(optometryList) {
@@ -1553,6 +1525,45 @@ export default {
if (Number.isNaN(num)) return String(value); if (Number.isNaN(num)) return String(value);
return '+' + num.toFixed(2); return '+' + num.toFixed(2);
}, },
isAxisAEditable(row) {
const val = row && row.cylinderC;
if (val == null || val === '') return false;
const num = Number(val);
return !Number.isNaN(num) && num !== 0;
},
getSphereSign(sphereS) {
if (sphereS == null || sphereS === '') return 0;
const num = Number(sphereS);
if (Number.isNaN(num) || num === 0) return 0;
return num > 0 ? 1 : -1;
},
getCylinderCMin(row) {
return this.getSphereSign(row && row.sphereS) > 0 ? 0 : -10;
},
getCylinderCMax(row) {
return this.getSphereSign(row && row.sphereS) < 0 ? 0 : 10;
},
alignCylinderSignWithSphere(row) {
if (!row || row.cylinderC == null || row.cylinderC === '') return;
let cylinder = Number(row.cylinderC);
if (Number.isNaN(cylinder) || cylinder === 0) return;
const sphereSign = this.getSphereSign(row.sphereS);
if (sphereSign > 0 && cylinder < 0) cylinder = Math.abs(cylinder);
else if (sphereSign < 0 && cylinder > 0) cylinder = -Math.abs(cylinder);
row.cylinderC = Math.min(this.getCylinderCMax(row), Math.max(this.getCylinderCMin(row), cylinder));
},
onSphereSChange(val, row) {
this.alignCylinderSignWithSphere(row);
if (!this.isAxisAEditable(row)) row.axisA = null;
},
onCylinderCChange(val, row) {
if (val == null || val === '' || Number(val) === 0) {
row.axisA = null;
return;
}
this.alignCylinderSignWithSphere(row);
if (!this.isAxisAEditable(row)) row.axisA = null;
},
cancel() { cancel() {
this.open = false; this.open = false;
this.reset(); this.reset();
@@ -1560,6 +1571,12 @@ export default {
handleProgressiveMultifocalChange(value) { handleProgressiveMultifocalChange(value) {
if (Number(value) !== 1) { if (Number(value) !== 1) {
this.form.progressiveMultifocalPrice = 0; this.form.progressiveMultifocalPrice = 0;
if (this.form.optometryList) {
this.form.optometryList.forEach(row => {
row.nearAdd = null;
row.pupilHeight = null;
});
}
return; return;
} }
if (!this.form.lensFactoryTenantId) { if (!this.form.lensFactoryTenantId) {
@@ -1730,13 +1747,17 @@ export default {
}, },
canViewLogistics(row) { canViewLogistics(row) {
if (!row || row.orderStatus == null) return false; if (!row || row.orderStatus == null) return false;
const status = Number(row.orderStatus); const status = String(row.orderStatus);
return status === Number(this.ORDER_STATUS_SHIPPED) || status === 9; return status === this.ORDER_STATUS_SHIPPED
|| status === this.ORDER_STATUS_EXCHANGE_LOGISTICS
|| status === this.ORDER_STATUS_COMPLETED
|| status === this.ORDER_STATUS_EXCHANGE_COMPLETED;
}, },
canCompleteOrder(row) { canCompleteOrder(row) {
if (!row || row.orderStatus == null) return false; if (!row || row.orderStatus == null) return false;
const status = String(row.orderStatus); const status = String(row.orderStatus);
return status === this.ORDER_STATUS_SHIPPED || status === this.ORDER_STATUS_EXCHANGE_REJECTED; return status === this.ORDER_STATUS_SHIPPED
|| status === this.ORDER_STATUS_EXCHANGE_LOGISTICS;
}, },
handleViewLogistics(row) { handleViewLogistics(row) {
if (!row || !row.id) return; if (!row || !row.id) return;
@@ -1864,28 +1885,13 @@ export default {
if (!row || !row.id) { if (!row || !row.id) {
return; return;
} }
const isExchangeRejected = String(row.orderStatus) === this.ORDER_STATUS_EXCHANGE_REJECTED; this.$modal.confirm(`确认完成订单"${row.orderNo}"吗?`).then(() => {
const confirmMsg = isExchangeRejected return completeOrder({ orderId: row.id });
? `确认将拒绝换货申请订单"${row.orderNo}"标记为已完成吗?`
: `确认完成订单"${row.orderNo}"吗?`;
const requestFn = isExchangeRejected
? () => completeExchangeRejectedOrder({ orderId: row.id })
: () => completeOrder({ orderId: row.id });
this.$modal.confirm(confirmMsg).then(() => {
return requestFn();
}).then(() => { }).then(() => {
this.$modal.msgSuccess('操作成功'); this.$modal.msgSuccess('操作成功');
this.getList(); this.getList();
}).catch(() => {}); }).catch(() => {});
}, },
handleBatchCompleteExchangeRejected() {
this.$modal.confirm('确认将所有拒绝换货申请状态的订单更新为已完成吗?').then(() => {
return completeAllExchangeRejectedOrder();
}).then((res) => {
this.$modal.msgSuccess((res && res.msg) || '操作成功');
this.getList();
}).catch(() => {});
},
handleAgreeExchange(row) { handleAgreeExchange(row) {
if (!row || !row.id) { if (!row || !row.id) {
return; return;
@@ -2027,6 +2033,19 @@ export default {
? this.resolveProgressiveMultifocalPrice() ? this.resolveProgressiveMultifocalPrice()
: 0 : 0
}; };
if (Number(payload.progressiveMultifocal) !== 1 && payload.optometryList) {
payload.optometryList.forEach(row => {
row.nearAdd = null;
row.pupilHeight = null;
});
}
if (payload.optometryList) {
payload.optometryList.forEach(row => {
if (!this.isAxisAEditable(row)) {
row.axisA = null;
}
});
}
if (Number(payload.progressiveMultifocal) === 1) { if (Number(payload.progressiveMultifocal) === 1) {
if (!payload.lensFactoryTenantId) { if (!payload.lensFactoryTenantId) {
this.$modal.msgWarning("选择渐进多焦点时需先选择镜片厂"); this.$modal.msgWarning("选择渐进多焦点时需先选择镜片厂");

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="login"> <div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form"> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">觉远创智后台管理系统</h3> <h3 class="title">觉远创智功能镜片配镜系统</h3>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="loginForm.username" v-model="loginForm.username"

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="register"> <div class="register">
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form"> <el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">觉远创智后台管理系统</h3> <h3 class="title">觉远创智功能镜片配镜系统</h3>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号"> <el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />

View File

@@ -7,8 +7,8 @@ function resolve(dir) {
const CompressionPlugin = require('compression-webpack-plugin') const CompressionPlugin = require('compression-webpack-plugin')
// const name = process.env.VUE_APP_TITLE || '觉远创智管理系统' // 网页标题 // const name = process.env.VUE_APP_TITLE || '觉远创智功能镜片配镜系统' // 网页标题
const name = '觉远创智管理系统' // 网页标题 const name = '觉远创智功能镜片配镜系统' // 网页标题
const port = process.env.port || process.env.npm_config_port || 2091 // 端口 const port = process.env.port || process.env.npm_config_port || 2091 // 端口

View File

@@ -55,7 +55,7 @@ public class ExpressQueryUtils {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "APPCODE " + appCode); headers.set("Authorization", "APPCODE " + appCode);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setContentType(MediaType.parseMediaType("application/x-www-form-urlencoded; charset=UTF-8"));
MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("expressNo", expressNo); body.add("expressNo", expressNo);
@@ -75,21 +75,46 @@ public class ExpressQueryUtils {
return result; return result;
} catch (HttpStatusCodeException e) { } catch (HttpStatusCodeException e) {
int status = e.getRawStatusCode(); int status = e.getRawStatusCode();
log.error("调用阿里云物流查询失败 expressNo={} status={}", expressNo, status, e); String caError = e.getResponseHeaders() != null
? e.getResponseHeaders().getFirst("X-Ca-Error-Message") : null;
log.warn("调用阿里云物流查询失败 expressNo={} status={} caError={}", expressNo, status, caError);
result.put("errorCode", "HTTP_" + status); result.put("errorCode", "HTTP_" + status);
if (status == 403) { if (status == 403)
result.put("errorMsg", "物流查询服务授权失效HTTP 403请在阿里云 API 市场续费快递查询接口并更新 express.ali.app-code"); {
} else { result.put("errorMsg", buildExpress403Message(caError));
}
else
{
String errorBody = e.getResponseBodyAsString(); String errorBody = e.getResponseBodyAsString();
result.put("errorMsg", StringUtils.isNotEmpty(errorBody) ? errorBody : ("物流查询请求失败HTTP状态码" + status)); result.put("errorMsg", StringUtils.isNotEmpty(errorBody) ? errorBody : ("物流查询请求失败HTTP状态码" + status));
} }
return result; return result;
} catch (Exception e) { }
catch (Exception e)
{
log.error("调用阿里云物流查询失败 expressNo={}", expressNo, e); log.error("调用阿里云物流查询失败 expressNo={}", expressNo, e);
result.put("errorCode", "EXCEPTION"); result.put("errorCode", "EXCEPTION");
result.put("errorMsg", e.getMessage()); result.put("errorMsg", e.getMessage());
return result; return result;
} }
} }
private String buildExpress403Message(String caError)
{
if (StringUtils.isEmpty(caError))
{
return "物流查询未授权HTTP 403请在阿里云 API 市场购买「快递物流查询」接口,"
+ "并使用该商品对应的 AppCode 配置 express.ali.app-code不能与实名认证等其它 API 混用)";
}
if (caError.contains("Quota") || caError.contains("quota") || caError.contains("exhausted"))
{
return "物流查询套餐次数已用完HTTP 403请在阿里云 API 市场续费快递查询接口";
}
if (caError.contains("Unauthorized") || caError.contains("Invalid AppCode") || caError.contains("not exists"))
{
return "物流查询 AppCode 无效或未订阅该 APIHTTP 403请更新 express.ali.app-code 为快递查询商品的 AppCode";
}
return "物流查询未授权HTTP 403" + caError;
}
} }

View File

@@ -203,7 +203,7 @@ wechat:
# 业务短信template 为短信平台模板 Code变量由代码传入 # 业务短信template 为短信平台模板 Code变量由代码传入
# assign-order 模板:您有新分配的制片单(订单号${code}),请立即登录觉远配片系统查看并处理。 # assign-order 模板:您有新分配的制片单(订单号${code}),请立即登录觉远配片系统查看并处理。
# ship-order 模板:您的配片单(订单号${code})已发出,由${expressCompany}承运,单号${expressNo},可在官网查询物流进度 # ship-order 模板:您的配片单(订单号${code})已发出,由${expressCompany}承运。
sms: sms:
sign-name: 金华觉远创智科技 sign-name: 金华觉远创智科技
assign-order: assign-order:

View File

@@ -1,10 +1,10 @@
# 页面标题 # 页面标题
VUE_APP_TITLE=觉远创智配件系统 VUE_APP_TITLE=觉远创智功能镜片配镜系统
# 开发环境配置 # 开发环境配置
ENV = 'development' ENV = 'development'
# 觉远创智管理系统/开发环境 # 觉远创智功能镜片配镜系统/开发环境
VUE_APP_BASE_API = '/dev-api' VUE_APP_BASE_API = '/dev-api'
# 路由懒加载 # 路由懒加载

View File

@@ -1,8 +1,8 @@
# 页面标题 # 页面标题
VUE_APP_TITLE=觉远创智配件系统 VUE_APP_TITLE=觉远创智功能镜片配镜系统
# 生产环境配置 # 生产环境配置
ENV = 'production' ENV = 'production'
# 觉远创智管理系统/生产环境 # 觉远创智功能镜片配镜系统/生产环境
VUE_APP_BASE_API = '/prod-api' VUE_APP_BASE_API = '/prod-api'

View File

@@ -1,10 +1,10 @@
# 页面标题 # 页面标题
VUE_APP_TITLE=觉远创智配件系统 VUE_APP_TITLE=觉远创智功能镜片配镜系统
NODE_ENV = production NODE_ENV = production
# 测试环境配置 # 测试环境配置
ENV = 'staging' ENV = 'staging'
# 觉远创智管理系统/测试环境 # 觉远创智功能镜片配镜系统/测试环境
VUE_APP_BASE_API = '/stage-api' VUE_APP_BASE_API = '/stage-api'

View File

@@ -1,7 +1,7 @@
{ {
"name": "ruoyi", "name": "ruoyi",
"version": "3.8.8", "version": "3.8.8",
"description": "觉远创智配件系统", "description": "觉远创智功能镜片配镜系统",
"author": "觉远创智", "author": "觉远创智",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View File

@@ -15,7 +15,7 @@ export default {
return { return {
title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title, title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,
titleTemplate: title => { titleTemplate: title => {
const appTitle = process.env.VUE_APP_TITLE || '觉远创智配件系统' const appTitle = process.env.VUE_APP_TITLE || '觉远创智功能镜片配镜系统'
return title ? `${title} - ${appTitle}` : appTitle return title ? `${title} - ${appTitle}` : appTitle
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -3,21 +3,6 @@ import store from '@/store'
import DataDict from '@/utils/dict' import DataDict from '@/utils/dict'
import { getDicts as getDicts } from '@/api/system/dict/data' import { getDicts as getDicts } from '@/api/system/dict/data'
function searchDictByKey(dict, key) {
if (key == null && key == "") {
return null
}
try {
for (let i = 0; i < dict.length; i++) {
if (dict[i].key == key) {
return dict[i].value
}
}
} catch (e) {
return null
}
}
function install() { function install() {
Vue.use(DataDict, { Vue.use(DataDict, {
metas: { metas: {
@@ -25,10 +10,6 @@ function install() {
labelField: 'dictLabel', labelField: 'dictLabel',
valueField: 'dictValue', valueField: 'dictValue',
request(dictMeta) { request(dictMeta) {
const storeDict = searchDictByKey(store.getters.dict, dictMeta.type)
if (storeDict) {
return new Promise(resolve => { resolve(storeDict) })
} else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getDicts(dictMeta.type).then(res => { getDicts(dictMeta.type).then(res => {
store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data }) store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data })
@@ -37,7 +18,6 @@ function install() {
reject(error) reject(error)
}) })
}) })
}
}, },
}, },
}, },

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<template v-for="(item, index) in options"> <template v-for="(item, index) in options">
<template v-if="values.includes(item.value)"> <template v-if="values.includes(String(item.value))">
<span <span
v-if="(item.raw.listClass == 'default' || item.raw.listClass == '') && (item.raw.cssClass == '' || item.raw.cssClass == null)" v-if="(item.raw.listClass == 'default' || item.raw.listClass == '') && (item.raw.cssClass == '' || item.raw.cssClass == null)"
:key="item.value" :key="item.value"
@@ -63,7 +63,7 @@ export default {
// 传入值为数组 // 传入值为数组
let unmatch = false // 添加一个标志来判断是否有未匹配项 let unmatch = false // 添加一个标志来判断是否有未匹配项
this.values.forEach(item => { this.values.forEach(item => {
if (!this.options.some(v => v.value === item)) { if (!this.options.some(v => String(v.value) === String(item))) {
this.unmatchArray.push(item) this.unmatchArray.push(item)
unmatch = true // 如果有未匹配项将标志设置为true unmatch = true // 如果有未匹配项将标志设置为true
} }

View File

@@ -36,7 +36,7 @@ export default {
data() { data() {
return { return {
// title: process.env.VUE_APP_TITLE, // title: process.env.VUE_APP_TITLE,
title: '觉远创智配件系统', title: '觉远创智功能镜片配镜系统',
logo: logoImg logo: logoImg
} }
} }

View File

@@ -4,11 +4,16 @@ const state = {
const mutations = { const mutations = {
SET_DICT: (state, { key, value }) => { SET_DICT: (state, { key, value }) => {
if (key !== null && key !== "") { if (key !== null && key !== "") {
const index = state.dict.findIndex(item => item.key === key)
if (index >= 0) {
state.dict.splice(index, 1, { key: key, value: value })
} else {
state.dict.push({ state.dict.push({
key: key, key: key,
value: value value: value
}) })
} }
}
}, },
REMOVE_DICT: (state, key) => { REMOVE_DICT: (state, key) => {
try { try {

View File

@@ -2,7 +2,7 @@
<nav class="nav-bar"> <nav class="nav-bar">
<div class="logo"> <div class="logo">
<img :src="require('@/assets/images/home/logo.c3029a2f.png')" alt="JUEYUAN" /> <img :src="require('@/assets/images/home/logo.c3029a2f.png')" alt="JUEYUAN" />
<h1 class="logo-text">配件系统</h1> <h1 class="logo-text">功能镜片配镜系统</h1>
</div> </div>
<div class="menu-toggle" @click="toggleMenu"> <div class="menu-toggle" @click="toggleMenu">
<span class="hamburger" :class="{ active: isMenuOpen }"></span> <span class="hamburger" :class="{ active: isMenuOpen }"></span>

View File

@@ -22,34 +22,32 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table v-loading="loading" :data="orderList"> <el-table v-loading="loading" :data="orderList" class="factory-order-table">
<el-table-column label="订单编号" align="center" prop="orderNo" min-width="180" show-overflow-tooltip /> <el-table-column label="订单编号" align="center" prop="orderNo" min-width="200" show-overflow-tooltip />
<el-table-column label="品牌方" align="center" prop="brandTenantName" min-width="100" /> <el-table-column label="通用镜片" align="center" min-width="200" show-overflow-tooltip>
<el-table-column label="通用镜片" align="center" min-width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatLens(scope.row) }}</span> <span>{{ formatLens(scope.row) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="C价格" align="center" prop="priceC" width="80" /> <el-table-column label="总价" align="center" width="100">
<el-table-column label="渐进多焦点价格" align="center" min-width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatProgressiveMultifocalPrice(scope.row) }}</span> <span>{{ formatPriceAmount(formatDetailPriceTotal(scope.row, 'priceC')) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="订单状态" align="center" prop="orderStatus" width="100"> <el-table-column label="订单状态" align="center" prop="orderStatus" width="112">
<template slot-scope="scope"> <template slot-scope="scope">
<dict-tag :options="dict.type.jy_order_status" :value="scope.row.orderStatus"/> <dict-tag :options="dict.type.jy_order_status" :value="scope.row.orderStatus"/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="结算" align="center" prop="purchaseSettleStatus" width="80"> <el-table-column label="结算" align="center" prop="purchaseSettleStatus" width="88">
<template slot-scope="scope"> <template slot-scope="scope">
<dict-tag :options="dict.type.jy_purchase_settle_status" :value="scope.row.purchaseSettleStatus"/> <dict-tag :options="dict.type.jy_purchase_settle_status" :value="scope.row.purchaseSettleStatus"/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="收货人" align="center" prop="receiverName" width="80" /> <el-table-column label="收货人" align="center" prop="receiverName" width="100" show-overflow-tooltip />
<el-table-column label="手机号" align="center" prop="receiverPhone" width="110" /> <el-table-column label="手机号" align="center" prop="receiverPhone" width="126" />
<el-table-column label="创建时间" align="center" prop="createTime" width="150" /> <el-table-column label="创建时间" align="center" prop="createTime" width="168" />
<el-table-column label="操作" align="center" min-width="280"> <el-table-column label="操作" align="center" min-width="300" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" @click="handleDetail(scope.row)">详情</el-button> <el-button size="mini" type="text" @click="handleDetail(scope.row)">详情</el-button>
<el-button <el-button
@@ -139,7 +137,6 @@
<el-descriptions-item label="订单状态"> <el-descriptions-item label="订单状态">
<dict-tag :options="dict.type.jy_order_status" :value="detail.orderStatus" /> <dict-tag :options="dict.type.jy_order_status" :value="detail.orderStatus" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="品牌方">{{ detail.brandTenantName || '-' }}</el-descriptions-item>
<el-descriptions-item label="通用镜片">{{ formatLens(detail) }}</el-descriptions-item> <el-descriptions-item label="通用镜片">{{ formatLens(detail) }}</el-descriptions-item>
<el-descriptions-item label="渐进多焦点">{{ detail.progressiveMultifocal === 1 ? '是' : '否' }}</el-descriptions-item> <el-descriptions-item label="渐进多焦点">{{ detail.progressiveMultifocal === 1 ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="附件" :span="2"> <el-descriptions-item label="附件" :span="2">
@@ -187,11 +184,8 @@
</div> </div>
<div class="section-body"> <div class="section-body">
<el-descriptions :column="2" border size="small" class="detail-descriptions detail-descriptions--price"> <el-descriptions :column="2" border size="small" class="detail-descriptions detail-descriptions--price">
<el-descriptions-item label="C价格">{{ formatPriceAmount(detail.priceC) }}</el-descriptions-item>
<el-descriptions-item label="物流费用">{{ formatPriceAmount(formatDetailLogisticsFee(detail)) }}</el-descriptions-item>
<el-descriptions-item label="渐进多焦点价格">{{ formatPriceAmount(formatProgressiveMultifocalPrice(detail)) }}</el-descriptions-item>
<el-descriptions-item label="合计"> <el-descriptions-item label="合计">
<span class="price-total-value">{{ formatPriceAmount(formatPriceTotal(detail, 'priceC')) }}</span> <span class="price-total-value">{{ formatPriceAmount(formatDetailPriceTotal(detail, 'priceC')) }}</span>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
<div class="detail-logistics-fee-block"> <div class="detail-logistics-fee-block">
@@ -203,26 +197,6 @@
<span>{{ formatPriceAmount(formatLogisticsLogFee(scope.row, detail)) }}</span> <span>{{ formatPriceAmount(formatLogisticsLogFee(scope.row, detail)) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="应收结算" align="center" width="100">
<template slot-scope="scope">
<dict-tag
v-if="formatLogSaleSettleStatus(scope.row) != null"
:options="dict.type.jy_sale_settle_status"
:value="formatLogSaleSettleStatus(scope.row)"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="应付结算" align="center" width="100">
<template slot-scope="scope">
<dict-tag
v-if="formatLogPurchaseSettleStatus(scope.row) != null"
:options="dict.type.jy_purchase_settle_status"
:value="formatLogPurchaseSettleStatus(scope.row)"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="发货日期" align="center" width="120"> <el-table-column label="发货日期" align="center" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatShipDate(scope.row.operateTime) }}</span> <span>{{ formatShipDate(scope.row.operateTime) }}</span>
@@ -277,7 +251,7 @@
<span>{{ formatDetailPdValue(scope.row, scope.$index) }}</span> <span>{{ formatDetailPdValue(scope.row, scope.$index) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="瞳高(mm)" align="center" min-width="80"> <el-table-column v-if="detail.progressiveMultifocal === 1" label="瞳高(mm)" align="center" min-width="80">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ scope.row.pupilHeight != null ? scope.row.pupilHeight : '-' }}</span> <span>{{ scope.row.pupilHeight != null ? scope.row.pupilHeight : '-' }}</span>
</template> </template>
@@ -287,7 +261,7 @@
<span>{{ scope.row.distanceVa != null && scope.row.distanceVa !== '' ? scope.row.distanceVa : '-' }}</span> <span>{{ scope.row.distanceVa != null && scope.row.distanceVa !== '' ? scope.row.distanceVa : '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="近用ADD(屈光度)" align="center" min-width="100"> <el-table-column v-if="detail.progressiveMultifocal === 1" label="近用ADD(屈光度)" align="center" min-width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatDetailNearAdd(scope.row.nearAdd) }}</span> <span>{{ formatDetailNearAdd(scope.row.nearAdd) }}</span>
</template> </template>
@@ -311,26 +285,6 @@
<dict-tag :options="dict.type.jy_order_status" :value="scope.row.orderStatus" /> <dict-tag :options="dict.type.jy_order_status" :value="scope.row.orderStatus" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="应收结算" align="center" width="100">
<template slot-scope="scope">
<dict-tag
v-if="formatLogSaleSettleStatus(scope.row) != null"
:options="dict.type.jy_sale_settle_status"
:value="formatLogSaleSettleStatus(scope.row)"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="应付结算" align="center" width="100">
<template slot-scope="scope">
<dict-tag
v-if="formatLogPurchaseSettleStatus(scope.row) != null"
:options="dict.type.jy_purchase_settle_status"
:value="formatLogPurchaseSettleStatus(scope.row)"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip /> <el-table-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip />
<el-table-column label="操作人" align="center" prop="operator" width="100" /> <el-table-column label="操作人" align="center" prop="operator" width="100" />
<el-table-column label="操作时间" align="center" prop="operateTime" width="160" /> <el-table-column label="操作时间" align="center" prop="operateTime" width="160" />
@@ -574,7 +528,10 @@ export default {
ORDER_STATUS_PRODUCING: '7', ORDER_STATUS_PRODUCING: '7',
ORDER_STATUS_SHIPPED: '8', ORDER_STATUS_SHIPPED: '8',
ORDER_STATUS_COMPLETED: '9', ORDER_STATUS_COMPLETED: '9',
ORDER_STATUS_EXCHANGE_APPLY: '12',
ORDER_STATUS_EXCHANGE_AGREED: '13', ORDER_STATUS_EXCHANGE_AGREED: '13',
ORDER_STATUS_EXCHANGE_LOGISTICS: '15',
ORDER_STATUS_EXCHANGE_COMPLETED: '17',
ORDER_STATUS_WITHDRAWN: '20', ORDER_STATUS_WITHDRAWN: '20',
ORDER_STATUS_SUPPLEMENTED: '19', ORDER_STATUS_SUPPLEMENTED: '19',
logisticsDialogVisible: false, logisticsDialogVisible: false,
@@ -703,8 +660,32 @@ export default {
}, },
formatProgressiveMultifocalPrice(row) { formatProgressiveMultifocalPrice(row) {
if (!row) return '-' if (!row) return '-'
if (this.isExchangeOrder(row)) return '-'
return row.progressiveMultifocalPrice != null ? row.progressiveMultifocalPrice : '-' return row.progressiveMultifocalPrice != null ? row.progressiveMultifocalPrice : '-'
}, },
isExchangeOrder(row) {
if (!row) return false
if (row.orderNo && String(row.orderNo).startsWith('HH')) return true
const status = row.orderStatus != null ? String(row.orderStatus) : ''
return status === this.ORDER_STATUS_EXCHANGE_APPLY || status === this.ORDER_STATUS_EXCHANGE_AGREED
|| status === this.ORDER_STATUS_EXCHANGE_LOGISTICS
|| status === this.ORDER_STATUS_EXCHANGE_COMPLETED
},
formatLogisticsFee(row) {
if (!row) return '-'
if (!this.isExchangeOrder(row)) return '-'
const fee = this.parseLogisticsFeeValue(row.logisticsFee)
return fee != null ? fee : '-'
},
formatPriceC(row) {
if (!row) return '-'
if (this.isExchangeOrder(row)) return '-'
return row.priceC != null ? row.priceC : '-'
},
formatDetailPriceTotal(detail, priceKey) {
if (this.isExchangeOrder(detail)) return this.formatDetailLogisticsFee(detail)
return this.formatPriceTotal(detail, priceKey)
},
formatPriceAmount(value) { formatPriceAmount(value) {
if (value == null || value === '' || value === '-') return '-' if (value == null || value === '' || value === '-') return '-'
if (value === '未核算') return '未核算' if (value === '未核算') return '未核算'
@@ -750,7 +731,7 @@ export default {
hasFee = true hasFee = true
} }
}) })
if (!hasFee && logs.length > 0 && detail.logisticsFee != null) { if (!hasFee && detail.logisticsFee != null) {
const legacyFee = this.parseLogisticsFeeValue(detail.logisticsFee) const legacyFee = this.parseLogisticsFeeValue(detail.logisticsFee)
if (legacyFee != null) return legacyFee if (legacyFee != null) return legacyFee
} }
@@ -760,24 +741,6 @@ export default {
formatDetailLogisticsFee(detail) { formatDetailLogisticsFee(detail) {
return this.sumDetailLogisticsFee(detail) return this.sumDetailLogisticsFee(detail)
}, },
formatLogSaleSettleStatus(log) {
if (!log || log.expressNo == null || String(log.expressNo).trim() === '') {
return null
}
if (log.saleSettleStatus != null && log.saleSettleStatus !== '') {
return log.saleSettleStatus
}
return '0'
},
formatLogPurchaseSettleStatus(log) {
if (!log || log.expressNo == null || String(log.expressNo).trim() === '') {
return null
}
if (log.purchaseSettleStatus != null && log.purchaseSettleStatus !== '') {
return log.purchaseSettleStatus
}
return '0'
},
formatShipDate(time) { formatShipDate(time) {
if (!time) { if (!time) {
return '-' return '-'
@@ -915,12 +878,17 @@ export default {
}, },
canCompleteOrder(row) { canCompleteOrder(row) {
if (!row || row.orderStatus == null) return false if (!row || row.orderStatus == null) return false
return String(row.orderStatus) === this.ORDER_STATUS_SHIPPED const status = String(row.orderStatus)
return status === this.ORDER_STATUS_SHIPPED
|| status === this.ORDER_STATUS_EXCHANGE_LOGISTICS
}, },
canViewLogistics(row) { canViewLogistics(row) {
if (!row || row.orderStatus == null) return false if (!row || row.orderStatus == null) return false
const status = Number(row.orderStatus) const status = String(row.orderStatus)
return status === Number(this.ORDER_STATUS_SHIPPED) || status === 9 return status === this.ORDER_STATUS_SHIPPED
|| status === this.ORDER_STATUS_EXCHANGE_LOGISTICS
|| status === this.ORDER_STATUS_COMPLETED
|| status === this.ORDER_STATUS_EXCHANGE_COMPLETED
}, },
handleViewLogistics(row) { handleViewLogistics(row) {
if (!row || !row.id) return if (!row || !row.id) return
@@ -1112,6 +1080,10 @@ export default {
box-sizing: border-box; box-sizing: border-box;
} }
.factory-order-table {
width: 100%;
}
.logistics-basic { .logistics-basic {
margin-bottom: 16px; margin-bottom: 16px;
color: #606266; color: #606266;

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ function resolve(dir) {
const CompressionPlugin = require('compression-webpack-plugin') const CompressionPlugin = require('compression-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '觉远创智配件系统' const name = process.env.VUE_APP_TITLE || '觉远创智功能镜片配镜系统'
const port = process.env.port || process.env.npm_config_port || 2092 // ?? const port = process.env.port || process.env.npm_config_port || 2092 // ??
@@ -77,7 +77,7 @@ module.exports = {
}, },
chainWebpack(config) { chainWebpack(config) {
config.plugin('html').tap(args => { config.plugin('html').tap(args => {
args[0].title = process.env.VUE_APP_TITLE || '觉远创智配件系统' args[0].title = process.env.VUE_APP_TITLE || '觉远创智功能镜片配镜系统'
return args return args
}) })

View File

@@ -16,7 +16,7 @@ public class SmsProperties
/** 配片订单分配至镜片厂(模板变量 code=订单号) */ /** 配片订单分配至镜片厂(模板变量 code=订单号) */
private SceneConfig assignOrder = new SceneConfig(); private SceneConfig assignOrder = new SceneConfig();
/** 配片订单发货通知(模板变量 code、expressCompany、expressNo */ /** 配片订单发货通知(模板变量 code、expressCompany */
private SceneConfig shipOrder = new SceneConfig(); private SceneConfig shipOrder = new SceneConfig();
public String getSignName() public String getSignName()

View File

@@ -199,32 +199,6 @@ public class JySysOrderController extends BaseController
rejectDTO.getOrderId(), rejectDTO.getReason().trim(), getUsername())); rejectDTO.getOrderId(), rejectDTO.getReason().trim(), getUsername()));
} }
@PreAuthorize("@ss.hasPermi('jysystem:order:edit')")
@Log(title = "拒绝换货申请订单完成", businessType = BusinessType.UPDATE)
@PutMapping("/exchangeRejectedComplete")
public AjaxResult exchangeRejectedComplete(@RequestBody OrderCloseDTO completeDTO)
{
if (completeDTO == null || completeDTO.getOrderId() == null)
{
return error("订单ID不能为空");
}
return toAjax(jySysOrderService.adminCompleteExchangeRejectedOrder(
completeDTO.getOrderId(), getUsername()));
}
@PreAuthorize("@ss.hasPermi('jysystem:order:edit')")
@Log(title = "批量完成拒绝换货申请订单", businessType = BusinessType.UPDATE)
@PutMapping("/exchangeRejectedCompleteAll")
public AjaxResult exchangeRejectedCompleteAll()
{
int rows = jySysOrderService.adminCompleteAllExchangeRejectedOrders(getUsername());
if (rows <= 0)
{
return success("没有需要更新的订单");
}
return success("已成功更新 " + rows + " 条订单");
}
@PreAuthorize("@ss.hasPermi('jysystem:order:edit')") @PreAuthorize("@ss.hasPermi('jysystem:order:edit')")
@Log(title = "订单状态变更", businessType = BusinessType.UPDATE) @Log(title = "订单状态变更", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus") @PutMapping("/changeStatus")

View File

@@ -210,7 +210,7 @@ public class JySysOrderPortalController extends BaseController
{ {
return error("换货申请原因不能为空"); return error("换货申请原因不能为空");
} }
return toAjax(jySysOrderPortalService.exchangeApplyPortalOrder( return success(jySysOrderPortalService.exchangeApplyPortalOrder(
exchangeDTO.getOrderId(), exchangeDTO.getReason().trim())); exchangeDTO.getOrderId(), exchangeDTO.getReason().trim()));
} }

View File

@@ -62,6 +62,9 @@ public class JySysOrder extends BaseEntity
@Excel(name = "渐进多焦点价格") @Excel(name = "渐进多焦点价格")
private BigDecimal progressiveMultifocalPrice; private BigDecimal progressiveMultifocalPrice;
@Excel(name = "是否老花", readConverterExp = "0=否,1=是")
private Integer presbyopia;
@Excel(name = "订单状态", dictType = "jy_order_status") @Excel(name = "订单状态", dictType = "jy_order_status")
private String orderStatus; private String orderStatus;
@@ -261,6 +264,16 @@ public class JySysOrder extends BaseEntity
this.progressiveMultifocalPrice = progressiveMultifocalPrice; this.progressiveMultifocalPrice = progressiveMultifocalPrice;
} }
public Integer getPresbyopia()
{
return presbyopia;
}
public void setPresbyopia(Integer presbyopia)
{
this.presbyopia = presbyopia;
}
public String getOrderStatus() public String getOrderStatus()
{ {
return orderStatus; return orderStatus;
@@ -385,6 +398,7 @@ public class JySysOrder extends BaseEntity
.append("priceC", getPriceC()) .append("priceC", getPriceC())
.append("logisticsFee", getLogisticsFee()) .append("logisticsFee", getLogisticsFee())
.append("progressiveMultifocal", getProgressiveMultifocal()) .append("progressiveMultifocal", getProgressiveMultifocal())
.append("presbyopia", getPresbyopia())
.append("orderStatus", getOrderStatus()) .append("orderStatus", getOrderStatus())
.append("purchaseSettleStatus", getPurchaseSettleStatus()) .append("purchaseSettleStatus", getPurchaseSettleStatus())
.append("saleSettleStatus", getSaleSettleStatus()) .append("saleSettleStatus", getSaleSettleStatus())

View File

@@ -25,12 +25,6 @@ public class JySysOrderStatusLog
private BigDecimal logisticsFee; private BigDecimal logisticsFee;
/** 应收结算状态0未结算 1已结算 */
private String saleSettleStatus;
/** 应付结算状态0未结算 1已结算 */
private String purchaseSettleStatus;
private String remark; private String remark;
private String operator; private String operator;
@@ -98,26 +92,6 @@ public class JySysOrderStatusLog
this.logisticsFee = logisticsFee; this.logisticsFee = logisticsFee;
} }
public String getSaleSettleStatus()
{
return saleSettleStatus;
}
public void setSaleSettleStatus(String saleSettleStatus)
{
this.saleSettleStatus = saleSettleStatus;
}
public String getPurchaseSettleStatus()
{
return purchaseSettleStatus;
}
public void setPurchaseSettleStatus(String purchaseSettleStatus)
{
this.purchaseSettleStatus = purchaseSettleStatus;
}
public String getRemark() public String getRemark()
{ {
return remark; return remark;
@@ -158,8 +132,6 @@ public class JySysOrderStatusLog
.append("expressCompany", getExpressCompany()) .append("expressCompany", getExpressCompany())
.append("expressNo", getExpressNo()) .append("expressNo", getExpressNo())
.append("logisticsFee", getLogisticsFee()) .append("logisticsFee", getLogisticsFee())
.append("saleSettleStatus", getSaleSettleStatus())
.append("purchaseSettleStatus", getPurchaseSettleStatus())
.append("remark", getRemark()) .append("remark", getRemark())
.append("operator", getOperator()) .append("operator", getOperator())
.append("operateTime", getOperateTime()) .append("operateTime", getOperateTime())

View File

@@ -28,6 +28,8 @@ public interface JySysOrderMapper extends BaseMapper<JySysOrder>
int countByTenantId(@Param("tenantId") Long tenantId); int countByTenantId(@Param("tenantId") Long tenantId);
int countByOrderNo(@Param("orderNo") String orderNo);
List<JySysOrder> selectCompletedOrdersByYearMonth(@Param("settleYearMonth") String settleYearMonth); List<JySysOrder> selectCompletedOrdersByYearMonth(@Param("settleYearMonth") String settleYearMonth);
int updatePurchaseSettleStatusByOrderIds(@Param("orderIds") List<Long> orderIds, int updatePurchaseSettleStatusByOrderIds(@Param("orderIds") List<Long> orderIds,

View File

@@ -17,10 +17,4 @@ public interface JySysOrderStatusLogMapper
List<JySysOrderStatusLog> selectUnsettledShipLogsByOrderIdAndType(@Param("orderId") Long orderId, List<JySysOrderStatusLog> selectUnsettledShipLogsByOrderIdAndType(@Param("orderId") Long orderId,
@Param("settlementType") String settlementType); @Param("settlementType") String settlementType);
int updateSaleSettleStatusByIds(@Param("ids") List<Long> ids, @Param("settleStatus") String settleStatus);
int updatePurchaseSettleStatusByIds(@Param("ids") List<Long> ids, @Param("settleStatus") String settleStatus);
int resetShipLogSettleStatusByOrderIds(@Param("orderIds") List<Long> orderIds);
} }

View File

@@ -46,7 +46,7 @@ public interface IJySysOrderPortalService
int completePortalOrder(Long orderId); int completePortalOrder(Long orderId);
int exchangeApplyPortalOrder(Long orderId, String reason); JySysOrder exchangeApplyPortalOrder(Long orderId, String reason);
int receivePortalOrder(Long orderId); int receivePortalOrder(Long orderId);

View File

@@ -52,7 +52,7 @@ public interface IJySysOrderService extends IService<JySysOrder>
int brandReceiveExchangeRejectedOrder(Long orderId, Long brandTenantId, String operator); int brandReceiveExchangeRejectedOrder(Long orderId, Long brandTenantId, String operator);
int brandExchangeApplyOrder(Long orderId, Long brandTenantId, String reason, String operator); JySysOrder brandExchangeApplyOrder(Long orderId, Long brandTenantId, String reason, String operator);
int adminRejectOrder(Long orderId, String reason, String operator); int adminRejectOrder(Long orderId, String reason, String operator);
@@ -64,10 +64,6 @@ public interface IJySysOrderService extends IService<JySysOrder>
int adminRejectExchangeOrder(Long orderId, String reason, String operator); int adminRejectExchangeOrder(Long orderId, String reason, String operator);
int adminCompleteExchangeRejectedOrder(Long orderId, String operator);
int adminCompleteAllExchangeRejectedOrders(String operator);
int brandResubmitOrder(Long orderId, Long brandTenantId, String reason, String operator); int brandResubmitOrder(Long orderId, Long brandTenantId, String reason, String operator);
int changeOrderStatus(Long orderId, String orderStatus, String operator); int changeOrderStatus(Long orderId, String orderStatus, String operator);

View File

@@ -14,12 +14,11 @@ public interface SmsNotificationService
void trySendOrderAssignedNotify(String phone, String orderNo); void trySendOrderAssignedNotify(String phone, String orderNo);
/** /**
* 订单发货后,向收件人手机号发送通知(失败不影响主流程) * 订单发货后发送通知(失败不影响主流程)
* *
* @param phone 手机号 * @param phone 手机号(品牌方租户手机号,或收货人手机号)
* @param orderNo 订单号,对应模板变量 code * @param orderNo 订单号,对应模板变量 code
* @param expressCompany 快递公司,对应模板变量 expressCompany * @param expressCompany 快递公司,对应模板变量 expressCompany
* @param expressNo 快递单号,对应模板变量 expressNo
*/ */
void trySendOrderShippedNotify(String phone, String orderNo, String expressCompany, String expressNo); void trySendOrderShippedNotify(String phone, String orderNo, String expressCompany);
} }

View File

@@ -31,6 +31,7 @@ import com.ruoyi.jysystem.utils.FeeSettlementExcelExporter;
import com.ruoyi.jysystem.utils.FeeSettlementExportHelper; import com.ruoyi.jysystem.utils.FeeSettlementExportHelper;
import com.ruoyi.jysystem.utils.FeeSettlementPdfExporter; import com.ruoyi.jysystem.utils.FeeSettlementPdfExporter;
import com.ruoyi.jysystem.utils.IdUtil; import com.ruoyi.jysystem.utils.IdUtil;
import com.ruoyi.jysystem.utils.OrderExchangeHelper;
import com.ruoyi.jysystem.vo.FeeSettlementCheckVO; import com.ruoyi.jysystem.vo.FeeSettlementCheckVO;
import com.ruoyi.jysystem.vo.FeeSettlementDetailVO; import com.ruoyi.jysystem.vo.FeeSettlementDetailVO;
@@ -130,10 +131,6 @@ public class JySysFeeSettlementServiceImpl implements IJySysFeeSettlementService
{ {
throw new ServiceException("该年月没有已完成状态的订单,无法生成结算单"); throw new ServiceException("该年月没有已完成状态的订单,无法生成结算单");
} }
if (forceRegenerate && exists)
{
resetShipLogSettleStatusForOrders(orders);
}
Map<Long, List<JySysOrder>> brandGroups = new LinkedHashMap<>(); Map<Long, List<JySysOrder>> brandGroups = new LinkedHashMap<>();
Map<Long, List<JySysOrder>> factoryGroups = new LinkedHashMap<>(); Map<Long, List<JySysOrder>> factoryGroups = new LinkedHashMap<>();
@@ -210,27 +207,9 @@ public class JySysFeeSettlementServiceImpl implements IJySysFeeSettlementService
update.setUpdateTime(DateUtils.getNowDate()); update.setUpdateTime(DateUtils.getNowDate());
int rows = feeSettlementMapper.updateSettleStatus(update); int rows = feeSettlementMapper.updateSettleStatus(update);
updateOrderSettleStatusBySettlement(settlement, operator); updateOrderSettleStatusBySettlement(settlement, operator);
updateStatusLogSettleStatusBySettlement(settlement);
return rows; return rows;
} }
private void updateStatusLogSettleStatusBySettlement(JySysFeeSettlement settlement)
{
List<Long> statusLogIds = collectStatusLogIdsBySettlement(settlement.getId());
if (statusLogIds.isEmpty())
{
return;
}
if (JySysFeeSettlement.TYPE_AP.equals(settlement.getSettlementType()))
{
jySysOrderStatusLogMapper.updatePurchaseSettleStatusByIds(statusLogIds, "1");
}
else if (JySysFeeSettlement.TYPE_AR.equals(settlement.getSettlementType()))
{
jySysOrderStatusLogMapper.updateSaleSettleStatusByIds(statusLogIds, "1");
}
}
private void updateOrderSettleStatusBySettlement(JySysFeeSettlement settlement, String operator) private void updateOrderSettleStatusBySettlement(JySysFeeSettlement settlement, String operator)
{ {
List<JySysFeeSettlementItem> items = feeSettlementItemMapper.selectBySettlementId(settlement.getId()); List<JySysFeeSettlementItem> items = feeSettlementItemMapper.selectBySettlementId(settlement.getId());
@@ -314,10 +293,13 @@ public class JySysFeeSettlementServiceImpl implements IJySysFeeSettlementService
private JySysFeeSettlementItem buildSettlementItem(Long settlementId, String type, JySysOrder order) private JySysFeeSettlementItem buildSettlementItem(Long settlementId, String type, JySysOrder order)
{ {
BigDecimal lensPrice = JySysFeeSettlement.TYPE_AR.equals(type) boolean exchangeOrder = isExchangeOrder(order);
? defaultAmount(order.getPriceC()) : defaultAmount(order.getPriceB()); BigDecimal lensPrice = exchangeOrder ? BigDecimal.ZERO
BigDecimal progressivePrice = Integer.valueOf(1).equals(order.getProgressiveMultifocal()) : (JySysFeeSettlement.TYPE_AR.equals(type)
? defaultAmount(order.getProgressiveMultifocalPrice()) : BigDecimal.ZERO; ? defaultAmount(order.getPriceC()) : defaultAmount(order.getPriceB()));
BigDecimal progressivePrice = exchangeOrder ? BigDecimal.ZERO
: (Integer.valueOf(1).equals(order.getProgressiveMultifocal())
? defaultAmount(order.getProgressiveMultifocalPrice()) : BigDecimal.ZERO);
List<JySysOrderStatusLog> shipLogs = jySysOrderStatusLogMapper List<JySysOrderStatusLog> shipLogs = jySysOrderStatusLogMapper
.selectUnsettledShipLogsByOrderIdAndType(order.getId(), type); .selectUnsettledShipLogsByOrderIdAndType(order.getId(), type);
@@ -374,7 +356,6 @@ public class JySysFeeSettlementServiceImpl implements IJySysFeeSettlementService
{ {
continue; continue;
} }
revertLogSettleStatusBySettlement(settlement);
if ("1".equals(settlement.getSettleStatus())) if ("1".equals(settlement.getSettleStatus()))
{ {
revertOrderSettleStatusBySettlement(settlement); revertOrderSettleStatusBySettlement(settlement);
@@ -392,24 +373,6 @@ public class JySysFeeSettlementServiceImpl implements IJySysFeeSettlementService
} }
} }
/** 删除结算单时回滚关联发货日志的结算状态,便于重新生成时再次纳入物流费用 */
private void revertLogSettleStatusBySettlement(JySysFeeSettlement settlement)
{
List<Long> statusLogIds = collectStatusLogIdsBySettlement(settlement.getId());
if (statusLogIds.isEmpty())
{
return;
}
if (JySysFeeSettlement.TYPE_AP.equals(settlement.getSettlementType()))
{
jySysOrderStatusLogMapper.updatePurchaseSettleStatusByIds(statusLogIds, "0");
}
else if (JySysFeeSettlement.TYPE_AR.equals(settlement.getSettlementType()))
{
jySysOrderStatusLogMapper.updateSaleSettleStatusByIds(statusLogIds, "0");
}
}
private void revertOrderSettleStatusBySettlement(JySysFeeSettlement settlement) private void revertOrderSettleStatusBySettlement(JySysFeeSettlement settlement)
{ {
List<JySysFeeSettlementItem> items = feeSettlementItemMapper.selectBySettlementId(settlement.getId()); List<JySysFeeSettlementItem> items = feeSettlementItemMapper.selectBySettlementId(settlement.getId());
@@ -433,38 +396,6 @@ public class JySysFeeSettlementServiceImpl implements IJySysFeeSettlementService
} }
} }
private void resetShipLogSettleStatusForOrders(List<JySysOrder> orders)
{
if (orders == null || orders.isEmpty())
{
return;
}
List<Long> orderIds = orders.stream()
.map(JySysOrder::getId)
.filter(orderId -> orderId != null)
.distinct()
.collect(Collectors.toList());
if (!orderIds.isEmpty())
{
jySysOrderStatusLogMapper.resetShipLogSettleStatusByOrderIds(orderIds);
}
}
private List<Long> collectStatusLogIdsBySettlement(Long settlementId)
{
List<JySysFeeSettlementItem> items = feeSettlementItemMapper.selectBySettlementId(settlementId);
if (items == null || items.isEmpty())
{
return new ArrayList<>();
}
List<Long> itemIds = items.stream().map(JySysFeeSettlementItem::getId).collect(Collectors.toList());
return feeSettlementLogisticsMapper.selectBySettlementItemIds(itemIds).stream()
.map(JySysFeeSettlementLogistics::getStatusLogId)
.filter(statusLogId -> statusLogId != null)
.distinct()
.collect(Collectors.toList());
}
private String nextSettlementNo(String type) private String nextSettlementNo(String type)
{ {
String datePart = new SimpleDateFormat("yyyyMMdd").format(new Date()); String datePart = new SimpleDateFormat("yyyyMMdd").format(new Date());
@@ -496,6 +427,11 @@ public class JySysFeeSettlementServiceImpl implements IJySysFeeSettlementService
return value != null ? value : BigDecimal.ZERO; return value != null ? value : BigDecimal.ZERO;
} }
private boolean isExchangeOrder(JySysOrder order)
{
return OrderExchangeHelper.isExchangeOrder(order);
}
@Override @Override
public byte[] exportPdf(Long id) public byte[] exportPdf(Long id)
{ {

View File

@@ -229,7 +229,7 @@ public class JySysOrderPortalServiceImpl implements IJySysOrderPortalService
} }
@Override @Override
public int exchangeApplyPortalOrder(Long orderId, String reason) public JySysOrder exchangeApplyPortalOrder(Long orderId, String reason)
{ {
JySysTenant tenant = requireBrandTenant(); JySysTenant tenant = requireBrandTenant();
return jySysOrderService.brandExchangeApplyOrder( return jySysOrderService.brandExchangeApplyOrder(

View File

@@ -16,6 +16,7 @@ import com.ruoyi.jysystem.mapper.JySysTenantMapper;
import com.ruoyi.jysystem.service.IJySysOrderService; import com.ruoyi.jysystem.service.IJySysOrderService;
import com.ruoyi.jysystem.service.IJySysTenantMessageService; import com.ruoyi.jysystem.service.IJySysTenantMessageService;
import com.ruoyi.jysystem.utils.JySysOrderListPdfExporter; import com.ruoyi.jysystem.utils.JySysOrderListPdfExporter;
import com.ruoyi.jysystem.utils.OrderExchangeHelper;
import com.ruoyi.jysystem.utils.TenantTypeHelper; import com.ruoyi.jysystem.utils.TenantTypeHelper;
import com.ruoyi.jysystem.vo.JySysTenantLensVO; import com.ruoyi.jysystem.vo.JySysTenantLensVO;
import com.ruoyi.jysystem.vo.OrderAssignPriceVO; import com.ruoyi.jysystem.vo.OrderAssignPriceVO;
@@ -59,6 +60,10 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
private static final String ORDER_STATUS_EXCHANGE_AGREED = "13"; private static final String ORDER_STATUS_EXCHANGE_AGREED = "13";
private static final String ORDER_STATUS_EXCHANGE_LOGISTICS = "15";
private static final String ORDER_STATUS_EXCHANGE_COMPLETED = "17";
private static final String ORDER_STATUS_EXCHANGE_REJECTED = "21"; private static final String ORDER_STATUS_EXCHANGE_REJECTED = "21";
private static final String ORDER_STATUS_CANCELLED = "18"; private static final String ORDER_STATUS_CANCELLED = "18";
@@ -347,7 +352,7 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
throw new ServiceException("物流费用不能小于0"); throw new ServiceException("物流费用不能小于0");
} }
saveStatusLog(orderId, ORDER_STATUS_SHIPPED, operator, expressCompany, expressNo, null, logisticsFee); saveStatusLog(orderId, resolveShipStatus(order), operator, expressCompany, expressNo, null, logisticsFee);
BigDecimal totalLogisticsFee = jySysOrderStatusLogMapper.sumLogisticsFeeByOrderId(orderId); BigDecimal totalLogisticsFee = jySysOrderStatusLogMapper.sumLogisticsFeeByOrderId(orderId);
if (totalLogisticsFee == null) if (totalLogisticsFee == null)
{ {
@@ -362,7 +367,7 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
update.setAttachment(order.getAttachment()); update.setAttachment(order.getAttachment());
update.setLensFactoryTenantId(order.getLensFactoryTenantId()); update.setLensFactoryTenantId(order.getLensFactoryTenantId());
update.setLogisticsFee(totalLogisticsFee); update.setLogisticsFee(totalLogisticsFee);
update.setOrderStatus(ORDER_STATUS_SHIPPED); update.setOrderStatus(resolveShipStatus(order));
update.setUpdateBy(operator); update.setUpdateBy(operator);
update.setUpdateTime(DateUtils.getNowDate()); update.setUpdateTime(DateUtils.getNowDate());
@@ -383,12 +388,13 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
update.setAttachment(order.getAttachment()); update.setAttachment(order.getAttachment());
update.setLensFactoryTenantId(order.getLensFactoryTenantId()); update.setLensFactoryTenantId(order.getLensFactoryTenantId());
update.setLogisticsFee(order.getLogisticsFee()); update.setLogisticsFee(order.getLogisticsFee());
update.setOrderStatus(ORDER_STATUS_COMPLETED); String completeStatus = resolveCompleteStatus(order);
update.setOrderStatus(completeStatus);
update.setUpdateBy(operator); update.setUpdateBy(operator);
update.setUpdateTime(DateUtils.getNowDate()); update.setUpdateTime(DateUtils.getNowDate());
int rows = jySysOrderMapper.updateJySysOrder(update); int rows = jySysOrderMapper.updateJySysOrder(update);
saveStatusLog(orderId, ORDER_STATUS_COMPLETED, operator, null, null, null); saveStatusLog(orderId, completeStatus, operator, null, null, null);
return rows; return rows;
} }
@@ -406,12 +412,13 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
update.setAttachment(order.getAttachment()); update.setAttachment(order.getAttachment());
update.setLensFactoryTenantId(order.getLensFactoryTenantId()); update.setLensFactoryTenantId(order.getLensFactoryTenantId());
update.setLogisticsFee(order.getLogisticsFee()); update.setLogisticsFee(order.getLogisticsFee());
update.setOrderStatus(ORDER_STATUS_COMPLETED); String completeStatus = resolveCompleteStatus(order);
update.setOrderStatus(completeStatus);
update.setUpdateBy(operator); update.setUpdateBy(operator);
update.setUpdateTime(DateUtils.getNowDate()); update.setUpdateTime(DateUtils.getNowDate());
int rows = jySysOrderMapper.updateJySysOrder(update); int rows = jySysOrderMapper.updateJySysOrder(update);
saveStatusLog(orderId, ORDER_STATUS_COMPLETED, operator, null, null, null); saveStatusLog(orderId, completeStatus, operator, null, null, null);
return rows; return rows;
} }
@@ -440,29 +447,19 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int brandExchangeApplyOrder(Long orderId, Long brandTenantId, String reason, String operator) public JySysOrder brandExchangeApplyOrder(Long orderId, Long brandTenantId, String reason, String operator)
{ {
JySysOrder order = requireBrandExchangeAppliableOrder(orderId, brandTenantId); requireBrandExchangeAppliableOrder(orderId, brandTenantId);
if (StringUtils.isEmpty(reason)) if (StringUtils.isEmpty(reason))
{ {
throw new ServiceException("换货申请原因不能为空"); throw new ServiceException("换货申请原因不能为空");
} }
JySysOrder source = selectJySysOrderById(orderId);
JySysOrder update = new JySysOrder(); if (source == null)
update.setId(orderId); {
update.setBrandTenantId(order.getBrandTenantId()); throw new ServiceException("订单不存在");
update.setLensId(order.getLensId()); }
update.setProductId(order.getProductId()); return copyOrderForExchangeApply(source, reason, operator);
update.setAttachment(order.getAttachment());
update.setLensFactoryTenantId(order.getLensFactoryTenantId());
update.setLogisticsFee(order.getLogisticsFee());
update.setOrderStatus(ORDER_STATUS_EXCHANGE_APPLY);
update.setUpdateBy(operator);
update.setUpdateTime(DateUtils.getNowDate());
int rows = jySysOrderMapper.updateJySysOrder(update);
saveStatusLog(orderId, ORDER_STATUS_EXCHANGE_APPLY, operator, null, null, reason);
return rows;
} }
@Override @Override
@@ -557,11 +554,13 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
{ {
throw new ServiceException("订单不存在"); throw new ServiceException("订单不存在");
} }
if (!ORDER_STATUS_SHIPPED.equals(order.getOrderStatus())) if (!ORDER_STATUS_SHIPPED.equals(order.getOrderStatus())
&& !ORDER_STATUS_EXCHANGE_LOGISTICS.equals(order.getOrderStatus()))
{ {
throw new ServiceException("仅已发货状态的订单可完成"); throw new ServiceException("仅已发货或换货物流状态的订单可完成");
} }
String completeStatus = resolveCompleteStatus(order);
JySysOrder update = new JySysOrder(); JySysOrder update = new JySysOrder();
update.setId(orderId); update.setId(orderId);
update.setBrandTenantId(order.getBrandTenantId()); update.setBrandTenantId(order.getBrandTenantId());
@@ -570,12 +569,12 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
update.setAttachment(order.getAttachment()); update.setAttachment(order.getAttachment());
update.setLensFactoryTenantId(order.getLensFactoryTenantId()); update.setLensFactoryTenantId(order.getLensFactoryTenantId());
update.setLogisticsFee(order.getLogisticsFee()); update.setLogisticsFee(order.getLogisticsFee());
update.setOrderStatus(ORDER_STATUS_COMPLETED); update.setOrderStatus(completeStatus);
update.setUpdateBy(operator); update.setUpdateBy(operator);
update.setUpdateTime(DateUtils.getNowDate()); update.setUpdateTime(DateUtils.getNowDate());
int rows = jySysOrderMapper.updateJySysOrder(update); int rows = jySysOrderMapper.updateJySysOrder(update);
saveStatusLog(orderId, ORDER_STATUS_COMPLETED, operator, null, null, null); saveStatusLog(orderId, completeStatus, operator, null, null, null);
return rows; return rows;
} }
@@ -629,33 +628,6 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
return rows; return rows;
} }
@Override
@Transactional(rollbackFor = Exception.class)
public int adminCompleteExchangeRejectedOrder(Long orderId, String operator)
{
return completeExchangeRejectedToCompleted(orderId, operator);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int adminCompleteAllExchangeRejectedOrders(String operator)
{
JySysOrder query = new JySysOrder();
query.setOrderStatus(ORDER_STATUS_EXCHANGE_REJECTED);
List<JySysOrder> orderList = jySysOrderMapper.selectJySysOrderList(query);
if (orderList == null || orderList.isEmpty())
{
return 0;
}
int count = 0;
for (JySysOrder order : orderList)
{
completeExchangeRejectedToCompleted(order.getId(), operator);
count++;
}
return count;
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int brandResubmitOrder(Long orderId, Long brandTenantId, String reason, String operator) public int brandResubmitOrder(Long orderId, Long brandTenantId, String reason, String operator)
@@ -801,9 +773,10 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
private JySysOrder requireFactoryCompletableOrder(Long orderId, Long lensFactoryTenantId) private JySysOrder requireFactoryCompletableOrder(Long orderId, Long lensFactoryTenantId)
{ {
JySysOrder order = requireFactoryOwnedOrder(orderId, lensFactoryTenantId); JySysOrder order = requireFactoryOwnedOrder(orderId, lensFactoryTenantId);
if (!ORDER_STATUS_SHIPPED.equals(order.getOrderStatus())) if (!ORDER_STATUS_SHIPPED.equals(order.getOrderStatus())
&& !ORDER_STATUS_EXCHANGE_LOGISTICS.equals(order.getOrderStatus()))
{ {
throw new ServiceException("仅已发货状态的订单可完成"); throw new ServiceException("仅已发货或换货物流状态的订单可完成");
} }
return order; return order;
} }
@@ -827,9 +800,10 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
{ {
throw new ServiceException("无权操作该订单"); throw new ServiceException("无权操作该订单");
} }
if (!ORDER_STATUS_SHIPPED.equals(order.getOrderStatus())) if (!ORDER_STATUS_SHIPPED.equals(order.getOrderStatus())
&& !ORDER_STATUS_EXCHANGE_LOGISTICS.equals(order.getOrderStatus()))
{ {
throw new ServiceException("仅已发货状态的订单可完成"); throw new ServiceException("仅已发货或换货物流状态的订单可完成");
} }
return order; return order;
} }
@@ -880,46 +854,14 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
throw new ServiceException("无权操作该订单"); throw new ServiceException("无权操作该订单");
} }
if (!ORDER_STATUS_COMPLETED.equals(order.getOrderStatus()) if (!ORDER_STATUS_COMPLETED.equals(order.getOrderStatus())
&& !ORDER_STATUS_EXCHANGE_COMPLETED.equals(order.getOrderStatus())
&& !ORDER_STATUS_EXCHANGE_REJECTED.equals(order.getOrderStatus())) && !ORDER_STATUS_EXCHANGE_REJECTED.equals(order.getOrderStatus()))
{ {
throw new ServiceException("仅已完成或拒绝换货申请状态的订单可申请换货"); throw new ServiceException("仅已完成、换货完成或拒绝换货申请状态的订单可申请换货");
} }
return order; return order;
} }
private int completeExchangeRejectedToCompleted(Long orderId, String operator)
{
if (orderId == null)
{
throw new ServiceException("订单ID不能为空");
}
JySysOrder order = jySysOrderMapper.selectJySysOrderById(orderId);
if (order == null)
{
throw new ServiceException("订单不存在");
}
if (!ORDER_STATUS_EXCHANGE_REJECTED.equals(order.getOrderStatus()))
{
throw new ServiceException("仅拒绝换货申请状态的订单可完成");
}
JySysOrder update = new JySysOrder();
update.setId(orderId);
update.setBrandTenantId(order.getBrandTenantId());
update.setLensId(order.getLensId());
update.setProductId(order.getProductId());
update.setAttachment(order.getAttachment());
update.setLensFactoryTenantId(order.getLensFactoryTenantId());
update.setLogisticsFee(order.getLogisticsFee());
update.setOrderStatus(ORDER_STATUS_COMPLETED);
update.setUpdateBy(operator);
update.setUpdateTime(DateUtils.getNowDate());
int rows = jySysOrderMapper.updateJySysOrder(update);
saveStatusLog(orderId, ORDER_STATUS_COMPLETED, operator, null, null, null);
return rows;
}
private JySysOrder requireAdminExchangeAppliableOrder(Long orderId) private JySysOrder requireAdminExchangeAppliableOrder(Long orderId)
{ {
if (orderId == null) if (orderId == null)
@@ -1126,6 +1068,50 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
} }
} }
private String resolveShipStatus(JySysOrder order)
{
if (order != null && isExchangeOrder(order.getId()))
{
return ORDER_STATUS_EXCHANGE_LOGISTICS;
}
return ORDER_STATUS_SHIPPED;
}
private String resolveCompleteStatus(JySysOrder order)
{
if (order != null && ORDER_STATUS_EXCHANGE_LOGISTICS.equals(order.getOrderStatus()))
{
return ORDER_STATUS_EXCHANGE_COMPLETED;
}
return ORDER_STATUS_COMPLETED;
}
private boolean isExchangeOrder(Long orderId)
{
if (orderId == null)
{
return false;
}
JySysOrder order = jySysOrderMapper.selectJySysOrderById(orderId);
if (OrderExchangeHelper.isExchangeOrder(order))
{
return true;
}
List<JySysOrderStatusLog> logs = jySysOrderStatusLogMapper.selectJySysOrderStatusLogByOrderId(orderId);
if (logs == null || logs.isEmpty())
{
return false;
}
for (JySysOrderStatusLog statusLog : logs)
{
if (statusLog != null && ORDER_STATUS_EXCHANGE_APPLY.equals(statusLog.getOrderStatus()))
{
return true;
}
}
return false;
}
private void saveOptometryList(JySysOrder jySysOrder) private void saveOptometryList(JySysOrder jySysOrder)
{ {
if (jySysOrder.getId() == null) if (jySysOrder.getId() == null)
@@ -1200,12 +1186,6 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
statusLog.setExpressCompany(expressCompany); statusLog.setExpressCompany(expressCompany);
statusLog.setExpressNo(expressNo); statusLog.setExpressNo(expressNo);
statusLog.setLogisticsFee(logisticsFee); statusLog.setLogisticsFee(logisticsFee);
if (ORDER_STATUS_SHIPPED.equals(orderStatus)
&& StringUtils.isNotEmpty(expressNo))
{
statusLog.setSaleSettleStatus("0");
statusLog.setPurchaseSettleStatus("0");
}
statusLog.setRemark(remark); statusLog.setRemark(remark);
statusLog.setOperator(operator); statusLog.setOperator(operator);
statusLog.setOperateTime(DateUtils.getNowDate()); statusLog.setOperateTime(DateUtils.getNowDate());
@@ -1219,4 +1199,94 @@ public class JySysOrderServiceImpl extends ServiceImpl<JySysOrderMapper, JySysOr
int randomPart = ThreadLocalRandom.current().nextInt(1000, 10000); int randomPart = ThreadLocalRandom.current().nextInt(1000, 10000);
return "JY" + timePart + randomPart; return "JY" + timePart + randomPart;
} }
private JySysOrder copyOrderForExchangeApply(JySysOrder source, String reason, String operator)
{
Date now = DateUtils.getNowDate();
JySysOrder newOrder = new JySysOrder();
newOrder.setOrderNo(buildExchangeOrderNo(source.getOrderNo()));
newOrder.setBrandTenantId(source.getBrandTenantId());
newOrder.setLensFactoryTenantId(source.getLensFactoryTenantId());
newOrder.setProductId(source.getProductId());
newOrder.setLensId(source.getLensId());
newOrder.setPriceB(source.getPriceB());
newOrder.setPriceC(source.getPriceC());
newOrder.setLogisticsFee(BigDecimal.ZERO);
newOrder.setProgressiveMultifocal(source.getProgressiveMultifocal());
newOrder.setProgressiveMultifocalPrice(source.getProgressiveMultifocalPrice());
newOrder.setOrderStatus(ORDER_STATUS_EXCHANGE_APPLY);
newOrder.setPurchaseSettleStatus("0");
newOrder.setSaleSettleStatus("0");
newOrder.setReceiverName(source.getReceiverName());
newOrder.setReceiverPhone(source.getReceiverPhone());
newOrder.setReceiverAddress(source.getReceiverAddress());
newOrder.setAttachment(source.getAttachment());
newOrder.setRemark(source.getRemark());
newOrder.setDelFlag(0);
newOrder.setCreateBy(operator);
newOrder.setUpdateBy(operator);
newOrder.setCreateTime(now);
newOrder.setUpdateTime(now);
newOrder.setOptometryList(copyOptometryList(source.getOptometryList()));
fillDefaultValue(newOrder);
jySysOrderMapper.insertJySysOrder(newOrder);
saveOptometryList(newOrder);
saveStatusLog(newOrder.getId(), ORDER_STATUS_EXCHANGE_APPLY, operator, null, null, reason);
return selectJySysOrderById(newOrder.getId());
}
private List<JySysOrderOptometry> copyOptometryList(List<JySysOrderOptometry> sourceList)
{
List<JySysOrderOptometry> copiedList = new ArrayList<>();
if (sourceList == null || sourceList.isEmpty())
{
return copiedList;
}
for (JySysOrderOptometry source : sourceList)
{
if (source == null || StringUtils.isEmpty(source.getEyeSide()))
{
continue;
}
JySysOrderOptometry copy = new JySysOrderOptometry();
copy.setEyeSide(source.getEyeSide());
copy.setSphereS(source.getSphereS());
copy.setCylinderC(source.getCylinderC());
copy.setAxisA(source.getAxisA());
copy.setPd(source.getPd());
copy.setDualPd(source.getDualPd());
copy.setPdType(source.getPdType());
copy.setPupilHeight(source.getPupilHeight());
copy.setDistanceVa(source.getDistanceVa());
copy.setNearAdd(source.getNearAdd());
copiedList.add(copy);
}
return copiedList;
}
private String buildExchangeOrderNo(String sourceOrderNo)
{
String numericPart = StringUtils.isEmpty(sourceOrderNo)
? new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
+ ThreadLocalRandom.current().nextInt(1000, 10000)
: sourceOrderNo.replaceAll("\\D", "");
if (StringUtils.isEmpty(numericPart))
{
numericPart = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
+ ThreadLocalRandom.current().nextInt(1000, 10000);
}
String orderNo = "HH" + numericPart;
int retry = 0;
while (jySysOrderMapper.countByOrderNo(orderNo) > 0 && retry < 10)
{
orderNo = "HH" + numericPart + ThreadLocalRandom.current().nextInt(1000, 10000);
retry++;
}
if (jySysOrderMapper.countByOrderNo(orderNo) > 0)
{
throw new ServiceException("生成换货订单号失败,请稍后重试");
}
return orderNo;
}
} }

View File

@@ -2,6 +2,8 @@ package com.ruoyi.jysystem.service.impl;
import java.util.List; import java.util.List;
import javax.annotation.Resource; import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DateUtils;
@@ -15,6 +17,7 @@ import com.ruoyi.jysystem.mapper.JySysTenantMessageMapper;
import com.ruoyi.jysystem.service.IJySysTenantMessageService; import com.ruoyi.jysystem.service.IJySysTenantMessageService;
import com.ruoyi.jysystem.service.IJySysTenantService; import com.ruoyi.jysystem.service.IJySysTenantService;
import com.ruoyi.jysystem.service.SmsNotificationService; import com.ruoyi.jysystem.service.SmsNotificationService;
import com.ruoyi.jysystem.utils.OrderExchangeHelper;
import com.ruoyi.jysystem.utils.TenantTypeHelper; import com.ruoyi.jysystem.utils.TenantTypeHelper;
/** /**
@@ -23,12 +26,12 @@ import com.ruoyi.jysystem.utils.TenantTypeHelper;
@Service @Service
public class JySysTenantMessageServiceImpl implements IJySysTenantMessageService public class JySysTenantMessageServiceImpl implements IJySysTenantMessageService
{ {
private static final Logger log = LoggerFactory.getLogger(JySysTenantMessageServiceImpl.class);
private static final String ORDER_STATUS_DRAFT = "1"; private static final String ORDER_STATUS_DRAFT = "1";
private static final String ORDER_STATUS_ASSIGNED = "3"; private static final String ORDER_STATUS_ASSIGNED = "3";
private static final String ORDER_STATUS_SHIPPED = "8";
@Resource @Resource
private JySysTenantMessageMapper jySysTenantMessageMapper; private JySysTenantMessageMapper jySysTenantMessageMapper;
@@ -102,16 +105,18 @@ public class JySysTenantMessageServiceImpl implements IJySysTenantMessageService
} }
insertMessage(order.getBrandTenantId(), order.getId(), orderNo, insertMessage(order.getBrandTenantId(), order.getId(), orderNo,
JySysTenantMessage.TYPE_STATUS_CHANGE, "订单状态变更", content, orderStatus); JySysTenantMessage.TYPE_STATUS_CHANGE, "订单状态变更", content, orderStatus);
if (ORDER_STATUS_SHIPPED.equals(orderStatus)) if (shouldSendShipNotifySms(orderStatus, order))
{ {
String smsPhone = StringUtils.isNotEmpty(order.getReceiverPhone()) log.info("[发货短信] 触发检查: orderId={}, orderNo={}, orderStatus={}, exchange={}, expressCompany={}, brandTenantId={}, brandPhone={}, receiverPhone={}",
? order.getReceiverPhone() orderId, orderNo, orderStatus, OrderExchangeHelper.isExchangeOrder(order), expressCompany,
: brandTenant.getPhone(); brandTenant.getId(), brandTenant.getPhone(), order.getReceiverPhone());
if (StringUtils.isNotEmpty(smsPhone)) sendShipNotifySms(orderNo, expressCompany, brandTenant.getPhone(), order.getReceiverPhone());
{
smsNotificationService.trySendOrderShippedNotify(smsPhone, orderNo, expressCompany, expressNo);
} }
} }
else
{
log.warn("[发货短信] 跳过: 品牌方租户无效, orderId={}, brandTenantId={}, tenantType={}",
orderId, order.getBrandTenantId(), brandTenant != null ? brandTenant.getTenantType() : null);
} }
} }
@@ -170,4 +175,59 @@ public class JySysTenantMessageServiceImpl implements IJySysTenantMessageService
} }
return tenant; return tenant;
} }
private void sendShipNotifySms(String orderNo, String expressCompany, String brandPhone, String receiverPhone)
{
String smsPhone = resolveShipNotifyPhone(brandPhone, receiverPhone);
if (StringUtils.isNotEmpty(smsPhone))
{
log.info("[发货短信] 准备发送: orderNo={}, smsPhone={}, expressCompany={}", orderNo, smsPhone, expressCompany);
smsNotificationService.trySendOrderShippedNotify(smsPhone, orderNo, expressCompany);
return;
}
log.warn("[发货短信] 跳过: 无有效手机号, orderNo={}, brandPhone={}, receiverPhone={}",
orderNo, brandPhone, receiverPhone);
}
private boolean shouldSendShipNotifySms(String orderStatus, JySysOrder order)
{
return OrderExchangeHelper.isShipNotifyStatus(orderStatus);
}
private String resolveShipNotifyPhone(String brandPhone, String receiverPhone)
{
if (isValidMobile(brandPhone))
{
return normalizeMobile(brandPhone);
}
if (isValidMobile(receiverPhone))
{
return normalizeMobile(receiverPhone);
}
return null;
}
private boolean isValidMobile(String phone)
{
return normalizeMobile(phone) != null;
}
private String normalizeMobile(String phone)
{
if (StringUtils.isEmpty(phone))
{
return null;
}
String digits = phone.replaceAll("\\D", "");
if (digits.length() >= 11)
{
String mobile = digits.substring(digits.length() - 11);
if (mobile.matches("^1[3-9]\\d{9}$"))
{
return mobile;
}
}
String trimmed = phone.trim();
return trimmed.matches("^1[3-9]\\d{9}$") ? trimmed : null;
}
} }

View File

@@ -35,56 +35,67 @@ public class SmsNotificationServiceImpl implements SmsNotificationService
} }
@Override @Override
public void trySendOrderShippedNotify(String phone, String orderNo, String expressCompany, String expressNo) public void trySendOrderShippedNotify(String phone, String orderNo, String expressCompany)
{ {
Map<String, String> params = new HashMap<>(3); Map<String, String> params = new HashMap<>(2);
params.put("code", nullToEmpty(orderNo)); params.put("code", nullToEmpty(orderNo));
params.put("expressCompany", nullToEmpty(expressCompany)); params.put("expressCompany", nullToEmpty(expressCompany));
params.put("expressNo", nullToEmpty(expressNo));
trySendScene("配片订单发货", smsProperties.getShipOrder(), phone, params); trySendScene("配片订单发货", smsProperties.getShipOrder(), phone, params);
} }
private void trySendScene(String sceneLabel, SmsProperties.SceneConfig scene, String phone, Map<String, String> templateParams) private void trySendScene(String sceneLabel, SmsProperties.SceneConfig scene, String phone, Map<String, String> templateParams)
{ {
if (scene == null || !scene.isEnabled()) if (scene == null)
{ {
log.warn("[发货短信] 跳过: {}场景配置为空", sceneLabel);
return;
}
if (!scene.isEnabled())
{
log.warn("[发货短信] 跳过: {}短信未启用, enabled=false", sceneLabel);
return; return;
} }
if (StringUtils.isEmpty(phone)) if (StringUtils.isEmpty(phone))
{ {
log.warn("[发货短信] 跳过: {}手机号为空", sceneLabel);
return; return;
} }
String templateCode = StringUtils.isNotEmpty(scene.getTemplate()) ? scene.getTemplate().trim() : null; String templateCode = StringUtils.isNotEmpty(scene.getTemplate()) ? scene.getTemplate().trim() : null;
if (StringUtils.isEmpty(templateCode)) if (StringUtils.isEmpty(templateCode))
{ {
log.warn("未配置{}短信模板请在 application.yml 的 sms.{} 中填写 template", log.warn("[发货短信] 跳过: 未配置{}短信模板, 请在 application.yml 的 sms.{} 中填写 template",
sceneLabel, sceneLabel.contains("分配") ? "assign-order" : "ship-order"); sceneLabel, sceneLabel.contains("分配") ? "assign-order" : "ship-order");
return; return;
} }
String mobile = phone.trim(); String mobile = normalizeMobile(phone);
if (!MOBILE_PATTERN.matcher(mobile).matches()) if (mobile == null)
{ {
log.warn("{}短信手机号格式无效,跳过通知: {}", sceneLabel, phone); log.warn("[发货短信] 跳过: {}手机号格式无效, rawPhone={}", sceneLabel, phone);
return; return;
} }
String signName = smsProperties.getSignName();
String templateParam = JSON.toJSONString(templateParams);
log.info("[发货短信] 调用阿里云: scene={}, phone={}, signName={}, template={}, params={}",
sceneLabel, mobile, signName, templateCode, templateParam);
try try
{ {
String templateParam = JSON.toJSONString(templateParams); Map<String, Object> result = SmsUtils.sendNotifyMsg(mobile, signName, templateCode, templateParam);
Map<String, Object> result = SmsUtils.sendNotifyMsg(mobile, smsProperties.getSignName(),
templateCode, templateParam);
Integer statusCode = (Integer) result.get("statusCode"); Integer statusCode = (Integer) result.get("statusCode");
String bodyCode = (String) result.get("bodyCode"); String bodyCode = (String) result.get("bodyCode");
String bodyMessage = (String) result.get("bodyMessage");
String bizId = (String) result.get("bizId");
String requestId = (String) result.get("requestId");
if (statusCode == null || statusCode != 200 || !"OK".equalsIgnoreCase(bodyCode)) if (statusCode == null || statusCode != 200 || !"OK".equalsIgnoreCase(bodyCode))
{ {
log.warn("{}短信发送失败: phone={}, template={}, params={}, statusCode={}, bodyCode={}", log.warn("[发货短信] 发送失败: scene={}, phone={}, template={}, params={}, statusCode={}, bodyCode={}, bodyMessage={}, bizId={}, requestId={}",
sceneLabel, mobile, templateCode, templateParam, statusCode, bodyCode); sceneLabel, mobile, templateCode, templateParam, statusCode, bodyCode, bodyMessage, bizId, requestId);
return; return;
} }
log.info("{}短信已发送: {}", sceneLabel, mobile); log.info("[发货短信] 发送成功: scene={}, phone={}, bizId={}, requestId={}", sceneLabel, mobile, bizId, requestId);
} }
catch (Exception e) catch (Exception e)
{ {
log.warn("{}短信发送异常: phone={}, err={}", sceneLabel, mobile, e.getMessage()); log.warn("[发货短信] 发送异常: scene={}, phone={}, err={}", sceneLabel, mobile, e.getMessage(), e);
} }
} }
@@ -92,4 +103,22 @@ public class SmsNotificationServiceImpl implements SmsNotificationService
{ {
return value != null ? value : ""; return value != null ? value : "";
} }
private String normalizeMobile(String phone)
{
if (StringUtils.isEmpty(phone))
{
return null;
}
String digits = phone.replaceAll("\\D", "");
if (digits.length() >= 11)
{
String mobile = digits.substring(digits.length() - 11);
if (MOBILE_PATTERN.matcher(mobile).matches())
{
return mobile;
}
}
return MOBILE_PATTERN.matcher(phone.trim()).matches() ? phone.trim() : null;
}
} }

View File

@@ -0,0 +1,50 @@
package com.ruoyi.jysystem.utils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.jysystem.domain.JySysOrder;
/**
* 换货单识别(与前端 isExchangeOrder 一致)
*/
public final class OrderExchangeHelper
{
private static final String ORDER_STATUS_EXCHANGE_APPLY = "12";
private static final String ORDER_STATUS_EXCHANGE_AGREED = "13";
private static final String ORDER_STATUS_EXCHANGE_LOGISTICS = "15";
private static final String ORDER_STATUS_EXCHANGE_COMPLETED = "17";
private static final String ORDER_STATUS_SHIPPED = "8";
private OrderExchangeHelper()
{
}
public static boolean isExchangeOrder(JySysOrder order)
{
if (order == null)
{
return false;
}
if (StringUtils.isNotEmpty(order.getOrderNo()) && order.getOrderNo().startsWith("HH"))
{
return true;
}
String status = order.getOrderStatus();
return ORDER_STATUS_EXCHANGE_APPLY.equals(status)
|| ORDER_STATUS_EXCHANGE_AGREED.equals(status)
|| ORDER_STATUS_EXCHANGE_LOGISTICS.equals(status)
|| ORDER_STATUS_EXCHANGE_COMPLETED.equals(status);
}
/**
* 订单进入发货/换货物流状态时发送短信
*/
public static boolean isShipNotifyStatus(String orderStatus)
{
return ORDER_STATUS_SHIPPED.equals(orderStatus)
|| ORDER_STATUS_EXCHANGE_LOGISTICS.equals(orderStatus);
}
}

View File

@@ -22,6 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="logisticsFee" column="logistics_fee" /> <result property="logisticsFee" column="logistics_fee" />
<result property="progressiveMultifocal" column="progressive_multifocal" /> <result property="progressiveMultifocal" column="progressive_multifocal" />
<result property="progressiveMultifocalPrice" column="progressive_multifocal_price" /> <result property="progressiveMultifocalPrice" column="progressive_multifocal_price" />
<result property="presbyopia" column="presbyopia" />
<result property="orderStatus" column="order_status" /> <result property="orderStatus" column="order_status" />
<result property="purchaseSettleStatus" column="purchase_settle_status" /> <result property="purchaseSettleStatus" column="purchase_settle_status" />
<result property="saleSettleStatus" column="sale_settle_status" /> <result property="saleSettleStatus" column="sale_settle_status" />
@@ -42,7 +43,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
o.lens_factory_tenant_id, lt.name as lens_factory_tenant_name, o.lens_factory_tenant_id, lt.name as lens_factory_tenant_name,
o.product_id, p.product_model, p.product_name, o.product_id, p.product_model, p.product_name,
o.lens_id, l.product_name as lens_product_name, l.material as lens_material, o.lens_id, l.product_name as lens_product_name, l.material as lens_material,
o.price_b, o.price_c, o.logistics_fee, o.progressive_multifocal, o.progressive_multifocal_price, o.order_status, o.purchase_settle_status, o.sale_settle_status, o.price_b, o.price_c, o.logistics_fee, o.progressive_multifocal, o.progressive_multifocal_price, o.presbyopia, o.order_status, o.purchase_settle_status, o.sale_settle_status,
o.receiver_name, o.receiver_phone, o.receiver_address, o.attachment, o.receiver_name, o.receiver_phone, o.receiver_address, o.attachment,
o.remark, o.del_flag, o.create_by, o.create_time, o.update_by, o.update_time o.remark, o.del_flag, o.create_by, o.create_time, o.update_by, o.update_time
from jy_sys_order o from jy_sys_order o
@@ -98,6 +99,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="logisticsFee != null">logistics_fee,</if> <if test="logisticsFee != null">logistics_fee,</if>
<if test="progressiveMultifocal != null">progressive_multifocal,</if> <if test="progressiveMultifocal != null">progressive_multifocal,</if>
<if test="progressiveMultifocalPrice != null">progressive_multifocal_price,</if> <if test="progressiveMultifocalPrice != null">progressive_multifocal_price,</if>
<if test="presbyopia != null">presbyopia,</if>
<if test="orderStatus != null">order_status,</if> <if test="orderStatus != null">order_status,</if>
<if test="purchaseSettleStatus != null">purchase_settle_status,</if> <if test="purchaseSettleStatus != null">purchase_settle_status,</if>
<if test="saleSettleStatus != null">sale_settle_status,</if> <if test="saleSettleStatus != null">sale_settle_status,</if>
@@ -123,6 +125,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="logisticsFee != null">#{logisticsFee},</if> <if test="logisticsFee != null">#{logisticsFee},</if>
<if test="progressiveMultifocal != null">#{progressiveMultifocal},</if> <if test="progressiveMultifocal != null">#{progressiveMultifocal},</if>
<if test="progressiveMultifocalPrice != null">#{progressiveMultifocalPrice},</if> <if test="progressiveMultifocalPrice != null">#{progressiveMultifocalPrice},</if>
<if test="presbyopia != null">#{presbyopia},</if>
<if test="orderStatus != null">#{orderStatus},</if> <if test="orderStatus != null">#{orderStatus},</if>
<if test="purchaseSettleStatus != null">#{purchaseSettleStatus},</if> <if test="purchaseSettleStatus != null">#{purchaseSettleStatus},</if>
<if test="saleSettleStatus != null">#{saleSettleStatus},</if> <if test="saleSettleStatus != null">#{saleSettleStatus},</if>
@@ -152,6 +155,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="logisticsFee != null">logistics_fee = #{logisticsFee},</if> <if test="logisticsFee != null">logistics_fee = #{logisticsFee},</if>
<if test="progressiveMultifocal != null">progressive_multifocal = #{progressiveMultifocal},</if> <if test="progressiveMultifocal != null">progressive_multifocal = #{progressiveMultifocal},</if>
<if test="progressiveMultifocalPrice != null">progressive_multifocal_price = #{progressiveMultifocalPrice},</if> <if test="progressiveMultifocalPrice != null">progressive_multifocal_price = #{progressiveMultifocalPrice},</if>
<if test="presbyopia != null">presbyopia = #{presbyopia},</if>
<if test="orderStatus != null">order_status = #{orderStatus},</if> <if test="orderStatus != null">order_status = #{orderStatus},</if>
<if test="purchaseSettleStatus != null">purchase_settle_status = #{purchaseSettleStatus},</if> <if test="purchaseSettleStatus != null">purchase_settle_status = #{purchaseSettleStatus},</if>
<if test="saleSettleStatus != null">sale_settle_status = #{saleSettleStatus},</if> <if test="saleSettleStatus != null">sale_settle_status = #{saleSettleStatus},</if>
@@ -185,11 +189,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and (brand_tenant_id = #{tenantId} or lens_factory_tenant_id = #{tenantId}) and (brand_tenant_id = #{tenantId} or lens_factory_tenant_id = #{tenantId})
</select> </select>
<select id="countByOrderNo" resultType="int">
select count(1)
from jy_sys_order
where del_flag = '0'
and order_no = #{orderNo}
</select>
<select id="selectCompletedOrdersByYearMonth" resultMap="JySysOrderResult"> <select id="selectCompletedOrdersByYearMonth" resultMap="JySysOrderResult">
select o.id, o.order_no, o.brand_tenant_id, bt.name as brand_tenant_name, select o.id, o.order_no, o.brand_tenant_id, bt.name as brand_tenant_name,
o.lens_factory_tenant_id, lf.name as lens_factory_tenant_name, o.lens_factory_tenant_id, lf.name as lens_factory_tenant_name,
o.product_id, o.lens_id, o.price_b, o.price_c, o.logistics_fee, o.product_id, o.lens_id, o.price_b, o.price_c, o.logistics_fee,
o.progressive_multifocal, o.progressive_multifocal_price, o.order_status, o.progressive_multifocal, o.progressive_multifocal_price, o.presbyopia, o.order_status,
o.purchase_settle_status, o.sale_settle_status, o.purchase_settle_status, o.sale_settle_status,
o.receiver_name, o.receiver_phone, o.receiver_address, o.attachment, o.remark, o.receiver_name, o.receiver_phone, o.receiver_address, o.attachment, o.remark,
o.del_flag, o.create_by, o.create_time, o.update_by, o.update_time o.del_flag, o.create_by, o.create_time, o.update_by, o.update_time
@@ -197,11 +208,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
left join jy_sys_tenant bt on o.brand_tenant_id = bt.id left join jy_sys_tenant bt on o.brand_tenant_id = bt.id
left join jy_sys_tenant lf on o.lens_factory_tenant_id = lf.id left join jy_sys_tenant lf on o.lens_factory_tenant_id = lf.id
where o.del_flag = '0' where o.del_flag = '0'
and o.order_status = '9' and o.order_status in ('9', '17')
and exists ( and exists (
select 1 from jy_sys_order_status_log sl select 1 from jy_sys_order_status_log sl
where sl.order_id = o.id where sl.order_id = o.id
and sl.order_status = '9' and sl.order_status in ('9', '17')
and DATE_FORMAT(sl.operate_time, '%Y-%m') = #{settleYearMonth} and DATE_FORMAT(sl.operate_time, '%Y-%m') = #{settleYearMonth}
) )
order by o.id asc order by o.id asc

View File

@@ -11,8 +11,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="expressCompany" column="express_company" /> <result property="expressCompany" column="express_company" />
<result property="expressNo" column="express_no" /> <result property="expressNo" column="express_no" />
<result property="logisticsFee" column="logistics_fee" /> <result property="logisticsFee" column="logistics_fee" />
<result property="saleSettleStatus" column="sale_settle_status" />
<result property="purchaseSettleStatus" column="purchase_settle_status" />
<result property="remark" column="remark" /> <result property="remark" column="remark" />
<result property="operator" column="operator" /> <result property="operator" column="operator" />
<result property="operateTime" column="operate_time" /> <result property="operateTime" column="operate_time" />
@@ -20,7 +18,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectJySysOrderStatusLogByOrderId" parameterType="Long" resultMap="JySysOrderStatusLogResult"> <select id="selectJySysOrderStatusLogByOrderId" parameterType="Long" resultMap="JySysOrderStatusLogResult">
select id, order_id, order_status, express_company, express_no, logistics_fee, select id, order_id, order_status, express_company, express_no, logistics_fee,
sale_settle_status, purchase_settle_status, remark, operator, operate_time remark, operator, operate_time
from jy_sys_order_status_log from jy_sys_order_status_log
where order_id = #{orderId} where order_id = #{orderId}
order by operate_time desc, id desc order by operate_time desc, id desc
@@ -43,8 +41,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="expressCompany != null">express_company,</if> <if test="expressCompany != null">express_company,</if>
<if test="expressNo != null">express_no,</if> <if test="expressNo != null">express_no,</if>
<if test="logisticsFee != null">logistics_fee,</if> <if test="logisticsFee != null">logistics_fee,</if>
<if test="saleSettleStatus != null">sale_settle_status,</if>
<if test="purchaseSettleStatus != null">purchase_settle_status,</if>
<if test="remark != null">remark,</if> <if test="remark != null">remark,</if>
<if test="operator != null">operator,</if> <if test="operator != null">operator,</if>
<if test="operateTime != null">operate_time,</if> <if test="operateTime != null">operate_time,</if>
@@ -55,8 +51,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="expressCompany != null">#{expressCompany},</if> <if test="expressCompany != null">#{expressCompany},</if>
<if test="expressNo != null">#{expressNo},</if> <if test="expressNo != null">#{expressNo},</if>
<if test="logisticsFee != null">#{logisticsFee},</if> <if test="logisticsFee != null">#{logisticsFee},</if>
<if test="saleSettleStatus != null">#{saleSettleStatus},</if>
<if test="purchaseSettleStatus != null">#{purchaseSettleStatus},</if>
<if test="remark != null">#{remark},</if> <if test="remark != null">#{remark},</if>
<if test="operator != null">#{operator},</if> <if test="operator != null">#{operator},</if>
<if test="operateTime != null">#{operateTime},</if> <if test="operateTime != null">#{operateTime},</if>
@@ -64,52 +58,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</insert> </insert>
<select id="selectUnsettledShipLogsByOrderIdAndType" resultMap="JySysOrderStatusLogResult"> <select id="selectUnsettledShipLogsByOrderIdAndType" resultMap="JySysOrderStatusLogResult">
select id, order_id, order_status, express_company, express_no, logistics_fee, select l.id, l.order_id, l.order_status, l.express_company, l.express_no, l.logistics_fee,
sale_settle_status, purchase_settle_status, remark, operator, operate_time l.remark, l.operator, l.operate_time
from jy_sys_order_status_log from jy_sys_order_status_log l
where order_id = #{orderId} where l.order_id = #{orderId}
and order_status = '8' and l.order_status in ('8', '15')
and express_no is not null and l.express_no is not null
and TRIM(express_no) != '' and TRIM(l.express_no) != ''
<choose> and l.id not in (
<when test="settlementType == 'AR'"> select fl.status_log_id
and (sale_settle_status is null or sale_settle_status = '0') from jy_sys_fee_settlement_logistics fl
</when> inner join jy_sys_fee_settlement_item fi on fl.settlement_item_id = fi.id
<when test="settlementType == 'AP'"> inner join jy_sys_fee_settlement fs on fi.settlement_id = fs.id
and (purchase_settle_status is null or purchase_settle_status = '0') where fl.status_log_id is not null
</when> and fs.settlement_type = #{settlementType}
</choose> )
order by operate_time asc, id asc order by l.operate_time asc, l.id asc
</select> </select>
<update id="updateSaleSettleStatusByIds">
update jy_sys_order_status_log
set sale_settle_status = #{settleStatus}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<update id="updatePurchaseSettleStatusByIds">
update jy_sys_order_status_log
set purchase_settle_status = #{settleStatus}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<update id="resetShipLogSettleStatusByOrderIds">
update jy_sys_order_status_log
set sale_settle_status = '0',
purchase_settle_status = '0'
where order_id in
<foreach collection="orderIds" item="orderId" open="(" separator="," close=")">
#{orderId}
</foreach>
and order_status = '8'
and express_no is not null
and TRIM(express_no) != ''
</update>
</mapper> </mapper>

View File

@@ -16,6 +16,7 @@ CREATE TABLE `jy_sys_order` (
`logistics_fee` decimal(10, 2) NULL DEFAULT NULL COMMENT '物流费用', `logistics_fee` decimal(10, 2) NULL DEFAULT NULL COMMENT '物流费用',
`progressive_multifocal` tinyint(4) NULL DEFAULT 0 COMMENT '是否渐进多焦点0否 1是', `progressive_multifocal` tinyint(4) NULL DEFAULT 0 COMMENT '是否渐进多焦点0否 1是',
`progressive_multifocal_price` decimal(10, 2) NULL DEFAULT 0 COMMENT '渐进多焦点价格', `progressive_multifocal_price` decimal(10, 2) NULL DEFAULT 0 COMMENT '渐进多焦点价格',
`presbyopia` tinyint(4) NULL DEFAULT 0 COMMENT '是否老花0否 1是',
`order_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '订单状态(字典 jy_order_status', `order_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '订单状态(字典 jy_order_status',
`purchase_settle_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '采购结算状态0未结算 1已结算', `purchase_settle_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '采购结算状态0未结算 1已结算',
`sale_settle_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '销售结算状态0未结算 1已结算', `sale_settle_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '销售结算状态0未结算 1已结算',

View File

@@ -0,0 +1,3 @@
-- 订单表增加:是否老花
ALTER TABLE `jy_sys_order`
ADD COLUMN `presbyopia` tinyint(4) NULL DEFAULT 0 COMMENT '是否老花0否 1是' AFTER `progressive_multifocal_price`;

View File

@@ -51,7 +51,7 @@ INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value
SELECT 166, 14, '退货物流', '14', 'jy_order_status', NULL, 'primary', 'N', '0', 'admin', NOW(), NULL SELECT 166, 14, '退货物流', '14', 'jy_order_status', NULL, 'primary', 'N', '0', 'admin', NOW(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'jy_order_status' AND `dict_value` = '14'); FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'jy_order_status' AND `dict_value` = '14');
INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`) INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
SELECT 167, 15, '换货已发货', '15', 'jy_order_status', NULL, 'primary', 'N', '0', 'admin', NOW(), NULL SELECT 167, 15, '换货物流', '15', 'jy_order_status', NULL, 'primary', 'N', '0', 'admin', NOW(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'jy_order_status' AND `dict_value` = '15'); FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'jy_order_status' AND `dict_value` = '15');
INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`) INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
SELECT 168, 16, '退货完成', '16', 'jy_order_status', NULL, 'success', 'N', '0', 'admin', NOW(), NULL SELECT 168, 16, '退货完成', '16', 'jy_order_status', NULL, 'success', 'N', '0', 'admin', NOW(), NULL

View File

@@ -0,0 +1,16 @@
-- 订单状态字典:恢复/确保换货物流、换货完成标签
UPDATE `sys_dict_data`
SET `dict_label` = '换货物流'
WHERE `dict_type` = 'jy_order_status' AND `dict_value` = '15';
UPDATE `sys_dict_data`
SET `dict_label` = '换货完成'
WHERE `dict_type` = 'jy_order_status' AND `dict_value` = '17';
INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
SELECT 167, 15, '换货物流', '15', 'jy_order_status', NULL, 'primary', 'N', '0', 'admin', NOW(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'jy_order_status' AND `dict_value` = '15');
INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
SELECT 169, 17, '换货完成', '17', 'jy_order_status', NULL, 'success', 'N', '0', 'admin', NOW(), NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'jy_order_status' AND `dict_value` = '17');

View File

@@ -0,0 +1,4 @@
-- 订单状态日志:移除物流应收/应付结算状态字段(改由费用结算单物流明细关联发货日志)
ALTER TABLE `jy_sys_order_status_log`
DROP COLUMN `sale_settle_status`,
DROP COLUMN `purchase_settle_status`;

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "OpticalSystem",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -3,16 +3,16 @@ package com.ruoyi.common.utils;
import com.aliyun.dysmsapi20170525.Client; import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest; import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse; import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.tea.*;
import com.aliyun.teaopenapi.models.Config; import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions; import com.aliyun.teautil.models.RuntimeOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import static com.aliyun.teautil.Common.assertAsString;
public class SmsUtils { public class SmsUtils {
private static final Logger log = LoggerFactory.getLogger(SmsUtils.class);
public static Client createClient() throws Exception { public static Client createClient() throws Exception {
Config config = new Config() Config config = new Config()
// 配置 AccessKey ID请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。 // 配置 AccessKey ID请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
@@ -43,6 +43,8 @@ public class SmsUtils {
*/ */
public static Map<String, Object> sendNotifyMsg(String phone, String signName, String templateCode, String templateParam) throws Exception { public static Map<String, Object> sendNotifyMsg(String phone, String signName, String templateCode, String templateParam) throws Exception {
HashMap<String, Object> sendSmsResponseMap = new HashMap<>(); HashMap<String, Object> sendSmsResponseMap = new HashMap<>();
log.info("[发货短信] SmsUtils 请求: phone={}, signName={}, templateCode={}, templateParam={}",
phone, signName, templateCode, templateParam);
try { try {
Client client = SmsUtils.createClient(); Client client = SmsUtils.createClient();
SendSmsRequest sendSmsRequest = new SendSmsRequest() SendSmsRequest sendSmsRequest = new SendSmsRequest()
@@ -54,19 +56,26 @@ public class SmsUtils {
SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, new RuntimeOptions()); SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, new RuntimeOptions());
Integer statusCode = sendSmsResponse.getStatusCode(); Integer statusCode = sendSmsResponse.getStatusCode();
String responseCode = sendSmsResponse.getBody().getCode(); String responseCode = sendSmsResponse.getBody() != null ? sendSmsResponse.getBody().getCode() : null;
String responseMessage = sendSmsResponse.getBody() != null ? sendSmsResponse.getBody().getMessage() : null;
String bizId = sendSmsResponse.getBody() != null ? sendSmsResponse.getBody().getBizId() : null;
String requestId = sendSmsResponse.getBody() != null ? sendSmsResponse.getBody().getRequestId() : null;
sendSmsResponseMap.put("statusCode", statusCode); sendSmsResponseMap.put("statusCode", statusCode);
sendSmsResponseMap.put("bodyCode", responseCode); sendSmsResponseMap.put("bodyCode", responseCode);
} catch (TeaException error) { sendSmsResponseMap.put("bodyMessage", responseMessage);
System.out.println(error.getMessage()); sendSmsResponseMap.put("bizId", bizId);
System.out.println(error.getData().get("Recommend")); sendSmsResponseMap.put("requestId", requestId);
assertAsString(error.message); log.info("[发货短信] SmsUtils 响应: phone={}, statusCode={}, bodyCode={}, bodyMessage={}, bizId={}, requestId={}",
phone, statusCode, responseCode, responseMessage, bizId, requestId);
} catch (com.aliyun.tea.TeaException error) {
log.warn("[发货短信] SmsUtils TeaException: phone={}, code={}, message={}",
phone, error.getCode(), error.getMessage());
sendSmsResponseMap.put("bodyCode", error.getCode());
sendSmsResponseMap.put("bodyMessage", error.getMessage());
} catch (Exception _error) { } catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error); log.warn("[发货短信] SmsUtils 异常: phone={}, err={}", phone, _error.getMessage(), _error);
System.out.println(error.getMessage()); sendSmsResponseMap.put("bodyMessage", _error.getMessage());
System.out.println("诊断地址" + error.getData().get("Recommend"));
assertAsString(error.message);
} }
return sendSmsResponseMap; return sendSmsResponseMap;
} }

2
uniapp/.env.development Normal file
View File

@@ -0,0 +1,2 @@
# H5 开发环境(走 vite 代理)
VITE_APP_BASE_API=/dev-api

3
uniapp/.env.production Normal file
View File

@@ -0,0 +1,3 @@
# App 打包:改为手机能访问的后端地址(不要用 localhost
# 示例:局域网 IP 或正式域名
VITE_APP_BASE_API=http://8.133.20.230:2092/prod-api

6
uniapp/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
dist/
unpackage/
.hbuilderx/
.DS_Store
*.log

86
uniapp/README.md Normal file
View File

@@ -0,0 +1,86 @@
# 功能镜片配镜系统 - UniApp 移动端
基于 `developer-front` 门户端的 UniApp 实现,覆盖认证、配片单/制片单、消息通知、账户中心等核心业务。
## 项目结构
```
uniapp/
├── src/
│ ├── api/ # 接口封装
│ ├── components/ # NavBar、DictTag
│ ├── config/ # API 配置
│ ├── pages/ # 页面
│ ├── store/ # Pinia 状态
│ ├── utils/ # 工具函数
│ ├── App.vue
│ ├── main.js
│ ├── pages.json
│ └── manifest.json
├── index.html
├── vite.config.js
└── package.json
```
## 快速开始
```bash
cd uniapp
npm install
npm run dev:h5
```
默认端口 `2093`API 代理至 `http://localhost:8082/dev-api`
## 与 developer-front 的对应关系
| Web 端 | UniApp |
|--------|--------|
| Cookie Developer-Token | uni.setStorageSync |
| axios + Element UI | uni.request + 原生组件 |
| vue-router | pages.json |
| Vuex | Pinia |
| NavBar.vue | components/NavBar |
| lens-order.vue | pages/order/lens-order |
| factory-order.vue | pages/order/factory-order |
## Android 编译器/SDK 版本不匹配
若 APK 提示「HBuilderX 4.15 编译SDK 5.07 不匹配」:
- **原因:** CLI 项目编译器在 `package.json`,不随 HBuilderX 升级;云打包 SDK 已是 5.x。
- **处理:** HBuilderX 升级到 **5.07** → 执行 `npm run update:uni``npm install` → 删除 `unpackage` → 重新云打包。
- **验证后可选:** `manifest.json``app-plus.compatible.ignoreVersion` 设为 `true` 关闭弹窗。
## APK 卡在 Logo 启动页
1. 已在 `App.vue` 中调用 `plus.navigator.closeSplashscreen()` 手动关闭启动页
2. CLI 构建产物里 `autoclose` 可能为 `false`,需重新云打包
3. **adb 包名过滤错误**AppID 为 `__UNI__C17C074`,应使用:
```powershell
adb logcat -c
adb logcat | findstr /i "UNIC17C074 DCloud console uni-app JS"
adb logcat *:E | findstr /i "uni dcloud"
```
4. 查看已安装包名:`adb shell pm list packages | findstr uni`
## AppID 配置(打包 Android/iOS 必做)
CLI 创建的项目不会自动生成 AppID需在 **HBuilderX** 中获取并写入 `src/manifest.json`
1. 安装 [HBuilderX](https://www.dcloud.io/hbuilderx.html),使用 DCloud 账号登录
2. **文件 → 打开目录**,选择本项目的 `uniapp` 文件夹
3. 打开 `src/manifest.json`,切换到 **「基础配置」** 可视化界面
4. 点击 **「应用标识(AppID)」** 右侧的 **「重新获取」**
5. 保存后,`appid` 会自动写入(格式类似 `__UNI__1A2B3C4`
也可在 [DCloud 开发者中心](https://dev.dcloud.net.cn) 手动创建应用,将生成的 AppID 粘贴到 `manifest.json` 的 `"appid"` 字段。
> AppID 与 DCloud 账号绑定,请勿使用他人 AppID 或随意编造(如 `__UNI__OPTICAL_SYSTEM`)。
## 待完善项
- 收货地址 CRUD
- 商城模块
- 配片单完整验光表单与附件上传
- 小程序/App 支付

17
uniapp/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title>功能镜片配镜系统</title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:usesCleartextTraffic="true" />
</manifest>

12590
uniapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
uniapp/package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "optical-system-uniapp",
"version": "1.0.0",
"description": "觉远创智功能镜片配镜系统 - UniApp 移动端",
"scripts": {
"dev:h5": "uni",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:app": "uni -p app",
"build:h5": "uni build",
"build:mp-weixin": "uni build -p mp-weixin",
"build:app": "uni build -p app && node scripts/patch-app-manifest.js && node scripts/patch-app-config.js",
"update:uni": "npx @dcloudio/uvm@latest"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-5000720260410001",
"@dcloudio/uni-app-harmony": "3.0.0-5000720260410001",
"@dcloudio/uni-app-plus": "3.0.0-5000720260410001",
"@dcloudio/uni-components": "3.0.0-5000720260410001",
"@dcloudio/uni-h5": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-alipay": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-baidu": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-harmony": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-jd": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-lark": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-qq": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-toutiao": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
"@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
"pinia": "^2.1.7",
"vue": "3.5.39",
"vue-i18n": "9.14.4"
},
"devDependencies": {
"@dcloudio/types": "3.4.28",
"@dcloudio/uni-automator": "3.0.0-5000720260410001",
"@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
"@dcloudio/uni-stacktracey": "3.0.0-5000720260410001",
"@dcloudio/vite-plugin-uni": "3.0.0-5000720260410001",
"@vue/runtime-core": "3.5.39",
"sass": "^1.77.0",
"vite": "5.2.8"
}
}

View File

@@ -0,0 +1,11 @@
# 用法:先启动 App再运行此脚本
adb devices
Write-Host "`n=== 已安装的 uni 包 ==="
adb shell pm list packages | findstr /i uni
Write-Host "`n=== 清空日志,请手动点开 App ==="
adb logcat -c
Start-Sleep -Seconds 2
Write-Host "`n=== 抓取 DCloud / 崩溃 / WebView 日志30秒==="
adb logcat -v time -t 800 | findstr /i "dcloud uni jyuan UNIOPTICAL NIOPTICAL AndroidRuntime FATAL chromium bootstrap splash WebView loadUrl"

View File

@@ -0,0 +1,26 @@
/**
* 强制 App 使用 webview 渲染,避免 renderer:auto 在部分设备上卡启动页。
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '..')
const targets = [
path.join(root, 'dist/build/app/app-config-service.js'),
path.join(root, 'dist/build/app-plus/app-config-service.js')
]
for (const file of targets) {
if (!fs.existsSync(file)) continue
let content = fs.readFileSync(file, 'utf8')
const next = content
.replace(/"renderer":"auto"/g, '"renderer":"webview"')
.replace(/"waiting":true/g, '"waiting":false')
if (next !== content) {
fs.writeFileSync(file, next, 'utf8')
console.log('[patch-app-config] patched', path.relative(root, file))
}
}

View File

@@ -0,0 +1,40 @@
/**
* CLI 构建 app manifest 中 splashscreen.autoclose 恒为 false打包后补丁修正。
* HBuilderX 云打包会读取 dist/build/app 或 app-plus 下的 manifest.json。
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '..')
const targets = [
path.join(root, 'dist/build/app/manifest.json'),
path.join(root, 'dist/build/app-plus/manifest.json')
]
for (const file of targets) {
if (!fs.existsSync(file)) continue
const manifest = JSON.parse(fs.readFileSync(file, 'utf8'))
if (manifest.plus) {
manifest.plus.splashscreen = {
...(manifest.plus.splashscreen || {}),
autoclose: true,
waiting: false,
delay: 0
}
manifest.plus.renderer = 'webview'
manifest.plus.compatible = { ignoreVersion: true }
manifest.plus.distribute = manifest.plus.distribute || {}
manifest.plus.distribute.google = {
...(manifest.plus.distribute.google || {}),
packagename: 'com.jyuan.optical'
}
if (manifest.plus['uni-app']) {
manifest.plus['uni-app'].renderer = 'webview'
}
}
fs.writeFileSync(file, JSON.stringify(manifest, null, 2), 'utf8')
console.log('[patch-app-manifest] patched', path.relative(root, file))
}

View File

@@ -0,0 +1,33 @@
# 用法:.\scripts\verify-apk.ps1 "D:\path\to\app.apk"
param(
[Parameter(Mandatory = $true)]
[string]$ApkPath
)
if (-not (Test-Path $ApkPath)) {
Write-Error "APK 不存在: $ApkPath"
exit 1
}
$sdk = $env:ANDROID_HOME
if (-not $sdk) { $sdk = "$env:LOCALAPPDATA\Android\Sdk" }
$aapt = Get-ChildItem -Path "$sdk\build-tools" -Recurse -Filter "aapt.exe" -ErrorAction SilentlyContinue |
Sort-Object FullName -Descending |
Select-Object -First 1
Write-Host "=== APK 文件 ==="
Write-Host $ApkPath
Write-Host ""
if ($aapt) {
Write-Host "=== 包名aapt dump badging==="
& $aapt.FullName dump badging $ApkPath | Select-String "package:|launchable-activity"
} else {
Write-Host "未找到 aapt.exeAndroid SDK build-tools"
Write-Host "请在 HBuilderX 中打开 src/manifest.json -> 基础配置 -> Android包名确认是否为 com.jyuan.optical"
}
Write-Host ""
Write-Host "=== 手机已安装的相关包 ==="
adb shell pm list packages | findstr /i "uni jyuan optical"

179
uniapp/src/App.vue Normal file
View File

@@ -0,0 +1,179 @@
<script>
import { closeSplashscreen } from '@/utils/splash.js'
export default {
onLaunch() {
closeSplashscreen()
},
onShow() {
closeSplashscreen()
},
onHide() {
},
onError(err) {
console.error('[App] onError', err)
closeSplashscreen()
}
}
</script>
<style lang="scss">
page {
background-color: #f5f7fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 28rpx;
color: #333;
}
.container {
padding: 24rpx;
}
.btn-primary {
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
color: #fff;
border: none;
border-radius: 12rpx;
font-size: 30rpx;
height: 88rpx;
line-height: 88rpx;
}
.btn-primary[disabled] {
opacity: 0.6;
}
.card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.text-muted {
color: #94a3b8;
font-size: 24rpx;
}
.text-primary {
color: #2563eb;
}
.text-danger {
color: #ef4444;
}
.text-success {
color: #22c55e;
}
.flex-row {
display: flex;
flex-direction: row;
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.empty-tip {
text-align: center;
padding: 80rpx 0;
color: #94a3b8;
font-size: 28rpx;
}
</style>

9
uniapp/src/api/dict.js Normal file
View File

@@ -0,0 +1,9 @@
import request from '@/utils/request.js'
export function getDicts(dictType) {
return request({ url: '/system/dict/data/type/' + dictType, method: 'GET' })
}
export function listLensOptions() {
return request({ url: '/jysystem/lens/options', method: 'GET' })
}

64
uniapp/src/api/login.js Normal file
View File

@@ -0,0 +1,64 @@
import request from '@/utils/request.js'
export function login(username, password, code, uuid, directLogin) {
return request({
url: '/login',
method: 'POST',
isToken: false,
data: { username, password, code, uuid, directLogin }
})
}
export function register(data) {
return request({ url: '/register', method: 'POST', isToken: false, data })
}
export function registerByCode(data) {
return request({ url: '/register/code', method: 'POST', isToken: false, data })
}
export function getInfo() {
return request({ url: '/getInfo', method: 'GET' })
}
export function logout() {
return request({ url: '/logout', method: 'POST' })
}
export function getCodeImg() {
return request({ url: '/captchaImage', method: 'GET', isToken: false, timeout: 20000 })
}
export function sendSmsCode(mobile) {
return request({ url: '/sms/code', method: 'POST', isToken: false, data: { mobile } })
}
export function sendEmailCode(email) {
return request({ url: '/email/code', method: 'POST', isToken: false, data: { email } })
}
export function emailLogin(email, emailCode, uuid, directLogin) {
return request({
url: '/email/login',
method: 'POST',
isToken: false,
data: { email, emailCode, uuid, directLogin }
})
}
export function smsLogin(mobile, smsCode, uuid, directLogin) {
return request({
url: '/sms/login',
method: 'POST',
isToken: false,
data: { mobile, smsCode, uuid, directLogin }
})
}
export function resetPasswordByAccount(data) {
return request({ url: '/resetPassword/account', method: 'POST', isToken: false, data })
}
export function resetPasswordByCode(data) {
return request({ url: '/resetPassword/code', method: 'POST', isToken: false, data })
}

17
uniapp/src/api/message.js Normal file
View File

@@ -0,0 +1,17 @@
import request from '@/utils/request.js'
export function listPortalMessage(query) {
return request({ url: '/jysystem/message/portal/list', method: 'GET', params: query })
}
export function getPortalUnreadCount() {
return request({ url: '/jysystem/message/portal/unreadCount', method: 'GET' })
}
export function markPortalMessageRead(id) {
return request({ url: '/jysystem/message/portal/read/' + id, method: 'PUT' })
}
export function markPortalMessageAllRead() {
return request({ url: '/jysystem/message/portal/readAll', method: 'PUT' })
}

73
uniapp/src/api/order.js Normal file
View File

@@ -0,0 +1,73 @@
import request from '@/utils/request.js'
export function listPortalOrder(query) {
return request({ url: '/jysystem/order/portal/list', method: 'GET', params: query })
}
export function getPortalOrderContext() {
return request({ url: '/jysystem/order/portal/context', method: 'GET' })
}
export function listPortalProductOptions() {
return request({ url: '/jysystem/order/portal/productOptions', method: 'GET' })
}
export function getPortalOrder(id) {
return request({ url: '/jysystem/order/portal/' + id, method: 'GET' })
}
export function addPortalOrder(data) {
return request({ url: '/jysystem/order/portal', method: 'POST', data })
}
export function updatePortalOrder(data) {
return request({ url: '/jysystem/order/portal', method: 'PUT', data })
}
export function delPortalOrder(id) {
return request({ url: '/jysystem/order/portal/' + id, method: 'DELETE' })
}
export function supplementPortalOrder(data) {
return request({ url: '/jysystem/order/portal/supplement', method: 'PUT', data })
}
export function acceptPortalOrder(id) {
return request({ url: '/jysystem/order/portal/accept/' + id, method: 'PUT' })
}
export function producePortalOrder(id) {
return request({ url: '/jysystem/order/portal/produce/' + id, method: 'PUT' })
}
export function withdrawPortalOrder(data) {
return request({ url: '/jysystem/order/portal/withdraw', method: 'PUT', data })
}
export function getPortalOrderLogistics(id) {
return request({ url: '/jysystem/order/portal/logistics/' + id, method: 'GET' })
}
export function shipPortalOrder(data) {
return request({ url: '/jysystem/order/portal/ship', method: 'PUT', data })
}
export function completePortalOrder(id) {
return request({ url: '/jysystem/order/portal/complete/' + id, method: 'PUT' })
}
export function exchangeApplyPortalOrder(data) {
return request({ url: '/jysystem/order/portal/exchangeApply', method: 'PUT', data })
}
export function receivePortalOrder(id) {
return request({ url: '/jysystem/order/portal/receive/' + id, method: 'PUT' })
}
export function resubmitPortalOrder(data) {
return request({ url: '/jysystem/order/portal/resubmit', method: 'PUT', data })
}
export function rejectPortalOrder(data) {
return request({ url: '/jysystem/order/portal/reject', method: 'PUT', data })
}

9
uniapp/src/api/tenant.js Normal file
View File

@@ -0,0 +1,9 @@
import request from '@/utils/request.js'
export function getCurrentTenant() {
return request({ url: '/jysystem/tenant/current', method: 'GET' })
}
export function listLensFactoryTenant() {
return request({ url: '/jysystem/tenant/lensFactoryList', method: 'GET' })
}

View File

@@ -0,0 +1,43 @@
<template>
<text :class="['dict-tag', tagClass]">{{ label }}</text>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
options: { type: Array, default: () => [] },
value: { type: [String, Number], default: '' }
})
const label = computed(() => {
const item = props.options.find(d => String(d.dictValue) === String(props.value))
return item ? item.dictLabel : (props.value || '-')
})
const tagClass = computed(() => {
const v = String(props.value)
if (['1'].includes(v)) return 'tag-info'
if (['2', '3', '6', '7'].includes(v)) return 'tag-primary'
if (['8', '9', '17'].includes(v)) return 'tag-success'
if (['4', '5', '12'].includes(v)) return 'tag-warning'
if (['10', '11', '21'].includes(v)) return 'tag-danger'
return 'tag-default'
})
</script>
<style lang="scss" scoped>
.dict-tag {
display: inline-block;
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-size: 22rpx;
}
.tag-default { background: #f1f5f9; color: #64748b; }
.tag-info { background: #e0f2fe; color: #0284c7; }
.tag-primary { background: #dbeafe; color: #2563eb; }
.tag-success { background: #dcfce7; color: #16a34a; }
.tag-warning { background: #fef3c7; color: #d97706; }
.tag-danger { background: #fee2e2; color: #dc2626; }
</style>

View File

@@ -0,0 +1,346 @@
<template>
<view class="nav-bar-wrap">
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<view class="logo" @click="goHome">
<text class="logo-text">功能镜片配镜系统</text>
</view>
<view class="nav-right">
<view
v-if="showMessageBell"
class="message-bell"
@click="goMessage"
>
<text class="bell-icon">🔔</text>
<view v-if="unreadCount > 0" class="message-badge">
{{ unreadCount > 99 ? '99+' : unreadCount }}
</view>
</view>
<view v-if="isLoggedIn" class="user-info" @click="toggleUserMenu">
<image v-if="userStore.avatar" :src="userStore.avatar" class="user-avatar" mode="aspectFill" />
<view v-else class="avatar-placeholder">{{ avatarText }}</view>
<text class="user-name">{{ userStore.name || '用户' }}</text>
</view>
<view v-else class="auth-buttons">
<text class="login-link" @click="goLogin">登录</text>
<text class="register-link" @click="goRegister">注册</text>
</view>
</view>
</view>
<!-- 快捷菜单 -->
<scroll-view v-if="showMenu" scroll-x class="nav-menu" show-scrollbar="false">
<view
v-for="item in menuItems"
:key="item.path"
class="menu-item"
:class="{ active: currentPath === item.path }"
@click="navigateTo(item.path)"
>
{{ item.label }}
</view>
</scroll-view>
<!-- 用户下拉菜单 -->
<view v-if="isUserMenuOpen" class="user-dropdown-mask" @click="closeUserMenu">
<view class="user-dropdown" @click.stop>
<view class="dropdown-item" @click="goRealNameAuth">实名认证</view>
<view class="dropdown-item" @click="goAccount">账户中心</view>
<view v-if="showLensOrderMenu" class="dropdown-item" @click="navigateTo('/pages/order/lens-order')">我的配片单</view>
<view v-if="showFactoryOrderMenu" class="dropdown-item" @click="navigateTo('/pages/order/factory-order')">我的制片单</view>
<view class="dropdown-item logout" @click="handleLogout">退出登录</view>
</view>
</view>
</view>
<view class="nav-placeholder" :style="{ height: navHeight + 'px' }" />
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useUserStore } from '@/store/index.js'
import { getPortalUnreadCount } from '@/api/message.js'
import { canAccessLensOrder, isBrandTenant, isLensFactoryTenant } from '@/utils/tenantType.js'
const props = defineProps({
showMenu: { type: Boolean, default: true }
})
const userStore = useUserStore()
const statusBarHeight = ref(0)
const navHeight = ref(88)
const isUserMenuOpen = ref(false)
const unreadCount = ref(0)
const currentPath = ref('')
let pollTimer = null
const isLoggedIn = computed(() => userStore.isLoggedIn)
const showLensOrderMenu = computed(() => isLoggedIn.value && userStore.tenantId && canAccessLensOrder(userStore.tenantType))
const showFactoryOrderMenu = computed(() => isLoggedIn.value && userStore.tenantId && isLensFactoryTenant(userStore.tenantType))
const showMessageBell = computed(() => isLoggedIn.value && userStore.tenantId && (isBrandTenant(userStore.tenantType) || isLensFactoryTenant(userStore.tenantType)))
const avatarText = computed(() => (userStore.name ? userStore.name.charAt(0) : 'U'))
const menuItems = computed(() => {
const items = [{ label: '首页', path: '/pages/index/index' }]
if (showLensOrderMenu.value) items.push({ label: '配片单', path: '/pages/order/lens-order' })
if (showFactoryOrderMenu.value) items.push({ label: '制片单', path: '/pages/order/factory-order' })
items.push({ label: '关于', path: '/pages/about/about' })
return items
})
onMounted(() => {
const sys = uni.getSystemInfoSync()
statusBarHeight.value = sys.statusBarHeight || 0
navHeight.value = (props.showMenu ? 160 : 88) + statusBarHeight.value
const pages = getCurrentPages()
if (pages.length) {
currentPath.value = '/' + pages[pages.length - 1].route
}
ensureTenantContext()
startUnreadPolling()
})
onUnmounted(() => stopUnreadPolling())
function ensureTenantContext() {
if (userStore.token && (!userStore.tenantId || !userStore.tenantType)) {
userStore.getInfo().then(() => fetchUnreadCount()).catch(() => {})
}
}
function startUnreadPolling() {
stopUnreadPolling()
if (!showMessageBell.value) return
fetchUnreadCount()
pollTimer = setInterval(fetchUnreadCount, 60000)
}
function stopUnreadPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
function fetchUnreadCount() {
if (!showMessageBell.value) return
getPortalUnreadCount().then(res => {
unreadCount.value = Number(res.data || 0)
}).catch(() => {})
}
function toggleUserMenu() {
isUserMenuOpen.value = !isUserMenuOpen.value
}
function closeUserMenu() {
isUserMenuOpen.value = false
}
function navigateTo(path) {
closeUserMenu()
if (currentPath.value === path) return
uni.navigateTo({ url: path, fail: () => uni.redirectTo({ url: path }) })
}
function goHome() {
uni.reLaunch({ url: '/pages/index/index' })
}
function goLogin() {
uni.navigateTo({ url: '/pages/login/login' })
}
function goRegister() {
uni.navigateTo({ url: '/pages/register/register' })
}
function goMessage() {
uni.navigateTo({ url: '/pages/message/message' })
}
function goAccount() {
closeUserMenu()
uni.navigateTo({ url: '/pages/account/index' })
}
function goRealNameAuth() {
closeUserMenu()
uni.navigateTo({ url: '/pages/account/realNameAuth' })
}
function handleLogout() {
closeUserMenu()
uni.showModal({
title: '提示',
content: '确定退出登录吗?',
success: (res) => {
if (res.confirm) {
userStore.logout().finally(() => {
uni.reLaunch({ url: '/pages/login/login' })
})
}
}
})
}
</script>
<style lang="scss" scoped>
.nav-bar-wrap {
width: 100%;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.08);
}
.nav-inner {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 24rpx;
height: 88rpx;
}
.logo-text {
font-size: 28rpx;
font-weight: bold;
color: #2563eb;
}
.nav-right {
display: flex;
align-items: center;
gap: 20rpx;
}
.message-bell {
position: relative;
padding: 8rpx;
}
.bell-icon {
font-size: 36rpx;
}
.message-badge {
position: absolute;
top: 0;
right: 0;
background: #ef4444;
color: #fff;
font-size: 18rpx;
min-width: 28rpx;
height: 28rpx;
line-height: 28rpx;
text-align: center;
border-radius: 14rpx;
padding: 0 6rpx;
}
.user-info {
display: flex;
align-items: center;
gap: 12rpx;
}
.user-avatar, .avatar-placeholder {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
}
.avatar-placeholder {
background: linear-gradient(135deg, #2563eb, #7c3aed);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
}
.user-name {
font-size: 26rpx;
color: #334155;
max-width: 120rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.auth-buttons {
display: flex;
gap: 16rpx;
font-size: 26rpx;
}
.login-link {
color: #64748b;
}
.register-link {
color: #2563eb;
font-weight: 600;
}
.nav-menu {
white-space: nowrap;
padding: 0 16rpx 12rpx;
}
.menu-item {
display: inline-block;
padding: 8rpx 24rpx;
margin: 0 8rpx;
font-size: 26rpx;
color: #64748b;
border-radius: 32rpx;
&.active {
background: rgba(37, 99, 235, 0.1);
color: #2563eb;
font-weight: 600;
}
}
.user-dropdown-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.user-dropdown {
position: absolute;
top: 160rpx;
right: 24rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
min-width: 240rpx;
overflow: hidden;
}
.dropdown-item {
padding: 24rpx 32rpx;
font-size: 28rpx;
color: #334155;
border-bottom: 1rpx solid #f1f5f9;
&.logout {
color: #ef4444;
}
}
.nav-placeholder {
width: 100%;
}
</style>

View File

@@ -0,0 +1,16 @@
/** API 地址:打包前请改为可访问的后端地址(手机无法访问 localhost */
const API_BASE = import.meta.env.VITE_APP_BASE_API || ''
// #ifdef H5
const baseUrl = API_BASE || (import.meta.env.DEV ? '/dev-api' : '/prod-api')
// #endif
// #ifndef H5
const baseUrl = API_BASE || 'http://192.168.1.100:8082'
// #endif
export default {
baseUrl,
timeout: 10000,
tokenKey: 'Developer-Token'
}

15
uniapp/src/main.js Normal file
View File

@@ -0,0 +1,15 @@
import { closeSplashscreen } from '@/utils/splash.js'
import { createSSRApp } from 'vue'
import App from './App.vue'
import pinia from './store/pinia.js'
export function createApp() {
closeSplashscreen()
const app = createSSRApp(App)
app.use(pinia)
app.config.errorHandler = (err, instance, info) => {
console.error('[App Error]', info, err)
closeSplashscreen()
}
return { app }
}

77
uniapp/src/manifest.json Normal file
View File

@@ -0,0 +1,77 @@
{
"name" : "功能镜片配镜系统",
"appid" : "__UNI__C17C074",
"description" : "觉远创智功能镜片配镜系统 UniApp 移动端",
"versionName" : "1.0.2",
"versionCode" : "102",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"renderer" : "webview",
"compatible" : {
"ignoreVersion" : true
},
"splashscreen" : {
"alwaysShowBeforeRender" : false,
"autoclose" : true,
"waiting" : false,
"delay" : 0,
"target" : "id:1"
},
"modules" : {},
"distribute" : {
"android" : {
"packagename" : "com.jyuan.optical",
"permissions" : [
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>"
],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ]
},
"ios" : {
"dSYMs" : false
},
"sdkConfigs" : {}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false,
"es6" : true,
"minified" : true
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"h5" : {
"title" : "功能镜片配镜系统",
"router" : {
"mode" : "hash"
},
"devServer" : {
"port" : 2093,
"proxy" : {
"/dev-api" : {
"target" : "http://localhost:8082",
"changeOrigin" : true,
"pathRewrite" : {
"^/dev-api" : ""
}
},
"/prod-api" : {
"target" : "http://localhost:8082",
"changeOrigin" : true,
"pathRewrite" : {
"^/prod-api" : ""
}
}
}
}
},
"vueVersion" : "3"
}

115
uniapp/src/pages.json Normal file
View File

@@ -0,0 +1,115 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/register/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/resetPassword/resetPassword",
"style": {
"navigationBarTitleText": "重置密码"
}
},
{
"path": "pages/order/lens-order",
"style": {
"navigationBarTitleText": "我的配片单",
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
"path": "pages/order/factory-order",
"style": {
"navigationBarTitleText": "我的制片单",
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
"path": "pages/order/lens-order-form",
"style": {
"navigationBarTitleText": "配片单"
}
},
{
"path": "pages/order/order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "pages/order/logistics",
"style": {
"navigationBarTitleText": "物流信息"
}
},
{
"path": "pages/message/message",
"style": {
"navigationBarTitleText": "消息通知",
"enablePullDownRefresh": true
}
},
{
"path": "pages/account/index",
"style": {
"navigationBarTitleText": "账户中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/account/address",
"style": {
"navigationBarTitleText": "收货地址"
}
},
{
"path": "pages/account/realNameAuth",
"style": {
"navigationBarTitleText": "实名认证"
}
},
{
"path": "pages/about/about",
"style": {
"navigationBarTitleText": "关于我们"
}
},
{
"path": "pages/user/profile",
"style": {
"navigationBarTitleText": "个人中心"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "功能镜片配镜系统",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f5f7fa"
},
"easycom": {
"autoscan": true,
"custom": {
"^NavBar$": "@/components/NavBar/NavBar.vue",
"^DictTag$": "@/components/DictTag/DictTag.vue"
}
}
}

View File

@@ -0,0 +1,24 @@
<template>
<view class="page container">
<view class="card">
<text class="company-name">金华觉远创智科技有限公司</text>
<text class="desc">觉远创智功能镜片配镜系统为镜片行业提供配片单制片单全流程数字化管理助力品牌租户与镜片厂高效协作</text>
</view>
<view class="card">
<text class="section-title">联系我们</text>
<text class="contact-item">官网www.jyuan.com</text>
<text class="contact-item">邮箱support@jyuan.com</text>
</view>
<view class="footer">
<text>© 2025 金华觉远创智科技有限公司 版权所有</text>
</view>
</view>
</template>
<style lang="scss" scoped>
.company-name { display: block; font-size: 36rpx; font-weight: bold; margin-bottom: 16rpx; }
.desc { font-size: 28rpx; color: #64748b; line-height: 1.6; }
.section-title { display: block; font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx; }
.contact-item { display: block; font-size: 28rpx; color: #64748b; padding: 8rpx 0; }
.footer { text-align: center; padding: 48rpx; color: #94a3b8; font-size: 24rpx; }
</style>

View File

@@ -0,0 +1,10 @@
<template>
<view class="page container">
<view class="empty-tip">收货地址管理功能与 Web API 一致请对接 /shop/userAddr 相关接口后完善</view>
<button class="btn-primary" @click="goBack">返回</button>
</view>
</template>
<script setup>
function goBack() { uni.navigateBack() }
</script>

View File

@@ -0,0 +1,83 @@
<template>
<view class="page">
<NavBar />
<view class="container">
<view v-if="userStore.isLoggedIn" class="user-card card">
<image v-if="userStore.avatar" :src="userStore.avatar" class="avatar" mode="aspectFill" />
<view v-else class="avatar-placeholder">{{ avatarText }}</view>
<view class="user-info">
<text class="user-name">{{ userStore.name || '用户' }}</text>
<text class="tenant">{{ userStore.tenantName || '未关联租户' }}</text>
</view>
</view>
<view class="menu-list card">
<view v-if="showLensOrder" class="menu-item" @click="go('/pages/order/lens-order')">
<text>我的配片单</text><text class="arrow"></text>
</view>
<view v-if="showFactoryOrder" class="menu-item" @click="go('/pages/order/factory-order')">
<text>我的制片单</text><text class="arrow"></text>
</view>
<view class="menu-item" @click="go('/pages/message/message')">
<text>消息通知</text><text class="arrow"></text>
</view>
<view class="menu-item" @click="go('/pages/account/realNameAuth')">
<text>实名认证</text><text class="arrow"></text>
</view>
<view class="menu-item" @click="go('/pages/account/address')">
<text>收货地址</text><text class="arrow"></text>
</view>
<view class="menu-item" @click="go('/pages/about/about')">
<text>关于我们</text><text class="arrow"></text>
</view>
</view>
<button v-if="userStore.isLoggedIn" class="logout-btn" @click="handleLogout">退出登录</button>
<button v-else class="btn-primary" @click="go('/pages/login/login')">去登录</button>
</view>
</view>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { useUserStore } from '@/store/index.js'
import { canAccessLensOrder, isLensFactoryTenant } from '@/utils/tenantType.js'
import NavBar from '@/components/NavBar/NavBar.vue'
const userStore = useUserStore()
const avatarText = computed(() => (userStore.name ? userStore.name.charAt(0) : 'U'))
const showLensOrder = computed(() => userStore.isLoggedIn && canAccessLensOrder(userStore.tenantType))
const showFactoryOrder = computed(() => userStore.isLoggedIn && isLensFactoryTenant(userStore.tenantType))
onMounted(() => {
if (userStore.token && !userStore.tenantId) {
userStore.getInfo().catch(() => {})
}
})
function go(url) {
uni.navigateTo({ url })
}
function handleLogout() {
uni.showModal({
title: '提示', content: '确定退出登录吗?',
success: (res) => {
if (res.confirm) {
userStore.logout().finally(() => uni.reLaunch({ url: '/pages/login/login' }))
}
}
})
}
</script>
<style lang="scss" scoped>
.user-card { display: flex; align-items: center; gap: 24rpx; margin-bottom: 24rpx; }
.avatar, .avatar-placeholder { width: 100rpx; height: 100rpx; border-radius: 50%; }
.avatar-placeholder { background: linear-gradient(135deg, #2563eb, #7c3aed); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 40rpx; }
.user-name { display: block; font-size: 32rpx; font-weight: 600; }
.tenant { font-size: 24rpx; color: #64748b; }
.menu-item { display: flex; justify-content: space-between; padding: 28rpx 0; border-bottom: 1rpx solid #f1f5f9; font-size: 30rpx; .arrow { color: #94a3b8; } }
.logout-btn { margin-top: 48rpx; background: #fff; color: #ef4444; border: 2rpx solid #fee2e2; border-radius: 12rpx; }
.btn-primary { width: 100%; margin-top: 48rpx; }
</style>

View File

@@ -0,0 +1,69 @@
<template>
<view class="page container">
<view v-if="loading" class="empty-tip">加载中...</view>
<template v-else-if="authInfo">
<view class="card">
<view class="detail-row"><text class="label">认证状态</text><text>{{ statusLabel }}</text></view>
<view class="detail-row"><text class="label">认证类型</text><text>{{ authInfo.authType === 'enterprise' ? '企业' : '个人' }}</text></view>
<view class="detail-row"><text class="label">姓名</text><text>{{ authInfo.realName || '-' }}</text></view>
<view class="detail-row"><text class="label">证件号</text><text>{{ maskId(authInfo.idNumber) }}</text></view>
</view>
</template>
<template v-else>
<view class="card">
<text class="tip">您尚未完成实名认证请在 Web 端完成认证或在此提交需对接 /system/realNameAuth 接口</text>
<view class="form-item"><input v-model="form.realName" placeholder="真实姓名" /></view>
<view class="form-item"><input v-model="form.idNumber" placeholder="身份证号" /></view>
<button class="btn-primary" @click="handleSubmit">提交认证</button>
</view>
</template>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import request from '@/utils/request.js'
const loading = ref(true)
const authInfo = ref(null)
const form = ref({ realName: '', idNumber: '' })
const statusLabel = computed(() => {
const s = authInfo.value?.status
if (s === '1') return '已认证'
if (s === '2') return '审核中'
if (s === '3') return '已驳回'
return '未认证'
})
onMounted(async () => {
try {
const res = await request({ url: '/system/realNameAuth/my', method: 'GET' })
authInfo.value = res.data
} catch {
authInfo.value = null
} finally {
loading.value = false
}
})
function maskId(id) {
if (!id || id.length < 8) return id || '-'
return id.slice(0, 4) + '****' + id.slice(-4)
}
async function handleSubmit() {
if (!form.value.realName || !form.value.idNumber) {
return uni.showToast({ title: '请填写完整信息', icon: 'none' })
}
await request({ url: '/system/realNameAuth/submit', method: 'POST', data: form.value })
uni.showToast({ title: '提交成功', icon: 'success' })
}
</script>
<style lang="scss" scoped>
.detail-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #f1f5f9; .label { color: #64748b; } }
.tip { display: block; font-size: 26rpx; color: #64748b; margin-bottom: 24rpx; }
.form-item { background: #f8fafc; border-radius: 8rpx; padding: 0 20rpx; margin-bottom: 16rpx; input { height: 80rpx; font-size: 28rpx; } }
.btn-primary { width: 100%; margin-top: 24rpx; }
</style>

View File

@@ -0,0 +1,230 @@
<template>
<view class="page">
<NavBar />
<view class="hero">
<text class="hero-title">下一代智能眼镜</text>
<text class="hero-subtitle">开放平台</text>
<text class="hero-desc">为开发者提供完整的智能眼镜解决方案包括硬件接入SDKAPI 文档和技术支持</text>
<view class="hero-actions">
<button v-if="showFactoryOrderEntry" class="btn-primary action-btn" @click="goFactoryOrder">我的制片单</button>
<button v-if="showLensOrderEntry" class="btn-primary action-btn" @click="goLensOrder">我的配片单</button>
<button class="btn-outline action-btn" @click="goAbout">关于我们</button>
</view>
</view>
<view class="section">
<text class="section-title">平台概览</text>
<view class="feature-grid">
<view v-for="item in features" :key="item.title" class="feature-card card">
<text class="feature-icon">{{ item.icon }}</text>
<text class="feature-name">{{ item.title }}</text>
<text class="feature-desc">{{ item.desc }}</text>
</view>
</view>
</view>
<view class="section">
<text class="section-title">快速上手</text>
<view class="steps">
<view v-for="(step, i) in steps" :key="i" class="step-card card">
<text class="step-num">{{ i + 1 }}</text>
<view class="step-content">
<text class="step-title">{{ step.title }}</text>
<text class="step-desc">{{ step.desc }}</text>
</view>
</view>
</view>
</view>
<view class="footer">
<text>© 2025 金华觉远创智科技有限公司</text>
</view>
</view>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { closeSplashscreen } from '@/utils/splash.js'
import { useUserStore } from '@/store/index.js'
import { canAccessLensOrder, isLensFactoryTenant } from '@/utils/tenantType.js'
import NavBar from '@/components/NavBar/NavBar.vue'
const userStore = useUserStore()
const showLensOrderEntry = computed(() => userStore.isLoggedIn && userStore.tenantId && canAccessLensOrder(userStore.tenantType))
const showFactoryOrderEntry = computed(() => userStore.isLoggedIn && userStore.tenantId && isLensFactoryTenant(userStore.tenantType))
const features = [
{ icon: '⚙️', title: '是什么', desc: '面向智能眼镜的开发平台,提供 SDK、API 和工具链。' },
{ icon: '🚀', title: '能做什么', desc: '快速接入设备,构建 AR 叠加、手势与语音交互等能力。' },
{ icon: '👥', title: '为谁而建', desc: '面向开发者与企业,支持多场景应用。' }
]
const steps = [
{ title: '注册并创建应用', desc: '获取 API Key 与项目配置。' },
{ title: '接入 SDK', desc: '按文档完成设备接入与调试。' },
{ title: '发布应用', desc: '提交审核并上线您的智能眼镜应用。' }
]
onLoad(() => {
closeSplashscreen()
})
onMounted(() => {
if (userStore.token && !userStore.tenantId) {
userStore.getInfo().catch(() => {})
}
})
function goLensOrder() {
uni.navigateTo({ url: '/pages/order/lens-order' })
}
function goFactoryOrder() {
uni.navigateTo({ url: '/pages/order/factory-order' })
}
function goAbout() {
uni.navigateTo({ url: '/pages/about/about' })
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: linear-gradient(180deg, #eff6ff 0%, #f5f7fa 40%);
}
.hero {
padding: 48rpx 32rpx 64rpx;
text-align: center;
}
.hero-title {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #1e293b;
}
.hero-subtitle {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #2563eb;
margin: 12rpx 0 24rpx;
}
.hero-desc {
display: block;
font-size: 28rpx;
color: #64748b;
line-height: 1.6;
margin-bottom: 40rpx;
}
.hero-actions {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.action-btn {
width: 100%;
}
.btn-outline {
background: #fff;
color: #334155;
border: 2rpx solid #e2e8f0;
border-radius: 12rpx;
font-size: 30rpx;
height: 88rpx;
line-height: 88rpx;
}
.section {
padding: 32rpx;
}
.section-title {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #1e293b;
text-align: center;
margin-bottom: 32rpx;
}
.feature-grid {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.feature-card {
text-align: center;
}
.feature-icon {
font-size: 48rpx;
display: block;
margin-bottom: 12rpx;
}
.feature-name {
display: block;
font-size: 30rpx;
font-weight: 600;
margin-bottom: 8rpx;
}
.feature-desc {
font-size: 24rpx;
color: #64748b;
}
.steps {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.step-card {
display: flex;
align-items: flex-start;
gap: 20rpx;
}
.step-num {
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #2563eb, #7c3aed);
color: #fff;
border-radius: 50%;
text-align: center;
line-height: 48rpx;
font-size: 24rpx;
flex-shrink: 0;
}
.step-title {
display: block;
font-size: 28rpx;
font-weight: 600;
margin-bottom: 8rpx;
}
.step-desc {
font-size: 24rpx;
color: #64748b;
}
.footer {
text-align: center;
padding: 48rpx;
color: #94a3b8;
font-size: 24rpx;
}
</style>

View File

@@ -0,0 +1,358 @@
<template>
<view class="page">
<NavBar :show-menu="false" />
<view class="login-container">
<view class="login-header">
<text class="login-title">欢迎登录</text>
<text class="login-subtitle">Welcome Back</text>
</view>
<view class="tabs">
<view :class="['tab', activeTab === 'password' && 'active']" @click="activeTab = 'password'">密码登录</view>
<view :class="['tab', activeTab === 'code' && 'active']" @click="activeTab = 'code'">验证码登录</view>
</view>
<!-- 密码登录 -->
<view v-if="activeTab === 'password'" class="form">
<view class="form-item">
<input v-model="passwordForm.username" placeholder="请输入手机号或邮箱" />
</view>
<view class="form-item">
<input v-model="passwordForm.password" password placeholder="请输入密码" />
</view>
<view v-if="captchaEnabled" class="form-item code-row">
<input v-model="passwordForm.code" placeholder="验证码" class="code-input" />
<image :src="codeUrl" class="code-img" @click="getCode" mode="aspectFit" />
</view>
<view class="form-options">
<label class="remember">
<checkbox :checked="passwordForm.rememberMe" @click="passwordForm.rememberMe = !passwordForm.rememberMe" />
<text>记住密码</text>
</label>
<text class="forgot" @click="goReset">忘记密码</text>
</view>
<button class="btn-primary login-btn" :loading="loading" @click="handlePasswordLogin"> </button>
</view>
<!-- 验证码登录 -->
<view v-else class="form">
<view class="form-item">
<input v-model="codeForm.username" placeholder="请输入手机号或邮箱" />
</view>
<view class="form-item code-row">
<input v-model="codeForm.code" placeholder="验证码" class="code-input" />
<button class="send-code-btn" :disabled="codeCountdown > 0 || sendingCode" @click="sendCode">
{{ codeCountdown > 0 ? codeCountdown + 's' : '发送验证码' }}
</button>
</view>
<button class="btn-primary login-btn" :loading="loading" @click="handleCodeLogin"> </button>
</view>
<view class="login-links">
<text>还没有账户</text>
<text class="link" @click="goRegister">立即注册</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { getCodeImg, sendSmsCode, sendEmailCode, emailLogin, smsLogin, login } from '@/api/login.js'
import { useUserStore } from '@/store/index.js'
import NavBar from '@/components/NavBar/NavBar.vue'
const userStore = useUserStore()
const activeTab = ref('password')
const loading = ref(false)
const captchaEnabled = ref(true)
const codeUrl = ref('')
const codeCountdown = ref(0)
const sendingCode = ref(false)
let countdownTimer = null
const passwordForm = ref({
username: '',
password: '',
code: '',
uuid: '',
rememberMe: false,
directLogin: false
})
const codeForm = ref({
username: '',
code: '',
uuid: '',
directLogin: false
})
onMounted(() => {
getCode()
const saved = uni.getStorageSync('login_remember')
if (saved) {
passwordForm.value.username = saved.username || ''
passwordForm.value.password = saved.password || ''
passwordForm.value.rememberMe = true
}
})
onUnmounted(() => {
if (countdownTimer) clearInterval(countdownTimer)
})
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled !== false
if (captchaEnabled.value) {
codeUrl.value = 'data:image/gif;base64,' + res.img
passwordForm.value.uuid = res.uuid
}
})
}
function isPhone(value) {
return /^1[3-9]\d{9}$/.test(value)
}
function sendCode() {
const username = codeForm.value.username
if (!username) {
uni.showToast({ title: '请输入手机号或邮箱', icon: 'none' })
return
}
sendingCode.value = true
const promise = isPhone(username) ? sendSmsCode(username) : sendEmailCode(username)
promise.then(res => {
codeForm.value.uuid = res.data?.uuid || res.uuid
uni.showToast({ title: '验证码已发送', icon: 'success' })
codeCountdown.value = 60
countdownTimer = setInterval(() => {
codeCountdown.value--
if (codeCountdown.value <= 0) clearInterval(countdownTimer)
}, 1000)
}).finally(() => { sendingCode.value = false })
}
async function handlePasswordLogin() {
if (!passwordForm.value.username || !passwordForm.value.password) {
uni.showToast({ title: '请填写账号和密码', icon: 'none' })
return
}
loading.value = true
try {
await doLogin(() => login(
passwordForm.value.username,
passwordForm.value.password,
passwordForm.value.code,
passwordForm.value.uuid,
passwordForm.value.directLogin
))
if (passwordForm.value.rememberMe) {
uni.setStorageSync('login_remember', {
username: passwordForm.value.username,
password: passwordForm.value.password
})
} else {
uni.removeStorageSync('login_remember')
}
} catch (e) {
if (e && e.code === 201) {
uni.showModal({
title: '提示',
content: e.msg || '该账号已在其他设备登录,是否继续?',
success: (res) => {
if (res.confirm) {
passwordForm.value.directLogin = true
handlePasswordLogin()
}
}
})
}
if (captchaEnabled.value) getCode()
} finally {
loading.value = false
passwordForm.value.directLogin = false
}
}
async function handleCodeLogin() {
const username = codeForm.value.username
if (!username || !codeForm.value.code) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
loading.value = true
try {
const promise = isPhone(username)
? smsLogin(username, codeForm.value.code, codeForm.value.uuid, codeForm.value.directLogin)
: emailLogin(username, codeForm.value.code, codeForm.value.uuid, codeForm.value.directLogin)
await doLogin(() => promise)
} catch (e) {
if (e && e.code === 201) {
uni.showModal({
title: '提示',
content: e.msg || '该账号已在其他设备登录,是否继续?',
success: (res) => {
if (res.confirm) {
codeForm.value.directLogin = true
handleCodeLogin()
}
}
})
}
} finally {
loading.value = false
codeForm.value.directLogin = false
}
}
async function doLogin(loginFn) {
const res = await loginFn()
await userStore.loginWithToken(res.token)
await userStore.getInfo()
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => uni.reLaunch({ url: '/pages/index/index' }), 500)
}
function goRegister() {
uni.navigateTo({ url: '/pages/register/register' })
}
function goReset() {
uni.navigateTo({ url: '/pages/resetPassword/resetPassword' })
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: linear-gradient(180deg, #eff6ff, #fff);
}
.login-container {
padding: 48rpx 40rpx;
}
.login-header {
text-align: center;
margin-bottom: 48rpx;
}
.login-title {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #1e293b;
}
.login-subtitle {
display: block;
font-size: 28rpx;
color: #94a3b8;
margin-top: 8rpx;
}
.tabs {
display: flex;
margin-bottom: 32rpx;
background: #f1f5f9;
border-radius: 12rpx;
padding: 6rpx;
}
.tab {
flex: 1;
text-align: center;
padding: 16rpx;
font-size: 28rpx;
color: #64748b;
border-radius: 8rpx;
&.active {
background: #fff;
color: #2563eb;
font-weight: 600;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
}
.form-item {
background: #fff;
border-radius: 12rpx;
padding: 0 24rpx;
margin-bottom: 20rpx;
border: 2rpx solid #e2e8f0;
input {
height: 88rpx;
font-size: 28rpx;
}
}
.code-row {
display: flex;
align-items: center;
gap: 16rpx;
padding: 12rpx 24rpx;
}
.code-input {
flex: 1;
}
.code-img {
width: 200rpx;
height: 72rpx;
}
.send-code-btn {
font-size: 24rpx;
padding: 0 20rpx;
height: 64rpx;
line-height: 64rpx;
background: #2563eb;
color: #fff;
border-radius: 8rpx;
white-space: nowrap;
&[disabled] {
background: #94a3b8;
}
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
font-size: 26rpx;
}
.remember {
display: flex;
align-items: center;
gap: 8rpx;
color: #64748b;
}
.forgot {
color: #2563eb;
}
.login-btn {
width: 100%;
margin-bottom: 32rpx;
}
.login-links {
text-align: center;
font-size: 26rpx;
color: #64748b;
.link {
color: #2563eb;
margin-left: 8rpx;
}
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<view class="page container">
<view class="header flex-between">
<text class="title">消息通知</text>
<text v-if="unreadCount > 0" class="read-all" @click="handleReadAll">全部已读</text>
</view>
<view v-if="loading && !messageList.length" class="empty-tip">加载中...</view>
<view v-else-if="!messageList.length" class="empty-tip">暂无消息</view>
<view
v-for="item in messageList"
:key="item.id"
class="message-card card"
:class="{ unread: item.readFlag === '0' }"
@click="handleClick(item)"
>
<view class="flex-between">
<text class="msg-title">{{ item.title }}</text>
<view v-if="item.readFlag === '0'" class="unread-dot" />
</view>
<text class="msg-content">{{ item.content }}</text>
<text class="msg-time">{{ item.createTime }}</text>
</view>
<view v-if="hasMore" class="load-more" @click="loadMore">加载更多</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/index.js'
import { isLensFactoryTenant } from '@/utils/tenantType.js'
import { listPortalMessage, getPortalUnreadCount, markPortalMessageRead, markPortalMessageAllRead } from '@/api/message.js'
const userStore = useUserStore()
const loading = ref(false)
const messageList = ref([])
const total = ref(0)
const unreadCount = ref(0)
const query = ref({ pageNum: 1, pageSize: 20 })
const hasMore = computed(() => messageList.value.length < total.value)
onMounted(() => {
if (!userStore.isLoggedIn) {
uni.navigateTo({ url: '/pages/login/login' })
return
}
fetchUnread()
getList()
})
onPullDownRefresh(() => {
query.value.pageNum = 1
getList().finally(() => uni.stopPullDownRefresh())
})
onReachBottom(() => loadMore())
function fetchUnread() {
getPortalUnreadCount().then(res => { unreadCount.value = Number(res.data || 0) })
}
function getList(append = false) {
loading.value = true
return listPortalMessage(query.value).then(res => {
messageList.value = append ? [...messageList.value, ...(res.rows || [])] : (res.rows || [])
total.value = res.total || 0
}).finally(() => { loading.value = false })
}
function loadMore() {
if (!hasMore.value || loading.value) return
query.value.pageNum++
getList(true)
}
function handleClick(item) {
if (item.readFlag === '0') {
markPortalMessageRead(item.id).then(() => {
item.readFlag = '1'
fetchUnread()
})
}
const path = isLensFactoryTenant(userStore.tenantType)
? '/pages/order/factory-order'
: '/pages/order/lens-order'
uni.navigateTo({ url: path })
}
function handleReadAll() {
markPortalMessageAllRead().then(() => {
messageList.value.forEach(i => { i.readFlag = '1' })
unreadCount.value = 0
uni.showToast({ title: '已全部标记已读', icon: 'success' })
})
}
</script>
<style lang="scss" scoped>
.header { margin-bottom: 24rpx; }
.title { font-size: 36rpx; font-weight: bold; }
.read-all { font-size: 26rpx; color: #2563eb; }
.message-card { margin-bottom: 16rpx; &.unread { border-left: 6rpx solid #2563eb; } }
.msg-title { font-size: 28rpx; font-weight: 600; }
.unread-dot { width: 16rpx; height: 16rpx; background: #ef4444; border-radius: 50%; }
.msg-content { display: block; font-size: 26rpx; color: #64748b; margin: 12rpx 0; }
.msg-time { font-size: 22rpx; color: #94a3b8; }
.load-more { text-align: center; padding: 24rpx; color: #2563eb; }
</style>

View File

@@ -0,0 +1,322 @@
<template>
<view class="page">
<NavBar />
<view class="container">
<view class="page-header">
<text class="page-title">我的制片单</text>
<text v-if="userStore.tenantName" class="tenant-name">{{ userStore.tenantName }}</text>
</view>
<view class="search-bar card">
<input v-model="queryParams.orderNo" placeholder="订单编号" class="search-input" />
<picker :range="statusLabels" :value="statusIndex" @change="onStatusChange">
<view class="status-picker">{{ statusLabels[statusIndex] || '全部状态' }}</view>
</picker>
<button size="mini" class="search-btn" @click="handleQuery">搜索</button>
</view>
<view v-if="loading && !orderList.length" class="empty-tip">加载中...</view>
<view v-else-if="!orderList.length" class="empty-tip">暂无制片单</view>
<view v-for="item in orderList" :key="item.id" class="order-card card" @click="handleDetail(item)">
<view class="flex-between order-head">
<text class="order-no">{{ item.orderNo }}</text>
<DictTag :options="orderStatusDict" :value="item.orderStatus" />
</view>
<view class="order-info">
<text>镜片{{ formatLens(item) }}</text>
<text>收货人{{ item.receiverName }} {{ item.receiverPhone }}</text>
<text>创建{{ item.createTime }}</text>
</view>
<view class="order-actions" @click.stop>
<text class="action-link" @click="handleDetail(item)">详情</text>
<text v-if="canViewLogistics(item)" class="action-link" @click="viewLogistics(item)">物流</text>
<text v-if="canAcceptOrder(item)" class="action-link success" @click="handleAccept(item)">接单</text>
<text v-if="canSupplementOrder(item)" class="action-link" @click="handleSupplement(item)">需补充</text>
<text v-if="isAccepted(item)" class="action-link primary" @click="handleProduce(item)">制作中</text>
<text v-if="isProducing(item)" class="action-link warning" @click="handleShip(item)">发货</text>
<text v-if="canCompleteOrder(item)" class="action-link success" @click="handleComplete(item)">完成</text>
<text v-if="canWithdrawOrder(item)" class="action-link" @click="handleWithdraw(item)">撤单</text>
</view>
</view>
<view v-if="hasMore" class="load-more" @click="loadMore">加载更多</view>
</view>
<!-- 发货弹窗 -->
<view v-if="shipVisible" class="modal-mask" @click="shipVisible = false">
<view class="modal-content" @click.stop>
<text class="modal-title">发货信息</text>
<view class="form-item"><input v-model="shipForm.expressCompany" placeholder="物流公司" /></view>
<view class="form-item"><input v-model="shipForm.expressNo" placeholder="物流单号" /></view>
<view class="form-item"><input v-model="shipForm.logisticsFee" type="digit" placeholder="物流费用" /></view>
<view class="modal-actions">
<button @click="shipVisible = false">取消</button>
<button class="btn-primary" @click="submitShip">确定发货</button>
</view>
</view>
</view>
<!-- 需补充弹窗 -->
<view v-if="supplementVisible" class="modal-mask" @click="supplementVisible = false">
<view class="modal-content" @click.stop>
<text class="modal-title">需补充说明</text>
<text class="modal-subtitle">订单{{ supplementForm.orderNo }}</text>
<textarea v-model="supplementForm.remark" placeholder="请输入需要品牌方补充的内容" class="remark-input" />
<view class="modal-actions">
<button @click="supplementVisible = false">取消</button>
<button class="btn-primary" @click="submitSupplement">确定</button>
</view>
</view>
</view>
<!-- 撤单弹窗 -->
<view v-if="withdrawVisible" class="modal-mask" @click="withdrawVisible = false">
<view class="modal-content" @click.stop>
<text class="modal-title">撤单</text>
<text class="modal-subtitle">订单{{ withdrawForm.orderNo }}</text>
<textarea v-model="withdrawForm.reason" placeholder="请输入撤单理由" class="remark-input" />
<view class="modal-actions">
<button @click="withdrawVisible = false">取消</button>
<button class="btn-primary" @click="submitWithdraw">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { useUserStore, useDictStore } from '@/store/index.js'
import {
ORDER_STATUS,
FACTORY_ACCEPTABLE_STATUSES,
normalizeOrderStatus,
formatLens
} from '@/utils/orderHelper.js'
import {
listPortalOrder, acceptPortalOrder, producePortalOrder, shipPortalOrder,
completePortalOrder, withdrawPortalOrder, supplementPortalOrder
} from '@/api/order.js'
import NavBar from '@/components/NavBar/NavBar.vue'
import DictTag from '@/components/DictTag/DictTag.vue'
const userStore = useUserStore()
const dictStore = useDictStore()
const loading = ref(false)
const orderList = ref([])
const total = ref(0)
const orderStatusDict = ref([])
const statusIndex = ref(0)
const shipVisible = ref(false)
const supplementVisible = ref(false)
const withdrawVisible = ref(false)
const shipForm = ref({ orderId: null, expressCompany: '', expressNo: '', logisticsFee: '' })
const supplementForm = ref({ orderId: null, orderNo: '', remark: '' })
const withdrawForm = ref({ orderId: null, orderNo: '', reason: '' })
const queryParams = ref({ pageNum: 1, pageSize: 10, orderNo: null, orderStatus: null })
const statusLabels = computed(() => ['全部状态', ...orderStatusDict.value.map(d => d.dictLabel)])
const hasMore = computed(() => orderList.value.length < total.value)
onMounted(async () => {
orderStatusDict.value = await dictStore.getDict('jy_order_status')
getList()
})
onPullDownRefresh(() => {
queryParams.value.pageNum = 1
getList().finally(() => uni.stopPullDownRefresh())
})
onReachBottom(() => loadMore())
function onStatusChange(e) {
statusIndex.value = Number(e.detail.value)
queryParams.value.orderStatus = statusIndex.value === 0 ? null : orderStatusDict.value[statusIndex.value - 1]?.dictValue
}
function getList(append = false) {
loading.value = true
return listPortalOrder(queryParams.value).then(res => {
orderList.value = append ? [...orderList.value, ...(res.rows || [])] : (res.rows || [])
total.value = res.total || 0
}).finally(() => { loading.value = false })
}
function handleQuery() { queryParams.value.pageNum = 1; getList() }
function loadMore() { if (!hasMore.value || loading.value) return; queryParams.value.pageNum++; getList(true) }
function canAcceptOrder(row) {
return FACTORY_ACCEPTABLE_STATUSES.includes(normalizeOrderStatus(row?.orderStatus))
}
function canSupplementOrder(row) {
const status = normalizeOrderStatus(row?.orderStatus)
return status === ORDER_STATUS.ASSIGNED || status === ORDER_STATUS.SUPPLEMENTED
}
function isAccepted(row) {
return normalizeOrderStatus(row?.orderStatus) === ORDER_STATUS.ACCEPTED
}
function isProducing(row) {
return normalizeOrderStatus(row?.orderStatus) === ORDER_STATUS.PRODUCING
}
function canViewLogistics(row) {
const status = normalizeOrderStatus(row?.orderStatus)
return [
ORDER_STATUS.SHIPPED,
ORDER_STATUS.EXCHANGE_LOGISTICS,
ORDER_STATUS.COMPLETED,
ORDER_STATUS.EXCHANGE_COMPLETED
].includes(status)
}
function canCompleteOrder(row) {
const status = normalizeOrderStatus(row?.orderStatus)
return status === ORDER_STATUS.SHIPPED || status === ORDER_STATUS.EXCHANGE_LOGISTICS
}
function canWithdrawOrder(row) {
const status = normalizeOrderStatus(row?.orderStatus)
return [
ORDER_STATUS.ASSIGNED,
ORDER_STATUS.ACCEPTED,
ORDER_STATUS.PRODUCING
].includes(status)
}
function handleDetail(row) {
uni.navigateTo({ url: '/pages/order/order-detail?id=' + row.id + '&type=factory' })
}
function viewLogistics(row) {
uni.navigateTo({ url: '/pages/order/logistics?id=' + row.id })
}
function handleAccept(row) {
uni.showModal({
title: '确认接单',
content: '确定接受该制片单吗?',
success: (res) => {
if (res.confirm) {
acceptPortalOrder(row.id).then(() => {
uni.showToast({ title: '接单成功', icon: 'success' })
handleQuery()
})
}
}
})
}
function handleSupplement(row) {
supplementForm.value = { orderId: row.id, orderNo: row.orderNo, remark: '' }
supplementVisible.value = true
}
function submitSupplement() {
if (!supplementForm.value.remark?.trim()) {
return uni.showToast({ title: '请输入补充内容', icon: 'none' })
}
supplementPortalOrder({
orderId: supplementForm.value.orderId,
remark: supplementForm.value.remark.trim()
}).then(() => {
supplementVisible.value = false
uni.showToast({ title: '补充成功', icon: 'success' })
handleQuery()
})
}
function handleProduce(row) {
uni.showModal({
title: '确认操作',
content: '确定将该订单标记为制作中吗?',
success: (res) => {
if (res.confirm) {
producePortalOrder(row.id).then(() => {
uni.showToast({ title: '已标记制作中', icon: 'success' })
handleQuery()
})
}
}
})
}
function handleShip(row) {
shipForm.value = { orderId: row.id, expressCompany: '', expressNo: '', logisticsFee: '' }
shipVisible.value = true
}
function submitShip() {
if (!shipForm.value.expressCompany || !shipForm.value.expressNo) {
return uni.showToast({ title: '请填写物流信息', icon: 'none' })
}
shipPortalOrder(shipForm.value).then(() => {
shipVisible.value = false
uni.showToast({ title: '发货成功', icon: 'success' })
handleQuery()
})
}
function handleComplete(row) {
uni.showModal({
title: '确认完成',
content: '确定标记该订单为已完成吗?',
success: (res) => {
if (res.confirm) {
completePortalOrder(row.id).then(() => {
uni.showToast({ title: '操作成功', icon: 'success' })
handleQuery()
})
}
}
})
}
function handleWithdraw(row) {
withdrawForm.value = { orderId: row.id, orderNo: row.orderNo, reason: '' }
withdrawVisible.value = true
}
function submitWithdraw() {
if (!withdrawForm.value.reason?.trim()) {
return uni.showToast({ title: '请输入撤单理由', icon: 'none' })
}
withdrawPortalOrder({
orderId: withdrawForm.value.orderId,
reason: withdrawForm.value.reason.trim()
}).then(() => {
withdrawVisible.value = false
uni.showToast({ title: '撤单成功', icon: 'success' })
handleQuery()
})
}
</script>
<style lang="scss" scoped>
.page-header { margin-bottom: 24rpx; }
.page-title { display: block; font-size: 36rpx; font-weight: bold; }
.tenant-name { font-size: 24rpx; color: #64748b; }
.search-bar { display: flex; gap: 12rpx; align-items: center; margin-bottom: 20rpx; }
.search-input { flex: 1; font-size: 26rpx; }
.status-picker { font-size: 26rpx; color: #64748b; padding: 8rpx 16rpx; background: #f1f5f9; border-radius: 8rpx; }
.search-btn { background: #2563eb; color: #fff; }
.order-card { margin-bottom: 20rpx; }
.order-head { margin-bottom: 12rpx; }
.order-no { font-size: 28rpx; font-weight: 600; }
.order-info { display: flex; flex-direction: column; gap: 8rpx; font-size: 24rpx; color: #64748b; margin-bottom: 16rpx; }
.order-actions { display: flex; flex-wrap: wrap; gap: 16rpx; border-top: 1rpx solid #f1f5f9; padding-top: 16rpx; }
.action-link { font-size: 26rpx; color: #64748b; padding: 4rpx 0; &.primary { color: #2563eb; } &.success { color: #22c55e; } &.warning { color: #f59e0b; } }
.load-more { text-align: center; padding: 24rpx; color: #2563eb; font-size: 26rpx; }
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 999; }
.modal-content { background: #fff; border-radius: 16rpx; padding: 32rpx; width: 80%; }
.modal-title { display: block; font-size: 32rpx; font-weight: bold; margin-bottom: 12rpx; }
.modal-subtitle { display: block; font-size: 24rpx; color: #64748b; margin-bottom: 20rpx; }
.form-item { background: #f8fafc; border-radius: 8rpx; padding: 0 20rpx; margin-bottom: 16rpx; input { height: 80rpx; font-size: 28rpx; } }
.remark-input { width: 100%; height: 200rpx; background: #f8fafc; border-radius: 8rpx; padding: 16rpx; font-size: 28rpx; margin-bottom: 16rpx; box-sizing: border-box; }
.modal-actions { display: flex; gap: 16rpx; margin-top: 24rpx; button { flex: 1; } }
</style>

View File

@@ -0,0 +1,391 @@
<template>
<view class="page container">
<view v-if="!pageReady" class="empty-tip">加载中...</view>
<template v-else>
<view class="section card">
<text class="section-title">产品与镜片</text>
<view class="form-item">
<text class="label">产品</text>
<radio-group @change="onProductChange">
<label v-for="item in normalizedProductOptions" :key="item.id" class="radio-item">
<radio :value="String(item.id)" :checked="form.productId === item.id" />
<view class="option-text">
<text class="option-main">{{ formatProductOption(item) }}</text>
</view>
</label>
</radio-group>
<text v-if="!normalizedProductOptions.length" class="text-muted">暂无可选产品</text>
</view>
<view class="form-item">
<text class="label">通用镜片</text>
<radio-group @change="onLensChange">
<label v-for="item in filteredLensOptions" :key="item.id" class="radio-item">
<radio :value="String(item.id)" :checked="form.lensId === item.id" />
<view class="option-text">
<text class="option-main">{{ formatLensOption(item) }}</text>
</view>
</label>
</radio-group>
<text v-if="!filteredLensOptions.length" class="text-muted">
{{ form.productId ? '当前产品暂无可选镜片' : '请先选择产品' }}
</text>
</view>
<view class="form-item switch-row">
<text class="label">渐进多焦点</text>
<switch :checked="form.progressiveMultifocal === 1" @change="onProgressiveMultifocalChange" />
</view>
</view>
<view class="section card">
<text class="section-title">收货信息</text>
<view class="form-item"><input v-model="form.receiverName" placeholder="收货人姓名" /></view>
<view class="form-item"><input v-model="form.receiverPhone" placeholder="收货人手机号" maxlength="11" /></view>
<view class="form-item"><input v-model="form.receiverAddress" placeholder="收货地址" /></view>
</view>
<view class="section card">
<text class="section-title">验光参数</text>
<view class="pd-mode-row">
<text class="label">瞳距类型</text>
<radio-group :disabled="form.progressiveMultifocal === 1" @change="onPdModeChange">
<label class="pd-mode-item">
<radio value="single" :checked="pdMode === 'single'" :disabled="form.progressiveMultifocal === 1" />
<text>单瞳距</text>
</label>
<label class="pd-mode-item">
<radio value="dual" :checked="pdMode === 'dual'" :disabled="form.progressiveMultifocal === 1" />
<text>双瞳距</text>
</label>
</radio-group>
<text class="pd-mode-tip">{{ pdModeTip }}</text>
</view>
<view v-for="(eye, idx) in form.optometryList" :key="idx" class="eye-block">
<text class="eye-label">{{ eye.eyeSide === 'OS' ? '左眼(OS)' : '右眼(OD)' }}</text>
<view class="eye-grid">
<view class="grid-item"><text>球镜S</text><input v-model="eye.sphereS" type="digit" @blur="onSphereSChange(eye)" /></view>
<view class="grid-item"><text>柱镜C</text><input v-model="eye.cylinderC" type="digit" @blur="onCylinderCChange(eye)" /></view>
<view class="grid-item" :class="{ 'is-error': isAxisAInvalid(eye) }">
<text>轴位A</text>
<input v-model="eye.axisA" type="number" :disabled="!isAxisAEditable(eye.cylinderC)" @blur="onAxisAChange(eye)" />
<text v-if="isAxisAInvalid(eye)" class="field-error-tip">轴位不能为0</text>
</view>
<view v-if="pdMode === 'dual'" class="grid-item"><text>双瞳距</text><input v-model="eye.dualPd" type="digit" /></view>
<view v-else-if="idx === 0" class="grid-item grid-item--span2"><text>单瞳距</text><input v-model="singlePd" type="digit" @blur="onSinglePdChange" /></view>
<view v-if="form.progressiveMultifocal === 1" class="grid-item"><text>瞳高</text><input v-model="eye.pupilHeight" type="digit" /></view>
<view
v-if="form.progressiveMultifocal === 1"
class="grid-item"
:class="{ 'is-error': isNearAddInvalid(eye) }"
>
<text>近用ADD</text>
<input v-model="eye.nearAdd" type="digit" placeholder="+0.75~+4.00" @blur="onNearAddChange(eye)" />
<text v-if="isNearAddInvalid(eye)" class="field-error-tip">ADD不能为0</text>
</view>
</view>
</view>
</view>
<view class="section card">
<text class="section-title">备注</text>
<textarea v-model="form.remark" placeholder="备注信息" class="remark-input" />
</view>
<button class="btn-primary submit-btn" :loading="submitting" @click="handleSubmit"> </button>
</template>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import {
createEmptyForm,
formatProductOption,
formatLensOption,
filterLensOptionsByProduct,
isAxisAEditable,
applyProgressiveMultifocalState,
alignCylinderWithSphere,
validateCylinderSignWithSphere,
clampSinglePd,
clampDualPd,
SINGLE_PD_DEFAULT,
DUAL_PD_DEFAULT,
normalizeOrderForm,
unwrapOrderResponse
} from '@/utils/orderHelper.js'
import { listPortalProductOptions, getPortalOrder, addPortalOrder, updatePortalOrder } from '@/api/order.js'
import { listLensOptions } from '@/api/dict.js'
const mode = ref('create')
const orderId = ref(null)
const pageReady = ref(false)
const submitting = ref(false)
const form = ref(createEmptyForm())
const pdMode = ref('single')
const singlePd = ref(SINGLE_PD_DEFAULT)
const productOptions = ref([])
const lensOptions = ref([])
const pdModeTip = computed(() => {
return pdMode.value === 'dual'
? '双瞳距范围20.00 ~ 40.00 mm左右眼分别填写'
: '单瞳距范围40.00 ~ 80.00 mm左右眼共用一个值'
})
const normalizedProductOptions = computed(() => {
return (productOptions.value || []).map(item => ({
...item,
id: item.productId != null ? Number(item.productId) : (item.id != null ? Number(item.id) : null)
})).filter(item => item.id != null && !Number.isNaN(item.id))
})
const filteredLensOptions = computed(() => {
return filterLensOptionsByProduct(
lensOptions.value,
normalizedProductOptions.value,
form.value.productId,
form.value.lensId,
{
productName: form.value.lensProductName,
material: form.value.lensMaterial
}
)
})
watch(() => form.value.productId, (val, oldVal) => {
if (val === oldVal) return
const allowedIds = filteredLensOptions.value.map(item => item.id)
if (form.value.lensId != null && !allowedIds.includes(Number(form.value.lensId))) {
form.value.lensId = null
}
})
function syncPdModeFromForm() {
const state = applyProgressiveMultifocalState(form.value)
pdMode.value = state.pdMode
singlePd.value = state.singlePd != null ? clampSinglePd(state.singlePd) : SINGLE_PD_DEFAULT
}
onLoad((options) => {
mode.value = options?.mode || 'create'
orderId.value = options?.id || null
if (mode.value === 'edit') {
uni.setNavigationBarTitle({ title: '修改配片单' })
}
loadPageData()
})
async function loadPageData() {
try {
const [products, lenses] = await Promise.all([
listPortalProductOptions().then(r => r.data || r.rows || []),
listLensOptions().then(r => r.data || r.rows || [])
])
productOptions.value = products
lensOptions.value = (lenses || []).map(item => ({
...item,
id: item && item.id != null ? Number(item.id) : null
}))
if (orderId.value) {
const res = await getPortalOrder(orderId.value)
form.value = normalizeOrderForm(unwrapOrderResponse(res))
syncPdModeFromForm()
}
} catch (e) {
uni.showToast({ title: e?.message || '加载失败', icon: 'none' })
} finally {
pageReady.value = true
}
}
function onProductChange(e) {
form.value.productId = Number(e.detail.value)
}
function onLensChange(e) {
form.value.lensId = Number(e.detail.value)
}
function onProgressiveMultifocalChange(e) {
form.value.progressiveMultifocal = e.detail.value ? 1 : 0
syncPdModeFromForm()
}
function onPdModeChange(e) {
if (form.value.progressiveMultifocal === 1) return
pdMode.value = e.detail.value
if (pdMode.value === 'dual') {
form.value.optometryList.forEach(eye => {
eye.dualPd = clampDualPd(eye.dualPd != null ? eye.dualPd : DUAL_PD_DEFAULT)
eye.pd = null
eye.pdValue = null
eye.pdType = '2'
})
form.value.pdType = '2'
} else {
const pd = clampSinglePd(singlePd.value)
singlePd.value = pd
form.value.optometryList.forEach(eye => {
eye.pd = pd
eye.pdValue = pd
eye.dualPd = null
eye.pdType = '1'
})
form.value.pdType = '1'
}
}
function onSinglePdChange() {
if (pdMode.value !== 'single') return
const pd = clampSinglePd(singlePd.value)
singlePd.value = pd
form.value.optometryList.forEach(eye => {
eye.pd = pd
eye.pdValue = pd
})
}
function onSphereSChange(eye) {
eye.cylinderC = alignCylinderWithSphere(eye.sphereS, eye.cylinderC)
if (!isAxisAEditable(eye.cylinderC)) {
eye.axisA = null
}
}
function onCylinderCChange(eye) {
eye.cylinderC = alignCylinderWithSphere(eye.sphereS, eye.cylinderC)
if (!isAxisAEditable(eye.cylinderC)) {
eye.axisA = null
}
}
function isAxisAInvalid(eye) {
if (!isAxisAEditable(eye.cylinderC)) return false
if (eye.axisA == null || eye.axisA === '') return true
const axis = Number(eye.axisA)
return Number.isNaN(axis) || axis === 0
}
function onAxisAChange(eye) {
if (!isAxisAEditable(eye.cylinderC)) {
eye.axisA = null
return
}
if (eye.axisA == null || eye.axisA === '') {
eye.axisA = 0
return
}
const axis = Number(eye.axisA)
if (Number.isNaN(axis)) {
eye.axisA = 0
return
}
eye.axisA = Math.min(180, Math.max(0, axis))
}
function validate() {
if (!form.value.productId) return '请选择产品'
if (!form.value.lensId) return '请选择通用镜片'
if (!form.value.receiverName) return '请输入收货人姓名'
if (!/^1[3-9]\d{9}$/.test(form.value.receiverPhone)) return '请输入正确的手机号'
if (!form.value.receiverAddress) return '请输入收货地址'
const cylinderErr = validateCylinderSignWithSphere(form.value.optometryList)
if (cylinderErr) return cylinderErr
if (pdMode.value === 'dual') {
for (const eye of form.value.optometryList || []) {
if (eye.dualPd == null || eye.dualPd === '') continue
const num = Number(eye.dualPd)
if (Number.isNaN(num) || num < 20 || num > 40) {
return '双瞳距范围应为20.0040.00'
}
}
} else if (singlePd.value != null && singlePd.value !== '') {
const num = Number(singlePd.value)
if (Number.isNaN(num) || num < 40 || num > 80) {
return '单瞳距范围应为40.0080.00'
}
}
for (const eye of form.value.optometryList || []) {
if (isAxisAInvalid(eye)) {
const eyeLabel = eye.eyeSide === 'OS' ? '左眼' : '右眼'
return `${eyeLabel}有散光时轴位不能为0请填写1~180`
}
}
if (form.value.progressiveMultifocal === 1) {
for (const eye of form.value.optometryList || []) {
if (eye.nearAdd == null || eye.nearAdd === '' || Number(eye.nearAdd) === 0) {
const eyeLabel = eye.eyeSide === 'OS' ? '左眼' : '右眼'
return `${eyeLabel}选择渐进多焦点时ADD不能为0请填写+0.75+4.00`
}
const num = Number(eye.nearAdd)
if (Number.isNaN(num) || num < 0.75 || num > 4) {
return '近用ADD范围应在+0.75+4.00之间'
}
}
}
return null
}
function isNearAddInvalid(eye) {
if (form.value.progressiveMultifocal !== 1) return false
if (eye.nearAdd == null || eye.nearAdd === '') return true
const num = Number(eye.nearAdd)
return Number.isNaN(num) || num === 0 || num < 0.75 || num > 4
}
function onNearAddChange(eye) {
if (form.value.progressiveMultifocal !== 1) {
eye.nearAdd = null
return
}
if (eye.nearAdd == null || eye.nearAdd === '') return
const num = Number(eye.nearAdd)
if (Number.isNaN(num) || num === 0) {
eye.nearAdd = null
return
}
eye.nearAdd = Math.min(4, Math.max(0.75, num))
}
async function handleSubmit() {
const err = validate()
if (err) return uni.showToast({ title: err, icon: 'none' })
submitting.value = true
try {
const payload = normalizeOrderForm(form.value)
const api = mode.value === 'edit' ? updatePortalOrder : addPortalOrder
await api(payload)
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 800)
} finally {
submitting.value = false
}
}
</script>
<style lang="scss" scoped>
.section { margin-bottom: 24rpx; }
.section-title { display: block; font-size: 30rpx; font-weight: 600; margin-bottom: 20rpx; color: #2563eb; }
.form-item { margin-bottom: 16rpx; input { height: 80rpx; background: #f8fafc; border-radius: 8rpx; padding: 0 20rpx; font-size: 28rpx; } }
.label { display: block; font-size: 26rpx; color: #64748b; margin-bottom: 12rpx; }
.radio-item { display: flex; align-items: flex-start; gap: 12rpx; padding: 16rpx 0; border-bottom: 1rpx solid #f1f5f9; }
.option-text { flex: 1; }
.option-main { display: block; font-size: 28rpx; color: #1e293b; line-height: 1.5; }
.switch-row { display: flex; justify-content: space-between; align-items: center; }
.pd-mode-row { display: flex; align-items: center; gap: 24rpx; margin-bottom: 16rpx; flex-wrap: wrap; }
.pd-mode-item { display: inline-flex; align-items: center; gap: 8rpx; margin-right: 24rpx; font-size: 26rpx; color: #334155; }
.pd-mode-tip { width: 100%; font-size: 22rpx; color: #94a3b8; }
.eye-block { margin-bottom: 24rpx; padding: 16rpx; background: #f8fafc; border-radius: 12rpx; }
.eye-label { display: block; font-weight: 600; margin-bottom: 12rpx; }
.eye-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; }
.grid-item { text { display: block; font-size: 22rpx; color: #64748b; margin-bottom: 4rpx; } input { height: 64rpx; background: #fff; border-radius: 8rpx; padding: 0 12rpx; font-size: 26rpx; border: 1rpx solid transparent; box-sizing: border-box; } input:disabled { background: #f1f5f9; color: #94a3b8; } }
.grid-item.is-error input { border-color: #f56c6c; background: #fef0f0; }
.field-error-tip { display: block; margin-top: 4rpx; font-size: 20rpx; color: #f56c6c; }
.grid-item--span2 { grid-column: span 2; }
.remark-input { width: 100%; height: 160rpx; background: #f8fafc; border-radius: 8rpx; padding: 16rpx; font-size: 28rpx; }
.submit-btn { width: 100%; margin: 32rpx 0; }
.text-muted { font-size: 24rpx; color: #94a3b8; }
</style>

View File

@@ -0,0 +1,228 @@
<template>
<view class="page">
<NavBar />
<view class="container">
<view class="page-header flex-between">
<view>
<text class="page-title">我的配片单</text>
<text v-if="userStore.tenantName" class="tenant-name">{{ userStore.tenantName }}</text>
</view>
<button v-if="canManage" class="add-btn" size="mini" @click="handleAdd">+ 新增</button>
</view>
<!-- 搜索 -->
<view class="search-bar card">
<input v-model="queryParams.orderNo" placeholder="订单编号" class="search-input" />
<picker :range="statusLabels" :value="statusIndex" @change="onStatusChange">
<view class="status-picker">{{ statusLabels[statusIndex] || '全部状态' }}</view>
</picker>
<button size="mini" class="search-btn" @click="handleQuery">搜索</button>
</view>
<!-- 列表 -->
<view v-if="loading && !orderList.length" class="empty-tip">加载中...</view>
<view v-else-if="!orderList.length" class="empty-tip">暂无配片单</view>
<view v-for="item in orderList" :key="item.id" class="order-card card" @click="handleDetail(item)">
<view class="flex-between order-head">
<text class="order-no">{{ item.orderNo }}</text>
<DictTag :options="orderStatusDict" :value="item.orderStatus" />
</view>
<view class="order-info">
<text>镜片{{ formatLens(item) }}</text>
<text>收货人{{ item.receiverName }} {{ item.receiverPhone }}</text>
<text>创建{{ item.createTime }}</text>
</view>
<view class="order-actions" @click.stop>
<text class="action-link" @click="handleDetail(item)">详情</text>
<text v-if="canViewLogistics(item)" class="action-link" @click="viewLogistics(item)">物流</text>
<text v-if="canCompleteOrder(item)" class="action-link success" @click="handleComplete(item)">完成</text>
<text v-if="canReceiveOrder(item)" class="action-link success" @click="handleReceive(item)">接收</text>
<text v-if="canManage && canEditOrder(item)" class="action-link" @click="handleEdit(item)">修改</text>
<text v-if="canManage && canEditOrder(item)" class="action-link primary" @click="handleSubmitDraft(item)">提交</text>
<text v-if="canManage && canDeleteOrder(item)" class="action-link danger" @click="handleDelete(item)">删除</text>
</view>
</view>
<view v-if="hasMore" class="load-more" @click="loadMore">加载更多</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { useUserStore, useDictStore } from '@/store/index.js'
import { isBrandTenant } from '@/utils/tenantType.js'
import { ORDER_STATUS, formatLens } from '@/utils/orderHelper.js'
import {
listPortalOrder, delPortalOrder, completePortalOrder, receivePortalOrder, updatePortalOrder
} from '@/api/order.js'
import NavBar from '@/components/NavBar/NavBar.vue'
import DictTag from '@/components/DictTag/DictTag.vue'
const userStore = useUserStore()
const dictStore = useDictStore()
const loading = ref(false)
const orderList = ref([])
const total = ref(0)
const orderStatusDict = ref([])
const statusIndex = ref(0)
const queryParams = ref({ pageNum: 1, pageSize: 10, orderNo: null, orderStatus: null })
const canManage = computed(() => isBrandTenant(userStore.tenantType))
const statusLabels = computed(() => ['全部状态', ...orderStatusDict.value.map(d => d.dictLabel)])
const hasMore = computed(() => orderList.value.length < total.value)
onMounted(async () => {
orderStatusDict.value = await dictStore.getDict('jy_order_status')
getList()
})
onPullDownRefresh(() => {
queryParams.value.pageNum = 1
getList().finally(() => uni.stopPullDownRefresh())
})
onReachBottom(() => loadMore())
function onStatusChange(e) {
statusIndex.value = Number(e.detail.value)
queryParams.value.orderStatus = statusIndex.value === 0 ? null : orderStatusDict.value[statusIndex.value - 1]?.dictValue
}
function getList(append = false) {
loading.value = true
return listPortalOrder(queryParams.value).then(res => {
orderList.value = append ? [...orderList.value, ...(res.rows || [])] : (res.rows || [])
total.value = res.total || 0
}).finally(() => { loading.value = false })
}
function handleQuery() {
queryParams.value.pageNum = 1
getList()
}
function loadMore() {
if (!hasMore.value || loading.value) return
queryParams.value.pageNum++
getList(true)
}
function canEditOrder(row) {
return String(row.orderStatus) === ORDER_STATUS.DRAFT
}
function canDeleteOrder(row) {
return String(row.orderStatus) === ORDER_STATUS.DRAFT
}
function canViewLogistics(row) {
const s = String(row.orderStatus)
return [ORDER_STATUS.SHIPPED, ORDER_STATUS.COMPLETED, ORDER_STATUS.EXCHANGE_LOGISTICS, ORDER_STATUS.EXCHANGE_COMPLETED].includes(s)
}
function canCompleteOrder(row) {
return String(row.orderStatus) === ORDER_STATUS.SHIPPED
}
function canReceiveOrder(row) {
return String(row.orderStatus) === ORDER_STATUS.EXCHANGE_REJECTED
}
function handleAdd() {
uni.navigateTo({ url: '/pages/order/lens-order-form?mode=create' })
}
function handleEdit(row) {
uni.navigateTo({ url: '/pages/order/lens-order-form?mode=edit&id=' + row.id })
}
function handleDetail(row) {
uni.navigateTo({ url: '/pages/order/order-detail?id=' + row.id + '&type=lens' })
}
function viewLogistics(row) {
uni.navigateTo({ url: '/pages/order/logistics?id=' + row.id })
}
function handleSubmitDraft(row) {
uni.showModal({
title: '确认提交',
content: '确定提交该配片单吗?',
success: (res) => {
if (res.confirm) {
updatePortalOrder({ ...row, orderStatus: ORDER_STATUS.SUBMIT }).then(() => {
uni.showToast({ title: '提交成功', icon: 'success' })
handleQuery()
})
}
}
})
}
function handleComplete(row) {
uni.showModal({
title: '确认完成',
content: '确定标记该订单为已完成吗?',
success: (res) => {
if (res.confirm) {
completePortalOrder(row.id).then(() => {
uni.showToast({ title: '操作成功', icon: 'success' })
handleQuery()
})
}
}
})
}
function handleReceive(row) {
uni.showModal({
title: '确认接收',
content: '确定接收该订单吗?',
success: (res) => {
if (res.confirm) {
receivePortalOrder(row.id).then(() => {
uni.showToast({ title: '操作成功', icon: 'success' })
handleQuery()
})
}
}
})
}
function handleDelete(row) {
uni.showModal({
title: '确认删除',
content: '确定删除该配片单吗?',
success: (res) => {
if (res.confirm) {
delPortalOrder(row.id).then(() => {
uni.showToast({ title: '删除成功', icon: 'success' })
handleQuery()
})
}
}
})
}
</script>
<style lang="scss" scoped>
.page-header { margin-bottom: 24rpx; }
.page-title { display: block; font-size: 36rpx; font-weight: bold; }
.tenant-name { font-size: 24rpx; color: #64748b; }
.add-btn { background: #2563eb; color: #fff; font-size: 24rpx; border-radius: 8rpx; }
.search-bar { display: flex; gap: 12rpx; align-items: center; margin-bottom: 20rpx; }
.search-input { flex: 1; font-size: 26rpx; }
.status-picker { font-size: 26rpx; color: #64748b; padding: 8rpx 16rpx; background: #f1f5f9; border-radius: 8rpx; }
.search-btn { background: #2563eb; color: #fff; }
.order-card { margin-bottom: 20rpx; }
.order-head { margin-bottom: 12rpx; }
.order-no { font-size: 28rpx; font-weight: 600; }
.order-info { display: flex; flex-direction: column; gap: 8rpx; font-size: 24rpx; color: #64748b; margin-bottom: 16rpx; }
.order-actions { display: flex; flex-wrap: wrap; gap: 16rpx; border-top: 1rpx solid #f1f5f9; padding-top: 16rpx; }
.action-link { font-size: 26rpx; color: #64748b; &.primary { color: #2563eb; } &.success { color: #22c55e; } &.danger { color: #ef4444; } }
.load-more { text-align: center; padding: 24rpx; color: #2563eb; font-size: 26rpx; }
</style>

View File

@@ -0,0 +1,69 @@
<template>
<view class="page container">
<view v-if="loading" class="empty-tip">加载中...</view>
<view v-else-if="loadError" class="empty-tip">{{ loadError }}</view>
<template v-else>
<view v-if="basic" class="card">
<text class="section-title">物流概要</text>
<view class="detail-row"><text class="label">订单编号</text><text>{{ basic.orderNo }}</text></view>
<view class="detail-row"><text class="label">物流公司</text><text>{{ basic.expressCompany || '-' }}</text></view>
<view class="detail-row"><text class="label">物流单号</text><text>{{ basic.expressNo || '-' }}</text></view>
</view>
<view class="card">
<text class="section-title">物流轨迹</text>
<view v-if="traces.length" class="timeline">
<view v-for="(item, idx) in traces" :key="idx" class="timeline-item">
<view class="dot" :class="{ first: idx === 0 }" />
<view class="timeline-content">
<text class="timeline-text">{{ item.line }}</text>
<text class="timeline-time">{{ item.timeDesc }}</text>
</view>
</view>
</view>
<view v-else class="empty-tip">未查到物流轨迹</view>
</view>
</template>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getPortalOrderLogistics } from '@/api/order.js'
import { unwrapOrderResponse } from '@/utils/orderHelper.js'
const loading = ref(true)
const loadError = ref('')
const basic = ref(null)
const traces = ref([])
onLoad((options) => {
const id = options?.id
if (!id) {
loading.value = false
loadError.value = '缺少订单ID'
return
}
getPortalOrderLogistics(id).then(res => {
const data = unwrapOrderResponse(res) || {}
basic.value = data.basic || data
traces.value = (data.detail && data.detail.traces) || data.traces || []
}).catch((e) => {
loadError.value = e?.message || '加载失败'
}).finally(() => { loading.value = false })
})
</script>
<style lang="scss" scoped>
.section-title { display: block; font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx; color: #2563eb; }
.detail-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #f1f5f9; font-size: 28rpx; .label { color: #64748b; } }
.timeline { padding-left: 24rpx; }
.timeline-item { display: flex; gap: 20rpx; padding-bottom: 32rpx; position: relative;
&::before { content: ''; position: absolute; left: 11rpx; top: 24rpx; bottom: 0; width: 2rpx; background: #e2e8f0; }
&:last-child::before { display: none; }
}
.dot { width: 24rpx; height: 24rpx; border-radius: 50%; background: #e2e8f0; flex-shrink: 0; margin-top: 4rpx; &.first { background: #2563eb; } }
.timeline-text { display: block; font-size: 28rpx; color: #334155; }
.timeline-time { display: block; font-size: 24rpx; color: #94a3b8; margin-top: 8rpx; }
</style>

View File

@@ -0,0 +1,105 @@
<template>
<view class="page container">
<view v-if="loading" class="empty-tip">加载中...</view>
<view v-else-if="loadError" class="empty-tip">{{ loadError }}</view>
<template v-else>
<view class="card">
<view class="detail-row"><text class="label">订单编号</text><text>{{ detail.orderNo }}</text></view>
<view class="detail-row"><text class="label">订单状态</text><DictTag :options="orderStatusDict" :value="detail.orderStatus" /></view>
<view class="detail-row"><text class="label">通用镜片</text><text>{{ formatLens(detail) }}</text></view>
<view class="detail-row"><text class="label">渐进多焦点</text><text>{{ detail.progressiveMultifocal === 1 ? '是' : '否' }}</text></view>
<view class="detail-row"><text class="label">备注</text><text>{{ detail.remark || '-' }}</text></view>
</view>
<view class="card">
<text class="section-title">收货信息</text>
<view class="detail-row"><text class="label">收货人</text><text>{{ detail.receiverName }}</text></view>
<view class="detail-row"><text class="label">手机号</text><text>{{ detail.receiverPhone }}</text></view>
<view class="detail-row"><text class="label">地址</text><text>{{ detail.receiverAddress }}</text></view>
</view>
<view class="card">
<text class="section-title">验光参数</text>
<view v-for="(eye, idx) in detailOptometryList" :key="idx" class="eye-row">
<text class="eye-side">{{ eye.eyeSide === 'OS' ? '左眼' : '右眼' }}</text>
<text>S: {{ eye.sphereS ?? '-' }} | C: {{ eye.cylinderC ?? '-' }} | A: {{ eye.axisA ?? '-' }}</text>
<text>瞳距: {{ formatDetailPd(eye) }}</text>
<text v-if="detail.progressiveMultifocal === 1">瞳高: {{ eye.pupilHeight ?? '-' }}</text>
<text v-if="detail.progressiveMultifocal === 1 && eye.nearAdd != null && eye.nearAdd !== ''">近用ADD: {{ formatNearAdd(eye.nearAdd) }}</text>
</view>
</view>
<view v-if="detail.statusLogList && detail.statusLogList.length" class="card">
<text class="section-title">状态记录</text>
<view v-for="(log, idx) in detail.statusLogList" :key="idx" class="log-row">
<DictTag :options="orderStatusDict" :value="log.orderStatus" />
<text class="log-time">{{ log.operateTime }}</text>
<text v-if="log.remark" class="log-remark">{{ log.remark }}</text>
</view>
</view>
</template>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getPortalOrder } from '@/api/order.js'
import { useDictStore } from '@/store/index.js'
import { formatLens, formatNearAdd, unwrapOrderResponse } from '@/utils/orderHelper.js'
import DictTag from '@/components/DictTag/DictTag.vue'
const loading = ref(true)
const loadError = ref('')
const detail = ref({})
const orderStatusDict = ref([])
const dictStore = useDictStore()
const detailOptometryList = computed(() => {
const list = detail.value.optometryList || []
return [...list].sort((a, b) => (a.eyeSide === 'OS' ? -1 : 1))
})
function formatDetailPd(eye) {
if (eye.dualPd != null && eye.dualPd !== '') return eye.dualPd
if (eye.pd != null && eye.pd !== '') return eye.pd
if (eye.pdValue != null && eye.pdValue !== '') return eye.pdValue
return '-'
}
async function loadDetail(id) {
loading.value = true
loadError.value = ''
try {
orderStatusDict.value = await dictStore.getDict('jy_order_status')
const res = await getPortalOrder(id)
const data = unwrapOrderResponse(res)
if (!data || !data.id) {
loadError.value = '未获取到订单详情'
return
}
detail.value = data
} catch (e) {
loadError.value = e?.message || '加载失败'
} finally {
loading.value = false
}
}
onLoad((options) => {
const id = options?.id
if (!id) {
loading.value = false
loadError.value = '缺少订单ID'
return
}
loadDetail(id)
})
</script>
<style lang="scss" scoped>
.detail-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #f1f5f9; font-size: 28rpx; .label { color: #64748b; flex-shrink: 0; margin-right: 24rpx; } }
.section-title { display: block; font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx; color: #2563eb; }
.eye-row { padding: 16rpx 0; border-bottom: 1rpx solid #f1f5f9; font-size: 26rpx; color: #64748b; .eye-side { display: block; font-weight: 600; color: #334155; margin-bottom: 8rpx; } }
.log-row { padding: 16rpx 0; border-bottom: 1rpx solid #f1f5f9; .log-time { display: block; font-size: 24rpx; color: #94a3b8; margin-top: 8rpx; } .log-remark { font-size: 26rpx; color: #64748b; } }
</style>

View File

@@ -0,0 +1,134 @@
<template>
<view class="page container">
<view class="tabs">
<view :class="['tab', activeTab === 'password' && 'active']" @click="activeTab = 'password'">密码注册</view>
<view :class="['tab', activeTab === 'code' && 'active']" @click="activeTab = 'code'">验证码注册</view>
</view>
<view v-if="activeTab === 'password'" class="form">
<view class="form-item"><input v-model="form.username" placeholder="用户名" /></view>
<view class="form-item"><input v-model="form.password" password placeholder="密码" /></view>
<view class="form-item"><input v-model="form.confirmPassword" password placeholder="确认密码" /></view>
<view v-if="captchaEnabled" class="form-item code-row">
<input v-model="form.code" placeholder="验证码" class="code-input" />
<image :src="codeUrl" class="code-img" @click="getCode" mode="aspectFit" />
</view>
<button class="btn-primary" :loading="loading" @click="handleRegister"> </button>
</view>
<view v-else class="form">
<view class="form-item"><input v-model="codeForm.username" placeholder="手机号或邮箱" /></view>
<view class="form-item"><input v-model="codeForm.password" password placeholder="设置密码" /></view>
<view class="form-item code-row">
<input v-model="codeForm.code" placeholder="验证码" class="code-input" />
<button class="send-code-btn" :disabled="countdown > 0" @click="sendCode">{{ countdown > 0 ? countdown + 's' : '发送' }}</button>
</view>
<button class="btn-primary" :loading="loading" @click="handleCodeRegister"> </button>
</view>
<view class="links">
<text>已有账户</text>
<text class="link" @click="goLogin">立即登录</text>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { register, registerByCode, getCodeImg, sendSmsCode, sendEmailCode } from '@/api/login.js'
const activeTab = ref('password')
const loading = ref(false)
const captchaEnabled = ref(true)
const codeUrl = ref('')
const countdown = ref(0)
let timer = null
const form = ref({ username: '', password: '', confirmPassword: '', code: '', uuid: '' })
const codeForm = ref({ username: '', password: '', code: '', uuid: '' })
onMounted(() => getCode())
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled !== false
if (captchaEnabled.value) {
codeUrl.value = 'data:image/gif;base64,' + res.img
form.value.uuid = res.uuid
}
})
}
function isPhone(v) { return /^1[3-9]\d{9}$/.test(v) }
function sendCode() {
if (!codeForm.value.username) return uni.showToast({ title: '请输入手机号或邮箱', icon: 'none' })
const p = isPhone(codeForm.value.username) ? sendSmsCode(codeForm.value.username) : sendEmailCode(codeForm.value.username)
p.then(res => {
codeForm.value.uuid = res.data?.uuid || res.uuid
countdown.value = 60
timer = setInterval(() => { countdown.value--; if (countdown.value <= 0) clearInterval(timer) }, 1000)
})
}
async function handleRegister() {
if (form.value.password !== form.value.confirmPassword) {
return uni.showToast({ title: '两次密码不一致', icon: 'none' })
}
loading.value = true
try {
await register(form.value)
uni.showToast({ title: '注册成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1000)
} finally {
loading.value = false
getCode()
}
}
async function handleCodeRegister() {
loading.value = true
try {
await registerByCode(codeForm.value)
uni.showToast({ title: '注册成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1000)
} finally {
loading.value = false
}
}
function goLogin() {
uni.navigateBack()
}
</script>
<style lang="scss" scoped>
.tabs {
display: flex;
margin-bottom: 32rpx;
background: #f1f5f9;
border-radius: 12rpx;
padding: 6rpx;
}
.tab {
flex: 1;
text-align: center;
padding: 16rpx;
font-size: 28rpx;
&.active { background: #fff; color: #2563eb; font-weight: 600; border-radius: 8rpx; }
}
.form-item {
background: #fff;
border-radius: 12rpx;
padding: 0 24rpx;
margin-bottom: 20rpx;
border: 2rpx solid #e2e8f0;
input { height: 88rpx; font-size: 28rpx; }
}
.code-row { display: flex; align-items: center; gap: 16rpx; padding: 12rpx 24rpx; }
.code-input { flex: 1; }
.code-img { width: 200rpx; height: 72rpx; }
.send-code-btn { font-size: 24rpx; background: #2563eb; color: #fff; border-radius: 8rpx; padding: 0 20rpx; }
.btn-primary { width: 100%; margin-top: 16rpx; }
.links { text-align: center; margin-top: 32rpx; font-size: 26rpx; .link { color: #2563eb; margin-left: 8rpx; } }
</style>

View File

@@ -0,0 +1,82 @@
<template>
<view class="page container">
<view class="tabs">
<view :class="['tab', activeTab === 'account' && 'active']" @click="activeTab = 'account'">账号重置</view>
<view :class="['tab', activeTab === 'code' && 'active']" @click="activeTab = 'code'">验证码重置</view>
</view>
<view v-if="activeTab === 'account'" class="form">
<view class="form-item"><input v-model="accountForm.username" placeholder="用户名" /></view>
<view class="form-item"><input v-model="accountForm.oldPassword" password placeholder="旧密码" /></view>
<view class="form-item"><input v-model="accountForm.newPassword" password placeholder="新密码" /></view>
<view class="form-item"><input v-model="accountForm.confirmPassword" password placeholder="确认新密码" /></view>
<button class="btn-primary" :loading="loading" @click="handleAccountReset">重置密码</button>
</view>
<view v-else class="form">
<view class="form-item"><input v-model="codeForm.username" placeholder="手机号或邮箱" /></view>
<view class="form-item"><input v-model="codeForm.newPassword" password placeholder="新密码" /></view>
<view class="form-item code-row">
<input v-model="codeForm.code" placeholder="验证码" class="code-input" />
<button class="send-code-btn" :disabled="countdown > 0" @click="sendCode">{{ countdown > 0 ? countdown + 's' : '发送' }}</button>
</view>
<button class="btn-primary" :loading="loading" @click="handleCodeReset">重置密码</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { resetPasswordByAccount, resetPasswordByCode, sendSmsCode, sendEmailCode } from '@/api/login.js'
const activeTab = ref('account')
const loading = ref(false)
const countdown = ref(0)
let timer = null
const accountForm = ref({ username: '', oldPassword: '', newPassword: '', confirmPassword: '' })
const codeForm = ref({ username: '', newPassword: '', code: '', uuid: '' })
function isPhone(v) { return /^1[3-9]\d{9}$/.test(v) }
function sendCode() {
if (!codeForm.value.username) return uni.showToast({ title: '请输入手机号或邮箱', icon: 'none' })
const p = isPhone(codeForm.value.username) ? sendSmsCode(codeForm.value.username) : sendEmailCode(codeForm.value.username)
p.then(res => {
codeForm.value.uuid = res.data?.uuid || res.uuid
countdown.value = 60
timer = setInterval(() => { countdown.value--; if (countdown.value <= 0) clearInterval(timer) }, 1000)
})
}
async function handleAccountReset() {
if (accountForm.value.newPassword !== accountForm.value.confirmPassword) {
return uni.showToast({ title: '两次密码不一致', icon: 'none' })
}
loading.value = true
try {
await resetPasswordByAccount(accountForm.value)
uni.showToast({ title: '密码重置成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1000)
} finally { loading.value = false }
}
async function handleCodeReset() {
loading.value = true
try {
await resetPasswordByCode(codeForm.value)
uni.showToast({ title: '密码重置成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1000)
} finally { loading.value = false }
}
</script>
<style lang="scss" scoped>
.tabs { display: flex; margin-bottom: 32rpx; background: #f1f5f9; border-radius: 12rpx; padding: 6rpx; }
.tab { flex: 1; text-align: center; padding: 16rpx; font-size: 28rpx; &.active { background: #fff; color: #2563eb; font-weight: 600; border-radius: 8rpx; } }
.form-item { background: #fff; border-radius: 12rpx; padding: 0 24rpx; margin-bottom: 20rpx; border: 2rpx solid #e2e8f0; input { height: 88rpx; font-size: 28rpx; } }
.code-row { display: flex; align-items: center; gap: 16rpx; padding: 12rpx 24rpx; }
.code-input { flex: 1; }
.send-code-btn { font-size: 24rpx; background: #2563eb; color: #fff; border-radius: 8rpx; padding: 0 20rpx; }
.btn-primary { width: 100%; margin-top: 16rpx; }
</style>

View File

@@ -0,0 +1,28 @@
<template>
<view class="page container">
<view class="card">
<view class="detail-row"><text class="label">用户名</text><text>{{ userStore.name }}</text></view>
<view class="detail-row"><text class="label">租户</text><text>{{ userStore.tenantName || '-' }}</text></view>
<view class="detail-row"><text class="label">租户类型</text><text>{{ tenantTypeLabel }}</text></view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
import { useUserStore } from '@/store/index.js'
import { isBrandTenant, isConsumerTenant, isLensFactoryTenant } from '@/utils/tenantType.js'
const userStore = useUserStore()
const tenantTypeLabel = computed(() => {
const t = userStore.tenantType
if (isBrandTenant(t)) return '品牌租户'
if (isConsumerTenant(t)) return '消费者'
if (isLensFactoryTenant(t)) return '镜片厂'
return t || '-'
})
</script>
<style lang="scss" scoped>
.detail-row { display: flex; justify-content: space-between; padding: 20rpx 0; border-bottom: 1rpx solid #f1f5f9; font-size: 28rpx; .label { color: #64748b; } }
</style>

View File

131
uniapp/src/store/index.js Normal file
View File

@@ -0,0 +1,131 @@
import { defineStore } from 'pinia'
import { login as loginApi, logout as logoutApi, getInfo } from '@/api/login.js'
import { getCurrentTenant } from '@/api/tenant.js'
import { getDicts } from '@/api/dict.js'
import { getToken, setToken, removeToken } from '@/utils/auth.js'
import config from '@/config/index.js'
function commitTenant(state, tenant, user) {
if (tenant && tenant.id) {
state.tenantId = tenant.id
state.tenantType = tenant.tenantType != null ? String(tenant.tenantType) : ''
state.tenantName = tenant.name || ''
state.tenantCode = tenant.code || (user && user.tenantCode) || ''
} else {
state.tenantId = null
state.tenantType = ''
state.tenantName = ''
state.tenantCode = ''
}
}
export const useUserStore = defineStore('user', {
state: () => ({
token: getToken(),
id: '',
name: '',
avatar: '',
roles: [],
permissions: [],
tenantId: null,
tenantType: '',
tenantName: '',
tenantCode: ''
}),
getters: {
isLoggedIn: (state) => !!state.token
},
actions: {
async login(userInfo) {
const res = await loginApi(
userInfo.username.trim(),
userInfo.password,
userInfo.code,
userInfo.uuid,
userInfo.directLogin
)
setToken(res.token)
this.token = res.token
},
async loginWithToken(token) {
setToken(token)
this.token = token
},
async getInfo() {
const res = await getInfo()
const user = res.user
const avatar = (!user.avatar)
? '/static/images/profile.jpg'
: (user.avatar.startsWith('http') ? user.avatar : config.baseUrl + user.avatar)
if (res.roles && res.roles.length > 0) {
this.roles = res.roles
this.permissions = res.permissions
} else {
this.roles = ['ROLE_DEFAULT']
}
this.id = user.userId
this.name = user.userName
this.avatar = avatar
if (res.tenant && res.tenant.id) {
commitTenant(this, res.tenant, user)
return res
}
try {
const tenantRes = await getCurrentTenant()
commitTenant(this, tenantRes.data || {}, user)
} catch {
commitTenant(this, null, user)
}
return res
},
async logout() {
try {
await logoutApi()
} finally {
this.token = ''
this.roles = []
this.permissions = []
this.tenantId = null
this.tenantType = ''
this.tenantName = ''
this.tenantCode = ''
removeToken()
}
},
fedLogout() {
this.token = ''
removeToken()
}
}
})
export const useDictStore = defineStore('dict', {
state: () => ({
cache: {}
}),
actions: {
async getDict(dictType) {
if (this.cache[dictType]) {
return this.cache[dictType]
}
const res = await getDicts(dictType)
this.cache[dictType] = res.data || []
return this.cache[dictType]
},
getDictLabel(dictType, value) {
const list = this.cache[dictType] || []
const item = list.find(d => String(d.dictValue) === String(value))
return item ? item.dictLabel : value
}
}
})

View File

@@ -0,0 +1,4 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia

10
uniapp/src/uni.scss Normal file
View File

@@ -0,0 +1,10 @@
$uni-color-primary: #2563eb;
$uni-color-success: #22c55e;
$uni-color-warning: #f59e0b;
$uni-color-error: #ef4444;
$page-bg: #f5f7fa;
$card-bg: #ffffff;
$text-primary: #1e293b;
$text-secondary: #64748b;
$border-color: rgba(37, 99, 235, 0.12);

13
uniapp/src/utils/auth.js Normal file
View File

@@ -0,0 +1,13 @@
import config from '@/config/index.js'
export function getToken() {
return uni.getStorageSync(config.tokenKey) || ''
}
export function setToken(token) {
uni.setStorageSync(config.tokenKey, token)
}
export function removeToken() {
uni.removeStorageSync(config.tokenKey)
}

Some files were not shown because too many files have changed in this diff Show More