Compare commits

..

2 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
58 changed files with 17198 additions and 87 deletions

View File

@@ -535,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">
@@ -553,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>
@@ -563,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>
@@ -963,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>
@@ -973,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>
@@ -1471,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) {
@@ -1516,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();
@@ -1523,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) {
@@ -1979,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

@@ -251,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>
@@ -261,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>

View File

@@ -279,12 +279,14 @@
<div class="section-body"> <div class="section-body">
<div class="pd-mode-switch"> <div class="pd-mode-switch">
<span class="pd-mode-label">瞳距类型</span> <span class="pd-mode-label">瞳距类型</span>
<el-radio-group v-model="pdMode" size="small" @change="handlePdModeChange"> <el-radio-group v-model="pdMode" size="small" :disabled="form.progressiveMultifocal === 1" @change="handlePdModeChange">
<el-radio-button label="single">单瞳距</el-radio-button> <el-radio-button label="single">单瞳距</el-radio-button>
<el-radio-button label="dual">双瞳距</el-radio-button> <el-radio-button label="dual">双瞳距</el-radio-button>
</el-radio-group> </el-radio-group>
<span :key="'pd-tip-text-' + pdMode" class="pd-mode-tip">{{ pdColumnHeaderTip }}</span>
</div> </div>
<el-table <el-table
:key="'optometry-table-' + pdMode + '-' + form.progressiveMultifocal"
:data="form.optometryList" :data="form.optometryList"
:span-method="formOptometrySpanMethod" :span-method="formOptometrySpanMethod"
class="lens-optometry-table" class="lens-optometry-table"
@@ -302,8 +304,8 @@
</el-table-column> </el-table-column>
<el-table-column align="center" min-width="90"> <el-table-column align="center" min-width="90">
<template slot="header"> <template slot="header">
<el-tooltip content="可录入范围:-20 ~ +20 度(负数为近视,正数为远视)" placement="top"> <el-tooltip content="可录入范围:-14.00 ~ +10.00(负数为近视,正数为远视)" placement="top">
<span class="optometry-header-tip">镜S</span> <span class="optometry-header-tip">面度数(SPH)</span>
</el-tooltip> </el-tooltip>
</template> </template>
<template slot-scope="scope"> <template slot-scope="scope">
@@ -319,18 +321,19 @@
v-model="scope.row.sphereS" v-model="scope.row.sphereS"
:precision="2" :precision="2"
:step="0.25" :step="0.25"
:min="-SPHERE_S_MAX" :min="SPHERE_S_MIN"
:max="SPHERE_S_MAX" :max="SPHERE_S_MAX"
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
@change="val => onSphereSChange(val, scope.row)"
/> />
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" min-width="90"> <el-table-column align="center" min-width="90">
<template slot="header"> <template slot="header">
<el-tooltip content="可录入范围:-10 ~ 0 度(仅负数,表示散光" placement="top"> <el-tooltip content="取值范围:-6.00 ~ +6.00球面为正则散光需为正球面为负则散光需为负球面为0时散光可正可负" placement="top">
<span class="optometry-header-tip">柱镜C</span> <span class="optometry-header-tip">散光度数(CYL)</span>
</el-tooltip> </el-tooltip>
</template> </template>
<template slot-scope="scope"> <template slot-scope="scope">
@@ -345,8 +348,8 @@
v-model="scope.row.cylinderC" v-model="scope.row.cylinderC"
:precision="2" :precision="2"
:step="0.25" :step="0.25"
:min="CYLINDER_C_MIN" :min="getCylinderCMin(scope.row)"
:max="0" :max="getCylinderCMax(scope.row)"
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
@change="val => onCylinderCChange(val, scope.row)" @change="val => onCylinderCChange(val, scope.row)"
@@ -356,15 +359,21 @@
</el-table-column> </el-table-column>
<el-table-column align="center" min-width="100"> <el-table-column align="center" min-width="100">
<template slot="header"> <template slot="header">
<el-tooltip content="可录入范围0 ~ 180 度" placement="top"> <el-tooltip content="有散光时轴位须为 1 ~ 180不能为 0" placement="top">
<span class="optometry-header-tip">轴位A</span> <span class="optometry-header-tip">(散光请填写)(AXI)</span>
</el-tooltip> </el-tooltip>
</template> </template>
<template slot-scope="scope"> <template slot-scope="scope">
<div class="axis-input-wrap"> <div
class="axis-input-wrap"
:class="{
'is-disabled': !isAxisAEditable(scope.row),
'is-error': isAxisAInvalid(scope.row)
}"
>
<div class="axis-step-actions"> <div class="axis-step-actions">
<button type="button" class="axis-step-btn" @click="adjustAxisA(scope.row, -5)">-5</button> <button type="button" class="axis-step-btn" :disabled="!isAxisAEditable(scope.row)" @click="adjustAxisA(scope.row, -5)">-5</button>
<button type="button" class="axis-step-btn" @click="adjustAxisA(scope.row, 5)">+5</button> <button type="button" class="axis-step-btn" :disabled="!isAxisAEditable(scope.row)" @click="adjustAxisA(scope.row, 5)">+5</button>
</div> </div>
<el-input-number <el-input-number
v-model="scope.row.axisA" v-model="scope.row.axisA"
@@ -372,16 +381,19 @@
:step="1" :step="1"
:min="0" :min="0"
:max="180" :max="180"
:disabled="!isAxisAEditable(scope.row)"
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
@change="val => onAxisAChange(val, scope.row)"
/> />
<div v-if="isAxisAInvalid(scope.row)" class="axis-error-tip">轴位不能为0</div>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" min-width="100"> <el-table-column :key="'pd-col-' + pdMode" align="center" min-width="120">
<template slot="header"> <template slot="header">
<el-tooltip :content="pdColumnHeaderTip" placement="top"> <el-tooltip :key="'pd-tip-' + pdMode" :content="pdColumnHeaderTip" placement="top">
<span class="optometry-header-tip">瞳距(mm)</span> <span class="optometry-header-tip">{{ pdColumnHeaderLabel }}</span>
</el-tooltip> </el-tooltip>
</template> </template>
<template slot-scope="scope"> <template slot-scope="scope">
@@ -392,9 +404,9 @@
</div> </div>
<el-input-number <el-input-number
v-model="scope.row.dualPd" v-model="scope.row.dualPd"
:precision="0" :precision="2"
:step="1" :step="0.5"
:min="0" :min="DUAL_PD_MIN"
:max="DUAL_PD_MAX" :max="DUAL_PD_MAX"
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
@@ -407,9 +419,9 @@
</div> </div>
<el-input-number <el-input-number
v-model="singlePd" v-model="singlePd"
:precision="0" :precision="2"
:step="1" :step="0.5"
:min="0" :min="SINGLE_PD_MIN"
:max="SINGLE_PD_MAX" :max="SINGLE_PD_MAX"
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
@@ -418,6 +430,59 @@
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column v-if="form.progressiveMultifocal === 1" align="center" min-width="100">
<template slot="header">
<el-tooltip content="可录入范围15 ~ 25 mm" placement="top">
<span class="optometry-header-tip">瞳高(mm)</span>
</el-tooltip>
</template>
<template slot-scope="scope">
<div class="axis-input-wrap">
<div class="axis-step-actions">
<button type="button" class="axis-step-btn" @click="adjustPupilHeight(scope.row, -1)">-1</button>
<button type="button" class="axis-step-btn" @click="adjustPupilHeight(scope.row, 1)">+1</button>
</div>
<el-input-number
v-model="scope.row.pupilHeight"
:precision="0"
:step="1"
:min="PUPIL_HEIGHT_MIN"
:max="PUPIL_HEIGHT_MAX"
controls-position="right"
style="width: 100%"
/>
</div>
</template>
</el-table-column>
<el-table-column v-if="form.progressiveMultifocal === 1" align="center" min-width="110">
<template slot="header">
<el-tooltip content="可录入范围:+0.75 ~ +4.00 屈光度(渐进多焦点近用加光)" placement="top">
<span class="optometry-header-tip">ADD(屈光度)</span>
</el-tooltip>
</template>
<template slot-scope="scope">
<div
class="axis-input-wrap"
:class="{ 'is-error': isNearAddInvalid(scope.row) }"
>
<div class="axis-step-actions">
<button type="button" class="axis-step-btn" @click="adjustNearAdd(scope.row, -0.25)">-0.25</button>
<button type="button" class="axis-step-btn" @click="adjustNearAdd(scope.row, 0.25)">+0.25</button>
</div>
<el-input-number
v-model="scope.row.nearAdd"
:precision="2"
:step="0.25"
:min="NEAR_ADD_MIN"
:max="NEAR_ADD_MAX"
controls-position="right"
style="width: 100%"
@change="val => onNearAddChange(val, scope.row)"
/>
<div v-if="isNearAddInvalid(scope.row)" class="axis-error-tip">ADD不能为0请填写+0.75~+4.00</div>
</div>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
</section> </section>
@@ -595,19 +660,19 @@
<span>{{ scope.row.eyeSide === 'OS' ? '左眼(OS)' : '右眼(OD)' }}</span> <span>{{ scope.row.eyeSide === 'OS' ? '左眼(OS)' : '右眼(OD)' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="球镜S" align="center" min-width="90"> <el-table-column label="球面度数(SPH)" align="center" min-width="90">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatDetailOptometryDecimal(scope.row.sphereS) }}</span> <span>{{ formatDetailOptometryDecimal(scope.row.sphereS) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="柱镜C" align="center" min-width="90"> <el-table-column label="散光度数(CYL)" align="center" min-width="90">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatDetailOptometryDecimal(scope.row.cylinderC) }}</span> <span>{{ formatDetailOptometryDecimal(scope.row.cylinderC) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" min-width="80"> <el-table-column align="center" min-width="80">
<template slot="header"> <template slot="header">
<span>轴位A</span> <span>(AXI)</span>
</template> </template>
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ scope.row.axisA != null ? scope.row.axisA : '-' }}</span> <span>{{ scope.row.axisA != null ? scope.row.axisA : '-' }}</span>
@@ -618,7 +683,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>
@@ -628,7 +693,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>
@@ -870,11 +935,17 @@ export default {
pdMode: 'single', pdMode: 'single',
singlePd: 50, singlePd: 50,
SINGLE_PD_DEFAULT: 50, SINGLE_PD_DEFAULT: 50,
SINGLE_PD_MAX: 100, SINGLE_PD_MIN: 40,
DUAL_PD_DEFAULT: 25, SINGLE_PD_MAX: 80,
DUAL_PD_MAX: 50, DUAL_PD_DEFAULT: 30,
SPHERE_S_MAX: 20, DUAL_PD_MIN: 20,
CYLINDER_C_MIN: -10, DUAL_PD_MAX: 40,
SPHERE_S_MIN: -14.00,
SPHERE_S_MAX: 10.00,
CYLINDER_C_MIN: -6.00,
CYLINDER_C_MAX: 6.00,
NEAR_ADD_MIN: 0.75,
NEAR_ADD_MAX: 4.00,
PUPIL_HEIGHT_MIN: 15, PUPIL_HEIGHT_MIN: 15,
PUPIL_HEIGHT_MAX: 25, PUPIL_HEIGHT_MAX: 25,
detailPdMode: 'single', detailPdMode: 'single',
@@ -1005,10 +1076,13 @@ export default {
} }
return '填写原因后提交换货申请' return '填写原因后提交换货申请'
}, },
pdColumnHeaderLabel() {
return this.pdMode === 'dual' ? '双瞳距(mm)' : '单瞳距(mm)'
},
pdColumnHeaderTip() { pdColumnHeaderTip() {
return this.pdMode === 'dual' return this.pdMode === 'dual'
? '可录入范围:0 ~ 50 mm单眼双瞳距' ? '双瞳距可录入范围:20.00 ~ 40.00 mm左右眼分别填写'
: '可录入范围0 ~ 100 mm单瞳距' : '单瞳距可录入范围:40.00 ~ 80.00 mm左右眼共用一个值'
}, },
logisticsDialogSubtitle() { logisticsDialogSubtitle() {
if (this.logisticsBasic && this.logisticsBasic.orderNo) { if (this.logisticsBasic && this.logisticsBasic.orderNo) {
@@ -1288,8 +1362,8 @@ export default {
}, },
createDefaultOptometryList() { createDefaultOptometryList() {
return [ return [
{ eyeSide: 'OS', sphereS: null, cylinderC: null, axisA: null, pd: this.SINGLE_PD_DEFAULT, dualPd: null, pdType: '1', pupilHeight: null, distanceVa: null, nearAdd: null }, { eyeSide: 'OS', sphereS: 0, cylinderC: null, axisA: null, pd: this.SINGLE_PD_DEFAULT, dualPd: null, pdType: '1', pupilHeight: null, distanceVa: null, nearAdd: null },
{ eyeSide: 'OD', sphereS: null, cylinderC: null, axisA: null, pd: this.SINGLE_PD_DEFAULT, dualPd: null, pdType: '1', pupilHeight: null, distanceVa: null, nearAdd: null } { eyeSide: 'OD', sphereS: 0, cylinderC: null, axisA: null, pd: this.SINGLE_PD_DEFAULT, dualPd: null, pdType: '1', pupilHeight: null, distanceVa: null, nearAdd: null }
] ]
}, },
detectPdMode(optometryList) { detectPdMode(optometryList) {
@@ -1348,7 +1422,7 @@ export default {
const osRow = this.form.optometryList.find(item => item.eyeSide === 'OS') const osRow = this.form.optometryList.find(item => item.eyeSide === 'OS')
const odRow = this.form.optometryList.find(item => item.eyeSide === 'OD') const odRow = this.form.optometryList.find(item => item.eyeSide === 'OD')
const pd = osRow && osRow.pd != null ? osRow.pd : (odRow && odRow.pd != null ? odRow.pd : this.SINGLE_PD_DEFAULT) const pd = osRow && osRow.pd != null ? osRow.pd : (odRow && odRow.pd != null ? odRow.pd : this.SINGLE_PD_DEFAULT)
this.singlePd = Math.min(this.SINGLE_PD_MAX, Math.max(0, Number(pd))) this.singlePd = Math.min(this.SINGLE_PD_MAX, Math.max(this.SINGLE_PD_MIN, Number(pd)))
this.form.optometryList.forEach(row => { this.form.optometryList.forEach(row => {
if (row.pd == null) { if (row.pd == null) {
row.pd = this.singlePd row.pd = this.singlePd
@@ -1357,7 +1431,7 @@ export default {
}, },
onSinglePdChange(val) { onSinglePdChange(val) {
if (this.pdMode !== 'single' || !this.form.optometryList) return if (this.pdMode !== 'single' || !this.form.optometryList) return
const pd = val == null || val === '' ? null : Math.min(this.SINGLE_PD_MAX, Math.max(0, Number(val))) const pd = val == null || val === '' ? null : Math.min(this.SINGLE_PD_MAX, Math.max(this.SINGLE_PD_MIN, Number(val)))
if (pd != null && Number.isNaN(pd)) return if (pd != null && Number.isNaN(pd)) return
this.singlePd = pd this.singlePd = pd
this.form.optometryList.forEach(row => { this.form.optometryList.forEach(row => {
@@ -1371,7 +1445,7 @@ export default {
const num = Number(val) const num = Number(val)
if (Number.isNaN(num) || num === 0) return '' if (Number.isNaN(num) || num === 0) return ''
const degree = Math.abs(num).toFixed(2) const degree = Math.abs(num).toFixed(2)
return num > 0 ? `远视 ${degree} ` : `近视 ${degree} ` return num > 0 ? `远视 ${degree} ` : `近视 ${degree} `
}, },
getSphereSHintClass(row) { getSphereSHintClass(row) {
const num = Number(row.sphereS) const num = Number(row.sphereS)
@@ -1384,33 +1458,141 @@ export default {
const num = Number(val) const num = Number(val)
if (Number.isNaN(num) || num === 0) return '' if (Number.isNaN(num) || num === 0) return ''
const degree = Math.abs(num).toFixed(2) const degree = Math.abs(num).toFixed(2)
return `散光 ${degree} ` return `散光 ${degree} `
},
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) {
const sign = this.getSphereSign(row && row.sphereS)
return sign > 0 ? 0 : this.CYLINDER_C_MIN
},
getCylinderCMax(row) {
const sign = this.getSphereSign(row && row.sphereS)
return sign < 0 ? 0 : this.CYLINDER_C_MAX
},
alignCylinderSignWithSphere(row) {
if (!row) return
if (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.CYLINDER_C_MAX, Math.max(this.CYLINDER_C_MIN, cylinder))
},
isAxisAEditable(row) {
const val = row && row.cylinderC
if (val == null || val === '') return false
const num = Number(val)
return !Number.isNaN(num) && num !== 0
},
isAxisAInvalid(row) {
if (!this.isAxisAEditable(row)) return false
if (row.axisA == null || row.axisA === '') return true
const axis = Number(row.axisA)
return Number.isNaN(axis) || axis === 0
},
onAxisAChange(val, row) {
if (!this.isAxisAEditable(row)) {
row.axisA = null
return
}
if (val == null || val === '') {
row.axisA = 0
return
}
const axis = Number(val)
if (Number.isNaN(axis)) {
row.axisA = 0
return
}
row.axisA = Math.min(180, Math.max(0, axis))
},
onSphereSChange(val, row) {
if (val == null || val === '') {
row.sphereS = 0
} else {
const num = Number(val)
if (Number.isNaN(num)) {
row.sphereS = 0
} else {
row.sphereS = Math.min(this.SPHERE_S_MAX, Math.max(this.SPHERE_S_MIN, num))
}
}
this.alignCylinderSignWithSphere(row)
if (!this.isAxisAEditable(row)) {
row.axisA = null
}
}, },
onCylinderCChange(val, row) { onCylinderCChange(val, row) {
if (val == null || val === '') return if (val == null || val === '') {
row.axisA = null
return
}
let num = Number(val) let num = Number(val)
if (Number.isNaN(num)) return if (Number.isNaN(num)) return
if (num > 0) { const sphereSign = this.getSphereSign(row.sphereS)
if (sphereSign > 0 && num < 0) {
num = Math.abs(num)
} else if (sphereSign < 0 && num > 0) {
num = -Math.abs(num) num = -Math.abs(num)
} }
row.cylinderC = Math.min(0, Math.max(this.CYLINDER_C_MIN, num)) num = Math.min(this.getCylinderCMax(row), Math.max(this.getCylinderCMin(row), num))
row.cylinderC = num
if (num === 0) {
row.axisA = null
}
},
validateCylinderSignWithSphere() {
const list = this.form.optometryList || []
for (const row of list) {
if (row.cylinderC == null || row.cylinderC === '') continue
const cylinder = Number(row.cylinderC)
if (Number.isNaN(cylinder) || cylinder === 0) continue
const sphereSign = this.getSphereSign(row.sphereS)
const eyeLabel = row.eyeSide === 'OS' ? '左眼' : '右眼'
if (sphereSign > 0 && cylinder < 0) {
return `${eyeLabel}:球面为正时,散光度数须为正`
}
if (sphereSign < 0 && cylinder > 0) {
return `${eyeLabel}:球面为负时,散光度数须为负`
}
}
return null
},
validateAxisARequired() {
const list = this.form.optometryList || []
for (const row of list) {
if (!this.isAxisAInvalid(row)) continue
const eyeLabel = row.eyeSide === 'OS' ? '左眼' : '右眼'
return `${eyeLabel}有散光时轴位不能为0请填写1~180`
}
return null
}, },
adjustAxisA(row, delta) { adjustAxisA(row, delta) {
if (!this.isAxisAEditable(row)) return
const current = row.axisA == null || row.axisA === '' ? 0 : Number(row.axisA) const current = row.axisA == null || row.axisA === '' ? 0 : Number(row.axisA)
if (Number.isNaN(current)) return if (Number.isNaN(current)) return
row.axisA = Math.min(180, Math.max(0, current + delta)) row.axisA = Math.min(180, Math.max(0, current + delta))
}, },
adjustSinglePd(delta) { adjustSinglePd(delta) {
const current = this.singlePd == null || this.singlePd === '' ? 0 : Number(this.singlePd) const current = this.singlePd == null || this.singlePd === '' ? this.SINGLE_PD_DEFAULT : Number(this.singlePd)
if (Number.isNaN(current)) return if (Number.isNaN(current)) return
const next = Math.min(this.SINGLE_PD_MAX, Math.max(0, current + delta)) const next = Math.min(this.SINGLE_PD_MAX, Math.max(this.SINGLE_PD_MIN, current + delta))
this.singlePd = next this.singlePd = next
this.onSinglePdChange(next) this.onSinglePdChange(next)
}, },
adjustDualPd(row, delta) { adjustDualPd(row, delta) {
const current = row.dualPd == null || row.dualPd === '' ? 0 : Number(row.dualPd) const current = row.dualPd == null || row.dualPd === '' ? this.DUAL_PD_DEFAULT : Number(row.dualPd)
if (Number.isNaN(current)) return if (Number.isNaN(current)) return
row.dualPd = Math.min(this.DUAL_PD_MAX, Math.max(0, current + delta)) row.dualPd = Math.min(this.DUAL_PD_MAX, Math.max(this.DUAL_PD_MIN, current + delta))
}, },
adjustPupilHeight(row, delta) { adjustPupilHeight(row, delta) {
const current = row.pupilHeight == null || row.pupilHeight === '' ? this.PUPIL_HEIGHT_MIN : Number(row.pupilHeight) const current = row.pupilHeight == null || row.pupilHeight === '' ? this.PUPIL_HEIGHT_MIN : Number(row.pupilHeight)
@@ -1427,20 +1609,53 @@ export default {
const current = row.nearAdd == null || row.nearAdd === '' ? 0 : Number(row.nearAdd) const current = row.nearAdd == null || row.nearAdd === '' ? 0 : Number(row.nearAdd)
if (Number.isNaN(current)) return if (Number.isNaN(current)) return
const next = Math.round((current + delta) * 100) / 100 const next = Math.round((current + delta) * 100) / 100
row.nearAdd = Math.min(3, Math.max(0.5, next)) row.nearAdd = Math.min(this.NEAR_ADD_MAX, Math.max(this.NEAR_ADD_MIN, next))
},
isNearAddInvalid(row) {
if (Number(this.form.progressiveMultifocal) !== 1) return false
if (row.nearAdd == null || row.nearAdd === '') return true
const num = Number(row.nearAdd)
return Number.isNaN(num) || num === 0 || num < this.NEAR_ADD_MIN || num > this.NEAR_ADD_MAX
},
onNearAddChange(val, row) {
if (Number(this.form.progressiveMultifocal) !== 1) {
row.nearAdd = null
return
}
if (val == null || val === '') {
row.nearAdd = null
return
}
const num = Number(val)
if (Number.isNaN(num) || num === 0) {
row.nearAdd = null
return
}
row.nearAdd = Math.min(this.NEAR_ADD_MAX, Math.max(this.NEAR_ADD_MIN, num))
},
validateNearAddRequired() {
if (Number(this.form.progressiveMultifocal) !== 1) return null
const list = this.form.optometryList || []
for (const row of list) {
if (!this.isNearAddInvalid(row)) continue
const eyeLabel = row.eyeSide === 'OS' ? '左眼' : '右眼'
return `${eyeLabel}选择渐进多焦点时ADD不能为0请填写+0.75~+4.00`
}
return null
}, },
handlePdModeChange(mode) { handlePdModeChange(mode) {
this.pdMode = mode === 'dual' ? 'dual' : 'single'
if (!this.form.optometryList) return if (!this.form.optometryList) return
if (mode === 'dual') { if (this.pdMode === 'dual') {
this.form.optometryList.forEach(row => { this.form.optometryList.forEach(row => {
row.dualPd = this.DUAL_PD_DEFAULT row.dualPd = row.dualPd != null ? row.dualPd : this.DUAL_PD_DEFAULT
row.pd = null row.pd = null
}) })
this.singlePd = null this.singlePd = null
} else { } else {
this.singlePd = this.SINGLE_PD_DEFAULT this.singlePd = this.singlePd != null ? this.singlePd : this.SINGLE_PD_DEFAULT
this.form.optometryList.forEach(row => { this.form.optometryList.forEach(row => {
row.pd = this.SINGLE_PD_DEFAULT row.pd = this.singlePd
row.dualPd = null row.dualPd = null
}) })
} }
@@ -1460,6 +1675,19 @@ export default {
row.pdType = pdType row.pdType = pdType
}) })
} }
if (Number(this.form.progressiveMultifocal) !== 1 && this.form.optometryList) {
this.form.optometryList.forEach(row => {
row.nearAdd = null
row.pupilHeight = null
})
}
if (this.form.optometryList) {
this.form.optometryList.forEach(row => {
if (!this.isAxisAEditable(row)) {
row.axisA = null
}
})
}
}, },
cancel() { cancel() {
this.open = false this.open = false
@@ -1468,8 +1696,19 @@ export default {
this.reset() this.reset()
}, },
handleProgressiveMultifocalChange(value) { handleProgressiveMultifocalChange(value) {
if (Number(value) !== 1) { if (Number(value) === 1) {
this.pdMode = 'dual'
this.handlePdModeChange('dual')
return
}
this.form.progressiveMultifocalPrice = 0 this.form.progressiveMultifocalPrice = 0
this.pdMode = 'single'
this.handlePdModeChange('single')
if (this.form.optometryList) {
this.form.optometryList.forEach(row => {
row.nearAdd = null
row.pupilHeight = null
})
} }
}, },
reset() { reset() {
@@ -1553,43 +1792,57 @@ export default {
row.distanceVa = Math.min(2, Math.max(0.02, num)) row.distanceVa = Math.min(2, Math.max(0.02, num))
} }
} }
if (row.sphereS == null || row.sphereS === '') {
row.sphereS = 0
} else {
const sphereS = Number(row.sphereS)
if (Number.isNaN(sphereS)) {
row.sphereS = 0
} else {
row.sphereS = Math.min(this.SPHERE_S_MAX, Math.max(this.SPHERE_S_MIN, sphereS))
}
}
if (row.cylinderC != null && row.cylinderC !== '') { if (row.cylinderC != null && row.cylinderC !== '') {
let cylinder = Number(row.cylinderC) let cylinder = Number(row.cylinderC)
if (!Number.isNaN(cylinder)) { if (!Number.isNaN(cylinder)) {
if (cylinder > 0) { 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) cylinder = -Math.abs(cylinder)
} }
row.cylinderC = Math.min(0, Math.max(this.CYLINDER_C_MIN, cylinder)) row.cylinderC = Math.min(this.getCylinderCMax(row), Math.max(this.getCylinderCMin(row), cylinder))
} }
} }
if (row.sphereS != null && row.sphereS !== '') { if (!this.isAxisAEditable(row)) {
const sphereS = Number(row.sphereS) row.axisA = null
if (!Number.isNaN(sphereS)) {
row.sphereS = Math.min(this.SPHERE_S_MAX, Math.max(-this.SPHERE_S_MAX, sphereS))
} }
} if (Number(form.progressiveMultifocal) === 1 && row.nearAdd != null && row.nearAdd !== '') {
if (row.nearAdd != null && row.nearAdd !== '') {
const nearAdd = Number(row.nearAdd) const nearAdd = Number(row.nearAdd)
if (!Number.isNaN(nearAdd)) { if (!Number.isNaN(nearAdd)) {
row.nearAdd = Math.min(3, Math.max(0.5, nearAdd)) row.nearAdd = Math.min(this.NEAR_ADD_MAX, Math.max(this.NEAR_ADD_MIN, nearAdd))
}
} else {
row.nearAdd = null
}
if (Number(form.progressiveMultifocal) !== 1) {
row.pupilHeight = null
} else if (row.pupilHeight != null && row.pupilHeight !== '') {
const pupilHeight = Number(row.pupilHeight)
if (!Number.isNaN(pupilHeight)) {
row.pupilHeight = Math.min(this.PUPIL_HEIGHT_MAX, Math.max(this.PUPIL_HEIGHT_MIN, pupilHeight))
} }
} }
if (row.pd != null && row.pd !== '') { if (row.pd != null && row.pd !== '') {
const pd = Number(row.pd) const pd = Number(row.pd)
if (!Number.isNaN(pd)) { if (!Number.isNaN(pd)) {
row.pd = Math.min(this.SINGLE_PD_MAX, Math.max(0, pd)) row.pd = Math.min(this.SINGLE_PD_MAX, Math.max(this.SINGLE_PD_MIN, pd))
} }
} }
if (row.dualPd != null && row.dualPd !== '') { if (row.dualPd != null && row.dualPd !== '') {
const dualPd = Number(row.dualPd) const dualPd = Number(row.dualPd)
if (!Number.isNaN(dualPd)) { if (!Number.isNaN(dualPd)) {
row.dualPd = Math.min(this.DUAL_PD_MAX, Math.max(0, dualPd)) row.dualPd = Math.min(this.DUAL_PD_MAX, Math.max(this.DUAL_PD_MIN, dualPd))
}
}
if (row.pupilHeight != null && row.pupilHeight !== '') {
const pupilHeight = Number(row.pupilHeight)
if (!Number.isNaN(pupilHeight)) {
row.pupilHeight = Math.min(this.PUPIL_HEIGHT_MAX, Math.max(this.PUPIL_HEIGHT_MIN, pupilHeight))
} }
} }
}) })
@@ -1604,7 +1857,9 @@ export default {
if (!this.form.optometryList || this.form.optometryList.length === 0) { if (!this.form.optometryList || this.form.optometryList.length === 0) {
this.form.optometryList = this.createDefaultOptometryList() this.form.optometryList = this.createDefaultOptometryList()
} }
this.pdMode = this.detectPdMode(this.form.optometryList) this.pdMode = Number(this.form.progressiveMultifocal) === 1
? 'dual'
: this.detectPdMode(this.form.optometryList)
if (this.pdMode === 'single') { if (this.pdMode === 'single') {
this.initSinglePd() this.initSinglePd()
} else { } else {
@@ -1613,6 +1868,8 @@ export default {
if (row.dualPd == null) { if (row.dualPd == null) {
row.dualPd = this.DUAL_PD_DEFAULT row.dualPd = this.DUAL_PD_DEFAULT
} }
row.pd = null
row.pdType = this.PD_TYPE_DUAL
}) })
} }
this.formMode = mode this.formMode = mode
@@ -1923,6 +2180,21 @@ export default {
}) })
}, },
saveForm(orderStatus, successMsg) { saveForm(orderStatus, successMsg) {
const cylinderErr = this.validateCylinderSignWithSphere()
if (cylinderErr) {
this.$modal.msgWarning(cylinderErr)
return Promise.reject(new Error(cylinderErr))
}
const axisErr = this.validateAxisARequired()
if (axisErr) {
this.$modal.msgWarning(axisErr)
return Promise.reject(new Error(axisErr))
}
const nearAddErr = this.validateNearAddRequired()
if (nearAddErr) {
this.$modal.msgWarning(nearAddErr)
return Promise.reject(new Error(nearAddErr))
}
this.prepareOptometryForSave() this.prepareOptometryForSave()
const progressiveMultifocal = this.form.progressiveMultifocal != null const progressiveMultifocal = this.form.progressiveMultifocal != null
? Number(this.form.progressiveMultifocal) : 0 ? Number(this.form.progressiveMultifocal) : 0
@@ -2140,13 +2412,22 @@ export default {
.pd-mode-switch { .pd-mode-switch {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px; margin-bottom: 12px;
.pd-mode-label { .pd-mode-label {
margin-right: 12px; margin-right: 4px;
color: #606266; color: #606266;
font-size: 14px; font-size: 14px;
} }
.pd-mode-tip {
margin-left: 8px;
color: #909399;
font-size: 12px;
line-height: 1.4;
}
} }
.degree-sup { .degree-sup {
@@ -2220,11 +2501,41 @@ export default {
background: #eff6ff; background: #eff6ff;
} }
&:disabled {
color: #94a3b8;
background: #f1f5f9;
border-color: #e2e8f0;
cursor: not-allowed;
}
&:active { &:active {
background: #dbeafe; background: #dbeafe;
} }
} }
.axis-input-wrap.is-disabled {
opacity: 0.72;
}
.axis-input-wrap.is-error {
::v-deep .el-input-number .el-input__inner {
border-color: #f56c6c;
background-color: #fef0f0;
}
::v-deep .el-input-number.is-controls-right .el-input-number__decrease,
::v-deep .el-input-number.is-controls-right .el-input-number__increase {
border-color: #f56c6c;
}
}
.axis-error-tip {
margin-top: 4px;
font-size: 12px;
line-height: 1.2;
color: #f56c6c;
}
.sphere-s-hint { .sphere-s-hint {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -2389,6 +2700,7 @@ export default {
.pd-mode-switch { .pd-mode-switch {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: 12px; gap: 12px;
margin-bottom: 14px; margin-bottom: 14px;
padding: 10px 14px; padding: 10px 14px;
@@ -2402,6 +2714,13 @@ export default {
color: #475569; color: #475569;
} }
.pd-mode-tip {
margin-left: 4px;
font-size: 12px;
color: #64748b;
line-height: 1.4;
}
.el-radio-button__inner { .el-radio-button__inner {
border-color: rgba(37, 99, 235, 0.2); border-color: rgba(37, 99, 235, 0.2);
color: #475569; color: #475569;

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

@@ -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>
@@ -196,7 +200,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
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

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`;

6
package-lock.json generated Normal file
View File

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

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)
}

View File

@@ -0,0 +1,6 @@
export default {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
'default': '系统未知错误,请反馈给管理员'
}

View File

@@ -0,0 +1,262 @@
/** 订单状态常量 */
export const ORDER_STATUS = {
DRAFT: '1',
SUBMIT: '2',
ASSIGNED: '3',
REJECTED: '5',
SUPPLEMENT_PENDING: '4',
SUPPLEMENTED: '19',
SHIPPED: '8',
COMPLETED: '9',
EXCHANGE_APPLY: '12',
EXCHANGE_AGREED: '13',
EXCHANGE_LOGISTICS: '15',
EXCHANGE_COMPLETED: '17',
EXCHANGE_REJECTED: '21',
ACCEPTED: '6',
PRODUCING: '7'
}
/** 制片单可接单状态(与 Web factory-order 一致) */
export const FACTORY_ACCEPTABLE_STATUSES = ['3', '13', '19']
export function normalizeOrderStatus(status) {
return status == null ? '' : String(status)
}
export function formatProductOption(item) {
if (!item) return '-'
const model = item.productModel || ''
const name = item.productName || item.name || ''
return model ? `${model}(${name})` : (name || '-')
}
export function formatLensOption(item) {
if (!item) return '-'
const name = item.productName || item.lensProductName || ''
const material = item.material || item.lensMaterial || ''
return material ? `${name}(${material})` : (name || '-')
}
export function formatLens(row) {
if (!row) return '-'
if (row.lensProductName || row.lensMaterial) {
return formatLensOption({ productName: row.lensProductName, material: row.lensMaterial })
}
if (row.productName || row.material) {
return formatLensOption(row)
}
return '-'
}
export function filterLensOptionsByProduct(lensList, productList, productId, currentLensId, currentLensInfo = {}) {
let options = (lensList || []).map(item => ({
...item,
id: item && item.id != null ? Number(item.id) : null
})).filter(item => item.id != null && !Number.isNaN(item.id))
const pid = productId != null && productId !== '' ? Number(productId) : null
if (pid != null && !Number.isNaN(pid)) {
const product = (productList || []).find(item => {
const id = item.productId != null ? Number(item.productId) : Number(item.id)
return id === pid
})
if (product && product.lensIds) {
const allowedIds = String(product.lensIds).split(',').filter(Boolean).map(Number)
if (allowedIds.length) {
options = options.filter(item => allowedIds.includes(item.id))
}
}
}
const lensId = currentLensId != null && currentLensId !== '' ? Number(currentLensId) : null
if (lensId != null && !Number.isNaN(lensId) && !options.some(item => item.id === lensId)) {
options.unshift({
id: lensId,
productName: currentLensInfo.productName || `镜片#${lensId}`,
material: currentLensInfo.material || ''
})
}
return options
}
export function formatPriceAmount(val) {
if (val == null || val === '' || val === '待定') return val || '-'
const num = Number(val)
if (isNaN(num)) return val
return '¥' + num.toFixed(2)
}
export function formatNearAdd(value) {
if (value == null || value === '') return '-'
const num = Number(value)
if (Number.isNaN(num)) return String(value)
return '+' + num.toFixed(2)
}
function normalizeNearAddValue(val) {
if (val == null || val === '') return null
const num = Number(val)
if (Number.isNaN(num)) return null
return Math.min(4, Math.max(0.75, Math.round(num * 100) / 100))
}
/** 柱镜 C 不为 0 时轴位 A 才可录入 */
export function isAxisAEditable(cylinderC) {
if (cylinderC == null || cylinderC === '') return false
const num = Number(cylinderC)
return !Number.isNaN(num) && num !== 0
}
/** 球面符号1正 / -1负 / 0零或空 */
export function 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
}
/** 按球面符号校正散光正负球为正则散光为正球为负则散光为负球为0可正可负 */
export function alignCylinderWithSphere(sphereS, cylinderC) {
if (cylinderC == null || cylinderC === '') return cylinderC
let cylinder = Number(cylinderC)
if (Number.isNaN(cylinder) || cylinder === 0) return cylinder === 0 ? 0 : cylinderC
const sphereSign = getSphereSign(sphereS)
if (sphereSign > 0 && cylinder < 0) cylinder = Math.abs(cylinder)
else if (sphereSign < 0 && cylinder > 0) cylinder = -Math.abs(cylinder)
return Math.min(10, Math.max(-10, cylinder))
}
export function validateCylinderSignWithSphere(optometryList) {
for (const row of optometryList || []) {
if (row.cylinderC == null || row.cylinderC === '') continue
const cylinder = Number(row.cylinderC)
if (Number.isNaN(cylinder) || cylinder === 0) continue
const sphereSign = getSphereSign(row.sphereS)
const eyeLabel = row.eyeSide === 'OS' ? '左眼' : '右眼'
if (sphereSign > 0 && cylinder < 0) return `${eyeLabel}:球面为正时,散光度数须为正`
if (sphereSign < 0 && cylinder > 0) return `${eyeLabel}:球面为负时,散光度数须为负`
}
return null
}
export function buildDefaultOptometryList() {
return [
{ eyeSide: 'OS', sphereS: 0, cylinderC: null, axisA: null, pd: 50, pdValue: 50, dualPd: null, pdType: '1', pupilHeight: null, distanceVa: '', nearAdd: null },
{ eyeSide: 'OD', sphereS: 0, cylinderC: null, axisA: null, pd: 50, pdValue: 50, dualPd: null, pdType: '1', pupilHeight: null, distanceVa: '', nearAdd: null }
]
}
export const SINGLE_PD_MIN = 40
export const SINGLE_PD_MAX = 80
export const SINGLE_PD_DEFAULT = 50
export const DUAL_PD_MIN = 20
export const DUAL_PD_MAX = 40
export const DUAL_PD_DEFAULT = 30
export function clampSinglePd(val) {
const num = Number(val)
if (Number.isNaN(num)) return SINGLE_PD_DEFAULT
return Math.min(SINGLE_PD_MAX, Math.max(SINGLE_PD_MIN, num))
}
export function clampDualPd(val) {
const num = Number(val)
if (Number.isNaN(num)) return DUAL_PD_DEFAULT
return Math.min(DUAL_PD_MAX, Math.max(DUAL_PD_MIN, num))
}
export function createEmptyForm() {
return {
id: null,
orderNo: '',
productId: null,
lensId: null,
progressiveMultifocal: 0,
receiverName: '',
receiverPhone: '',
receiverAddress: '',
remark: '',
optometryList: buildDefaultOptometryList(),
pdType: '1',
orderStatus: ORDER_STATUS.DRAFT
}
}
/** 渐进多焦点开启时显示近用/瞳高并使用双瞳距 */
export function applyProgressiveMultifocalState(form) {
const progressive = Number(form.progressiveMultifocal) === 1
const list = form.optometryList || []
if (progressive) {
list.forEach(row => {
row.pd = null
row.pdValue = null
row.pdType = '2'
row.dualPd = clampDualPd(row.dualPd == null || row.dualPd === '' ? DUAL_PD_DEFAULT : row.dualPd)
})
form.pdType = '2'
return { pdMode: 'dual', singlePd: null }
}
list.forEach(row => {
row.nearAdd = null
row.pupilHeight = null
row.dualPd = null
row.pdType = '1'
})
const osRow = list.find(item => item.eyeSide === 'OS')
const odRow = list.find(item => item.eyeSide === 'OD')
const rawSingle = osRow && (osRow.pd != null ? osRow.pd : osRow.pdValue) != null
? (osRow.pd != null ? osRow.pd : osRow.pdValue)
: (odRow && (odRow.pd != null ? odRow.pd : odRow.pdValue) != null
? (odRow.pd != null ? odRow.pd : odRow.pdValue)
: SINGLE_PD_DEFAULT)
const singlePd = clampSinglePd(rawSingle)
list.forEach(row => {
row.pd = singlePd
row.pdValue = singlePd
})
form.pdType = '1'
return { pdMode: 'single', singlePd }
}
/** 将详情接口数据转为表单结构 */
export function normalizeOrderForm(data) {
if (!data) return createEmptyForm()
const form = {
...createEmptyForm(),
...data,
productId: data.productId != null ? Number(data.productId) : null,
lensId: data.lensId != null ? Number(data.lensId) : null,
progressiveMultifocal: data.progressiveMultifocal != null ? Number(data.progressiveMultifocal) : 0
}
if (!Array.isArray(form.optometryList) || !form.optometryList.length) {
form.optometryList = buildDefaultOptometryList()
} else {
const progressive = Number(form.progressiveMultifocal) === 1
form.optometryList = form.optometryList.map(row => {
const sphereS = row.sphereS == null || row.sphereS === '' || Number.isNaN(Number(row.sphereS))
? 0
: Number(row.sphereS)
const cylinderC = alignCylinderWithSphere(sphereS, row.cylinderC)
return {
...row,
sphereS,
nearAdd: progressive ? normalizeNearAddValue(row.nearAdd) : null,
pupilHeight: progressive ? row.pupilHeight : null,
cylinderC,
axisA: isAxisAEditable(cylinderC) ? row.axisA : null
}
})
}
applyProgressiveMultifocalState(form)
return form
}
/** 解析详情接口返回体 */
export function unwrapOrderResponse(res) {
if (!res) return null
if (res.data && typeof res.data === 'object' && !Array.isArray(res.data)) {
return res.data
}
return res
}

127
uniapp/src/utils/request.js Normal file
View File

@@ -0,0 +1,127 @@
import config from '@/config/index.js'
import { getToken, removeToken } from '@/utils/auth.js'
import errorCode from '@/utils/errorCode.js'
let isRelogin = false
function tansParams(params) {
let result = ''
for (const prop in params) {
const value = params[prop]
if (value !== null && value !== undefined && value !== '') {
result += encodeURIComponent(prop) + '=' + encodeURIComponent(value) + '&'
}
}
return result
}
function request(options = {}) {
const isToken = options.isToken === false
const header = {
'Content-Type': 'application/json;charset=utf-8',
...options.header
}
if (getToken() && !isToken) {
header['Authorization'] = 'Bearer ' + getToken()
}
let url = config.baseUrl + options.url
if (options.method === 'GET' && options.params) {
const qs = tansParams(options.params)
if (qs) {
url += '?' + qs.slice(0, -1)
}
}
return new Promise((resolve, reject) => {
uni.request({
url,
method: options.method || 'GET',
data: options.data,
header,
timeout: options.timeout || config.timeout,
success: (res) => {
const data = res.data || {}
const code = data.code || 200
const msg = errorCode[code] || data.msg || errorCode['default']
if (code === 401) {
if (!isRelogin) {
isRelogin = true
uni.showModal({
title: '系统提示',
content: '登录状态已过期,请重新登录',
confirmText: '登录',
success: (modalRes) => {
isRelogin = false
if (modalRes.confirm) {
removeToken()
uni.reLaunch({ url: '/pages/login/login' })
}
}
})
}
reject(new Error('无效的会话,或者会话已过期,请重新登录。'))
return
}
if (code === 500) {
uni.showToast({ title: msg, icon: 'none' })
reject(new Error(msg))
return
}
if (code === 601) {
uni.showToast({ title: msg, icon: 'none' })
reject('error')
return
}
if (code === 201) {
reject(data)
return
}
if (code !== 200) {
uni.showToast({ title: msg, icon: 'none' })
reject('error')
return
}
resolve(data)
},
fail: (err) => {
let message = err.errMsg || '网络异常'
if (message.includes('timeout')) {
message = '系统接口请求超时'
}
uni.showToast({ title: message, icon: 'none' })
reject(err)
}
})
})
}
export default request
export function uploadFile(url, filePath, name = 'file') {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: config.baseUrl + url,
filePath,
name,
header: {
Authorization: 'Bearer ' + getToken()
},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.code === 200) {
resolve(data)
} else {
uni.showToast({ title: data.msg || '上传失败', icon: 'none' })
reject(data)
}
} catch (e) {
reject(e)
}
},
fail: reject
})
})
}

