mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-13 04:18:55 +08:00
Compare commits
10 Commits
feat/goods
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
792229a11b | ||
|
|
242d3a0f9f | ||
|
|
bfcd81ca81 | ||
|
|
8d2adf6732 | ||
|
|
935d56268c | ||
|
|
52c92731c8 | ||
|
|
3d77c58e63 | ||
|
|
c398ff4717 | ||
|
|
60de941749 | ||
|
|
402c9c9ebb |
@@ -168,6 +168,8 @@ lilishop-ui
|
||||
|
||||
| 时间 | 功能模块 | 完成内容 |
|
||||
| :--- | :--- | :--- |
|
||||
| 2026-11-12 | 优惠券礼包 | 支持券礼包活动创建、多券组合打包、管理端 H5 推广二维码生成及链接复制。 |
|
||||
| 2026-11-12 | 出库单 / 库存流水 | 支持商家端出库单管理(录入、审核与作废)及商品库存流水台账。 |
|
||||
| 2026-11-06 | 提货卡 | 支持提货卡活动与候选商品配置、制卡发卡、兑换核销及使用记录管理。 |
|
||||
| 2026-11-05 | 付费会员 | 单一付费会员、开通与续费、有效期、关联权益、商家与商品折扣、平台补贴及付费记录。 |
|
||||
| 2026-11-04 | 客户等级体系 / 客户权益 | 等级配置、权益介绍、经验值任务与明细、权益发放及重试,提供默认等级与权益数据。 |
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
import { onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import storage from "@/plugins/storage";
|
||||
import { getBaseSite, getThemeSetting } from "@/api/common.js";
|
||||
import { fetchAndApplyTheme } from "@/utils/theme";
|
||||
import { loadSiteSettings } from "@/composables/useSiteSettings";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "App" });
|
||||
@@ -49,27 +48,15 @@ function init() {
|
||||
});
|
||||
}
|
||||
getSite();
|
||||
getTheme();
|
||||
}
|
||||
|
||||
function getSite() {
|
||||
getBaseSite().then((res) => {
|
||||
if (res.success && res.result.settingValue) {
|
||||
try {
|
||||
const data = JSON.parse(res.result.settingValue);
|
||||
applySiteInfo(data);
|
||||
} catch {
|
||||
// 站点配置解析失败时使用缓存
|
||||
}
|
||||
loadSiteSettings().then((data) => {
|
||||
if (data) {
|
||||
applySiteInfo(data);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.warn('获取站点配置失败', err);
|
||||
});
|
||||
}
|
||||
|
||||
function getTheme() {
|
||||
fetchAndApplyTheme(getThemeSetting).catch((err) => {
|
||||
console.warn('获取主题配置失败', err);
|
||||
console.warn("获取站点配置失败", err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
import storage from '@/plugins/storage';
|
||||
import { logout } from '@/api/account';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
@@ -69,12 +71,22 @@ const username = computed(() => {
|
||||
return '';
|
||||
});
|
||||
|
||||
function handleLogout() {
|
||||
function clearLocalAuth() {
|
||||
storage.removeItem('userInfo');
|
||||
storage.removeItem('accessToken');
|
||||
storage.removeItem('refreshToken');
|
||||
storage.removeItem('cartNum');
|
||||
Cookies.remove('userInfo');
|
||||
Cookies.remove('accessToken');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
logout().finally(() => {
|
||||
clearLocalAuth();
|
||||
});
|
||||
}
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
const opt = props.options || {};
|
||||
|
||||
@@ -2,11 +2,14 @@ import { computed, onMounted, ref } from "vue";
|
||||
import { getBaseSite } from "@/api/common.js";
|
||||
import config from "@/config";
|
||||
|
||||
// 同页的页头、页脚共享请求;每次重新进入页面时刷新,避免旧缓存覆盖后台设置。
|
||||
// 全局共享站点配置;应用生命周期内仅拉取一次,同页与切页均复用请求与结果。
|
||||
const settings = ref(null);
|
||||
let pendingRequest;
|
||||
let pendingRequest = null;
|
||||
|
||||
export function loadSiteSettings() {
|
||||
if (settings.value) {
|
||||
return Promise.resolve(settings.value);
|
||||
}
|
||||
if (!pendingRequest) {
|
||||
pendingRequest = getBaseSite()
|
||||
.then((res) => {
|
||||
@@ -16,7 +19,9 @@ export function loadSiteSettings() {
|
||||
return settings.value;
|
||||
})
|
||||
.catch(() => settings.value)
|
||||
.finally(() => { pendingRequest = null; });
|
||||
.finally(() => {
|
||||
pendingRequest = null;
|
||||
});
|
||||
}
|
||||
return pendingRequest;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,11 @@ const reasonList = ref([]); // 投诉原因
|
||||
|
||||
function getOrderDetail() { // 获取订单详情
|
||||
orderDetail(route.query.sn).then(res => {
|
||||
if (res.result?.order?.orderStatus === 'CANCELLED') {
|
||||
Message.warning('订单已取消,无法发起投诉');
|
||||
router.push({ name: 'MyOrder' });
|
||||
return;
|
||||
}
|
||||
order.value = res.result
|
||||
orderGoods.value = res.result.orderItems[route.query.index]
|
||||
orderGoods.value.uploadList = []
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
<div class="goods-num">共{{ goods.num }}件</div>
|
||||
<div class="goods-extra">
|
||||
<el-tag v-if="refundPriceList(goods.isRefund) && !isUnpaid(order)" type="warning" size="small">{{ refundPriceList(goods.isRefund) }}</el-tag>
|
||||
<el-tag v-if="canShowRefundTag(order, goods)" type="warning" size="small">{{ refundPriceList(goods.isRefund) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@
|
||||
</div>
|
||||
<div class="goods-num">共{{ goods.num }}件</div>
|
||||
<div class="goods-extra">
|
||||
<el-tag v-if="refundPriceList(goods.isRefund) && !isUnpaid(order)" type="warning" size="small">{{ refundPriceList(goods.isRefund) }}</el-tag>
|
||||
<el-tag v-if="canShowRefundTag(order, goods)" type="warning" size="small">{{ refundPriceList(goods.isRefund) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,6 +264,11 @@ function refundPriceList(status) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function canShowRefundTag (order, goods) {
|
||||
// 未支付订单未发生资金交易,不展示退款标签
|
||||
if (!order || order.payStatus !== 'PAID') return false;
|
||||
return !!refundPriceList(goods?.isRefund);
|
||||
}
|
||||
function goodsDetail (skuId, goodsId) {
|
||||
// 跳转商品详情
|
||||
let routeUrl = router.resolve({
|
||||
@@ -445,13 +450,16 @@ function paymentLabel (order) {
|
||||
if (order.paymentMethod && map[order.paymentMethod]) {
|
||||
return map[order.paymentMethod]
|
||||
}
|
||||
if (order.orderStatus === 'CANCELLED') {
|
||||
return ''
|
||||
}
|
||||
if (isUnpaid(order)) {
|
||||
return '待支付'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
function isUnpaid (order) {
|
||||
return order.payStatus === 'UNPAID' || order.orderStatus === 'UNPAID'
|
||||
return (order.payStatus === 'UNPAID' || order.orderStatus === 'UNPAID') && order.orderStatus !== 'CANCELLED'
|
||||
}
|
||||
|
||||
watch(() => route.query.tag, () => {
|
||||
|
||||
@@ -297,7 +297,11 @@
|
||||
@click="comment(order.order.sn, goodsIndex)"
|
||||
>评价</button>
|
||||
<el-button
|
||||
v-if="goods.complainStatus == 'NO_APPLY'"
|
||||
v-if="
|
||||
order.order?.orderStatus !== 'CANCELLED' &&
|
||||
order.order?.payStatus === 'PAID' &&
|
||||
goods.complainStatus == 'NO_APPLY'
|
||||
"
|
||||
@click="complain(order.order.sn, goodsIndex)"
|
||||
type="danger"
|
||||
size="small"
|
||||
@@ -306,6 +310,7 @@
|
||||
<el-button
|
||||
v-if="
|
||||
!isECouponOrder &&
|
||||
order.order?.orderStatus !== 'CANCELLED' &&
|
||||
(goods.afterSaleStatus.includes('NOT_APPLIED') ||
|
||||
goods.afterSaleStatus.includes('PART_AFTER_SALE'))
|
||||
"
|
||||
|
||||
@@ -69,27 +69,39 @@ export function normalizeTheme(data = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
let pendingThemeRequest = null;
|
||||
|
||||
export async function fetchAndApplyTheme(requestFn) {
|
||||
const cachedTheme = loadCachedTheme();
|
||||
if (cachedTheme) {
|
||||
applyThemeToDocument(normalizeTheme(cachedTheme));
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await requestFn();
|
||||
if (res?.success && res?.result?.settingValue) {
|
||||
const data = JSON.parse(res.result.settingValue);
|
||||
const normalized = normalizeTheme(data);
|
||||
applyThemeToDocument(normalized);
|
||||
cacheTheme(normalized);
|
||||
return normalized;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[theme] 主题色加载失败,请确认 buyer-api 已启动且站点主题配置可用",
|
||||
error
|
||||
);
|
||||
if (pendingThemeRequest) {
|
||||
return pendingThemeRequest;
|
||||
}
|
||||
|
||||
return cachedTheme ? normalizeTheme(cachedTheme) : normalizeTheme();
|
||||
pendingThemeRequest = (async () => {
|
||||
try {
|
||||
const res = await requestFn();
|
||||
if (res?.success && res?.result?.settingValue) {
|
||||
const data = JSON.parse(res.result.settingValue);
|
||||
const normalized = normalizeTheme(data);
|
||||
applyThemeToDocument(normalized);
|
||||
cacheTheme(normalized);
|
||||
return normalized;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[theme] 主题色加载失败,请确认 buyer-api 已启动且站点主题配置可用",
|
||||
error
|
||||
);
|
||||
} finally {
|
||||
pendingThemeRequest = null;
|
||||
}
|
||||
|
||||
return cachedTheme ? normalizeTheme(cachedTheme) : normalizeTheme();
|
||||
})();
|
||||
|
||||
return pendingThemeRequest;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { post, get, upload, del } from "@/utils/request";
|
||||
import config from "@/config/config";
|
||||
|
||||
// 获取聊天列表服务接口
|
||||
export const ServeGetTalkList = (data) => {
|
||||
@@ -69,9 +70,9 @@ export const ServeSendTalkFile = (data) => {
|
||||
return post("/im/talk/message/file", data);
|
||||
};
|
||||
|
||||
// 发送聊天图片服务接口
|
||||
// 上传客服图片到独立 OSS;FormData 必须包含会话 ID,店铺及目录由后端推导。
|
||||
export const ServeSendTalkImage = (data) => {
|
||||
return upload("/buyer/common/upload/file", data);
|
||||
return upload("/im/message/image", data, { apiBase: config.BASE_API_URL });
|
||||
};
|
||||
|
||||
// 发送表情包服务接口
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="image-message no-select">
|
||||
<el-image
|
||||
fit="cover"
|
||||
fit="contain"
|
||||
:src="src"
|
||||
:lazy="true"
|
||||
:style="getImgStyle(src)"
|
||||
:preview-src-list="[src]"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-slot">图片加载失败...</div>
|
||||
@@ -29,6 +29,10 @@ defineProps({
|
||||
})
|
||||
|
||||
function getImgStyle(url) {
|
||||
// 商城上传地址不携带尺寸,提供可见的预览区域;原尺寸后缀仍沿用旧展示规则。
|
||||
if (!/\d+x\d+/.test(url.split('_').pop())) {
|
||||
return { width: '200px', height: '200px', maxWidth: '100%' }
|
||||
}
|
||||
return imgZoom(url, 200)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -295,7 +295,7 @@ function formatDateToString(date) {
|
||||
// margin-left: 12px;
|
||||
}
|
||||
:deep(.el-tabs__nav-scroll) {
|
||||
min-width: 362px;
|
||||
min-width: 0;
|
||||
}
|
||||
:deep(.el-tabs__item) {
|
||||
height: 100%;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<el-container class="ov-hidden flex-10 full-height ">
|
||||
<el-container class="ov-hidden flex-10 full-height chat-main">
|
||||
<PanelHeader ref="panelHeader" :data="params" :online="isOnline" :keyboard="inputEvent"
|
||||
@event="handleHeaderEvent" />
|
||||
<!-- 主体信息 -->
|
||||
@@ -65,7 +65,7 @@
|
||||
<pre v-if="!emojistwo.includes(item.text)" v-html="item.text" />
|
||||
<pre v-if="emojistwo.includes(item.text)" v-html="textReplaceEmoji(item.text)" />
|
||||
</div>
|
||||
|
||||
<ImageMessage v-if="item.messageType === 'PICTURE'" :src="item.text" />
|
||||
</div>
|
||||
<div v-if="item.webSocketStatus" class="tips">网络异常发送失败,请重新发送。</div>
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
<!-- 页脚信息 -->
|
||||
<el-footer class="footer-box" height="180">
|
||||
<template v-if="multiSelect.isOpen === false">
|
||||
<MeEditor @send="submitSendMessage" @keyboard-event="onKeyboardEvent" />
|
||||
<MeEditor :talk-id="params.talkId" @send="submitSendMessage" @send-image="submitSendImage" @keyboard-event="onKeyboardEvent" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PanelToolbar v-model="multiSelect.items.length" @event="handleMultiMode" />
|
||||
@@ -185,7 +185,7 @@
|
||||
</transition>
|
||||
|
||||
<!-- 链接信息 -->
|
||||
<OtherLink :toUser="toUser" :id="id" :goodsParams="goodsParams" class="flex-4" />
|
||||
<OtherLink :toUser="toUser" :id="id" :goodsParams="goodsParams" class="chat-sidebar" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
@@ -202,6 +202,7 @@ import { textReplaceEmoji, emojistwo } from "@/utils/emojis";
|
||||
import OtherLink from "@/components/chat/panel/OtherLink.vue";
|
||||
import TalkSearchRecord from "@/components/chat/TalkSearchRecord";
|
||||
import MeEditor from "@/components/editor/MeEditor";
|
||||
import ImageMessage from "@/components/chat/messaege/ImageMessage.vue";
|
||||
import PanelHeader from "./PanelHeader";
|
||||
import PanelToolbar from "./PanelToolbar";
|
||||
import SocketInstance from "@/im-server/socket-instance";
|
||||
@@ -414,16 +415,21 @@ function formateDateAndTimeToString(date) {
|
||||
// 重新发送消息
|
||||
function againSendMessage(val) {
|
||||
|
||||
submitSendMessage(val.text)
|
||||
submitSendMessage(val.text, val.messageType)
|
||||
}
|
||||
|
||||
// 回车键发送消息回调事件
|
||||
function submitSendMessage(content) {
|
||||
// 发送已上传图片,复用消息发送和失败重发流程。
|
||||
function submitSendImage(url) {
|
||||
submitSendMessage(url, "PICTURE");
|
||||
}
|
||||
|
||||
// 发送文本或图片,重发保持原类型,发送图片时保留文字草稿。
|
||||
function submitSendMessage(content, messageType = "MESSAGE") {
|
||||
const record = {
|
||||
operation_type: "MESSAGE",
|
||||
to: props.params.receiver_id,
|
||||
from: id.value,
|
||||
message_type: "MESSAGE",
|
||||
message_type: messageType,
|
||||
context: content,
|
||||
talk_id: props.params.talkId,
|
||||
};
|
||||
@@ -434,7 +440,7 @@ function submitSendMessage(content) {
|
||||
|
||||
talkStore.UPDATE_TALK_ITEM({
|
||||
index_name: index_name.value,
|
||||
draft_text: "",
|
||||
...(messageType === "MESSAGE" ? { draft_text: "" } : {}),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -445,7 +451,8 @@ function submitSendMessage(content) {
|
||||
fromUser: id.value,
|
||||
toUser: record.to,
|
||||
isRead: false,
|
||||
messageType: "MESSAGE",
|
||||
messageType,
|
||||
talkId: record.talk_id,
|
||||
text: content,
|
||||
float: "right",
|
||||
};
|
||||
@@ -465,7 +472,7 @@ function submitSendMessage(content) {
|
||||
});
|
||||
} else {
|
||||
talkStore.SET_TLAK_UNREAD_MESSAGE({
|
||||
content: content,
|
||||
content: messageType === "PICTURE" ? "[图片]" : content,
|
||||
nickname: record.name,
|
||||
});
|
||||
}
|
||||
@@ -889,6 +896,17 @@ function talkPanelScrollBottom() {
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
/* 固定侧栏宽度,防止 tab 内容最小宽度挤压聊天区域。 */
|
||||
.chat-main {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.chat-sidebar {
|
||||
flex: 0 0 362px;
|
||||
width: 362px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.flex-a-c{
|
||||
align-items: center;
|
||||
>div{
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
<legacy-el-icon name="el-icon-headset" />
|
||||
<p class="tip-title">语音消息</p>
|
||||
</li> -->
|
||||
<!-- #TODO 发图片功能暂时隐藏 此处会涉及到token过期 -->
|
||||
<!-- <li @click="$refs.restFile.click()">
|
||||
<li role="button" tabindex="0" aria-label="发送图片" @click="restFile.click()"
|
||||
@keydown.enter="restFile.click()" @keydown.space.prevent="restFile.click()">
|
||||
<legacy-el-icon name="el-icon-picture-outline-round" />
|
||||
<p class="tip-title">图片</p>
|
||||
</li> -->
|
||||
</li>
|
||||
<!-- <li @click="$refs.restFile2.click()">
|
||||
<legacy-el-icon name="el-icon-folder" />
|
||||
<p class="tip-title">附件</p>
|
||||
@@ -117,8 +117,12 @@ import {
|
||||
} from "@/api/chat";
|
||||
|
||||
defineOptions({ name: "MeEditor" });
|
||||
const props = defineProps({
|
||||
// 后端 li_im_talk.id;不使用接收方 ID 代替会话 ID,避免图片存入错误会话。
|
||||
talkId: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["keyboard-event", "send"]);
|
||||
const emit = defineEmits(["keyboard-event", "send", "send-image"]);
|
||||
|
||||
const dialogueStore = useDialogueStore();
|
||||
|
||||
@@ -141,6 +145,10 @@ const imageViewer = reactive({
|
||||
isShow: false,
|
||||
file: null,
|
||||
});
|
||||
// 预览绑定原会话,上传期间切换会话后不发送,避免图片误发。
|
||||
let imageTalk = null;
|
||||
let imageTalkId = null;
|
||||
let uploadingImage = false;
|
||||
|
||||
const codeBlock = reactive({
|
||||
isShow: false,
|
||||
@@ -165,6 +173,8 @@ const sendtime = ref(0);
|
||||
const interval = ref(1000);
|
||||
|
||||
watch(talkUser, (n_index_name) => {
|
||||
imageViewer.isShow = false;
|
||||
imageViewer.file = null;
|
||||
fileManageRef.value?.clear();
|
||||
editorText.value = getDraftText(n_index_name);
|
||||
});
|
||||
@@ -253,6 +263,21 @@ function uploadFileChange(e) {
|
||||
|
||||
// 打开图片查看器
|
||||
function openImageViewer(file) {
|
||||
if (!file || uploadingImage) return;
|
||||
if (!props.talkId) {
|
||||
ElNotification.warning({ message: "会话尚未就绪,请稍后重试" });
|
||||
return;
|
||||
}
|
||||
if (!/^image\/(png|jpeg|gif|webp)$/i.test(file.type) || !file.size) {
|
||||
ElNotification.warning({ message: "请选择有效的 PNG、JPG、GIF 或 WebP 图片" });
|
||||
return;
|
||||
}
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
ElNotification.warning({ message: "图片不能大于10MB" });
|
||||
return;
|
||||
}
|
||||
imageTalk = talkUser.value;
|
||||
imageTalkId = props.talkId;
|
||||
imageViewer.isShow = true;
|
||||
imageViewer.file = file;
|
||||
}
|
||||
@@ -278,29 +303,34 @@ function confirmCodeBlock(data) {
|
||||
});
|
||||
}
|
||||
|
||||
// 确认上传图片消息回调事件
|
||||
function confirmUploadImage() {
|
||||
let fileData = new FormData();
|
||||
fileData.append("file", imageViewer.file);
|
||||
|
||||
let imageView = imageViewerRef.value;
|
||||
|
||||
ServeSendTalkImage(fileData)
|
||||
.then((res) => {
|
||||
imageView.loading = false;
|
||||
if (res.code == 200) {
|
||||
imageView.closeBox();
|
||||
} else {
|
||||
ElNotification({
|
||||
title: "友情提示",
|
||||
message: res.message,
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
imageView.loading = false;
|
||||
});
|
||||
// 上传返回地址后通过现有消息通道发送;失败保留预览以便重试。
|
||||
async function confirmUploadImage() {
|
||||
if (uploadingImage) return;
|
||||
const imageView = imageViewerRef.value;
|
||||
const targetTalk = imageTalk;
|
||||
const targetTalkId = imageTalkId;
|
||||
uploadingImage = true;
|
||||
try {
|
||||
const fileData = new FormData();
|
||||
fileData.append("file", imageViewer.file);
|
||||
fileData.append("talkId", targetTalkId);
|
||||
const res = await ServeSendTalkImage(fileData);
|
||||
if (res.code !== 200 || typeof res.result !== "string" || !/^https?:\/\//i.test(res.result)) {
|
||||
throw new Error(res.message || "图片上传失败,请重试");
|
||||
}
|
||||
if (targetTalk !== talkUser.value || targetTalkId !== props.talkId || !imageViewer.isShow) {
|
||||
ElNotification.warning({ message: "会话已切换,图片未发送,请在目标会话重新选择" });
|
||||
return;
|
||||
}
|
||||
emit("send-image", res.result);
|
||||
imageView.loading = false;
|
||||
imageView.closeBox();
|
||||
} catch (error) {
|
||||
ElNotification.error({ message: error.message || "图片上传失败,请重试" });
|
||||
} finally {
|
||||
uploadingImage = false;
|
||||
imageView.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 选中表情包回调事件
|
||||
|
||||
@@ -62,7 +62,10 @@ function closeBox() {
|
||||
emit('close', false)
|
||||
}
|
||||
|
||||
// 清理切换会话后的预览,仅对有效文件读取内容。
|
||||
function loadFile(file) {
|
||||
src.value = ''
|
||||
if (!file) return
|
||||
let reader = new FileReader()
|
||||
fileSize.value = Math.ceil(file.size / 1024)
|
||||
fileName.value = file.name
|
||||
@@ -75,6 +78,7 @@ function loadFile(file) {
|
||||
|
||||
// 确认按钮事件
|
||||
function uploadImage() {
|
||||
if (loading.value || !src.value) return
|
||||
loading.value = true
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useDialogueStore } from "@/stores/dialogue";
|
||||
import config from "@/config/config";
|
||||
import WsSocket from "@/plugins/ws-socket";
|
||||
import { getToken } from "@/utils/auth";
|
||||
@@ -86,6 +87,15 @@ class SocketInstance {
|
||||
});
|
||||
|
||||
this.socket.on("event_error", (data) => {
|
||||
// 将被服务端拒绝的图片标记为失败,避免乐观展示被误认为发送成功。
|
||||
if (data.pictureUrl) {
|
||||
useDialogueStore().records.forEach((record) => {
|
||||
if (record.messageType === "PICTURE" && record.float === "right"
|
||||
&& record.talkId === data.talkId && record.text === data.pictureUrl) {
|
||||
record.webSocketStatus = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
ElNotification({
|
||||
title: "友情提示",
|
||||
message: data.message,
|
||||
|
||||
@@ -272,6 +272,12 @@ class WsSocket {
|
||||
* @param {Object} evt Websocket 消息
|
||||
*/
|
||||
onMessage(evt) {
|
||||
// 图片权限校验失败属于系统反馈,不能当成聊天消息插入记录。
|
||||
const envelope = JSON.parse(evt.data);
|
||||
if (envelope.messageResultType === "SYSTEM_TIPS") {
|
||||
this.onCallBacks["event_error"]?.(envelope.result);
|
||||
return;
|
||||
}
|
||||
let result = this.onParse(evt);
|
||||
// 部分推送(在线状态、系统通知等)没有 text 字段,不做商品/群名解析直接透传
|
||||
const text = result.text;
|
||||
|
||||
@@ -153,10 +153,12 @@ export const del = (url, data = {}, options = {}) => {
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export const upload = (url, data = {}, options = {}) => {
|
||||
// 默认沿用买家上传地址;客服图片显式选择 IM 服务,不将文件发往通用上传入口。
|
||||
const { apiBase = config.BASE_BUYER, ...requestOptions } = options;
|
||||
return request({
|
||||
url: config.BASE_BUYER + url,
|
||||
url: apiBase + url,
|
||||
method: "post",
|
||||
data: data,
|
||||
...options,
|
||||
...requestOptions,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -78,6 +78,15 @@ export const getOrderList = (params) => {
|
||||
return getRequest(`/order/order`, params)
|
||||
}
|
||||
|
||||
// 小程序专用接口由服务端固定来源与微信支付范围,并回填本地上报结果。
|
||||
export const getWechatOrderList = (params) => getRequest('/order/order/wechat', params)
|
||||
// 状态统计与专用列表共用上报状态筛选。
|
||||
export const getWechatOrderNum = (params) => getRequest('/order/order/wechat/orderNum', params)
|
||||
// 人工补报复用支付流水锁和真实履约校验,不受自动上报开关限制。
|
||||
export const retryWechatOrder = (sn) => postRequest(`/order/order/wechat/${encodeURIComponent(sn)}/retry`, {}, undefined, 120000)
|
||||
// 查询同笔微信支付下的操作日志,不返回原始支付和物流凭据。
|
||||
export const getWechatOrderLogs = (sn, params) => getRequest(`/order/order/wechat/${encodeURIComponent(sn)}/logs`, params)
|
||||
|
||||
// 导出订单列表
|
||||
export const exportOrder = (params) => {
|
||||
return getRequest(`/order/order/queryExportOrder`, params,'blob')
|
||||
|
||||
@@ -147,6 +147,11 @@ export const getCouponActivityList = params => {
|
||||
return getRequest("/promotion/couponActivity", params);
|
||||
};
|
||||
// 关闭优惠券活动
|
||||
// 获取优惠券礼包H5推广二维码及链接
|
||||
export const getCouponPackageQrCode = (id, params) => {
|
||||
return getRequest(`/promotion/couponActivity/package/qrcode/${id}`, params);
|
||||
};
|
||||
|
||||
export const closeActivity = id => {
|
||||
return deleteRequest(`/promotion/couponActivity/${id}`);
|
||||
};
|
||||
|
||||
@@ -182,10 +182,12 @@ export const postBlobRequest = (url, data) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const postRequest = (url, params, headers) => {
|
||||
// 个别需等待外部平台的操作可传超时毫秒数,现有调用保持 8 秒默认值。
|
||||
export const postRequest = (url, params, headers, timeout = 8000) => {
|
||||
let accessToken = getStore("accessToken");
|
||||
return service({
|
||||
method: "post",
|
||||
timeout,
|
||||
url: `${url}`,
|
||||
data: params,
|
||||
transformRequest: headers
|
||||
|
||||
23
manager/src/utils/wechatOrderSync.js
Normal file
23
manager/src/utils/wechatOrderSync.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 小程序订单上报状态及日志显示口径,成功仅表示发货接口受理。
|
||||
* @author Bulbasaur
|
||||
* @since 2026-11-09
|
||||
*/
|
||||
export const wechatSyncOptions = [
|
||||
{ value: "UNCHECKED", label: "待核查", type: "info" },
|
||||
{ value: "PENDING", label: "待上报", type: "warning" },
|
||||
{ value: "PROCESSING", label: "处理中", type: "warning" },
|
||||
{ value: "SUCCESS", label: "上报成功", type: "success" },
|
||||
{ value: "FAILED", label: "上报失败", type: "danger" },
|
||||
];
|
||||
|
||||
/** 查找页面和日志的统一状态标签,重复请求跳过使用独立说明。 */
|
||||
export function wechatSyncLabel(value) {
|
||||
if (value === "SKIPPED") return "已跳过重复";
|
||||
return wechatSyncOptions.find((item) => item.value === value)?.label || value || "—";
|
||||
}
|
||||
|
||||
/** 返回 Element Plus 标签类型,未知状态使用中性色。 */
|
||||
export function wechatSyncTag(value) {
|
||||
return wechatSyncOptions.find((item) => item.value === value)?.type || "info";
|
||||
}
|
||||
@@ -52,6 +52,7 @@
|
||||
@change="selectDateRange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<slot name="extra-filters" :form="searchForm" />
|
||||
<el-form-item class="order-search-action">
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
@@ -74,6 +75,9 @@
|
||||
<el-button type="warning" class="export" :loading="exporting" @click="exportOrder">导出订单</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="loadError" title="订单加载失败,请重试" type="error" :closable="false" show-icon>
|
||||
<el-button link type="primary" @click="init">重新加载</el-button>
|
||||
</el-alert>
|
||||
<el-table v-loading="loading" :data="data" ref="tableRef" class="mt_10" style="width: 100%">
|
||||
<el-table-column prop="sn" label="订单号" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="商品" min-width="280">
|
||||
@@ -126,9 +130,11 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="下单时间" width="170" />
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<slot name="extra-columns" />
|
||||
<el-table-column label="操作" :width="actionWidth" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<a class="link-text" @click="detail(row)">查看</a>
|
||||
<slot name="extra-actions" :row="row" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -162,6 +168,8 @@ const props = defineProps({
|
||||
fixedClientType: { type: String, default: "" },
|
||||
fixedPaymentMethod: { type: String, default: "" },
|
||||
allowExport: { type: Boolean, default: true },
|
||||
wechatSync: { type: Boolean, default: false },
|
||||
actionWidth: { type: Number, default: 80 },
|
||||
});
|
||||
|
||||
const PROMOTION_OPTIONS = [
|
||||
@@ -204,6 +212,7 @@ function emptySearchForm(pageSize = 20) {
|
||||
orderStatus: "",
|
||||
paymentMethod: props.fixedPaymentMethod,
|
||||
orderPromotionType: "",
|
||||
...(props.wechatSync ? { wechatSyncStatus: "" } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -216,6 +225,9 @@ const searchFormRef = ref(null);
|
||||
const tableRef = ref(null);
|
||||
|
||||
const loading = ref(true);
|
||||
const loadError = ref(false);
|
||||
let listRequestSeq = 0;
|
||||
let countRequestSeq = 0;
|
||||
const exporting = ref(false);
|
||||
const pageSizes = ref(PAGE_SIZES);
|
||||
const promotionOptions = ref(PROMOTION_OPTIONS);
|
||||
@@ -389,16 +401,28 @@ function listParams() {
|
||||
return params;
|
||||
}
|
||||
function getDataList() {
|
||||
const requestId = ++listRequestSeq;
|
||||
loading.value = true;
|
||||
API_Order.getOrderList(listParams())
|
||||
loadError.value = false;
|
||||
const fetchList = props.wechatSync ? API_Order.getWechatOrderList : API_Order.getOrderList;
|
||||
fetchList(listParams())
|
||||
.then((res) => {
|
||||
if (requestId !== listRequestSeq) return;
|
||||
if (res.success) {
|
||||
data.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
} else {
|
||||
throw new Error("订单查询失败");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId !== listRequestSeq) return;
|
||||
data.value = [];
|
||||
total.value = 0;
|
||||
loadError.value = true;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
if (requestId === listRequestSeq) loading.value = false;
|
||||
});
|
||||
}
|
||||
function detail(v) {
|
||||
@@ -454,14 +478,19 @@ function orderStatusClick(name) {
|
||||
getDataList();
|
||||
}
|
||||
function getOrderNumData() {
|
||||
API_Order.getOrderNum(countParams())
|
||||
const requestId = ++countRequestSeq;
|
||||
const fetchCount = props.wechatSync ? API_Order.getWechatOrderNum : API_Order.getOrderNum;
|
||||
fetchCount(countParams())
|
||||
.then((res) => {
|
||||
if (requestId !== countRequestSeq) return;
|
||||
if (res.success) {
|
||||
orderNumData.value = res.result;
|
||||
} else {
|
||||
orderNumData.value = {};
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
orderNumData.value = {};
|
||||
if (requestId === countRequestSeq) orderNumData.value = {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -476,6 +505,9 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
// 补报完成后刷新当前筛选和页码,不清空运营人员已选条件。
|
||||
defineExpose({ reload: init });
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
sessionStorage.removeItem("manager-order-list-query");
|
||||
|
||||
88
manager/src/views/order/wechat/SyncLogDrawer.vue
Normal file
88
manager/src/views/order/wechat/SyncLogDrawer.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<!--
|
||||
小程序订单同步日志抽屉:按支付流水展示审计记录,不显示原始身份或支付凭据。
|
||||
@author Bulbasaur
|
||||
@since 2026-11-09
|
||||
-->
|
||||
<template>
|
||||
<el-drawer :model-value="modelValue" title="微信订单同步日志" size="min(1100px, 95vw)" @update:model-value="emit('update:modelValue', $event)">
|
||||
<p>订单:{{ orderSn }}。日志包含同笔微信支付下的关联子订单操作;上报成功仅代表微信接口已受理。</p>
|
||||
<el-button :loading="loading" @click="load">刷新</el-button>
|
||||
<el-alert v-if="failed" title="日志加载失败,请点击刷新重试" type="error" :closable="false" class="mt_10" />
|
||||
<el-table v-loading="loading" :data="records" class="mt_10" empty-text="暂无本地同步记录,历史上报结果需核查">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="log-detail">
|
||||
<p>触发订单:{{ row.orderSn }}</p>
|
||||
<p>请求摘要:{{ row.summary || '—' }}</p>
|
||||
<p>错误码:{{ row.errorCode || '—' }}</p>
|
||||
<p>处理原因:{{ row.errorMessage || '—' }}</p>
|
||||
<p>结束时间:{{ row.finishTime || '—' }};耗时:{{ row.durationMs == null ? '—' : `${row.durationMs} 毫秒` }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="操作时间" width="175" />
|
||||
<el-table-column label="操作" width="110">
|
||||
<template #default="{ row }">{{ row.operation === 'RECEIVE' ? '收货通知' : '发货上报' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源" width="110">
|
||||
<template #default="{ row }">{{ row.triggerType === 'MANUAL' ? '人工补报' : '自动事件' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operatorName" label="操作人" min-width="100" />
|
||||
<el-table-column label="结果" min-width="120">
|
||||
<template #default="{ row }"><el-tag :type="wechatSyncTag(row.status)">{{ wechatSyncLabel(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="errorMessage" label="处理原因" min-width="220" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pageNumber" :page-size="20" :total="total" layout="total, prev, pager, next" class="mt_10" @current-change="load" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { getWechatOrderLogs } from "@/api/order";
|
||||
import { wechatSyncLabel, wechatSyncTag } from "@/utils/wechatOrderSync";
|
||||
|
||||
const props = defineProps({ modelValue: Boolean, orderSn: { type: String, default: "" } });
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
const records = ref([]);
|
||||
const loading = ref(false);
|
||||
const failed = ref(false);
|
||||
const total = ref(0);
|
||||
const pageNumber = ref(1);
|
||||
let requestSeq = 0;
|
||||
|
||||
/** 请求序号隔离不同订单及页码,关闭抽屉后不接受过期响应。 */
|
||||
async function load() {
|
||||
if (!props.modelValue || !props.orderSn) return;
|
||||
const id = ++requestSeq;
|
||||
loading.value = true;
|
||||
failed.value = false;
|
||||
try {
|
||||
const res = await getWechatOrderLogs(props.orderSn, { pageNumber: pageNumber.value, pageSize: 20 });
|
||||
if (id !== requestSeq) return;
|
||||
if (!res.success) throw new Error("日志加载失败");
|
||||
records.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
} catch (e) {
|
||||
if (id !== requestSeq) return;
|
||||
failed.value = true;
|
||||
records.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
if (id === requestSeq) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => [props.modelValue, props.orderSn], () => {
|
||||
requestSeq++;
|
||||
records.value = [];
|
||||
total.value = 0;
|
||||
pageNumber.value = 1;
|
||||
loading.value = false;
|
||||
if (props.modelValue) load();
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-detail { padding: 0 20px; overflow-wrap: anywhere; }
|
||||
</style>
|
||||
@@ -8,14 +8,76 @@
|
||||
class="wechat-order-notice"
|
||||
description="仅展示微信小程序下单且使用微信支付的订单。自动上报开关开启且微信侧已开通发货管理时,按现有履约流程上报。"
|
||||
/>
|
||||
<OrderList fixed-client-type="WECHAT_MP" fixed-payment-method="WECHAT" :allow-export="false" />
|
||||
<OrderList ref="orderListRef" fixed-client-type="WECHAT_MP" fixed-payment-method="WECHAT" :allow-export="false" wechat-sync :action-width="220">
|
||||
<template #extra-filters="{ form }">
|
||||
<el-form-item label="上报状态" prop="wechatSyncStatus">
|
||||
<el-select v-model="form.wechatSyncStatus" placeholder="全部上报状态" clearable>
|
||||
<el-option v-for="item in wechatSyncOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template #extra-columns>
|
||||
<el-table-column label="微信上报状态" width="130">
|
||||
<template #default="{ row }"><el-tag :type="wechatSyncTag(row.wechatSyncStatus)">{{ wechatSyncLabel(row.wechatSyncStatus) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="wechatSyncTime" label="最近上报时间" width="175" />
|
||||
<el-table-column prop="wechatSyncError" label="上报处理原因" min-width="230" show-overflow-tooltip />
|
||||
</template>
|
||||
<template #extra-actions="{ row }">
|
||||
<el-button link type="primary" @click="showLogs(row)">同步日志</el-button>
|
||||
<el-button link type="warning" :loading="retryingSn === row.sn" :disabled="!!retryingSn" @click="retry(row)">补报</el-button>
|
||||
</template>
|
||||
</OrderList>
|
||||
<SyncLogDrawer v-model="logVisible" :order-sn="logOrderSn" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OrderList from "../order/orderList.vue";
|
||||
import SyncLogDrawer from "./SyncLogDrawer.vue";
|
||||
import { ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { retryWechatOrder } from "@/api/order";
|
||||
import { wechatSyncOptions, wechatSyncLabel, wechatSyncTag } from "@/utils/wechatOrderSync";
|
||||
|
||||
defineOptions({ name: "wechatOrderList" });
|
||||
|
||||
const orderListRef = ref(null);
|
||||
const retryingSn = ref("");
|
||||
const logVisible = ref(false);
|
||||
const logOrderSn = ref("");
|
||||
|
||||
/** 展示当前订单所在微信支付流水的审计日志。 */
|
||||
function showLogs(row) {
|
||||
logOrderSn.value = row.sn;
|
||||
logVisible.value = true;
|
||||
}
|
||||
|
||||
/** 人工补报需要明确确认范围;同笔支付共用锁,已成功且内容未变时由后端跳过。 */
|
||||
async function retry(row) {
|
||||
if (retryingSn.value) return;
|
||||
try {
|
||||
const historyNotice = row.wechatSyncStatus === "UNCHECKED" ? "该订单没有本地上报记录,请先核查微信后台,确认需要补报后继续。" : "";
|
||||
await ElMessageBox.confirm(historyNotice + "将按该笔微信支付的全部关联订单归集补报。人工补报不受自动上报开关限制;已成功且内容未变化会跳过。", "补报微信发货信息", { type: "warning", confirmButtonText: "确认补报", cancelButtonText: "取消" });
|
||||
} catch (e) { return; }
|
||||
if (retryingSn.value) return;
|
||||
retryingSn.value = row.sn;
|
||||
try {
|
||||
const res = await retryWechatOrder(row.sn);
|
||||
if (!res.success) throw new Error(res.message || "补报失败");
|
||||
if (res.result === "SUCCESS") ElMessage.success("微信接口已受理发货信息");
|
||||
else if (res.result === "SKIPPED") ElMessage.success("相同发货信息已成功上报,本次已跳过");
|
||||
else {
|
||||
ElMessage.warning("本次未完成上报,请查看同步日志中的处理原因");
|
||||
showLogs(row);
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error("补报未完成,请刷新状态并查看日志后再处理");
|
||||
} finally {
|
||||
retryingSn.value = "";
|
||||
orderListRef.value?.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -202,7 +202,11 @@ const form = reactive({
|
||||
watch(
|
||||
() => route.query.couponActivityType,
|
||||
(type) => {
|
||||
if (["REGISTERED", "SPECIFY", "AUTO_COUPON"].includes(type)) {
|
||||
if (["REGISTERED", "SPECIFY", "AUTO_COUPON", "COUPON_PACKAGE"].includes(type)) {
|
||||
if (type === "COUPON_PACKAGE") {
|
||||
form.couponFrequencyEnum = "DAY";
|
||||
form.activityScope = "ALL";
|
||||
}
|
||||
form.couponActivityType = type;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<el-button type="primary" plain @click="addRegistered">注册赠券</el-button>
|
||||
<el-button type="primary" plain @click="addSpecify">精准发券</el-button>
|
||||
<el-button type="primary" plain @click="addAutoCoupon">自动赠券</el-button>
|
||||
<el-button type="success" plain @click="addCouponPackage">创建券礼包</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
@@ -47,6 +48,8 @@
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text" @click="info(row)">查看</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="openQrCode(row)">推广码</a>
|
||||
<template
|
||||
v-if="(!checked && row.promotionStatus === 'START') || row.promotionStatus === 'NEW'"
|
||||
>
|
||||
@@ -70,6 +73,22 @@
|
||||
@size-change="changePageSize"
|
||||
/>
|
||||
</div>
|
||||
<!-- 推广二维码弹窗 -->
|
||||
<el-dialog v-model="qrCodeDialogVisible" title="券礼包 H5 推广二维码" width="440px" align-center>
|
||||
<div style="text-align: center; padding: 10px 0;">
|
||||
<div v-if="qrCodeLoading" style="padding: 30px 0;">
|
||||
<div style="color: #999;">正在生成推广二维码...</div>
|
||||
</div>
|
||||
<div v-else-if="qrCodeInfo.qrCodeBase64">
|
||||
<img :src="qrCodeInfo.qrCodeBase64" alt="H5推广二维码" style="width: 220px; height: 220px; border: 1px solid #ebeef5; padding: 6px; border-radius: 8px;" />
|
||||
<div style="margin-top: 15px; font-size: 13px; color: #555; word-break: break-all; text-align: left; background: #f8f9fa; padding: 10px; border-radius: 6px;">
|
||||
<div style="font-weight: bold; margin-bottom: 4px;">H5 访问链接:</div>
|
||||
<div style="color: #409eff;">{{ qrCodeInfo.h5Url }}</div>
|
||||
</div>
|
||||
<el-button type="primary" style="margin-top: 15px; width: 100%;" @click="copyH5Url">复制 H5 访问链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -77,7 +96,7 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getCouponActivityList, closeActivity } from "@/api/promotion";
|
||||
import { getCouponActivityList, closeActivity, getCouponPackageQrCode } from "@/api/promotion";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
|
||||
defineOptions({ name: "coupon-activity" });
|
||||
@@ -105,6 +124,7 @@ const selectDate = ref([]);
|
||||
function activityTypeText(type) {
|
||||
const map = {
|
||||
REGISTERED: "注册赠券",
|
||||
COUPON_PACKAGE: "优惠券礼包",
|
||||
AUTO_COUPON: "自动发券",
|
||||
};
|
||||
return map[type] || "精准发券";
|
||||
@@ -147,6 +167,56 @@ function promotionStatusTagType(status) {
|
||||
function init() {
|
||||
getDataList();
|
||||
}
|
||||
const qrCodeDialogVisible = ref(false);
|
||||
const qrCodeLoading = ref(false);
|
||||
const qrCodeInfo = reactive({
|
||||
activityId: "",
|
||||
h5Url: "",
|
||||
qrCodeBase64: "",
|
||||
});
|
||||
|
||||
function addCouponPackage() {
|
||||
router.push({
|
||||
name: "add-coupon-activity",
|
||||
query: { couponActivityType: "COUPON_PACKAGE" },
|
||||
});
|
||||
}
|
||||
|
||||
function openQrCode(row) {
|
||||
qrCodeDialogVisible.value = true;
|
||||
qrCodeLoading.value = true;
|
||||
qrCodeInfo.activityId = row.id;
|
||||
qrCodeInfo.h5Url = "";
|
||||
qrCodeInfo.qrCodeBase64 = "";
|
||||
|
||||
getCouponPackageQrCode(row.id)
|
||||
.then((res) => {
|
||||
if (res.success && res.result) {
|
||||
qrCodeInfo.h5Url = res.result.h5Url;
|
||||
qrCodeInfo.qrCodeBase64 = res.result.qrCodeBase64;
|
||||
} else {
|
||||
Message.error(res.message || "生成推广二维码失败");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Message.error("获取二维码异常");
|
||||
})
|
||||
.finally(() => {
|
||||
qrCodeLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function copyH5Url() {
|
||||
if (!qrCodeInfo.h5Url) return;
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(qrCodeInfo.h5Url).then(() => {
|
||||
Message.success("H5 访问链接已复制到剪贴板!");
|
||||
});
|
||||
} else {
|
||||
Message.success("链接:" + qrCodeInfo.h5Url);
|
||||
}
|
||||
}
|
||||
|
||||
function addRegistered() {
|
||||
router.push({
|
||||
name: "add-coupon-activity",
|
||||
|
||||
@@ -6,6 +6,25 @@
|
||||
<span class="desc">配置买家端联系客服以及商家端登录客服跳转的路径</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">客服图片存储</el-divider>
|
||||
<el-form-item label="OSS Bucket" prop="imageOssBucket">
|
||||
<el-input v-model.trim="formValidate.imageOssBucket" placeholder="客服图片专用桶名称" />
|
||||
<span class="desc">使用独立于商品、头像的桶,并在 OSS 控制台设为公共读、私有写。</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="OSS Endpoint" prop="imageOssEndpoint">
|
||||
<el-input v-model.trim="formValidate.imageOssEndpoint" placeholder="oss-cn-beijing.aliyuncs.com" />
|
||||
<span class="desc">填写公网节点域名,不含 https:// 或路径。</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="AccessKey ID" prop="imageOssAccessKeyId">
|
||||
<el-input v-model.trim="formValidate.imageOssAccessKeyId" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AccessKey Secret" prop="imageOssAccessKeySecret">
|
||||
<el-input v-model.trim="formValidate.imageOssAccessKeySecret" type="password" show-password autocomplete="new-password" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<span class="desc">客户和客服的图片统一按会话所属店铺存储。支持 PNG、JPG、GIF、WebP,单张不超过10MB。四项均留空时无法发送图片,上传失败不会写入通用桶;持有图片链接即可查看。</span>
|
||||
</el-form-item>
|
||||
|
||||
<div class="label-btns">
|
||||
<el-button type="primary" @click="submit('formValidateRef')">保存</el-button>
|
||||
</div>
|
||||
@@ -26,7 +45,11 @@ const { proxy } = getCurrentInstance();
|
||||
// 只做属性合并的成组对象
|
||||
const ruleValidate = reactive({}); // 验证规则
|
||||
const formValidate = ref({ // 表单数据
|
||||
httpUrl: ""
|
||||
httpUrl: "",
|
||||
imageOssBucket: "",
|
||||
imageOssEndpoint: "",
|
||||
imageOssAccessKeyId: "",
|
||||
imageOssAccessKeySecret: "",
|
||||
});
|
||||
const result = ref("");
|
||||
|
||||
@@ -57,18 +80,30 @@ function setupSetting() {
|
||||
function init() {
|
||||
result.value = JSON.parse(props.res);
|
||||
|
||||
formValidate.value = { ...result.value };
|
||||
Object.keys(formValidate.value).forEach((item) => {
|
||||
ruleValidate[item] = [
|
||||
formValidate.value = { ...formValidate.value, ...result.value };
|
||||
ruleValidate.httpUrl = [
|
||||
{
|
||||
required: true,
|
||||
message: "请填写必填项",
|
||||
trigger: "blur",
|
||||
},
|
||||
];
|
||||
["imageOssBucket", "imageOssEndpoint", "imageOssAccessKeyId", "imageOssAccessKeySecret"].forEach((key) => {
|
||||
ruleValidate[key] = [{ validator: validateImageStorage, trigger: "blur" }];
|
||||
});
|
||||
}
|
||||
|
||||
// 四项配置可整体留空;填写任一项后必须完整,避免只保存节点或密钥导致上传不可用。
|
||||
function validateImageStorage(rule, value, callback) {
|
||||
const keys = ["imageOssBucket", "imageOssEndpoint", "imageOssAccessKeyId", "imageOssAccessKeySecret"];
|
||||
const values = keys.map((key) => (formValidate.value[key] || "").trim());
|
||||
if (values.some(Boolean) && !values.every(Boolean)) {
|
||||
callback(new Error("请完整填写客服图片 OSS 的四项配置"));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
|
||||
74
manager/tests/wechatOrderSync.test.cjs
Normal file
74
manager/tests/wechatOrderSync.test.cjs
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 小程序订单筛选及异步竞态回归,隔离网络验证旧响应不覆盖新页面。
|
||||
* @author Bulbasaur
|
||||
* @since 2026-11-09
|
||||
*/
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const { parse } = require('@vue/compiler-sfc');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const tick = () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
/** 从实际 Vue 脚本加载逻辑,替换网络及 Vue 生命周期,不启动业务服务。 */
|
||||
function script(file) {
|
||||
return parse(fs.readFileSync(path.join(root,file),'utf8')).descriptor.scriptSetup.content.replace(/^import .*;\r?$/gm,'');
|
||||
}
|
||||
|
||||
/** 为并发请求提供可控完成顺序。 */
|
||||
function deferred() {
|
||||
let resolve, reject;
|
||||
const promise = new Promise((yes,no) => {resolve=yes; reject=no;});
|
||||
return {promise,resolve,reject};
|
||||
}
|
||||
|
||||
/** 保留表单值及计算属性的最小运行环境。 */
|
||||
function primitives() {
|
||||
return { ref:value=>({value}), computed:fn=>({get value(){return fn();}}), defineOptions:()=>{}, defineExpose:()=>{}, onMounted:()=>{} };
|
||||
}
|
||||
|
||||
test('专用列表固定支付范围、重置筛选,且旧列表和数量响应不能覆盖新结果', async()=>{
|
||||
const requests=[];
|
||||
const send=kind=>params=>{const wait=deferred();requests.push({kind,params:{...params},...wait});return wait.promise;};
|
||||
const context={...primitives(),defineProps:()=>({fixedClientType:'WECHAT_MP',fixedPaymentMethod:'WECHAT',wechatSync:true,allowExport:false}),useRoute:()=>({query:{clientType:'PC',paymentMethod:'WALLET'}}),watch:()=>{},wechatSyncOptions:[],API_Order:{getWechatOrderList:send('list'),getWechatOrderNum:send('count')}};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(script('src/views/order/order/orderList.vue')+';globalThis.t={init,handleSearch,handleReset,changePage,orderStatusClick,searchForm,data,orderNumData,loading,loadError};',context);
|
||||
const t=context.t;
|
||||
t.init();
|
||||
t.searchForm.value.paymentMethod='WALLET';t.searchForm.value.wechatSyncStatus='FAILED';t.handleSearch();
|
||||
const newest=requests.filter(r=>r.kind==='list').at(-1);
|
||||
newest.resolve({success:true,result:{records:[{sn:'new'}],total:1}});
|
||||
requests.filter(r=>r.kind==='count').at(-1).resolve({success:true,result:{waitPayNum:5}});
|
||||
await tick();
|
||||
requests[0].resolve({success:true,result:{records:[{sn:'old'}],total:99}});
|
||||
requests[1].resolve({success:true,result:{waitPayNum:99}});
|
||||
await tick();
|
||||
assert.equal(t.data.value[0].sn,'new');assert.equal(t.orderNumData.value.waitPayNum,5);
|
||||
t.changePage(2);t.orderStatusClick('DELIVERED');t.handleReset();
|
||||
for(const r of requests){assert.equal(r.params.clientType,'WECHAT_MP');assert.equal(r.params.paymentMethod,'WECHAT');}
|
||||
assert.equal(t.searchForm.value.wechatSyncStatus,'');assert.equal(t.searchForm.value.paymentMethod,'WECHAT');
|
||||
requests.filter(r=>r.kind==='list').at(-1).reject(new Error('network'));
|
||||
await tick();assert.equal(t.loadError.value,true);assert.equal(t.data.value.length,0);assert.equal(t.loading.value,false);
|
||||
});
|
||||
|
||||
test('普通订单仍使用原接口且允许其他支付方式,不强制附加上报条件', async()=>{
|
||||
let sent;
|
||||
const context={...primitives(),defineProps:()=>({fixedClientType:'',fixedPaymentMethod:'',wechatSync:false,allowExport:true}),useRoute:()=>({query:{}}),watch:()=>{},wechatSyncOptions:[],API_Order:{getOrderList:params=>{sent={...params};return Promise.resolve({success:true,result:{records:[],total:0}});},getOrderNum:()=>Promise.resolve({success:true,result:{}})}};
|
||||
vm.createContext(context);vm.runInContext(script('src/views/order/order/orderList.vue')+';globalThis.t={init,searchForm};',context);
|
||||
context.t.searchForm.value.paymentMethod='WALLET';context.t.init();await tick();
|
||||
assert.equal(sent.paymentMethod,'WALLET');assert.equal(sent.clientType,undefined);assert.equal(sent.wechatSyncStatus,undefined);
|
||||
});
|
||||
|
||||
test('日志抽屉切换订单及关闭后,旧请求不会回填另一笔订单', async()=>{
|
||||
const props={modelValue:true,orderSn:'A'},requests=[];let changed;
|
||||
const context={...primitives(),defineProps:()=>props,defineEmits:()=>()=>{},watch:(source,callback)=>{changed=callback;callback();},getWechatOrderLogs:(sn,params)=>{const wait=deferred();requests.push({sn,...wait});return wait.promise;}};
|
||||
vm.createContext(context);vm.runInContext(script('src/views/order/wechat/SyncLogDrawer.vue')+';globalThis.t={records,loading,failed,load};',context);
|
||||
props.orderSn='B';changed();
|
||||
requests[1].resolve({success:true,result:{records:[{orderSn:'B'}],total:1}});await tick();
|
||||
requests[0].resolve({success:true,result:{records:[{orderSn:'A'}],total:1}});await tick();
|
||||
assert.equal(context.t.records.value[0].orderSn,'B');
|
||||
context.t.load();props.modelValue=false;changed();requests[2].resolve({success:true,result:{records:[{orderSn:'B'}],total:1}});await tick();
|
||||
assert.equal(context.t.records.value.length,0);assert.equal(context.t.loading.value,false);
|
||||
});
|
||||
26
seller/src/api/stock.js
Normal file
26
seller/src/api/stock.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 商家端库存管理 API(出库单与库存流水)
|
||||
* @author Bulbasaur
|
||||
* @since 2026-11-12
|
||||
*/
|
||||
import {
|
||||
getRequest,
|
||||
postRequest,
|
||||
putRequest,
|
||||
} from "@/libs/axios";
|
||||
|
||||
// ========== 出库单 ==========
|
||||
export const getStockReasonList = params => getRequest("/procurement/reason", params);
|
||||
export const getStockOutboundPage = params => getRequest("/stock/outbound", params);
|
||||
export const getStockOutboundDetail = id => getRequest(`/stock/outbound/${id}`);
|
||||
export const createStockOutbound = params =>
|
||||
postRequest("/stock/outbound", params, { "Content-Type": "application/json" });
|
||||
export const updateStockOutbound = (id, params) =>
|
||||
putRequest(`/stock/outbound/${id}`, params, { "Content-Type": "application/json" });
|
||||
export const submitStockOutbound = id => putRequest(`/stock/outbound/submit/${id}`);
|
||||
export const auditStockOutbound = (id, params) =>
|
||||
putRequest(`/stock/outbound/audit/${id}`, params, { "Content-Type": "application/json" });
|
||||
export const cancelStockOutbound = id => putRequest(`/stock/outbound/cancel/${id}`);
|
||||
|
||||
// ========== 库存流水 ==========
|
||||
export const getStockFlowPage = params => getRequest("/stock/flow", params);
|
||||
@@ -29,6 +29,7 @@ const DETAIL_MENUS = {
|
||||
"procurement-inbound-operation": ["procurementInboundList"],
|
||||
"procurement-inventory-count-detail": ["procurementInventoryCountList"],
|
||||
"procurement-damage-report-operation": ["procurementDamageReportList"],
|
||||
"stock-outbound-operation": ["stockOutboundList"],
|
||||
};
|
||||
|
||||
/** 将权限树展开为带栏目与分组信息的可跳转菜单,用于搜索和路由定位。 */
|
||||
|
||||
@@ -15,6 +15,26 @@ export const forgetPasswordRouter = {
|
||||
component: () => import("@/views/ForgetPassword.vue")
|
||||
};
|
||||
|
||||
// 移动端楼层装修编辑器:全屏独立页,不在 Main(左侧菜单)布局内
|
||||
export const floorDecorationRouter = {
|
||||
path: "/floorList/main",
|
||||
name: "main",
|
||||
meta: {
|
||||
title: "移动装修"
|
||||
},
|
||||
component: () => import("@/views/shop/wap/main.vue")
|
||||
};
|
||||
|
||||
// PC端楼层装修编辑器:全屏独立页,不在 Main(左侧菜单)布局内
|
||||
export const pcFloorDecorationRouter = {
|
||||
path: "/pcFloorList/main",
|
||||
name: "renovation",
|
||||
meta: {
|
||||
title: "PC装修"
|
||||
},
|
||||
component: () => import("@/views/shop/renovation.vue")
|
||||
};
|
||||
|
||||
// 作为Main组件的子页面展示但是不在左侧菜单显示的路由写在otherRouter里
|
||||
export const otherRouter = {
|
||||
path: "/",
|
||||
@@ -168,20 +188,6 @@ export const otherRouter = {
|
||||
name: "orderVerification",
|
||||
component: () => import("@/views/order/order/orderVerification.vue")
|
||||
},
|
||||
{
|
||||
path: "/floorList/main",
|
||||
title: "移动装修",
|
||||
name: "main",
|
||||
meta: { title: "移动装修" },
|
||||
component: () => import("@/views/shop/wap/main.vue"),
|
||||
},
|
||||
{
|
||||
path: "/pcFloorList/main",
|
||||
title: "PC装修",
|
||||
name: "renovation",
|
||||
meta: { title: "PC装修" },
|
||||
component: () => import("@/views/shop/renovation.vue"),
|
||||
},
|
||||
{
|
||||
path: "order-complaint-detail",
|
||||
title: "投诉详情",
|
||||
@@ -278,6 +284,7 @@ export const otherRouter = {
|
||||
name: "procurement-damage-report-operation",
|
||||
component: () => import("@/views/procurement/damage-report/operation.vue")
|
||||
},
|
||||
|
||||
{
|
||||
path: "alertQuantityWarnList",
|
||||
title: "预警商品",
|
||||
@@ -342,4 +349,4 @@ export const page500 = {
|
||||
component: () => import("@/views/error-page/500.vue")
|
||||
};
|
||||
// 所有上面定义的路由都要写在下面的routers里
|
||||
export const routers = [loginRouter, forgetPasswordRouter, otherRouter, page500, page403];
|
||||
export const routers = [loginRouter, forgetPasswordRouter, floorDecorationRouter, pcFloorDecorationRouter, otherRouter, page500, page403];
|
||||
|
||||
@@ -3,12 +3,10 @@ import category from "./category.vue";
|
||||
import pages from "./pages.vue";
|
||||
import goods from "../goods-dialog.vue";
|
||||
import other from "./other.vue";
|
||||
import shops from "./shops.vue";
|
||||
|
||||
export default {
|
||||
category: markRaw(category),
|
||||
goods: markRaw(goods),
|
||||
other: markRaw(other),
|
||||
pages: markRaw(pages),
|
||||
shops: markRaw(shops),
|
||||
};
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<div class="shop">
|
||||
<div class="wap-content">
|
||||
<div class="query-wrapper">
|
||||
<div class="query-item">
|
||||
<div>店铺名称</div>
|
||||
<el-input
|
||||
v-model="params.storeName"
|
||||
placeholder="请输入店铺名称"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
@clear="resetSearch"
|
||||
@keyup.enter="resetSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="query-item">
|
||||
<el-button type="primary" @click="resetSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-loading="loading" class="wap-content-list">
|
||||
<div
|
||||
v-for="(item, index) in shopsData"
|
||||
:key="index"
|
||||
class="wap-content-item"
|
||||
:class="{ active: selected == index }"
|
||||
@click="clickShop(item, index)"
|
||||
>
|
||||
<div>
|
||||
<img class="shop-logo" :src="item.storeLogo" alt="" />
|
||||
</div>
|
||||
<div class="wap-content-desc">
|
||||
<div class="wap-content-desc-title">{{ item.storeName }}</div>
|
||||
<div class="self-operated" :class="{ theme_color: item.selfOperated }">
|
||||
{{ item.selfOperated ? "自营" : "非自营" }}
|
||||
</div>
|
||||
<div
|
||||
class="wap-sku"
|
||||
:class="{ theme_color: item.storeDisable === 'OPEN' }"
|
||||
>
|
||||
{{ item.storeDisable === "OPEN" ? "开启中" : "未开启" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
class="pageration"
|
||||
size="small"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
:page-size="params.pageSize"
|
||||
@current-change="changePageSize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { getShopListData } from "@/api/shops.js";
|
||||
|
||||
const emit = defineEmits(["selected"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const params = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 12,
|
||||
storeDisable: "OPEN",
|
||||
storeName: "",
|
||||
});
|
||||
const shopsData = ref([]);
|
||||
const selected = ref(9999999999);
|
||||
|
||||
// created
|
||||
init();
|
||||
|
||||
function resetSearch() {
|
||||
shopsData.value = [];
|
||||
params.pageNumber = 1;
|
||||
init();
|
||||
}
|
||||
function changePageSize(v) {
|
||||
params.pageNumber = v;
|
||||
init();
|
||||
}
|
||||
function init() {
|
||||
loading.value = true;
|
||||
getShopListData(params).then((res) => {
|
||||
if (res.success) {
|
||||
total.value = res.result.total;
|
||||
shopsData.value = res.result.records;
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
function clickShop(val, i) {
|
||||
selected.value = i;
|
||||
val = { ...val, ___type: "shops" };
|
||||
emit("selected", [val]);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "../style.scss";
|
||||
.shop {
|
||||
display: flex;
|
||||
}
|
||||
.self-operated {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.wap-content-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: 340px;
|
||||
min-height: 120px;
|
||||
}
|
||||
.shop-logo {
|
||||
object-fit: cover;
|
||||
}
|
||||
.active {
|
||||
background: url("../../../assets/selected.png") no-repeat;
|
||||
background-position: right;
|
||||
background-size: 10%;
|
||||
}
|
||||
.pageration {
|
||||
margin-top: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -10,11 +10,6 @@ export default [
|
||||
url: "1",
|
||||
name: "category"
|
||||
},
|
||||
{
|
||||
title: "店铺",
|
||||
url: "2",
|
||||
name: "shops"
|
||||
},
|
||||
{
|
||||
title: "活动",
|
||||
url: "3",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
</el-tabs>
|
||||
<div class="operation mb_10">
|
||||
<el-button type="primary" @click="createTemp">添加页面</el-button>
|
||||
<el-button type="primary" tag="a" :href="addHref" target="_blank" class="add-page-btn">添加页面</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" style="width: 100%">
|
||||
<el-table-column prop="name" label="页面名称" min-width="180" show-overflow-tooltip>
|
||||
@@ -42,7 +42,7 @@
|
||||
<template v-if="row">
|
||||
<a class="link-text" @click="Template(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="decorate(row)">装修</a>
|
||||
<a class="link-text" :href="decorateHref(row)" target="_blank">装修</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="confirmDel(row.id)">删除</a>
|
||||
</template>
|
||||
@@ -85,7 +85,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import * as API_floor from "@/api/other.js";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
@@ -155,27 +155,29 @@ function handleTabChange(type) {
|
||||
getTemplateList();
|
||||
}
|
||||
|
||||
function createTemp() {
|
||||
router.push({
|
||||
const addHref = computed(
|
||||
() =>
|
||||
router.resolve({
|
||||
name: "renovation",
|
||||
query: { pageType: searchForm.pageType },
|
||||
}).href
|
||||
);
|
||||
|
||||
function decorateHref(val) {
|
||||
const data = { id: val.id, pageShow: val.pageShow, pageType: searchForm.pageType };
|
||||
return router.resolve({
|
||||
name: "renovation",
|
||||
query: { pageType: searchForm.pageType },
|
||||
});
|
||||
query: data,
|
||||
}).href;
|
||||
}
|
||||
|
||||
|
||||
function Template(item) {
|
||||
item.status = item.pageShow;
|
||||
formData.value = { ...item };
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
function decorate(val) {
|
||||
const data = { id: val.id, pageShow: val.pageShow, pageType: searchForm.pageType };
|
||||
|
||||
router.push({
|
||||
name: "renovation",
|
||||
query: data,
|
||||
});
|
||||
}
|
||||
|
||||
function changePageNum(val) {
|
||||
searchForm.pageNumber = val;
|
||||
@@ -248,4 +250,7 @@ function delTemplate(id) {
|
||||
.page-type-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.add-page-btn {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -177,6 +177,7 @@ const layoutOptions = computed(() => {
|
||||
];
|
||||
});
|
||||
|
||||
/* eslint-disable vue/no-side-effects-in-computed-properties */
|
||||
const compStyle = computed(() => {
|
||||
if (!props.component) return {};
|
||||
if (!props.component.componentStyle) {
|
||||
|
||||
@@ -419,8 +419,7 @@ const ALL_COMPONENT_DEFINITIONS = [
|
||||
url: '',
|
||||
autoPlay: false,
|
||||
loop: false,
|
||||
controls: true,
|
||||
height: 450
|
||||
controls: true
|
||||
},
|
||||
|
||||
defaultComponentStyle: {
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<el-input v-model="item.title" maxlength="10" />
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" @click="pickImage(item)">选择照片</el-button>
|
||||
<el-button plain type="primary" @click="pickLink(item)">选择链接</el-button>
|
||||
<el-button plain type="primary" size="small" @click="pickImage(item)">选择照片</el-button>
|
||||
<el-button plain type="primary" size="small" @click="pickLink(item)">选择链接</el-button>
|
||||
</div>
|
||||
<div class="link-view" v-if="item.url && item.url.___type">
|
||||
已选链接:{{ linkText(item.url) }}
|
||||
@@ -312,7 +312,13 @@ export default {
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
:deep(.el-button),
|
||||
.el-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
.link-view {
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
<div class="img-thumb placeholder" v-else>750*350</div>
|
||||
<div class="img-item-fields">
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" @click="pickImage(item)">选择照片</el-button>
|
||||
<el-button plain type="primary" @click="pickLink(item)">选择链接</el-button>
|
||||
<el-button plain type="primary" size="small" @click="pickImage(item)">选择照片</el-button>
|
||||
<el-button plain type="primary" size="small" @click="pickLink(item)">选择链接</el-button>
|
||||
</div>
|
||||
<div class="link-view" v-if="item.url && item.url.___type">
|
||||
已选链接:{{ linkText(item.url) }}
|
||||
@@ -410,8 +410,8 @@ export default {
|
||||
gap: 10px;
|
||||
}
|
||||
.img-thumb {
|
||||
width: 132px;
|
||||
height: 62px;
|
||||
width: 120px;
|
||||
height: 60px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
@@ -445,7 +445,14 @@ export default {
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
:deep(.el-button),
|
||||
.el-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0 4px;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
.link-view {
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
</div>
|
||||
<div class="img-placeholder" v-else>尺寸不限</div>
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" @click="pickImage">选择照片</el-button>
|
||||
<el-button plain type="primary" @click="openHotzone">绘制热区</el-button>
|
||||
<el-button plain type="primary" size="small" @click="pickImage">选择照片</el-button>
|
||||
<el-button plain type="primary" size="small" @click="openHotzone">绘制热区</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -263,6 +263,12 @@ export default {
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
:deep(.el-button),
|
||||
.el-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -177,3 +177,14 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
:deep(.el-button),
|
||||
.el-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@
|
||||
|
||||
.decorate {
|
||||
padding: 0 20px;
|
||||
height: calc(100vh - 96px);
|
||||
overflow-y: auto;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@ function selected(val) {
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.box {
|
||||
height: calc(100vh - 120px);
|
||||
width: 98%;
|
||||
margin: 0 auto;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
background: #fff;
|
||||
border-radius: 0.4em;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -254,6 +254,7 @@ function goBack() {
|
||||
if (window.history.state?.back) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
window.close();
|
||||
router.push("/wapList");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: calc(100vh - 96px);
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
.model-view-menu {
|
||||
flex: 1.5;
|
||||
min-width: 250px;
|
||||
flex: 0 0 250px;
|
||||
width: 250px;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
@@ -39,11 +41,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.model-config {
|
||||
flex: 2.5;
|
||||
}
|
||||
.model-view-content {
|
||||
flex: 6;
|
||||
flex: 1;
|
||||
min-width: 520px;
|
||||
}
|
||||
.model-config {
|
||||
flex: 0 0 420px;
|
||||
width: 420px;
|
||||
min-width: 420px;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.model-item {
|
||||
line-height: 1.4;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
</el-tabs>
|
||||
<div class="operation mb_10">
|
||||
<el-button type="primary" @click="handleAdd()">添加页面</el-button>
|
||||
<el-button type="primary" tag="a" :href="addHref" target="_blank" class="add-page-btn">添加页面</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" style="width: 100%">
|
||||
<el-table-column prop="name" label="页面名称" min-width="180" show-overflow-tooltip>
|
||||
@@ -35,7 +35,7 @@
|
||||
<el-table-column label="操作" width="150" align="right" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text" @click="handleEdit(row)">修改</a>
|
||||
<a class="link-text" :href="editHref(row)" target="_blank">修改</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="confirmDel(row)">删除</a>
|
||||
</template>
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import * as API_Other from "@/api/other.js";
|
||||
@@ -152,8 +152,16 @@ function init() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleEdit(val) {
|
||||
router.push({
|
||||
const addHref = computed(
|
||||
() =>
|
||||
router.resolve({
|
||||
path: "/floorList/main",
|
||||
query: { pagetype: params.pageType, mode: "add" },
|
||||
}).href
|
||||
);
|
||||
|
||||
function editHref(val) {
|
||||
const { href } = router.resolve({
|
||||
path: "/floorList/main",
|
||||
query: {
|
||||
id: val.id,
|
||||
@@ -162,14 +170,9 @@ function handleEdit(val) {
|
||||
pagetype: params.pageType,
|
||||
},
|
||||
});
|
||||
return href;
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
router.push({
|
||||
path: "/floorList/main",
|
||||
query: { pagetype: params.pageType },
|
||||
});
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
params.pageNumber = 1;
|
||||
@@ -200,4 +203,7 @@ function handleDel(val) {
|
||||
.page-type-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.add-page-btn {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
235
seller/src/views/stock/flow/list.vue
Normal file
235
seller/src/views/stock/flow/list.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<!--
|
||||
~ 商家端商品库存流水列表页面
|
||||
~ @author Bulbasaur
|
||||
~ @since 2026-11-12
|
||||
-->
|
||||
<template>
|
||||
<div class="stock-flow-list">
|
||||
<el-card>
|
||||
<el-form ref="searchFormRef" :model="searchForm" inline label-width="80px">
|
||||
<el-form-item label="商品名称">
|
||||
<el-input
|
||||
v-model="searchForm.goodsName"
|
||||
placeholder="请输入商品名称"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="SKU ID">
|
||||
<el-input
|
||||
v-model="searchForm.skuId"
|
||||
placeholder="请输入SKU ID"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="流水类型">
|
||||
<el-select
|
||||
v-model="searchForm.flowType"
|
||||
placeholder="全部类型"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in flowTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="变动方向">
|
||||
<el-select
|
||||
v-model="searchForm.direction"
|
||||
placeholder="全部方向"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="入库" value="IN" />
|
||||
<el-option label="出库" value="OUT" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务单号">
|
||||
<el-input
|
||||
v-model="searchForm.bizSn"
|
||||
placeholder="订单号/出库单号等"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="变动时间">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt_10">
|
||||
<el-table v-loading="loading" :data="data" border style="width: 100%">
|
||||
<el-table-column prop="createTime" label="发生时间" width="170" align="center" />
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="skuId" label="SKU ID" width="180" />
|
||||
<el-table-column prop="specs" label="规格" width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.specs || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="flowType" label="业务类型" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="flowTypeTag(row.flowType)">{{ flowTypeText(row.flowType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="direction" label="方向" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.direction === 'IN' ? 'success' : 'danger'" effect="light">
|
||||
{{ row.direction === 'IN' ? '入库' : '出库' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quantity" label="变动数量" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.direction === 'IN' ? 'text-success font-bold' : 'text-danger font-bold'">
|
||||
{{ row.direction === 'IN' ? '+' : '-' }}{{ row.quantity }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="beforeStock" label="变动前库存" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.beforeStock ?? "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="afterStock" label="变动后库存" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.afterStock ?? "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bizSn" label="关联业务单号" width="190" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.bizSn || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.operator || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || "-" }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10 flex flex-justify-end">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@current-change="getDataList"
|
||||
@size-change="getDataList"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { getStockFlowPage } from "@/api/stock";
|
||||
|
||||
defineOptions({ name: "stock-flow-list" });
|
||||
|
||||
const searchFormRef = ref(null);
|
||||
const dateRange = ref([]);
|
||||
|
||||
const flowTypeOptions = [
|
||||
{ label: "销售出库", value: "ORDER_PAID" },
|
||||
{ label: "退货入库", value: "ORDER_CANCEL" },
|
||||
{ label: "采购入库", value: "PROCUREMENT_INBOUND" },
|
||||
{ label: "领用/调拨出库", value: "STOCK_OUTBOUND" },
|
||||
{ label: "报损出库", value: "DAMAGE" },
|
||||
{ label: "手动调整", value: "MANUAL_ADJUST" },
|
||||
];
|
||||
|
||||
function buildSearchParams(params) {
|
||||
const p = { ...params };
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
p.startDate = dateRange.value[0];
|
||||
p.endDate = dateRange.value[1];
|
||||
} else {
|
||||
delete p.startDate;
|
||||
delete p.endDate;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const {
|
||||
loading,
|
||||
data,
|
||||
total,
|
||||
searchForm,
|
||||
getDataList,
|
||||
handleSearch: search,
|
||||
resetSearch: reset,
|
||||
} = useTable(getStockFlowPage, {
|
||||
form: {
|
||||
goodsName: "",
|
||||
skuId: "",
|
||||
flowType: "",
|
||||
direction: "",
|
||||
bizSn: "",
|
||||
},
|
||||
immediate: true,
|
||||
beforeRequest: (p) => buildSearchParams(p),
|
||||
});
|
||||
|
||||
function flowTypeText(type) {
|
||||
const item = flowTypeOptions.find((it) => it.value === type);
|
||||
return item ? item.label : type || "-";
|
||||
}
|
||||
|
||||
function flowTypeTag(type) {
|
||||
const map = {
|
||||
ORDER_PAID: "danger",
|
||||
ORDER_CANCEL: "success",
|
||||
PROCUREMENT_INBOUND: "success",
|
||||
STOCK_OUTBOUND: "warning",
|
||||
DAMAGE: "danger",
|
||||
MANUAL_ADJUST: "info",
|
||||
};
|
||||
return map[type] || "";
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
search();
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
dateRange.value = [];
|
||||
searchForm.goodsName = "";
|
||||
searchForm.skuId = "";
|
||||
searchForm.flowType = "";
|
||||
searchForm.direction = "";
|
||||
searchForm.bizSn = "";
|
||||
reset();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// useTable with immediate: true automatically fetches data
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stock-flow-list {
|
||||
padding: 10px;
|
||||
}
|
||||
.text-success {
|
||||
color: #67c23a;
|
||||
}
|
||||
.text-danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
339
seller/src/views/stock/outbound/list.vue
Normal file
339
seller/src/views/stock/outbound/list.vue
Normal file
@@ -0,0 +1,339 @@
|
||||
<!--
|
||||
商家端出库单列表页面
|
||||
@author Bulbasaur
|
||||
@since 2026-11-12
|
||||
-->
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card shadow="never">
|
||||
<el-form ref="searchFormRef" :model="searchForm" inline label-width="80px">
|
||||
<el-form-item label="出库单号">
|
||||
<el-input
|
||||
v-model="searchForm.outboundSn"
|
||||
placeholder="请输入出库单号"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt_10" shadow="never">
|
||||
<div class="order-tab">
|
||||
<el-tabs v-model="currentStatus" @tab-click="onStatusTabClick">
|
||||
<el-tab-pane
|
||||
v-for="item in statusTabs"
|
||||
:key="item.value"
|
||||
:label="item.title"
|
||||
:name="item.value"
|
||||
/>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="operation padding-row">
|
||||
<el-button type="primary" @click="goCreate">新建出库单</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
|
||||
<el-table-column prop="outboundSn" label="出库单号" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="reasonName" label="出库原因" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="totalQuantity" label="出库数量" width="100" align="center" />
|
||||
<el-table-column label="单据状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="出库备注" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="auditBy" label="审核人" width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="auditRemark" label="审核备注" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="220" align="right" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="goDetail(row)">查看</el-button>
|
||||
<template v-if="['DRAFT', 'REJECTED'].includes(row.status)">
|
||||
<span class="op-split">|</span>
|
||||
<el-button link type="primary" @click="goEdit(row)">编辑</el-button>
|
||||
<span class="op-split">|</span>
|
||||
<el-button link type="primary" @click="handleSubmit(row)">提交</el-button>
|
||||
</template>
|
||||
<template v-if="row.status === 'AUDITING'">
|
||||
<span class="op-split">|</span>
|
||||
<el-button link type="success" @click="openAuditDialog(row)">审核</el-button>
|
||||
</template>
|
||||
<template v-if="['DRAFT', 'AUDITING', 'REJECTED'].includes(row.status)">
|
||||
<span class="op-split">|</span>
|
||||
<el-button link type="danger" @click="handleCancel(row)">作废</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@current-change="getDataList"
|
||||
@size-change="getDataList"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<el-dialog v-model="auditDialogVisible" title="出库单审核" width="460px" destroy-on-close>
|
||||
<el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="90px">
|
||||
<el-form-item label="出库单号">
|
||||
<span>{{ currentOutbound?.outboundSn }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="出库数量">
|
||||
<span>{{ currentOutbound?.totalQuantity }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核结果" prop="pass">
|
||||
<el-radio-group v-model="auditForm.pass">
|
||||
<el-radio :label="true">通过(扣减库存)</el-radio>
|
||||
<el-radio :label="false">驳回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见" prop="auditRemark">
|
||||
<el-input
|
||||
v-model="auditForm.auditRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入审核意见或驳回原因"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="auditDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="auditLoading" @click="confirmAudit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import {
|
||||
getStockOutboundPage,
|
||||
submitStockOutbound,
|
||||
auditStockOutbound,
|
||||
cancelStockOutbound,
|
||||
} from "@/api/stock";
|
||||
|
||||
defineOptions({ name: "StockOutboundList" });
|
||||
|
||||
const searchFormRef = ref(null);
|
||||
const dateRange = ref([]);
|
||||
const currentStatus = ref("ALL");
|
||||
|
||||
const STATUS_MAP = {
|
||||
DRAFT: { text: "草稿", tag: "info" },
|
||||
AUDITING: { text: "待审核", tag: "warning" },
|
||||
COMPLETED: { text: "已完成", tag: "success" },
|
||||
REJECTED: { text: "已驳回", tag: "danger" },
|
||||
CANCELLED: { text: "已作废", tag: "info" },
|
||||
};
|
||||
|
||||
const statusTabs = computed(() => [
|
||||
{ title: "全部", value: "ALL" },
|
||||
{ title: "草稿", value: "DRAFT" },
|
||||
{ title: "待审核", value: "AUDITING" },
|
||||
{ title: "已完成", value: "COMPLETED" },
|
||||
{ title: "已驳回", value: "REJECTED" },
|
||||
{ title: "已作废", value: "CANCELLED" },
|
||||
]);
|
||||
|
||||
function statusText(status) {
|
||||
return STATUS_MAP[status]?.text || status || "-";
|
||||
}
|
||||
|
||||
function statusTagType(status) {
|
||||
return STATUS_MAP[status]?.tag || "info";
|
||||
}
|
||||
|
||||
const {
|
||||
loading,
|
||||
data,
|
||||
total,
|
||||
searchForm,
|
||||
getDataList,
|
||||
handleSearch: search,
|
||||
resetSearch: reset,
|
||||
} = useTable(getStockOutboundPage, {
|
||||
form: { outboundSn: "", status: "" },
|
||||
immediate: false,
|
||||
beforeRequest: (p) => {
|
||||
const params = { ...p };
|
||||
if (params.status === "ALL") {
|
||||
delete params.status;
|
||||
}
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
params.startDate = dateRange.value[0];
|
||||
params.endDate = dateRange.value[1];
|
||||
}
|
||||
return params;
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getDataList();
|
||||
});
|
||||
|
||||
function handleSearch() {
|
||||
search();
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
dateRange.value = [];
|
||||
currentStatus.value = "ALL";
|
||||
searchForm.outboundSn = "";
|
||||
searchForm.status = "";
|
||||
reset();
|
||||
}
|
||||
|
||||
function onStatusTabClick(tab) {
|
||||
const name = tab.paneName;
|
||||
if (name === "ALL") {
|
||||
searchForm.status = "";
|
||||
} else {
|
||||
searchForm.status = name;
|
||||
}
|
||||
searchForm.pageNumber = 1;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function goCreate() {
|
||||
customRouterPush({ name: "stock-outbound-operation" });
|
||||
}
|
||||
|
||||
function goEdit(row) {
|
||||
customRouterPush({
|
||||
name: "stock-outbound-operation",
|
||||
query: { id: row.id },
|
||||
});
|
||||
}
|
||||
|
||||
function goDetail(row) {
|
||||
customRouterPush({
|
||||
name: "stock-outbound-operation",
|
||||
query: { id: row.id, readonly: 1 },
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubmit(row) {
|
||||
Modal.confirm({
|
||||
title: "提交审核",
|
||||
content: `确认提交出库单 [${row.outboundSn}] 进行审核?`,
|
||||
onOk: () => {
|
||||
submitStockOutbound(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
Message.success("提交审核成功");
|
||||
getDataList();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleCancel(row) {
|
||||
Modal.confirm({
|
||||
title: "作废出库单",
|
||||
content: `确认作废出库单 [${row.outboundSn}]?作废后不可恢复。`,
|
||||
onOk: () => {
|
||||
cancelStockOutbound(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
Message.success("作废成功");
|
||||
getDataList();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 审核弹窗相关 ==========
|
||||
const auditDialogVisible = ref(false);
|
||||
const auditLoading = ref(false);
|
||||
const auditFormRef = ref(null);
|
||||
const currentOutbound = ref(null);
|
||||
const auditForm = reactive({
|
||||
pass: true,
|
||||
auditRemark: "",
|
||||
});
|
||||
|
||||
const auditRules = {
|
||||
pass: [{ required: true, message: "请选择审核结果", trigger: "change" }],
|
||||
auditRemark: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (auditForm.pass === false && (!value || !value.trim())) {
|
||||
callback(new Error("驳回时必须填写审核意见"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function openAuditDialog(row) {
|
||||
currentOutbound.value = row;
|
||||
auditForm.pass = true;
|
||||
auditForm.auditRemark = "";
|
||||
auditDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmAudit() {
|
||||
if (!auditFormRef.value) return;
|
||||
auditFormRef.value.validate((valid) => {
|
||||
if (!valid) return;
|
||||
auditLoading.value = true;
|
||||
auditStockOutbound(currentOutbound.value.id, {
|
||||
pass: auditForm.pass,
|
||||
auditRemark: auditForm.auditRemark,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
Message.success(auditForm.pass ? "审核通过,库存已扣减" : "已驳回出库单");
|
||||
auditDialogVisible.value = false;
|
||||
getDataList();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
auditLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.op-split {
|
||||
margin: 0 4px;
|
||||
color: #dcdfe6;
|
||||
}
|
||||
.order-tab :deep(.el-tabs__item) {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
375
seller/src/views/stock/outbound/operation.vue
Normal file
375
seller/src/views/stock/outbound/operation.vue
Normal file
@@ -0,0 +1,375 @@
|
||||
<!--
|
||||
~ 商家端出库单详情/新建/编辑页面
|
||||
~ @author Bulbasaur
|
||||
~ @since 2026-11-12
|
||||
-->
|
||||
<template>
|
||||
<div class="stock-outbound-operation">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header flex flex-justify-between flex-align-center">
|
||||
<span class="font-bold text-lg">{{ pageTitle }}</span>
|
||||
<el-button @click="goBack">返回列表</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 详情只读展示 -->
|
||||
<template v-if="readonly">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="出库单号">{{ detail.outboundSn || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="单据状态">
|
||||
<el-tag :type="statusTag(detail.status)">{{ statusText(detail.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="出库原因">{{ detail.reasonName || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出库总数量">{{ detail.totalQuantity ?? "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="制单人">{{ detail.createBy || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="制单时间">{{ detail.createTime || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核人">{{ detail.auditBy || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核时间">{{ detail.auditTime || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detail.auditRemark" label="审核备注" :span="2">
|
||||
<span :class="detail.status === 'REJECTED' ? 'text-danger' : ''">{{ detail.auditRemark }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "-" }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">出库商品明细</el-divider>
|
||||
|
||||
<el-table :data="items" border class="mt-4" style="width: 100%">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="skuId" label="SKU ID" width="180" />
|
||||
<el-table-column prop="specs" label="规格" width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.specs || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quantity" label="出库数量" width="120" align="center" />
|
||||
<el-table-column prop="unit" label="单位" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.unit || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="明细备注" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || "-" }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<!-- 新建/编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="出库原因" prop="reasonId">
|
||||
<el-select
|
||||
v-model="form.reasonId"
|
||||
placeholder="请选择出库原因"
|
||||
class="w-full"
|
||||
@change="handleReasonChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in reasonOptions"
|
||||
:key="item.id"
|
||||
:label="item.reason"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入出库单备注"
|
||||
style="max-width: 600px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="flex flex-justify-between flex-align-center mb-4">
|
||||
<el-divider content-position="left" class="flex-1 mr-4">出库商品明细</el-divider>
|
||||
<el-button type="primary" @click="openSkuPicker">选择商品</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="items" border style="width: 100%" empty-text="请点击上方按钮添加出库商品">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="skuId" label="SKU ID" width="180" />
|
||||
<el-table-column prop="specs" label="规格" width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.specs || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="出库数量" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.quantity"
|
||||
:min="1"
|
||||
:max="999999"
|
||||
:step="1"
|
||||
step-strictly
|
||||
controls-position="right"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.unit" maxlength="10" placeholder="件/个" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="明细备注" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" maxlength="100" placeholder="备注" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="removeItem($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="summary mt-4 flex flex-justify-end font-14">
|
||||
<span>出库品种数:<b class="text-primary">{{ items.length }}</b></span>
|
||||
<span class="ml-6">合计出库数量:<b class="text-primary">{{ totalQuantity }}</b></span>
|
||||
</div>
|
||||
|
||||
<div class="footer-actions mt-6 flex flex-justify-center">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSave(false)">保存草稿</el-button>
|
||||
<el-button type="success" :loading="submitLoading" @click="handleSave(true)">保存并提交</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 商品 SKU 选择弹窗 -->
|
||||
<liliDialog ref="liliDialogRef" @selectedGoodsData="onGoodsSelected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import liliDialog from "@/views/lili-dialog";
|
||||
import {
|
||||
createStockOutbound,
|
||||
getStockOutboundDetail,
|
||||
getStockReasonList,
|
||||
submitStockOutbound,
|
||||
updateStockOutbound,
|
||||
} from "@/api/stock";
|
||||
|
||||
defineOptions({ name: "stock-outbound-operation" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const readonly = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
id: "",
|
||||
reasonId: "",
|
||||
reasonName: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
reasonId: [{ required: true, message: "请选择出库原因", trigger: "change" }],
|
||||
};
|
||||
|
||||
const items = ref([]);
|
||||
const detail = ref({});
|
||||
const reasonOptions = ref([]);
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (readonly.value) return "出库单详情";
|
||||
return form.id ? "编辑出库单" : "新建出库单";
|
||||
});
|
||||
|
||||
const totalQuantity = computed(() =>
|
||||
items.value.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0)
|
||||
);
|
||||
|
||||
function statusText(status) {
|
||||
const map = {
|
||||
DRAFT: "草稿",
|
||||
AUDITING: "待审核",
|
||||
COMPLETED: "已完成",
|
||||
REJECTED: "已驳回",
|
||||
CANCELLED: "已作废",
|
||||
};
|
||||
return map[status] || status || "-";
|
||||
}
|
||||
|
||||
function statusTag(status) {
|
||||
const map = {
|
||||
DRAFT: "info",
|
||||
AUDITING: "warning",
|
||||
COMPLETED: "success",
|
||||
REJECTED: "danger",
|
||||
CANCELLED: "info",
|
||||
};
|
||||
return map[status] || "";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const isReadonly = route.query.readonly === "1";
|
||||
const id = route.query.id;
|
||||
readonly.value = isReadonly;
|
||||
loadReasons();
|
||||
if (id) {
|
||||
form.id = id;
|
||||
loadDetail(id);
|
||||
}
|
||||
});
|
||||
|
||||
function loadReasons() {
|
||||
getStockReasonList({ category: "OUTBOUND", pageNumber: 1, pageSize: 100 })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
reasonOptions.value = res.result?.records || res.result || [];
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function handleReasonChange(val) {
|
||||
const found = reasonOptions.value.find((item) => item.id === val);
|
||||
form.reasonName = found ? found.reason : "";
|
||||
}
|
||||
|
||||
function loadDetail(id) {
|
||||
getStockOutboundDetail(id).then((res) => {
|
||||
if (res.success && res.result) {
|
||||
const vo = res.result;
|
||||
const outbound = vo.outbound || vo;
|
||||
detail.value = outbound;
|
||||
form.id = outbound.id;
|
||||
form.reasonId = outbound.reasonId;
|
||||
form.reasonName = outbound.reasonName;
|
||||
form.remark = outbound.remark;
|
||||
items.value = (vo.items || []).map((it) => ({
|
||||
goodsId: it.goodsId,
|
||||
skuId: it.skuId,
|
||||
goodsName: it.goodsName,
|
||||
specs: it.specs,
|
||||
quantity: it.quantity || 1,
|
||||
unit: it.unit,
|
||||
remark: it.remark,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openSkuPicker() {
|
||||
if (liliDialogRef.value) {
|
||||
liliDialogRef.value.goodsData = items.value.map((item) => ({
|
||||
id: item.skuId,
|
||||
goodsId: item.goodsId,
|
||||
goodsName: item.goodsName,
|
||||
}));
|
||||
liliDialogRef.value.open("goods");
|
||||
}
|
||||
}
|
||||
|
||||
function onGoodsSelected(selected) {
|
||||
const existingMap = new Map(items.value.map((item) => [item.skuId, item]));
|
||||
items.value = selected.map((sku) => {
|
||||
const existed = existingMap.get(sku.id);
|
||||
if (existed) return existed;
|
||||
return {
|
||||
goodsId: sku.goodsId,
|
||||
skuId: sku.id,
|
||||
goodsName: sku.goodsName,
|
||||
specs: sku.specs || sku.simpleSpecs || "",
|
||||
quantity: 1,
|
||||
unit: sku.goodsUnit || "件",
|
||||
remark: "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function removeItem(index) {
|
||||
items.value.splice(index, 1);
|
||||
}
|
||||
|
||||
async function handleSave(submit = false) {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
if (!items.value.length) {
|
||||
ElMessage.warning("请添加至少一种出库商品");
|
||||
return;
|
||||
}
|
||||
for (const item of items.value) {
|
||||
if (!item.quantity || item.quantity <= 0) {
|
||||
ElMessage.warning(`商品【${item.goodsName}】的出库数量必须大于 0`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
reasonId: form.reasonId,
|
||||
reasonName: form.reasonName,
|
||||
remark: form.remark,
|
||||
items: items.value.map((it) => ({
|
||||
goodsId: it.goodsId,
|
||||
skuId: it.skuId,
|
||||
goodsName: it.goodsName,
|
||||
specs: it.specs,
|
||||
quantity: it.quantity,
|
||||
unit: it.unit,
|
||||
remark: it.remark,
|
||||
})),
|
||||
};
|
||||
|
||||
let id = form.id;
|
||||
if (id) {
|
||||
await updateStockOutbound(id, payload);
|
||||
} else {
|
||||
const res = await createStockOutbound(payload);
|
||||
id = res.result?.id || res.result?.outbound?.id || res.result;
|
||||
}
|
||||
|
||||
if (submit && id) {
|
||||
await submitStockOutbound(id);
|
||||
ElMessage.success("出库单保存并提交成功");
|
||||
} else {
|
||||
ElMessage.success("出库单保存成功");
|
||||
}
|
||||
goBack();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push({ name: "stockOutboundList" });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stock-outbound-operation {
|
||||
padding: 10px;
|
||||
}
|
||||
.card-header {
|
||||
min-height: 32px;
|
||||
}
|
||||
.summary {
|
||||
font-size: 14px;
|
||||
}
|
||||
.text-danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.text-primary {
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user