Fix seller all-in-one image and order flows

This commit is contained in:
Chopper711
2026-06-17 16:45:36 +08:00
parent 1c10b20e10
commit c404f663ad
33 changed files with 513 additions and 125 deletions

View File

@@ -10,6 +10,8 @@
想快速体验完整商城不需要分别部署后端、PC、商家端、运营端、IM、H5、MySQL 和 Redis。安装 Docker Desktop 后,直接执行下面脚本,按提示输入 IP/域名、端口、密码和数据目录即可启动单镜像单容器环境。
数据库口径:常规部署和项目技术栈仍以 MySQL 为准All-In-One 镜像内置 MariaDB 只是单容器本地体验中的 MySQL 协议兼容实现,不代表生产部署或常规部署已经切换为 MariaDB。
macOS / Linux:
```bash
@@ -26,7 +28,7 @@ Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\install-lilishop.ps1
```
默认镜像:`ccr.ccs.tencentyun.com/lilishop/lilishop-all-in-one:lite`。启动后可访问买家 PC、商家端、运营端、IM Web、uniapp H5后端 API。更多说明见 [docker/all-in-one 文档](https://gitee.com/beijing_hongye_huicheng/docker/tree/allinone-lite-mysql-redis/all-in-one)。
默认镜像:`ccr.ccs.tencentyun.com/lilishop/lilishop-all-in-one:lite`。启动后可访问买家 PC、商家端、运营端、IM Web、uniapp H5,统一后端 All-In-One 的同域 `/api/` 入口代理。更多说明见 [docker/all-in-one 文档](https://gitee.com/beijing_hongye_huicheng/docker/tree/allinone-lite-mysql-redis/all-in-one)。
LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开源商城系统,支持 B2B2C 多商户商城、小程序商城、微服务商城、直播电商、分销返佣、秒杀活动、Docker 私有化部署。
@@ -80,7 +82,7 @@ Lilishop 由 4 个独立仓库组成,均同步托管于 GitHub 与 Gitee
- **全端覆盖**: 一套代码库支持PC、H5、小程序、APP降低开发和维护成本。
- **商家入驻**: 支持多商家入驻,构建平台化电商生态。
- **分布式架构**: 后端API服务化支持独立部署和弹性伸缩
- **All-In-One 后端**: 当前分支推荐使用 `lilishop-all` 统一后端入口,降低本地体验和私有化部署门槛
- **前后端分离**: 清晰的职责划分,便于团队协作和独立开发。
- **容器化支持**: 提供Docker镜像和docker-compose配置实现一键部署。
- **功能完善**: 涵盖客户、订单、商品、促销、店铺、运营、统计等完整电商业务模块。

View File

@@ -3,15 +3,15 @@ var BASE = {
* @description api请求基础路径
*/
API_DEV: {
common: "https://common-api.pickmall.cn",
buyer: "https://buyer-api.pickmall.cn",
seller: "https://store-api.pickmall.cn",
manager: "https://admin-api.pickmall.cn"
common: "/api",
buyer: "/api",
seller: "/api",
manager: "/api"
},
API_PROD: {
common: "https://common-api.pickmall.cn",
buyer: "https://buyer-api.pickmall.cn",
seller: "https://store-api.pickmall.cn",
manager: "https://admin-api.pickmall.cn"
common: "/api",
buyer: "/api",
seller: "/api",
manager: "/api"
},
};

10
im/.env
View File

@@ -1,10 +1,10 @@
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=https://im-api.pickmall.cn
VUE_APP_WEB_SOCKET_URL=wss://im-api.pickmall.cn/lili/webSocket
VUE_APP_COMMON=https://common-api.pickmall.cn
VUE_APP_BUYER=https://buyer-api.pickmall.cn
VUE_APP_SELLER=https://store-api.pickmall.cn
VUE_APP_API_BASE_URL=/api
VUE_APP_WEB_SOCKET_URL=/lili/webSocket
VUE_APP_COMMON=/api
VUE_APP_BUYER=/api
VUE_APP_SELLER=/api
VUE_APP_WEBSITE_NAME="LiLi IM"
VUE_APP_PC_URL=https://pc-b2b2c.pickmall.cn/
VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/

View File

@@ -1,10 +1,10 @@
NODE_ENV=development
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=https://im-api.pickmall.cn
VUE_APP_WEB_SOCKET_URL=wss://im-api.pickmall.cn/lili/webSocket
VUE_APP_COMMON=https://common-api.pickmall.cn
VUE_APP_BUYER=https://buyer-api.pickmall.cn
VUE_APP_SELLER=https://store-api.pickmall.cn
VUE_APP_API_BASE_URL=/api
VUE_APP_WEB_SOCKET_URL=/lili/webSocket
VUE_APP_COMMON=/api
VUE_APP_BUYER=/api
VUE_APP_SELLER=/api
VUE_APP_WEBSITE_NAME="LiLi IM"
VUE_APP_PC_URL=https://pc-b2b2c.pickmall.cn/
VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/

View File

@@ -43,7 +43,7 @@
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
server_name im-api.pickmall.cn;
server_name shop.example.com;
location / {
proxy_pass http://127.0.0.1:8088;
}

View File

@@ -22,13 +22,24 @@ const normalizeUrl = (url) => {
return url.endsWith("/") ? url : `${url}/`;
};
const normalizeWsUrl = (url) => {
if (!url || /^wss?:\/\//.test(url)) {
return url || "";
}
if (typeof window !== "undefined" && url.startsWith("/")) {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
return `${protocol}//${window.location.host}${url}`;
}
return url;
};
export default {
// 网站名称
WEBSITE_NAME: process.env.VUE_APP_WEBSITE_NAME || "LiLi IM",
// 默认请求IM的API
BASE_API_URL: runtimeApi || process.env.VUE_APP_API_BASE_URL || "",
// 默认请求的WS
BASE_WS_URL: runtimeBase.IM_WS_URL || process.env.VUE_APP_WEB_SOCKET_URL || "",
BASE_WS_URL: normalizeWsUrl(runtimeBase.IM_WS_URL || process.env.VUE_APP_WEB_SOCKET_URL || ""),
// 默认请求公有接口相关 API
BASE_COMMON: apiProd.common || apiDev.common || process.env.VUE_APP_COMMON || "",
// 默认请求用户相关API

View File

@@ -3,16 +3,16 @@ var BASE = {
* @description api请求基础路径
*/
API_DEV: {
common: "https://common-api.pickmall.cn",
buyer: "https://buyer-api.pickmall.cn",
seller: "https://store-api.pickmall.cn",
manager: "https://admin-api.pickmall.cn",
common: "/api",
buyer: "/api",
seller: "/api",
manager: "/api",
},
API_PROD: {
common: "https://common-api.pickmall.cn",
buyer: "https://buyer-api.pickmall.cn",
seller: "https://store-api.pickmall.cn",
manager: "https://admin-api.pickmall.cn"
common: "/api",
buyer: "/api",
seller: "/api",
manager: "/api"
},
/**
* @description // 跳转买家端地址 pc端

View File

@@ -272,12 +272,12 @@
</dl>
<dl>
<dt>物流公司</dt>
<dd>{{ afterSaleInfo.mlogisticsName }}</dd>
<dd>{{ buyerReturnLogisticsName }}</dd>
</dl>
<dl>
<dt>物流单号</dt>
<dd>
{{ afterSaleInfo.mlogisticsNo }}
{{ buyerReturnLogisticsNo }}
</dd>
</dl>
<dl>
@@ -312,13 +312,13 @@
<dl>
<dt>物流公司:</dt>
<dd>
<div class="text-box">{{ afterSaleInfo.mlogisticsName }}</div>
<div class="text-box">{{ buyerReturnLogisticsName }}</div>
</dd>
</dl>
<dl>
<dt>物流单号:</dt>
<dd>
<div class="text-box">{{ afterSaleInfo.mlogisticsNo }}</div>
<div class="text-box">{{ buyerReturnLogisticsNo }}</div>
</dd>
</dl>
<div class="div-express-log">
@@ -410,7 +410,24 @@ export default {
],
};
},
computed: {
buyerReturnLogisticsName() {
return this.pickAfterSaleField("mlogisticsName", "mLogisticsName", "MLogisticsName", "m_logistics_name");
},
buyerReturnLogisticsNo() {
return this.pickAfterSaleField("mlogisticsNo", "mLogisticsNo", "MLogisticsNo", "m_logistics_no");
},
},
methods: {
pickAfterSaleField(...fields) {
for (const field of fields) {
const value = this.afterSaleInfo && this.afterSaleInfo[field];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
return "";
},
// 获取售后详情
getDetail() {
this.loading = true;

View File

@@ -3,16 +3,16 @@ var BASE = {
* @description api请求基础路径
*/
API_DEV: {
common: "https://common-api.pickmall.cn",
buyer: "https://buyer-api.pickmall.cn",
seller: "https://store-api.pickmall.cn",
manager: "https://admin-api.pickmall.cn",
common: "/api",
buyer: "/api",
seller: "/api",
manager: "/api",
},
API_PROD: {
common: "https://common-api.pickmall.cn",
buyer: "https://buyer-api.pickmall.cn",
seller: "https://store-api.pickmall.cn",
manager: "https://admin-api.pickmall.cn",
common: "/api",
buyer: "/api",
seller: "/api",
manager: "/api",
},
/**
* @description // 跳转买家端地址 pc端

View File

@@ -0,0 +1,19 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const orderApi = readFileSync(new URL('./order.js', import.meta.url), 'utf8');
const afterSaleDetail = readFileSync(new URL('../views/order/after-order/reurnGoodsOrderDetail.vue', import.meta.url), 'utf8');
const managerAfterSaleDetail = readFileSync(new URL('../../../manager/src/views/order/after-order/afterSaleOrderDetail.vue', import.meta.url), 'utf8');
test('complaint reply posts to the exact seller-api communication route', () => {
assert.match(orderApi, /postRequest\(`\/order\/complain\/communication`, params\)/);
assert.doesNotMatch(orderApi, /\/order\/complain\/communication\//);
});
test('after-sale detail displays buyer return logistics from backend JSON fields', () => {
assert.match(afterSaleDetail, /buyerReturnLogisticsName/);
assert.match(afterSaleDetail, /buyerReturnLogisticsNo/);
assert.match(managerAfterSaleDetail, /buyerReturnLogisticsName/);
assert.match(managerAfterSaleDetail, /buyerReturnLogisticsNo/);
});

View File

@@ -73,7 +73,7 @@ export const getComplainDetail = id => {
//添加交易投诉对话
export const addOrderComplaint = params => {
return postRequest(`/order/complain/communication/`, params);
return postRequest(`/order/complain/communication`, params);
};
//添加交易投诉对话

View File

@@ -0,0 +1,11 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const shopsApi = readFileSync(new URL('./shops.js', import.meta.url), 'utf8');
test('store pickup address list and create use backend route without trailing slash', () => {
assert.match(shopsApi, /getRequest\(`\/member\/storeAddress`, params\)/);
assert.match(shopsApi, /postRequest\(`\/member\/storeAddress`, params\)/);
assert.doesNotMatch(shopsApi, /\/member\/storeAddress\/`/);
});

View File

@@ -71,7 +71,7 @@ export const logisticsUnChecked = (id, params) => {
}
// 获取商家自提点
export const getShopAddress = (id, params) => {
return getRequest(`/member/storeAddress/`, params)
return getRequest(`/member/storeAddress`, params)
}
// 修改商家自提点
@@ -81,7 +81,7 @@ export const editShopAddress = (id, params) => {
// 添加商品自提点
export const addShopAddress = (params) => {
return postRequest(`/member/storeAddress/`, params)
return postRequest(`/member/storeAddress`, params)
}
// 添加商品自提点
@@ -140,4 +140,3 @@ export const editChecked = (logisticsId,params) => {
return putRequest(`/other/logistics/${logisticsId}/updateStoreLogistics`,params)
}

View File

@@ -0,0 +1,12 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
const source = readFileSync("seller/src/libs/axios.js", "utf8");
test("seller token refresh waits on the refresh request promise", () => {
assert.match(source, /let refreshPromise = null;/);
assert.match(source, /refreshPromise = handleRefreshToken\(oldRefreshToken\)/);
assert.match(source, /return refreshPromise;/);
assert.doesNotMatch(source, /setInterval\(/, "refresh should not depend on polling shared state");
});

View File

@@ -136,45 +136,29 @@ service.interceptors.response.use(
// 防抖闭包来一波
function getTokenDebounce() {
let lock = false;
let success = false;
let refreshPromise = null;
return function() {
if (!lock) {
lock = true;
if (!refreshPromise) {
let oldRefreshToken = getStore("refreshToken");
handleRefreshToken(oldRefreshToken)
.then(res => {
refreshPromise = handleRefreshToken(oldRefreshToken)
.then((res) => {
if (res.success) {
let { accessToken, refreshToken } = res.result;
setStore("accessToken", accessToken);
setStore("refreshToken", refreshToken);
success = true;
lock = false;
return "success";
} else {
success = false;
lock = false;
// router.push('/login')
return "fail";
}
})
.catch(err => {
success = false;
lock = false;
.catch(() => {
return "fail";
})
.finally(() => {
refreshPromise = null;
});
}
return new Promise(resolve => {
// 一直看lock,直到请求失败或者成功
const timer = setInterval(() => {
if (!lock) {
clearInterval(timer);
if (success) {
resolve("success");
} else {
resolve("fail");
}
}
}, 500); // 轮询时间间隔
});
return refreshPromise;
};
}
@@ -410,4 +394,3 @@ export const postRequestWithNoToken = (url, params) => {
data: params
});
};

View File

@@ -0,0 +1,28 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
const source = readFileSync("seller/src/utils/file-url.js", "utf8");
test("file url normalizer targets local storage placeholder paths only", () => {
assert.match(source, /LOCAL_FILE_PREFIX\s*=\s*["']\/files["']/, "should declare the local file placeholder prefix");
assert.match(source, /startsWith\(LOCAL_FILE_PREFIX \+ ["']\/["']\)/, "should only rewrite /files/... urls");
assert.match(source, /\^\(https\?:\)\?\\\/\\\//, "should keep absolute http urls");
assert.match(source, /data\|blob\|mailto\|tel/, "should keep non-http browser urls");
});
test("file url normalizer uses common api origin for resource records", () => {
assert.match(source, /function normalizeFileUrl\(url,\s*apiBase\)/, "should accept api origin explicitly");
assert.match(source, /String\(apiBase \|\| ["']["']\)\.replace\(\/\\\/\+\$\/,\s*["']["']\)/, "should trim trailing slashes from api origin");
assert.match(source, /return `\$\{base\}\$\{trimmed\}`/, "should prepend api origin to local file paths");
assert.match(source, /function normalizeFileRecords\(records,\s*apiBase\)/, "should normalize resource record lists");
assert.match(source, /url:\s*normalizeFileUrl\(item\.url,\s*apiBase\)/, "should normalize each record url");
});
test("file url normalizer parses selected resource values into objects", () => {
assert.match(source, /function parseSelectedFileValue\(value\)/, "should expose a parser for checkbox values");
assert.match(source, /value\.indexOf\(["'],["']\)/, "should split only at the id/url separator");
assert.match(source, /id:\s*value\.slice\(0,\s*separatorIndex\)/, "should preserve selected resource id");
assert.match(source, /url:\s*value\.slice\(separatorIndex \+ 1\)/, "should preserve the whole selected url");
assert.match(source, /function normalizeSelectedFileValues\(values\)/, "should expose selected value list normalization");
});

View File

@@ -0,0 +1,80 @@
const LOCAL_FILE_PREFIX = "/files";
export function normalizeFileUrl(url, apiBase) {
if (!url || typeof url !== "string") {
return url;
}
const trimmed = url.trim();
if (
/^(https?:)?\/\//i.test(trimmed) ||
/^(data|blob|mailto|tel):/i.test(trimmed)
) {
return url;
}
if (
trimmed !== LOCAL_FILE_PREFIX &&
!trimmed.startsWith(LOCAL_FILE_PREFIX + "/")
) {
return url;
}
const base = String(apiBase || "").replace(/\/+$/, "");
if (!base) {
return url;
}
return `${base}${trimmed}`;
}
export function normalizeFileRecords(records, apiBase) {
if (!Array.isArray(records)) {
return records;
}
return records.map((item) => {
if (!item) {
return item;
}
return {
...item,
url: normalizeFileUrl(item.url, apiBase),
};
});
}
export function parseSelectedFileValue(value) {
if (!value) {
return null;
}
if (typeof value === "object") {
return value.url ? value : null;
}
if (typeof value !== "string") {
return null;
}
const separatorIndex = value.indexOf(",");
if (separatorIndex < 0) {
return {
id: "",
url: value,
};
}
return {
id: value.slice(0, separatorIndex),
url: value.slice(separatorIndex + 1),
};
}
export function normalizeSelectedFileValues(values) {
if (!Array.isArray(values)) {
return [];
}
return values
.map((value) => parseSelectedFileValue(value))
.filter((item) => item && item.url);
}

View File

@@ -0,0 +1,25 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const component = readFileSync(new URL('./goodsOperationSec.vue', import.meta.url), 'utf8');
test('resource-library image import stores only image urls in goodsGalleryFiles', () => {
assert.match(component, /normalizeImageUrl/);
assert.match(component, /appendSelectedImages/);
assert.doesNotMatch(component, /baseInfoForm\[this\.selectedFormBtnName\] = \[\.\.\.this\.baseInfoForm\[this\.selectedFormBtnName\], \.\.\.this\.selectedImage\]/);
});
test('goods submit filters invalid gallery entries before building goodsGalleryList', () => {
assert.match(component, /filter\(Boolean\)/);
assert.match(component, /submit\.goodsGalleryFiles = submit\.goodsGalleryFiles/);
});
test('category parameter validation reset guards missing clearValidate method', () => {
assert.match(component, /typeof this\.\$refs\.baseInfoForm\.clearValidate === ["']function["']/);
});
test('direct goods uploads always send a fallback directory path', () => {
assert.match(component, /uploadDirectoryData:\s*\{\s*directoryPath:\s*["']default["']/);
assert.match(component, /:data=["']uploadDirectoryData["']/);
});

View File

@@ -156,7 +156,8 @@
<video :src="baseInfoForm.goodsVideo" class="video" controls style="max-width: 300px;" />
</div>
</div>
<Upload ref="upload" :action="uploadFileUrl" :format="['avi', 'wmv', 'mpeg', 'mp4', 'mov']"
<Upload ref="upload" :action="uploadFileUrl" :data="uploadDirectoryData"
:format="['avi', 'wmv', 'mpeg', 'mp4', 'mov']"
:headers="{ ...accessToken }" :max-size="10240" :on-error="() => { loadingVideo = false }"
:on-exceeded-size="handleVideoMaxSize" :on-format-error="handleFormatError"
:on-progress="() => { loadingVideo = true }" :on-success="handleSuccessGoodsVideo"
@@ -239,7 +240,8 @@
</template>
</div>
</vuedraggable>
<Upload ref="uploadSku" :action="uploadFileUrl" v-if="val.images < 1"
<Upload ref="uploadSku" :action="uploadFileUrl" :data="uploadDirectoryData"
v-if="val.images < 1"
:before-upload="handleBeforeUpload" :format="['jpg', 'jpeg', 'png', 'webp']"
:headers="{ ...accessToken }" :max-size="2048" :on-error="() => { $Spin.hide(); }"
:on-exceeded-size="handleMaxSize" :on-format-error="handleFormatError"
@@ -496,6 +498,9 @@ export default {
submitLoading: false,
//上传图片路径
uploadFileUrl: uploadFile,
uploadDirectoryData: {
directoryPath: "default",
},
// 预览图片路径
previewPicture: "",
//商品图片
@@ -696,19 +701,35 @@ export default {
callbackSelected(val) {
this.picModelFlag = false;
if (val && this.selectedFormBtnName == 'selectedSkuImages') {
this.selectedSku.images.push(val);
this.selectedSku.images = this.appendSelectedImages(this.selectedSku.images, [val]);
} else {
this.baseInfoForm[this.selectedFormBtnName].push(val.url);
this.baseInfoForm[this.selectedFormBtnName] = this.appendSelectedImages(this.baseInfoForm[this.selectedFormBtnName], [val]);
}
},
confirmUrls() {
if (this.selectedImage && this.selectedFormBtnName == 'selectedSkuImages') {
this.selectedSku.images = [...this.selectedSku.images, ...this.selectedImage];
this.selectedSku.images = this.appendSelectedImages(this.selectedSku.images, this.selectedImage);
} else {
this.baseInfoForm[this.selectedFormBtnName] = [...this.baseInfoForm[this.selectedFormBtnName], ...this.selectedImage];
this.baseInfoForm[this.selectedFormBtnName] = this.appendSelectedImages(this.baseInfoForm[this.selectedFormBtnName], this.selectedImage);
}
},
normalizeImageUrl(image) {
if (!image) {
return "";
}
if (typeof image === "string") {
return image;
}
return image.url || image.original || image.thumbnail || image.fileUrl || "";
},
appendSelectedImages(target, images) {
const current = Array.isArray(target) ? target : [];
const selected = (Array.isArray(images) ? images : [images])
.map((image) => this.normalizeImageUrl(image))
.filter(Boolean);
return [...current, ...selected];
},
// 局部刷新
refresh(v) {
if (v == 'template') {
@@ -1272,7 +1293,7 @@ export default {
// 确保表单验证能正确初始化
this.$nextTick(() => {
if (this.$refs.baseInfoForm) {
if (this.$refs.baseInfoForm && typeof this.$refs.baseInfoForm.clearValidate === "function") {
this.$refs.baseInfoForm.clearValidate();
}
});
@@ -2035,6 +2056,14 @@ export default {
this.$Message.error("请上传商品图片");
return;
}
submit.goodsGalleryFiles = submit.goodsGalleryFiles
.map((image) => this.normalizeImageUrl(image))
.filter(Boolean);
if (submit.goodsGalleryFiles.length <= 0) {
this.submitLoading = false;
this.$Message.error("请上传商品图片");
return;
}
if (submit.templateId === "") submit.templateId = 0;
let flag = false;
let paramValue = "";

View File

@@ -0,0 +1,11 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
const source = readFileSync("seller/src/views/lili-components/editor/upload-image.vue", "utf8");
test("editor resource import is confirmed once from selected resource objects", () => {
assert.doesNotMatch(source, /@callback=["']handleCallback["']/, "resource import should not also add images on checkbox callback");
assert.match(source, /normalizeSelectedImages\(this\.selectedImage\)/, "confirm should normalize selected resource objects");
assert.match(source, /this\.images\.push\(\.\.\.selectedImages\)/, "confirm should append normalized images once");
});

View File

@@ -57,13 +57,14 @@
</Modal>
<Modal width="1000" v-model="showOssManager" @on-ok="confirmUrls">
<OssManage ref="ossManage" :isComponent="true" :initialize="showOssManager" @selected="(list)=>{ selectedImage = list}" @callback="handleCallback" />
<OssManage ref="ossManage" :isComponent="true" :initialize="showOssManager" @selected="(list)=>{ selectedImage = list}" />
</Modal>
</div>
</template>
<script>
import vuedraggable from "vuedraggable";
import {uploadFile} from "@/libs/axios";
import { parseSelectedFileValue } from "@/utils/file-url";
// import OssManage from "@/views/sys/oss-manage/ossManage";
import OssManage from "@/views/shop/ossManage";
export default {
@@ -123,14 +124,19 @@ export default {
}
},
confirmUrls(){
this.selectedImage.length ? this.selectedImage.forEach(element => {
this.images.push({ url: element.url })
}):''
const selectedImages = this.normalizeSelectedImages(this.selectedImage);
if (selectedImages.length) {
this.images.push(...selectedImages);
}
this.selectedImage = [];
this.showOssManager = false
},
handleCallback(val){
this.$Message.success("导入成功")
this.images.push({url:val.url})
normalizeSelectedImages(images) {
const list = Array.isArray(images) ? images : [images];
return list
.map((image) => parseSelectedFileValue(image))
.filter((image) => image && image.url)
.map((image) => ({ url: image.url }));
},
// 从资源库中导入图片
importOSS(){

View File

@@ -336,6 +336,7 @@ export default {
this.$Message.error("请填写对话内容");
return;
}
this.submitLoading = true;
this.params.complainId = this.id;
API_Order.addOrderComplaint(this.params).then((res) => {
this.submitLoading = false;
@@ -344,6 +345,8 @@ export default {
this.params.content = "";
this.getDetail();
}
}).catch(() => {
this.submitLoading = false;
});
},
//申诉

View File

@@ -196,14 +196,14 @@
</dl>
</div>
<div class="div-form-default" v-if="afterSaleInfo.serviceStatus =='BUYER_RETURN' || afterSaleInfo.serviceStatus =='COMPLETE' && afterSaleInfo.serviceType !='RETURN_MONEY'">
<div class="div-form-default" v-if="showBuyerReturnLogistics">
<h3>回寄物流信息</h3>
<dl>
<dt>
物流公司
</dt>
<dd>
{{ afterSaleInfo.mlogisticsName }}
{{ buyerReturnLogisticsName }}
</dd>
</dl>
<dl>
@@ -211,7 +211,7 @@
物流单号
</dt>
<dd>
{{ afterSaleInfo.mlogisticsNo }}
{{ buyerReturnLogisticsNo }}
</dd>
</dl>
<dl>
@@ -408,7 +408,28 @@ export default {
],
};
},
computed: {
buyerReturnLogisticsName() {
return this.pickAfterSaleField("mlogisticsName", "mLogisticsName", "MLogisticsName", "m_logistics_name");
},
buyerReturnLogisticsNo() {
return this.pickAfterSaleField("mlogisticsNo", "mLogisticsNo", "MLogisticsNo", "m_logistics_no");
},
showBuyerReturnLogistics() {
const status = this.afterSaleInfo.serviceStatus;
return this.afterSaleInfo.serviceType !== "RETURN_MONEY" && (status === "BUYER_RETURN" || status === "COMPLETE");
},
},
methods: {
pickAfterSaleField(...fields) {
for (const field of fields) {
const value = this.afterSaleInfo && this.afterSaleInfo[field];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
return "";
},
// 获取售后详情
getDetail() {
this.loading = true;

View File

@@ -0,0 +1,11 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const component = readFileSync(new URL('./full-discount-add.vue', import.meta.url), 'utf8');
test('full discount submit clears mutually exclusive discount flags before saving', () => {
assert.match(component, /normalizeDiscountFlags/);
assert.match(component, /params\.fullMinusFlag = params\.discountType === "fullMinusFlag"/);
assert.match(component, /params\.fullRateFlag = params\.discountType === "fullRateFlag"/);
});

View File

@@ -314,11 +314,7 @@ export default {
});
params.scopeId = scopeId.toString();
}
if (params.discountType == "fullMinusFlag") {
params.fullMinusFlag = true;
} else {
params.fullRateFlag = true;
}
this.normalizeDiscountFlags(params);
delete params.rangeTime;
this.submitLoading = true;
if (!this.id) {
@@ -351,6 +347,16 @@ export default {
// 已选商品批量选择
this.selectedGoods = e;
},
normalizeDiscountFlags(params) {
params.fullMinusFlag = params.discountType === "fullMinusFlag";
params.fullRateFlag = params.discountType === "fullRateFlag";
if (!params.fullMinusFlag) {
params.fullMinus = null;
}
if (!params.fullRateFlag) {
params.fullRate = null;
}
},
delSelectGoods () {
// 多选删除商品
if (this.selectedGoods.length <= 0) {

View File

@@ -0,0 +1,47 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
const files = [
"seller/src/views/shop/ossManage.vue",
"seller/src/views/shop/ossManages.vue",
"seller/src/views/sys/oss-manage/ossManage.vue",
];
test("seller resource manager exposes owner name search", () => {
for (const file of files) {
const source = readFileSync(file, "utf8");
assert.match(source, /label=["']上传人["']/, `${file} should show uploader search field`);
assert.match(source, /v-model=["']searchForm\.ownerName["']/, `${file} should bind ownerName`);
assert.match(source, /ownerName:\s*["']["']/, `${file} should initialize ownerName query param`);
}
});
test("seller resource manager never uploads with an undefined directory path", () => {
for (const file of files) {
const source = readFileSync(file, "utf8");
assert.match(source, /uploadDirectoryData/, `${file} should use computed upload directory data`);
assert.match(source, /directoryPath:\s*this\.searchForm\.fileDirectoryId\s*\|\|\s*["']default["']/, `${file} should fallback to default directory`);
assert.doesNotMatch(source, /:data="\{\s*directoryPath:\s*searchForm\.fileDirectoryId,\s*\}"/, `${file} should not bind raw fileDirectoryId`);
}
});
test("seller resource manager normalizes local file urls before rendering and selecting", () => {
for (const file of files) {
const source = readFileSync(file, "utf8");
assert.match(source, /normalizeFileRecords/, `${file} should import the local file url normalizer`);
assert.match(source, /this\.data\s*=\s*normalizeFileRecords\(res\.result\.records,\s*commonUrl\)/, `${file} should normalize resource list urls with common api origin`);
assert.match(source, /<img :src="item\.url"/, `${file} should render the normalized url`);
assert.match(source, /:label="item\.id\+','\+item\.url"/, `${file} should select the normalized url`);
}
});
test("seller resource manager emits selected resources as objects", () => {
for (const file of files) {
const source = readFileSync(file, "utf8");
assert.match(source, /normalizeSelectedFileValues/, `${file} should import selected value normalization`);
assert.match(source, /const selectedFiles\s*=\s*normalizeSelectedFileValues\(e\)/, `${file} should parse checkbox values into selected file objects`);
assert.match(source, /this\.selectList\s*=\s*selectedFiles\.map\(item => \{\s*return \{\s*id:\s*item\.id\s*\}\s*\}\)/, `${file} should keep delete ids from parsed selected objects`);
assert.match(source, /this\.\$emit\(["']selected["'],\s*selectedFiles\)/, `${file} should emit selected file objects, not raw id,url strings`);
}
});

View File

@@ -55,6 +55,15 @@
@on-change="selectDateRange"
></DatePicker>
</Form-item>
<Form-item label="上传人" prop="ownerName">
<Input
v-model="searchForm.ownerName"
clearable
placeholder="图片拥有者名称"
style="width: 240px"
type="text"
/>
</Form-item>
<Button
class="search-btn"
icon="ios-search"
@@ -87,9 +96,7 @@
<Upload
ref="up"
:action="commonUrl + '/common/common/upload/file'"
:data="{
directoryPath: searchForm.fileDirectoryId,
}"
:data="uploadDirectoryData"
:headers="accessToken"
:max-size="20480"
:on-error="handleError"
@@ -413,6 +420,7 @@
} from "@/api/index";
import DPlayer from "dplayer";
import { commonUrl } from "@/libs/axios";
import { normalizeFileRecords, normalizeSelectedFileValues, parseSelectedFileValue } from "@/utils/file-url";
const config = require("@/config/index");
@@ -474,6 +482,7 @@
name: "",
fileKey: "",
fileType: "",
ownerName: "",
pageNumber: 1, // 当前页数
pageSize: 27, // 页面大小
sort: "createTime", // 默认排序字段
@@ -869,11 +878,21 @@
if (val) this.selectImage = val
},
selectedOss(val) {
if (val && val.length > 0 && val[val.length-1].split(',')[1]) {
this.$emit("callback", {url: val[val.length-1].split(',')[1]});
if (val && val.length > 0) {
const selectedFile = parseSelectedFileValue(val[val.length - 1]);
if (selectedFile && selectedFile.url) {
this.$emit("callback", selectedFile);
}
}
}
},
computed: {
uploadDirectoryData() {
return {
directoryPath: this.searchForm.fileDirectoryId || "default",
};
},
},
methods: {
onMouseOver(item, index) {
@@ -886,12 +905,13 @@
// 复选框值改变时触发
selectOssChange(e) {
if (e) {
this.selectList = e.map(item => {return { id: item.split(',')[0]}});
this.selectCount = e.length;
const selectedFiles = normalizeSelectedFileValues(e);
this.selectList = selectedFiles.map(item => {return { id: item.id }});
this.selectCount = selectedFiles.length;
// let size = 0;
// e.forEach((item) => {size += item.fileSize * 1.0;});
// this.totalSize = ((size * 1.0) / (1024 * 1024)).toFixed(2) + " MB";
this.$emit("selected", e);
this.$emit("selected", selectedFiles);
}
},
// 页码改变时回调
@@ -1176,7 +1196,7 @@
getFileListData(this.searchForm).then((res) => {
this.loading = false;
this.data = res.result.records;
this.data = normalizeFileRecords(res.result.records, commonUrl);
this.total = res.result.total;
if (type === 'refresh') {
this.$Message.success('刷新成功!');

View File

@@ -13,6 +13,9 @@
<Form-item label="上传时间">
<DatePicker v-model="selectDate" clearable format="yyyy-MM-dd" placeholder="选择起始时间" style="width: 200px" type="daterange" @on-change="selectDateRange"></DatePicker>
</Form-item>
<Form-item label="上传人" prop="ownerName">
<Input v-model="searchForm.ownerName" clearable placeholder="图片拥有者名称" style="width: 200px" type="text" />
</Form-item>
<Button class="search-btn" icon="ios-search" type="primary" @click="handleSearch">搜索
</Button>
</Form>
@@ -37,9 +40,7 @@
<Upload
ref="up"
:action="commonUrl + '/common/common/upload/file'"
:data="{
directoryPath: searchForm.fileDirectoryId,
}"
:data="uploadDirectoryData"
:headers="accessToken"
:max-size="20480"
:on-error="handleError"
@@ -203,6 +204,7 @@
} from "@/api/index";
import DPlayer from "dplayer";
import { commonUrl } from "@/libs/axios";
import { normalizeFileRecords, normalizeSelectedFileValues, parseSelectedFileValue } from "@/utils/file-url";
const config = require("@/config/index");
@@ -268,6 +270,7 @@
name: "",
fileKey: "",
fileType: "",
ownerName: "",
pageNumber: 1, // 当前页数
pageSize: 27, // 页面大小
sort: "createTime", // 默认排序字段
@@ -665,7 +668,10 @@
},
selectedOss(val) {
if (val && val.length) {
this.$emit("callback", {url: val[val.length-1].split(',')[1]});
const selectedFile = parseSelectedFileValue(val[val.length - 1]);
if (selectedFile && selectedFile.url) {
this.$emit("callback", selectedFile);
}
}
},
// 初始化监听 是否清空所选图片
@@ -675,6 +681,13 @@
}
}
},
computed: {
uploadDirectoryData() {
return {
directoryPath: this.searchForm.fileDirectoryId || "default",
};
},
},
methods: {
onMouseOver(item, index) {
@@ -687,12 +700,13 @@
// 复选框值改变时触发
selectOssChange(e) {
if (e) {
this.selectList = e.map(item => {return { id: item.split(',')[0]}});
this.selectCount = e.length;
const selectedFiles = normalizeSelectedFileValues(e);
this.selectList = selectedFiles.map(item => {return { id: item.id }});
this.selectCount = selectedFiles.length;
// let size = 0;
// e.forEach((item) => {size += item.fileSize * 1.0;});
// this.totalSize = ((size * 1.0) / (1024 * 1024)).toFixed(2) + " MB";
this.$emit("selected", e);
this.$emit("selected", selectedFiles);
}
},
// 页码改变时回调
@@ -977,7 +991,7 @@
getFileListData(this.searchForm).then((res) => {
this.loading = false;
this.data = res.result.records;
this.data = normalizeFileRecords(res.result.records, commonUrl);
this.total = res.result.total;
if (type === 'refresh') {
this.$Message.success('刷新成功!');

View File

@@ -179,6 +179,7 @@ export default {
UNPAID: "未付款",
PAID: "已付款",
DELIVERED: "已发货",
PARTS_DELIVERED: "部分发货",
CANCELLED: "已取消",
COMPLETED: "已完成",
TAKE: "已完成",

View File

@@ -79,6 +79,7 @@ export default {
UNPAID: "未付款",
PAID: "已付款",
DELIVERED: "已发货",
PARTS_DELIVERED: "部分发货",
CANCELLED: "已取消",
COMPLETED: "已完成",
TAKE: "已完成",

View File

@@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
const files = [
"seller/src/views/statistics/order.vue",
"seller/src/views/statistics/order/orderDetail.vue",
"seller/src/views/statistics/order/refundOrder.vue",
];
test("order statistics pages map PARTS_DELIVERED to 部分发货", () => {
for (const file of files) {
const source = readFileSync(file, "utf8");
assert.match(
source,
/PARTS_DELIVERED:\s*["']部分发货["']/,
`${file} should render partial delivery status instead of blank text`,
);
}
});

View File

@@ -82,6 +82,7 @@ export default {
UNPAID: "未付款",
PAID: "已付款",
DELIVERED: "已发货",
PARTS_DELIVERED: "部分发货",
CANCELLED: "已取消",
COMPLETED: "已完成",
TAKE: "已完成",

View File

@@ -93,9 +93,7 @@
<Upload
ref="up"
:action="commonUrl + '/common/common/upload/file'"
:data="{
directoryPath: searchForm.fileDirectoryId,
}"
:data="uploadDirectoryData"
:headers="accessToken"
:max-size="20480"
:on-error="handleError"
@@ -257,6 +255,7 @@ import {
} from "@/api/index";
import DPlayer from "dplayer";
import { commonUrl } from "@/libs/axios";
import { normalizeFileRecords, normalizeSelectedFileValues, parseSelectedFileValue } from "@/utils/file-url";
const config = require("@/config/index");
@@ -720,7 +719,10 @@ export default {
},
selectedOss(val) {
if (val && val.length) {
this.$emit("callback", {url: val[val.length-1].split(',')[1]});
const selectedFile = parseSelectedFileValue(val[val.length - 1]);
if (selectedFile && selectedFile.url) {
this.$emit("callback", selectedFile);
}
}
},
// 初始化监听 是否清空所选图片
@@ -735,6 +737,13 @@ export default {
}
},
},
computed: {
uploadDirectoryData() {
return {
directoryPath: this.searchForm.fileDirectoryId || "default",
};
},
},
methods: {
onMouseOver(item, index) {
@@ -747,12 +756,13 @@ export default {
// 复选框值改变时触发
selectOssChange(e) {
if (e) {
this.selectList = e.map(item => {return { id: item.split(',')[0]}});
this.selectCount = e.length;
const selectedFiles = normalizeSelectedFileValues(e);
this.selectList = selectedFiles.map(item => {return { id: item.id }});
this.selectCount = selectedFiles.length;
// let size = 0;
// e.forEach((item) => {size += item.fileSize * 1.0;});
// this.totalSize = ((size * 1.0) / (1024 * 1024)).toFixed(2) + " MB";
this.$emit("selected", e);
this.$emit("selected", selectedFiles);
}
},
// 页码改变时回调
@@ -1043,7 +1053,7 @@ export default {
getFileListData(this.searchForm).then((res) => {
this.loading = false;
this.data = res.result.records;
this.data = normalizeFileRecords(res.result.records, commonUrl);
this.total = res.result.total;
if (type === 'refresh') {
this.$Message.success('刷新成功!');