View File

@@ -0,0 +1,19 @@
/** App 启动时关闭原生 splashCLI 构建 autoclose 恒为 false */
export function closeSplashscreen() {
// #ifdef APP-PLUS
const close = () => {
try {
if (typeof plus !== 'undefined' && plus.navigator) {
plus.navigator.closeSplashscreen()
}
} catch (e) {
console.error('[splash] close failed', e)
}
}
if (typeof document !== 'undefined') {
document.addEventListener('plusready', close, { once: true })
}
close()
;[50, 100, 300, 500, 1000, 2000, 3000, 5000, 8000].forEach((ms) => setTimeout(close, ms))
// #endif
}

View File

@@ -0,0 +1,22 @@
export function normalizeTenantType(tenantType) {
return tenantType != null ? String(tenantType).toLowerCase() : ''
}
export function isBrandTenant(tenantType) {
const type = normalizeTenantType(tenantType)
return type === '4' || type === 'brand'
}
export function isConsumerTenant(tenantType) {
const type = normalizeTenantType(tenantType)
return type === '3' || type === 'consumer' || type === 'personal'
}
export function isLensFactoryTenant(tenantType) {
const type = normalizeTenantType(tenantType)
return type === '5' || type === 'lens_factory' || type === 'factory' || type === 'lenses'
}
export function canAccessLensOrder(tenantType) {
return isBrandTenant(tenantType) || isConsumerTenant(tenantType)
}

34
uniapp/vite.config.js Normal file
View File

@@ -0,0 +1,34 @@
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import path from 'path'
export default defineConfig({
plugins: [uni()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler'
}
}
},
server: {
port: 2093,
proxy: {
'/dev-api': {
target: 'http://8.133.20.230:8082',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/dev-api/, '')
},
'/prod-api': {
target: 'http://8.133.20.230:8082',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/prod-api/, '')
}
}
}
})