Compare commits
2 Commits
9be6fd6ec9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
721543f423 | ||
|
|
324679964c |
@@ -535,17 +535,26 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="球镜S(度)" align="center" min-width="90">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column label="柱镜C(度)" align="center" min-width="90">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column label="轴位A(度)" align="center" min-width="80">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<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%" />
|
||||
</template>
|
||||
</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">
|
||||
<el-input-number v-model="scope.row.pupilHeight" :precision="2" :min="0" controls-position="right" style="width: 100%" />
|
||||
</template>
|
||||
@@ -563,9 +572,9 @@
|
||||
<el-input v-model="scope.row.distanceVa" placeholder="远用VA" />
|
||||
</template>
|
||||
</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">
|
||||
<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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -963,7 +972,7 @@
|
||||
<span>{{ formatDetailPdValue(scope.row, scope.$index) }}</span>
|
||||
</template>
|
||||
</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">
|
||||
<span>{{ scope.row.pupilHeight != null ? scope.row.pupilHeight : '-' }}</span>
|
||||
</template>
|
||||
@@ -973,7 +982,7 @@
|
||||
<span>{{ scope.row.distanceVa != null && scope.row.distanceVa !== '' ? scope.row.distanceVa : '-' }}</span>
|
||||
</template>
|
||||
</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">
|
||||
<span>{{ formatDetailNearAdd(scope.row.nearAdd) }}</span>
|
||||
</template>
|
||||
@@ -1471,8 +1480,8 @@ export default {
|
||||
},
|
||||
createDefaultOptometryList() {
|
||||
return [
|
||||
{ eyeSide: 'OS', sphereS: null, 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: 'OS', sphereS: 0, 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) {
|
||||
@@ -1516,6 +1525,45 @@ export default {
|
||||
if (Number.isNaN(num)) return String(value);
|
||||
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() {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
@@ -1523,6 +1571,12 @@ export default {
|
||||
handleProgressiveMultifocalChange(value) {
|
||||
if (Number(value) !== 1) {
|
||||
this.form.progressiveMultifocalPrice = 0;
|
||||
if (this.form.optometryList) {
|
||||
this.form.optometryList.forEach(row => {
|
||||
row.nearAdd = null;
|
||||
row.pupilHeight = null;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.form.lensFactoryTenantId) {
|
||||
@@ -1979,6 +2033,19 @@ export default {
|
||||
? this.resolveProgressiveMultifocalPrice()
|
||||
: 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 (!payload.lensFactoryTenantId) {
|
||||
this.$modal.msgWarning("选择渐进多焦点时需先选择镜片厂");
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
<span>{{ formatDetailPdValue(scope.row, scope.$index) }}</span>
|
||||
</template>
|
||||
</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">
|
||||
<span>{{ scope.row.pupilHeight != null ? scope.row.pupilHeight : '-' }}</span>
|
||||
</template>
|
||||
@@ -261,7 +261,7 @@
|
||||
<span>{{ scope.row.distanceVa != null && scope.row.distanceVa !== '' ? scope.row.distanceVa : '-' }}</span>
|
||||
</template>
|
||||
</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">
|
||||
<span>{{ formatDetailNearAdd(scope.row.nearAdd) }}</span>
|
||||
</template>
|
||||
|
||||
@@ -279,12 +279,14 @@
|
||||
<div class="section-body">
|
||||
<div class="pd-mode-switch">
|
||||
<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="dual">双瞳距</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span :key="'pd-tip-text-' + pdMode" class="pd-mode-tip">{{ pdColumnHeaderTip }}</span>
|
||||
</div>
|
||||
<el-table
|
||||
:key="'optometry-table-' + pdMode + '-' + form.progressiveMultifocal"
|
||||
:data="form.optometryList"
|
||||
:span-method="formOptometrySpanMethod"
|
||||
class="lens-optometry-table"
|
||||
@@ -302,8 +304,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column align="center" min-width="90">
|
||||
<template slot="header">
|
||||
<el-tooltip content="可录入范围:-20 ~ +20 度(负数为近视,正数为远视)" placement="top">
|
||||
<span class="optometry-header-tip">球镜S(度)</span>
|
||||
<el-tooltip content="可录入范围:-14.00 ~ +10.00(负数为近视,正数为远视)" placement="top">
|
||||
<span class="optometry-header-tip">球面度数(SPH)</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
@@ -319,18 +321,19 @@
|
||||
v-model="scope.row.sphereS"
|
||||
:precision="2"
|
||||
:step="0.25"
|
||||
:min="-SPHERE_S_MAX"
|
||||
:min="SPHERE_S_MIN"
|
||||
:max="SPHERE_S_MAX"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
@change="val => onSphereSChange(val, scope.row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" min-width="90">
|
||||
<template slot="header">
|
||||
<el-tooltip content="可录入范围:-10 ~ 0 度(仅负数,表示散光)" placement="top">
|
||||
<span class="optometry-header-tip">柱镜C(度)</span>
|
||||
<el-tooltip content="取值范围:-6.00 ~ +6.00(球面为正则散光需为正;球面为负则散光需为负;球面为0时散光可正可负)" placement="top">
|
||||
<span class="optometry-header-tip">散光度数(CYL)</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
@@ -345,8 +348,8 @@
|
||||
v-model="scope.row.cylinderC"
|
||||
:precision="2"
|
||||
:step="0.25"
|
||||
:min="CYLINDER_C_MIN"
|
||||
:max="0"
|
||||
:min="getCylinderCMin(scope.row)"
|
||||
:max="getCylinderCMax(scope.row)"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
@change="val => onCylinderCChange(val, scope.row)"
|
||||
@@ -356,15 +359,21 @@
|
||||
</el-table-column>
|
||||
<el-table-column align="center" min-width="100">
|
||||
<template slot="header">
|
||||
<el-tooltip content="可录入范围:0 ~ 180 度" placement="top">
|
||||
<span class="optometry-header-tip">轴位A(度)</span>
|
||||
<el-tooltip content="有散光时轴位须为 1 ~ 180,不能为 0" placement="top">
|
||||
<span class="optometry-header-tip">軸(散光请填写)(AXI)</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<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">
|
||||
<button type="button" class="axis-step-btn" @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>
|
||||
<button type="button" class="axis-step-btn" :disabled="!isAxisAEditable(scope.row)" @click="adjustAxisA(scope.row, 5)">+5</button>
|
||||
</div>
|
||||
<el-input-number
|
||||
v-model="scope.row.axisA"
|
||||
@@ -372,16 +381,19 @@
|
||||
:step="1"
|
||||
:min="0"
|
||||
:max="180"
|
||||
:disabled="!isAxisAEditable(scope.row)"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
@change="val => onAxisAChange(val, scope.row)"
|
||||
/>
|
||||
<div v-if="isAxisAInvalid(scope.row)" class="axis-error-tip">轴位不能为0</div>
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
<el-tooltip :content="pdColumnHeaderTip" placement="top">
|
||||
<span class="optometry-header-tip">瞳距(mm)</span>
|
||||
<el-tooltip :key="'pd-tip-' + pdMode" :content="pdColumnHeaderTip" placement="top">
|
||||
<span class="optometry-header-tip">{{ pdColumnHeaderLabel }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
@@ -392,9 +404,9 @@
|
||||
</div>
|
||||
<el-input-number
|
||||
v-model="scope.row.dualPd"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
:min="DUAL_PD_MIN"
|
||||
:max="DUAL_PD_MAX"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
@@ -407,9 +419,9 @@
|
||||
</div>
|
||||
<el-input-number
|
||||
v-model="singlePd"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
:min="SINGLE_PD_MIN"
|
||||
:max="SINGLE_PD_MAX"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
@@ -418,6 +430,59 @@
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
@@ -595,19 +660,19 @@
|
||||
<span>{{ scope.row.eyeSide === 'OS' ? '左眼(OS)' : '右眼(OD)' }}</span>
|
||||
</template>
|
||||
</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">
|
||||
<span>{{ formatDetailOptometryDecimal(scope.row.sphereS) }}</span>
|
||||
</template>
|
||||
</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">
|
||||
<span>{{ formatDetailOptometryDecimal(scope.row.cylinderC) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" min-width="80">
|
||||
<template slot="header">
|
||||
<span>轴位A(度)</span>
|
||||
<span>軸(AXI)</span>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.axisA != null ? scope.row.axisA : '-' }}</span>
|
||||
@@ -618,7 +683,7 @@
|
||||
<span>{{ formatDetailPdValue(scope.row, scope.$index) }}</span>
|
||||
</template>
|
||||
</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">
|
||||
<span>{{ scope.row.pupilHeight != null ? scope.row.pupilHeight : '-' }}</span>
|
||||
</template>
|
||||
@@ -628,7 +693,7 @@
|
||||
<span>{{ scope.row.distanceVa != null && scope.row.distanceVa !== '' ? scope.row.distanceVa : '-' }}</span>
|
||||
</template>
|
||||
</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">
|
||||
<span>{{ formatDetailNearAdd(scope.row.nearAdd) }}</span>
|
||||
</template>
|
||||
@@ -870,11 +935,17 @@ export default {
|
||||
pdMode: 'single',
|
||||
singlePd: 50,
|
||||
SINGLE_PD_DEFAULT: 50,
|
||||
SINGLE_PD_MAX: 100,
|
||||
DUAL_PD_DEFAULT: 25,
|
||||
DUAL_PD_MAX: 50,
|
||||
SPHERE_S_MAX: 20,
|
||||
CYLINDER_C_MIN: -10,
|
||||
SINGLE_PD_MIN: 40,
|
||||
SINGLE_PD_MAX: 80,
|
||||
DUAL_PD_DEFAULT: 30,
|
||||
DUAL_PD_MIN: 20,
|
||||
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_MAX: 25,
|
||||
detailPdMode: 'single',
|
||||
@@ -1005,10 +1076,13 @@ export default {
|
||||
}
|
||||
return '填写原因后提交换货申请'
|
||||
},
|
||||
pdColumnHeaderLabel() {
|
||||
return this.pdMode === 'dual' ? '双瞳距(mm)' : '单瞳距(mm)'
|
||||
},
|
||||
pdColumnHeaderTip() {
|
||||
return this.pdMode === 'dual'
|
||||
? '可录入范围:0 ~ 50 mm(单眼双瞳距)'
|
||||
: '可录入范围:0 ~ 100 mm(单瞳距)'
|
||||
? '双瞳距可录入范围:20.00 ~ 40.00 mm(左右眼分别填写)'
|
||||
: '单瞳距可录入范围:40.00 ~ 80.00 mm(左右眼共用一个值)'
|
||||
},
|
||||
logisticsDialogSubtitle() {
|
||||
if (this.logisticsBasic && this.logisticsBasic.orderNo) {
|
||||
@@ -1288,8 +1362,8 @@ export default {
|
||||
},
|
||||
createDefaultOptometryList() {
|
||||
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: 'OD', 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: 0, cylinderC: null, axisA: null, pd: this.SINGLE_PD_DEFAULT, dualPd: null, pdType: '1', pupilHeight: null, distanceVa: null, nearAdd: null }
|
||||
]
|
||||
},
|
||||
detectPdMode(optometryList) {
|
||||
@@ -1348,7 +1422,7 @@ export default {
|
||||
const osRow = this.form.optometryList.find(item => item.eyeSide === 'OS')
|
||||
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)
|
||||
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 => {
|
||||
if (row.pd == null) {
|
||||
row.pd = this.singlePd
|
||||
@@ -1357,7 +1431,7 @@ export default {
|
||||
},
|
||||
onSinglePdChange(val) {
|
||||
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
|
||||
this.singlePd = pd
|
||||
this.form.optometryList.forEach(row => {
|
||||
@@ -1371,7 +1445,7 @@ export default {
|
||||
const num = Number(val)
|
||||
if (Number.isNaN(num) || num === 0) return ''
|
||||
const degree = Math.abs(num).toFixed(2)
|
||||
return num > 0 ? `远视 ${degree} 度` : `近视 ${degree} 度`
|
||||
return num > 0 ? `远视 ${degree} ` : `近视 ${degree} `
|
||||
},
|
||||
getSphereSHintClass(row) {
|
||||
const num = Number(row.sphereS)
|
||||
@@ -1384,33 +1458,141 @@ export default {
|
||||
const num = Number(val)
|
||||
if (Number.isNaN(num) || num === 0) return ''
|
||||
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) {
|
||||
if (val == null || val === '') return
|
||||
if (val == null || val === '') {
|
||||
row.axisA = null
|
||||
return
|
||||
}
|
||||
let num = Number(val)
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
if (!this.isAxisAEditable(row)) return
|
||||
const current = row.axisA == null || row.axisA === '' ? 0 : Number(row.axisA)
|
||||
if (Number.isNaN(current)) return
|
||||
row.axisA = Math.min(180, Math.max(0, current + 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
|
||||
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.onSinglePdChange(next)
|
||||
},
|
||||
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
|
||||
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) {
|
||||
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)
|
||||
if (Number.isNaN(current)) return
|
||||
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) {
|
||||
this.pdMode = mode === 'dual' ? 'dual' : 'single'
|
||||
if (!this.form.optometryList) return
|
||||
if (mode === 'dual') {
|
||||
if (this.pdMode === 'dual') {
|
||||
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
|
||||
})
|
||||
this.singlePd = null
|
||||
} else {
|
||||
this.singlePd = this.SINGLE_PD_DEFAULT
|
||||
this.singlePd = this.singlePd != null ? this.singlePd : this.SINGLE_PD_DEFAULT
|
||||
this.form.optometryList.forEach(row => {
|
||||
row.pd = this.SINGLE_PD_DEFAULT
|
||||
row.pd = this.singlePd
|
||||
row.dualPd = null
|
||||
})
|
||||
}
|
||||
@@ -1460,6 +1675,19 @@ export default {
|
||||
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() {
|
||||
this.open = false
|
||||
@@ -1468,8 +1696,19 @@ export default {
|
||||
this.reset()
|
||||
},
|
||||
handleProgressiveMultifocalChange(value) {
|
||||
if (Number(value) !== 1) {
|
||||
this.form.progressiveMultifocalPrice = 0
|
||||
if (Number(value) === 1) {
|
||||
this.pdMode = 'dual'
|
||||
this.handlePdModeChange('dual')
|
||||
return
|
||||
}
|
||||
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() {
|
||||
@@ -1553,43 +1792,57 @@ export default {
|
||||
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 !== '') {
|
||||
let cylinder = Number(row.cylinderC)
|
||||
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)
|
||||
}
|
||||
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 !== '') {
|
||||
const sphereS = Number(row.sphereS)
|
||||
if (!Number.isNaN(sphereS)) {
|
||||
row.sphereS = Math.min(this.SPHERE_S_MAX, Math.max(-this.SPHERE_S_MAX, sphereS))
|
||||
}
|
||||
if (!this.isAxisAEditable(row)) {
|
||||
row.axisA = null
|
||||
}
|
||||
if (row.nearAdd != null && row.nearAdd !== '') {
|
||||
if (Number(form.progressiveMultifocal) === 1 && row.nearAdd != null && row.nearAdd !== '') {
|
||||
const nearAdd = Number(row.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 !== '') {
|
||||
const pd = Number(row.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 !== '') {
|
||||
const dualPd = Number(row.dualPd)
|
||||
if (!Number.isNaN(dualPd)) {
|
||||
row.dualPd = Math.min(this.DUAL_PD_MAX, Math.max(0, 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))
|
||||
row.dualPd = Math.min(this.DUAL_PD_MAX, Math.max(this.DUAL_PD_MIN, dualPd))
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1604,7 +1857,9 @@ export default {
|
||||
if (!this.form.optometryList || this.form.optometryList.length === 0) {
|
||||
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') {
|
||||
this.initSinglePd()
|
||||
} else {
|
||||
@@ -1613,6 +1868,8 @@ export default {
|
||||
if (row.dualPd == null) {
|
||||
row.dualPd = this.DUAL_PD_DEFAULT
|
||||
}
|
||||
row.pd = null
|
||||
row.pdType = this.PD_TYPE_DUAL
|
||||
})
|
||||
}
|
||||
this.formMode = mode
|
||||
@@ -1923,6 +2180,21 @@ export default {
|
||||
})
|
||||
},
|
||||
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()
|
||||
const progressiveMultifocal = this.form.progressiveMultifocal != null
|
||||
? Number(this.form.progressiveMultifocal) : 0
|
||||
@@ -2140,13 +2412,22 @@ export default {
|
||||
.pd-mode-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.pd-mode-label {
|
||||
margin-right: 12px;
|
||||
margin-right: 4px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pd-mode-tip {
|
||||
margin-left: 8px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.degree-sup {
|
||||
@@ -2220,11 +2501,41 @@ export default {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #94a3b8;
|
||||
background: #f1f5f9;
|
||||
border-color: #e2e8f0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:active {
|
||||
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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -2389,6 +2700,7 @@ export default {
|
||||
.pd-mode-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
padding: 10px 14px;
|
||||
@@ -2402,6 +2714,13 @@ export default {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.pd-mode-tip {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.el-radio-button__inner {
|
||||
border-color: rgba(37, 99, 235, 0.2);
|
||||
color: #475569;
|
||||
|
||||
@@ -62,6 +62,9 @@ public class JySysOrder extends BaseEntity
|
||||
@Excel(name = "渐进多焦点价格")
|
||||
private BigDecimal progressiveMultifocalPrice;
|
||||
|
||||
@Excel(name = "是否老花", readConverterExp = "0=否,1=是")
|
||||
private Integer presbyopia;
|
||||
|
||||
@Excel(name = "订单状态", dictType = "jy_order_status")
|
||||
private String orderStatus;
|
||||
|
||||
@@ -261,6 +264,16 @@ public class JySysOrder extends BaseEntity
|
||||
this.progressiveMultifocalPrice = progressiveMultifocalPrice;
|
||||
}
|
||||
|
||||
public Integer getPresbyopia()
|
||||
{
|
||||
return presbyopia;
|
||||
}
|
||||
|
||||
public void setPresbyopia(Integer presbyopia)
|
||||
{
|
||||
this.presbyopia = presbyopia;
|
||||
}
|
||||
|
||||
public String getOrderStatus()
|
||||
{
|
||||
return orderStatus;
|
||||
@@ -385,6 +398,7 @@ public class JySysOrder extends BaseEntity
|
||||
.append("priceC", getPriceC())
|
||||
.append("logisticsFee", getLogisticsFee())
|
||||
.append("progressiveMultifocal", getProgressiveMultifocal())
|
||||
.append("presbyopia", getPresbyopia())
|
||||
.append("orderStatus", getOrderStatus())
|
||||
.append("purchaseSettleStatus", getPurchaseSettleStatus())
|
||||
.append("saleSettleStatus", getSaleSettleStatus())
|
||||
|
||||
@@ -22,6 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="logisticsFee" column="logistics_fee" />
|
||||
<result property="progressiveMultifocal" column="progressive_multifocal" />
|
||||
<result property="progressiveMultifocalPrice" column="progressive_multifocal_price" />
|
||||
<result property="presbyopia" column="presbyopia" />
|
||||
<result property="orderStatus" column="order_status" />
|
||||
<result property="purchaseSettleStatus" column="purchase_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.product_id, p.product_model, p.product_name,
|
||||
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.remark, o.del_flag, o.create_by, o.create_time, o.update_by, o.update_time
|
||||
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="progressiveMultifocal != null">progressive_multifocal,</if>
|
||||
<if test="progressiveMultifocalPrice != null">progressive_multifocal_price,</if>
|
||||
<if test="presbyopia != null">presbyopia,</if>
|
||||
<if test="orderStatus != null">order_status,</if>
|
||||
<if test="purchaseSettleStatus != null">purchase_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="progressiveMultifocal != null">#{progressiveMultifocal},</if>
|
||||
<if test="progressiveMultifocalPrice != null">#{progressiveMultifocalPrice},</if>
|
||||
<if test="presbyopia != null">#{presbyopia},</if>
|
||||
<if test="orderStatus != null">#{orderStatus},</if>
|
||||
<if test="purchaseSettleStatus != null">#{purchaseSettleStatus},</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="progressiveMultifocal != null">progressive_multifocal = #{progressiveMultifocal},</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="purchaseSettleStatus != null">purchase_settle_status = #{purchaseSettleStatus},</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,
|
||||
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.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.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
|
||||
|
||||
@@ -16,6 +16,7 @@ CREATE TABLE `jy_sys_order` (
|
||||
`logistics_fee` decimal(10, 2) NULL DEFAULT NULL COMMENT '物流费用',
|
||||
`progressive_multifocal` tinyint(4) NULL DEFAULT 0 COMMENT '是否渐进多焦点(0否 1是)',
|
||||
`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)',
|
||||
`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已结算)',
|
||||
|
||||
@@ -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
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "OpticalSystem",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
2
uniapp/.env.development
Normal file
2
uniapp/.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
# H5 开发环境(走 vite 代理)
|
||||
VITE_APP_BASE_API=/dev-api
|
||||
3
uniapp/.env.production
Normal file
3
uniapp/.env.production
Normal 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
6
uniapp/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
unpackage/
|
||||
.hbuilderx/
|
||||
.DS_Store
|
||||
*.log
|
||||
86
uniapp/README.md
Normal file
86
uniapp/README.md
Normal 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
17
uniapp/index.html
Normal 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>
|
||||
4
uniapp/nativeResources/android/AndroidManifest.xml
Normal file
4
uniapp/nativeResources/android/AndroidManifest.xml
Normal 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
12590
uniapp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
uniapp/package.json
Normal file
45
uniapp/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
11
uniapp/scripts/adb-debug.ps1
Normal file
11
uniapp/scripts/adb-debug.ps1
Normal 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"
|
||||
26
uniapp/scripts/patch-app-config.js
Normal file
26
uniapp/scripts/patch-app-config.js
Normal 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))
|
||||
}
|
||||
}
|
||||
40
uniapp/scripts/patch-app-manifest.js
Normal file
40
uniapp/scripts/patch-app-manifest.js
Normal 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))
|
||||
}
|
||||
33
uniapp/scripts/verify-apk.ps1
Normal file
33
uniapp/scripts/verify-apk.ps1
Normal 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.exe(Android 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
179
uniapp/src/App.vue
Normal 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
9
uniapp/src/api/dict.js
Normal 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
64
uniapp/src/api/login.js
Normal 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
17
uniapp/src/api/message.js
Normal 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
73
uniapp/src/api/order.js
Normal 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
9
uniapp/src/api/tenant.js
Normal 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' })
|
||||
}
|
||||
43
uniapp/src/components/DictTag/DictTag.vue
Normal file
43
uniapp/src/components/DictTag/DictTag.vue
Normal 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>
|
||||
346
uniapp/src/components/NavBar/NavBar.vue
Normal file
346
uniapp/src/components/NavBar/NavBar.vue
Normal 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>
|
||||
16
uniapp/src/config/index.js
Normal file
16
uniapp/src/config/index.js
Normal 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
15
uniapp/src/main.js
Normal 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
77
uniapp/src/manifest.json
Normal 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
115
uniapp/src/pages.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
24
uniapp/src/pages/about/about.vue
Normal file
24
uniapp/src/pages/about/about.vue
Normal 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>
|
||||
10
uniapp/src/pages/account/address.vue
Normal file
10
uniapp/src/pages/account/address.vue
Normal 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>
|
||||
83
uniapp/src/pages/account/index.vue
Normal file
83
uniapp/src/pages/account/index.vue
Normal 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>
|
||||
69
uniapp/src/pages/account/realNameAuth.vue
Normal file
69
uniapp/src/pages/account/realNameAuth.vue
Normal 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>
|
||||
230
uniapp/src/pages/index/index.vue
Normal file
230
uniapp/src/pages/index/index.vue
Normal 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">为开发者提供完整的智能眼镜解决方案,包括硬件接入、SDK、API 文档和技术支持。</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>
|
||||
358
uniapp/src/pages/login/login.vue
Normal file
358
uniapp/src/pages/login/login.vue
Normal 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>
|
||||
110
uniapp/src/pages/message/message.vue
Normal file
110
uniapp/src/pages/message/message.vue
Normal 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>
|
||||
322
uniapp/src/pages/order/factory-order.vue
Normal file
322
uniapp/src/pages/order/factory-order.vue
Normal 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>
|
||||
391
uniapp/src/pages/order/lens-order-form.vue
Normal file
391
uniapp/src/pages/order/lens-order-form.vue
Normal 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.00~40.00'
|
||||
}
|
||||
}
|
||||
} else if (singlePd.value != null && singlePd.value !== '') {
|
||||
const num = Number(singlePd.value)
|
||||
if (Number.isNaN(num) || num < 40 || num > 80) {
|
||||
return '单瞳距范围应为40.00~80.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>
|
||||
228
uniapp/src/pages/order/lens-order.vue
Normal file
228
uniapp/src/pages/order/lens-order.vue
Normal 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>
|
||||
69
uniapp/src/pages/order/logistics.vue
Normal file
69
uniapp/src/pages/order/logistics.vue
Normal 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>
|
||||
105
uniapp/src/pages/order/order-detail.vue
Normal file
105
uniapp/src/pages/order/order-detail.vue
Normal 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>
|
||||
134
uniapp/src/pages/register/register.vue
Normal file
134
uniapp/src/pages/register/register.vue
Normal 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>
|
||||
82
uniapp/src/pages/resetPassword/resetPassword.vue
Normal file
82
uniapp/src/pages/resetPassword/resetPassword.vue
Normal 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>
|
||||
28
uniapp/src/pages/user/profile.vue
Normal file
28
uniapp/src/pages/user/profile.vue
Normal 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>
|
||||
0
uniapp/src/static/images/.gitkeep
Normal file
0
uniapp/src/static/images/.gitkeep
Normal file
131
uniapp/src/store/index.js
Normal file
131
uniapp/src/store/index.js
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
4
uniapp/src/store/pinia.js
Normal file
4
uniapp/src/store/pinia.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
const pinia = createPinia()
|
||||
export default pinia
|
||||
10
uniapp/src/uni.scss
Normal file
10
uniapp/src/uni.scss
Normal 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
13
uniapp/src/utils/auth.js
Normal 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)
|
||||
}
|
||||
6
uniapp/src/utils/errorCode.js
Normal file
6
uniapp/src/utils/errorCode.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
'401': '认证失败,无法访问系统资源',
|
||||
'403': '当前操作没有权限',
|
||||
'404': '访问资源不存在',
|
||||
'default': '系统未知错误,请反馈给管理员'
|
||||
}
|
||||
262
uniapp/src/utils/orderHelper.js
Normal file
262
uniapp/src/utils/orderHelper.js
Normal 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
127
uniapp/src/utils/request.js
Normal 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
|
||||
})
|
||||
})
|
||||
}
|
||||
19
uniapp/src/utils/splash.js
Normal file
19
uniapp/src/utils/splash.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/** App 启动时关闭原生 splash(CLI 构建 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
|
||||
}
|
||||
22
uniapp/src/utils/tenantType.js
Normal file
22
uniapp/src/utils/tenantType.js
Normal 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
34
uniapp/vite.config.js
Normal 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/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user