refactor: 四端完成 Options API → script setup + vuex → pinia 迁移

- buyer(95) / seller(169) / im(53) / manager(280) 共 597 个组件全部从 Options API 转换为 <script setup>
- 四端 vuex 全部移除,状态管理统一迁移至 pinia 4(stores/ 目录)
- util.js 等基础设施去除 vm 首参依赖;全局属性($Message/$filters 等)在 script 内改为具名导入
- 被外部 ref 访问的公共组件均已补齐 defineExpose(清单见各端 MIGRATION-VUE3.md)
- 各端 eslint 启用 vue/component-api-style 防止新代码回退 Options API
- 四端 yarn build 全部通过;迁移约定与注意事项已更新至各端 MIGRATION-VUE3.md
This commit is contained in:
lifenlong
2026-10-13 07:06:59 +08:00
parent 870bacdd8c
commit 88fc1b0b2a
674 changed files with 71220 additions and 69943 deletions

View File

@@ -4,33 +4,36 @@
</div>
</template>
<script>
<script setup>
import { onMounted } from "vue";
import Cookies from "js-cookie";
import util from "@/libs/util";
import { getStore } from "@/libs/storage";
import { getCategoryTree } from "@/api/goods.js";
import { useAppStore } from "@/stores/app";
export default {
name: "App",
mounted() {
const loggedIn =
this.getStore("accessToken") || Cookies.get("userInfoSeller");
if (loggedIn) {
util.bootstrapDynamicRoutesFromCache();
util.initRouter(this);
this.$store.commit("setOpenedList");
this.$store.commit("initCachepage");
if (!localStorage.getItem("category")) {
getCategoryTree()
.then((res) => {
if (res.success && Array.isArray(res.result)) {
localStorage.setItem("category", JSON.stringify(res.result));
}
})
.catch(() => {});
}
defineOptions({ name: "App" });
const appStore = useAppStore();
onMounted(() => {
const loggedIn = getStore("accessToken") || Cookies.get("userInfoSeller");
if (loggedIn) {
util.bootstrapDynamicRoutesFromCache();
util.initRouter();
appStore.setOpenedList();
appStore.initCachepage();
if (!localStorage.getItem("category")) {
getCategoryTree()
.then((res) => {
if (res.success && Array.isArray(res.result)) {
localStorage.setItem("category", JSON.stringify(res.result));
}
})
.catch(() => {});
}
},
};
}
});
</script>
<style>

View File

@@ -29,99 +29,105 @@
</div>
</template>
<script>
export default {
name: "setPassword",
props: {
modelValue: String,
value: String,
size: String,
placeholder: {
type: String,
default: "请输入密码,长度为6-20个字符",
},
disabled: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
maxlength: {
type: Number,
default: 20,
},
<script setup>
import { ref, watch } from "vue";
defineOptions({ name: "setPassword" });
const props = defineProps({
modelValue: String,
value: String,
size: String,
placeholder: {
type: String,
default: "请输入密码,长度为6-20个字符",
},
emits: ["update:modelValue", "input", "on-change"],
data() {
return {
currentValue: this.modelValue ?? this.value ?? "",
tipStyle: "password-tip-none",
strengthValue: 0,
progressStatus: "",
strength: "无",
grade: 0,
};
disabled: {
type: Boolean,
default: false,
},
watch: {
modelValue(val) {
this.setCurrentValue(val);
},
value(val) {
this.setCurrentValue(val);
},
readonly: {
type: Boolean,
default: false,
},
methods: {
checkStrengthValue(v) {
let grade = 0;
if (/\d/.test(v)) grade++;
if (/[a-z]/.test(v)) grade++;
if (/[A-Z]/.test(v)) grade++;
if (/\W/.test(v)) grade++;
if (v.length >= 10) grade++;
this.grade = grade;
return grade;
},
strengthChange() {
if (!this.currentValue) {
this.tipStyle = "password-tip-none";
this.strength = "无";
this.strengthValue = 0;
return;
}
const grade = this.checkStrengthValue(this.currentValue);
if (grade <= 1) {
this.progressStatus = "exception";
this.tipStyle = "password-tip-weak";
this.strength = "弱";
this.strengthValue = 33;
} else if (grade >= 2 && grade <= 4) {
this.progressStatus = "";
this.tipStyle = "password-tip-middle";
this.strength = "中";
this.strengthValue = 66;
} else {
this.progressStatus = "success";
this.tipStyle = "password-tip-strong";
this.strength = "强";
this.strengthValue = 100;
}
},
handleChange() {
this.strengthChange();
this.$emit("update:modelValue", this.currentValue);
this.$emit("input", this.currentValue);
this.$emit("on-change", this.currentValue, this.grade, this.strength);
},
setCurrentValue(value) {
if (value === this.currentValue) return;
this.currentValue = value ?? "";
this.strengthChange();
this.$emit("on-change", this.currentValue, this.grade, this.strength);
},
maxlength: {
type: Number,
default: 20,
},
};
});
const emit = defineEmits(["update:modelValue", "input", "on-change"]);
const currentValue = ref(props.modelValue ?? props.value ?? "");
const tipStyle = ref("password-tip-none");
const strengthValue = ref(0);
const progressStatus = ref("");
const strength = ref("无");
const grade = ref(0);
watch(
() => props.modelValue,
(val) => {
setCurrentValue(val);
}
);
watch(
() => props.value,
(val) => {
setCurrentValue(val);
}
);
function checkStrengthValue(v) {
// 局部变量原名 grade 与顶层 ref 同名,改名避免遮蔽
let g = 0;
if (/\d/.test(v)) g++;
if (/[a-z]/.test(v)) g++;
if (/[A-Z]/.test(v)) g++;
if (/\W/.test(v)) g++;
if (v.length >= 10) g++;
grade.value = g;
return g;
}
function strengthChange() {
if (!currentValue.value) {
tipStyle.value = "password-tip-none";
strength.value = "无";
strengthValue.value = 0;
return;
}
const grade = checkStrengthValue(currentValue.value);
if (grade <= 1) {
progressStatus.value = "exception";
tipStyle.value = "password-tip-weak";
strength.value = "弱";
strengthValue.value = 33;
} else if (grade >= 2 && grade <= 4) {
progressStatus.value = "";
tipStyle.value = "password-tip-middle";
strength.value = "中";
strengthValue.value = 66;
} else {
progressStatus.value = "success";
tipStyle.value = "password-tip-strong";
strength.value = "强";
strengthValue.value = 100;
}
}
function handleChange() {
strengthChange();
emit("update:modelValue", currentValue.value);
emit("input", currentValue.value);
emit("on-change", currentValue.value, grade.value, strength.value);
}
function setCurrentValue(value) {
if (value === currentValue.value) return;
currentValue.value = value ?? "";
strengthChange();
emit("on-change", currentValue.value, grade.value, strength.value);
}
</script>
<style lang="scss" scoped>

View File

@@ -5,54 +5,56 @@
</span>
</template>
<script>
<script setup>
import { computed, getCurrentInstance } from "vue";
import { unitPrice } from "@/utils/filters";
export default {
name: "priceColorScheme",
props: {
value: {
default: 0,
validator(val) {
return (
val === null ||
val === undefined ||
typeof val === "number" ||
typeof val === "string"
);
},
},
unit: {
type: String,
default: "¥",
},
dot: {
type: String,
default: "",
},
color: {
type: String,
default: "",
},
customStyle: {
type: Object,
default: () => ({}),
defineOptions({ name: "priceColorScheme" });
const props = defineProps({
value: {
default: 0,
validator(val) {
return (
val === null ||
val === undefined ||
typeof val === "number" ||
typeof val === "string"
);
},
},
computed: {
displayText() {
const val = this.value;
if (val === null || val === undefined || val === "" || val === "null") {
return `${this.unit || "¥"}0.00`;
}
return unitPrice(val, this.unit);
},
priceStyle() {
const resolvedColor = this.color || this.$mainColor || "";
return resolvedColor
? { color: resolvedColor, ...this.customStyle }
: { ...this.customStyle };
},
unit: {
type: String,
default: "¥",
},
};
dot: {
type: String,
default: "",
},
color: {
type: String,
default: "",
},
customStyle: {
type: Object,
default: () => ({}),
},
});
// $mainColor 为 main.js 注册的全局属性
const { proxy } = getCurrentInstance();
const displayText = computed(() => {
const val = props.value;
if (val === null || val === undefined || val === "" || val === "null") {
return `${props.unit || "¥"}0.00`;
}
return unitPrice(val, props.unit);
});
const priceStyle = computed(() => {
const resolvedColor = props.color || proxy.$mainColor || "";
return resolvedColor
? { color: resolvedColor, ...props.customStyle }
: { ...props.customStyle };
});
</script>

View File

@@ -3,6 +3,7 @@ import lazyLoading from "./lazyLoading.js";
import { router } from "@/router/index";
import Cookies from "js-cookie";
import config from "@/config/index";
import { useAppStore } from "@/stores/app";
let util = {};
@@ -199,13 +200,14 @@ util.handleTitle = function (vm, item) {
return item.title;
};
util.setCurrentPath = function (vm, name) {
util.setCurrentPath = function (name) {
const appStore = useAppStore();
let title = "";
let isOtherRouter = false;
vm.$store.state.app.routers.forEach((item) => {
appStore.routers.forEach((item) => {
if (item.children.length == 1) {
if (item.children[0].name == name) {
title = util.handleTitle(vm, item);
title = util.handleTitle(null, item);
if (item.name == "otherRouter") {
isOtherRouter = true;
}
@@ -213,7 +215,7 @@ util.setCurrentPath = function (vm, name) {
} else {
item.children.forEach((child) => {
if (child.name == name) {
title = util.handleTitle(vm, child);
title = util.handleTitle(null, child);
if (item.name == "otherRouter") {
isOtherRouter = true;
}
@@ -226,8 +228,8 @@ util.setCurrentPath = function (vm, name) {
currentPathArr = [
{
title: util.handleTitle(
vm,
util.getRouterObjByName(vm.$store.state.app.routers, "home_index")
null,
util.getRouterObjByName(appStore.routers, "home_index")
),
path: "",
name: "home_index",
@@ -237,8 +239,8 @@ util.setCurrentPath = function (vm, name) {
currentPathArr = [
{
title: util.handleTitle(
vm,
util.getRouterObjByName(vm.$store.state.app.routers, "home_index")
null,
util.getRouterObjByName(appStore.routers, "home_index")
),
path: "/home",
name: "home_index",
@@ -250,7 +252,7 @@ util.setCurrentPath = function (vm, name) {
},
];
} else {
let currentPathObj = vm.$store.state.app.routers.filter((item) => {
let currentPathObj = appStore.routers.filter((item) => {
if (item.children.length <= 1) {
return item.children[0].name == name;
}
@@ -311,22 +313,20 @@ util.setCurrentPath = function (vm, name) {
];
}
}
vm.$store.commit("setCurrentPath", currentPathArr);
appStore.setCurrentPath(currentPathArr);
return currentPathArr;
};
util.openNewPage = function (vm, name, argu, query) {
if (!vm.$store) {
return;
}
let storeOpenedList = vm.$store.state.app.storeOpenedList;
util.openNewPage = function (name, argu, query) {
const appStore = useAppStore();
let storeOpenedList = appStore.storeOpenedList;
let openedPageLen = storeOpenedList.length;
let i = 0;
let tagHasOpened = false;
while (i < openedPageLen) {
if (name == storeOpenedList[i].name) {
vm.$store.commit("storeOpenedList", {
appStore.updateOpenedList({
index: i,
argu: argu,
query: query,
@@ -337,7 +337,7 @@ util.openNewPage = function (vm, name, argu, query) {
i++;
}
if (!tagHasOpened) {
let tag = vm.$store.state.app.tagsList.filter((item) => {
let tag = appStore.tagsList.filter((item) => {
if (item.children) {
return name == item.children[0].name;
}
@@ -348,10 +348,10 @@ util.openNewPage = function (vm, name, argu, query) {
tag = tag.children ? tag.children[0] : tag;
if (argu) tag.argu = argu;
if (query) tag.query = query;
vm.$store.commit("increateTag", tag);
appStore.increateTag(tag);
}
}
vm.$store.commit("setCurrentPageName", name);
appStore.setCurrentPageName(name);
};
util.toDefaultPage = function (routers, name, route, next) {
@@ -378,13 +378,14 @@ util.toDefaultPage = function (routers, name, route, next) {
}
};
util.initRouter = function (vm) {
util.initRouter = function () {
const appStore = useAppStore();
const constRoutes = [];
let userInfo = Cookies.get("userInfoSeller");
if (!userInfo) {
return;
}
if (!vm.$store.state.app.added) {
if (!appStore.added) {
getCurrentPermissionList().then((res) => {
if (!res.success) return false;
let menuData = res.result;
@@ -405,24 +406,23 @@ util.initRouter = function (vm) {
}
util.initAllMenuData(constRoutes, menuData);
util.registerDynamicRoutes(menuData);
vm.$store.commit(
"updateAppRouter",
appStore.updateAppRouter(
constRoutes.filter((item) => item.children && item.children.length > 0)
);
util.initMenuData(vm, menuData);
util.initMenuData(menuData);
window.localStorage.setItem("menuData", JSON.stringify(menuData));
vm.$store.commit("setAdded", true);
appStore.setAdded(true);
});
} else {
let data = window.localStorage.getItem("menuData");
if (!data) {
vm.$store.commit("setAdded", false);
util.initRouter(vm);
appStore.setAdded(false);
util.initRouter();
return;
}
let menuData = JSON.parse(data);
util.registerDynamicRoutes(menuData);
util.initMenuData(vm, menuData);
util.initMenuData(menuData);
}
};
@@ -438,7 +438,8 @@ util.initAllMenuData = function (constRoutes, data) {
util.initRouterNode(constRoutes, allMenuData);
};
util.initMenuData = function (vm, data) {
util.initMenuData = function (data) {
const appStore = useAppStore();
const menuRoutes = [];
let menuData = data;
let navList = [HOME_NAV_ITEM];
@@ -448,23 +449,23 @@ util.initMenuData = function (vm, data) {
if (navList.length < 1) {
return;
}
vm.$store.commit("setNavList", navList);
appStore.setNavList(navList);
let currNav = window.localStorage.getItem("currNav");
if (currNav) {
for (let item of navList) {
if (item.name == currNav) {
vm.$store.commit("setCurrNavTitle", item.title);
appStore.setCurrNavTitle(item.title);
break;
}
}
} else {
currNav = HOME_NAV_NAME;
vm.$store.commit("setCurrNavTitle", HOME_NAV_ITEM.title);
appStore.setCurrNavTitle(HOME_NAV_ITEM.title);
}
vm.$store.commit("setCurrNav", currNav);
appStore.setCurrNav(currNav);
if (currNav === HOME_NAV_NAME) {
vm.$store.commit("updateMenulist", []);
appStore.updateMenulist([]);
return;
}
@@ -475,20 +476,19 @@ util.initMenuData = function (vm, data) {
}
}
util.initRouterNode(menuRoutes, menuData);
vm.$store.commit(
"updateMenulist",
appStore.updateMenulist(
menuRoutes.filter((item) => item.children.length > 0)
);
let tagsList = [];
vm.$store.state.app.routers.map((item) => {
appStore.routers.map((item) => {
if (item.children.length <= 1) {
tagsList.push(item.children[0]);
} else {
tagsList.push(...item.children);
}
});
vm.$store.commit("setTagsList", tagsList);
appStore.setTagsList(tagsList);
};
util.initRouterNode = function (routers, data) {

View File

@@ -1,8 +1,8 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import "./styles/theme.less";
import App from "./App.vue";
import { router } from "./router/index";
import store from "./store";
import { setupElementPlus } from "@/plugins/element";
import { setupLegacyMessage } from "@/utils/message";
import liliDialog from "@/views/lili-dialog";
@@ -50,7 +50,7 @@ app.use(VueLazyload, {
});
app.use(router);
app.use(store);
app.use(createPinia());
app.component("liliDialog", liliDialog);
app.component("priceColorScheme", PriceColorScheme);

View File

@@ -3,7 +3,6 @@ import NProgress from "nprogress";
import "nprogress/nprogress.css";
import Util from "../libs/util";
import Cookies from "js-cookie";
import store from "@/store";
import { routers } from "./router";
NProgress.configure({ showSpinner: false });
@@ -44,7 +43,7 @@ router.beforeEach((to, from, next) => {
});
router.afterEach((to) => {
Util.openNewPage({ $store: store }, to.name, to.params, to.query);
Util.openNewPage(to.name, to.params, to.query);
NProgress.done();
window.scrollTo(0, 0);
});

View File

@@ -1,19 +0,0 @@
import { createStore } from "vuex";
import app from "./modules/app";
import setting from "./modules/setting";
import user from "./modules/user";
import dict from "./modules/dict";
const store = createStore({
state: {},
mutations: {},
actions: {},
modules: {
app,
user,
setting,
dict,
},
});
export default store;

View File

@@ -1,162 +0,0 @@
import { otherRouter } from "@/router/router";
import Util from "@/libs/util";
const app = {
state: {
shipTemplates: "",
regions: [],
styleStore: "",
loading: false,
added: false,
navList: [],
currNav: "",
currNavTitle: "",
cachePage: [],
lang: "",
isFullScreen: false,
openedSubmenuArr: [],
menuTheme: "dark",
themeColor: "",
storeOpenedList: [
{
title: "首页",
path: "",
name: "home_index",
},
],
currentPageName: "",
currentPath: [
{
title: "首页",
path: "",
name: "home_index",
},
],
menuList: [],
routers: [otherRouter],
tagsList: [...otherRouter.children],
messageCount: 0,
dontCache: ["test", "test"],
refMenu: "",
},
mutations: {
childrenMenu(state, v) {
state.refMenu = v;
},
updateAppRouter(state, routes) {
state.routers.push(...routes);
},
updateDefaultRouter() {},
setLoading(state, v) {
state.loading = v;
},
setAdded(state, v) {
state.added = v;
},
setNavList(state, list) {
state.navList = list;
},
setCurrNav(state, v) {
state.currNav = v;
},
setCurrNavTitle(state, v) {
state.currNavTitle = v;
},
setTagsList(state, list) {
state.tagsList.push(...list);
},
updateMenulist(state, routes) {
state.menuList = routes;
},
addOpenSubmenu(state, name) {
if (name.length && state.openedSubmenuArr.indexOf(name) === -1) {
state.openedSubmenuArr.push(name);
}
},
closePage(state, name) {
state.cachePage.forEach((item, index) => {
if (item == name) {
state.cachePage.splice(index, 1);
}
});
localStorage.cachePage = JSON.stringify(state.cachePage);
},
initCachepage(state) {
if (localStorage.cachePage) {
state.cachePage = JSON.parse(localStorage.cachePage);
}
},
removeTag(state, name) {
state.storeOpenedList.map((item, index) => {
if (item.name == name) {
state.storeOpenedList.splice(index, 1);
}
});
},
storeOpenedList(state, get) {
let openedPage = state.storeOpenedList[get.index];
if (get.argu) openedPage.argu = get.argu;
if (get.query) openedPage.query = get.query;
state.storeOpenedList.splice(get.index, 1, openedPage);
localStorage.storeOpenedList = JSON.stringify(state.storeOpenedList);
},
clearAllTags(state) {
state.storeOpenedList.splice(1);
state.cachePage.length = 0;
localStorage.cachePage = "";
localStorage.storeOpenedList = JSON.stringify(state.storeOpenedList);
},
clearOtherTags(state, vm) {
let currentName = vm.$route.name;
let currentIndex = 0;
state.storeOpenedList.forEach((item, index) => {
if (item.name == currentName) {
currentIndex = index;
}
});
if (currentIndex == 0) {
state.storeOpenedList.splice(1);
} else {
state.storeOpenedList.splice(currentIndex + 1);
state.storeOpenedList.splice(1, currentIndex - 1);
}
state.cachePage = state.cachePage.filter((item) => item == currentName);
localStorage.cachePage = JSON.stringify(state.cachePage);
localStorage.storeOpenedList = JSON.stringify(state.storeOpenedList);
},
setOpenedList(state) {
state.storeOpenedList = localStorage.storeOpenedList
? JSON.parse(localStorage.storeOpenedList)
: [otherRouter.children[0]];
},
setCurrentPath(state, pathArr) {
state.currentPath = pathArr;
},
setCurrentPageName(state, name) {
state.currentPageName = name;
},
setAvatarPath(state, path) {
localStorage.avatorImgPath = path;
},
switchLang(state, lang) {
state.lang = lang;
localStorage.lang = lang;
},
clearOpenedSubmenu(state) {
state.openedSubmenuArr.length = 0;
},
setMessageCount(state, count) {
state.messageCount = count;
},
increateTag(state, tagObj) {
if (!Util.oneOf(tagObj.name, state.dontCache)) {
state.cachePage.push(tagObj.name);
localStorage.cachePage = JSON.stringify(state.cachePage);
}
state.storeOpenedList.push(tagObj);
localStorage.storeOpenedList = JSON.stringify(state.storeOpenedList);
},
},
};
export default app;

View File

@@ -1,26 +0,0 @@
const dict = {
state: {
// 经常需要读取的数据字典
sex: [],
messageType: [],
priority: [],
leaveType: []
},
mutations: {
// 设置值的改变方法
setSex(state, list) {
state.sex = list;
},
setMessageType(state, list) {
state.messageType = list;
},
setPriority(state, list) {
state.priority = list;
},
setLeaveType(state, list) {
state.leaveType = list;
},
}
};
export default dict;

View File

@@ -1,17 +0,0 @@
import config from "@/config/index";
const { setting } = config;
const localSetting = window.localStorage.getItem('setting')
const settingData = {
state: {
setting:localSetting ? JSON.parse(localSetting) : setting,
},
mutations: {
updateSetting(state, data) {
state.setting = data;
},
},
};
export default settingData;

View File

@@ -1,27 +0,0 @@
import Cookies from 'js-cookie';
import { removeStore } from '@/libs/storage';
// 退出登录时需要清除的会话/店铺数据
// 保留 uuid(设备标识)、setting(主题)、currNav、category(缓存)、saveLogin(记住登录状态)等无关键
const LOGOUT_KEYS = [
'accessToken',
'refreshToken',
'menuData',
'seller_expiration_time',
'sellersiteName',
'sellerlogoImg',
'sellerIconImg'
];
const user = {
state: {},
mutations: {
logout () {
Cookies.remove('userInfoSeller');
// 精确清除会话相关键,不再 localStorage.clear() 误删无关数据
LOGOUT_KEYS.forEach(key => removeStore(key));
}
}
};
export default user;

162
seller/src/stores/app.js Normal file
View File

@@ -0,0 +1,162 @@
import { defineStore } from "pinia";
import { otherRouter } from "@/router/router";
// 原 vuex app 模块的 pinia 版本;mutation 全部转为同名 action
// 例外:storeOpenedList mutation 与 state 同名,改名 updateOpenedList;
// clearOtherTags 原接收组件实例 vm,改为直接接收当前路由名 currentName
export const useAppStore = defineStore("app", {
state: () => ({
shipTemplates: "",
regions: [],
styleStore: "",
loading: false,
added: false,
navList: [],
currNav: "",
currNavTitle: "",
cachePage: [],
lang: "",
isFullScreen: false,
openedSubmenuArr: [],
menuTheme: "dark",
themeColor: "",
storeOpenedList: [
{
title: "首页",
path: "",
name: "home_index",
},
],
currentPageName: "",
currentPath: [
{
title: "首页",
path: "",
name: "home_index",
},
],
menuList: [],
routers: [otherRouter],
tagsList: [...otherRouter.children],
messageCount: 0,
dontCache: ["test", "test"],
refMenu: "",
}),
actions: {
childrenMenu(v) {
this.refMenu = v;
},
updateAppRouter(routes) {
this.routers.push(...routes);
},
updateDefaultRouter() {},
setLoading(v) {
this.loading = v;
},
setAdded(v) {
this.added = v;
},
setNavList(list) {
this.navList = list;
},
setCurrNav(v) {
this.currNav = v;
},
setCurrNavTitle(v) {
this.currNavTitle = v;
},
setTagsList(list) {
this.tagsList.push(...list);
},
updateMenulist(routes) {
this.menuList = routes;
},
addOpenSubmenu(name) {
if (name.length && this.openedSubmenuArr.indexOf(name) === -1) {
this.openedSubmenuArr.push(name);
}
},
closePage(name) {
this.cachePage.forEach((item, index) => {
if (item == name) {
this.cachePage.splice(index, 1);
}
});
localStorage.cachePage = JSON.stringify(this.cachePage);
},
initCachepage() {
if (localStorage.cachePage) {
this.cachePage = JSON.parse(localStorage.cachePage);
}
},
removeTag(name) {
this.storeOpenedList.map((item, index) => {
if (item.name == name) {
this.storeOpenedList.splice(index, 1);
}
});
},
updateOpenedList(get) {
let openedPage = this.storeOpenedList[get.index];
if (get.argu) openedPage.argu = get.argu;
if (get.query) openedPage.query = get.query;
this.storeOpenedList.splice(get.index, 1, openedPage);
localStorage.storeOpenedList = JSON.stringify(this.storeOpenedList);
},
clearAllTags() {
this.storeOpenedList.splice(1);
this.cachePage.length = 0;
localStorage.cachePage = "";
localStorage.storeOpenedList = JSON.stringify(this.storeOpenedList);
},
clearOtherTags(currentName) {
let currentIndex = 0;
this.storeOpenedList.forEach((item, index) => {
if (item.name == currentName) {
currentIndex = index;
}
});
if (currentIndex == 0) {
this.storeOpenedList.splice(1);
} else {
this.storeOpenedList.splice(currentIndex + 1);
this.storeOpenedList.splice(1, currentIndex - 1);
}
this.cachePage = this.cachePage.filter((item) => item == currentName);
localStorage.cachePage = JSON.stringify(this.cachePage);
localStorage.storeOpenedList = JSON.stringify(this.storeOpenedList);
},
setOpenedList() {
this.storeOpenedList = localStorage.storeOpenedList
? JSON.parse(localStorage.storeOpenedList)
: [otherRouter.children[0]];
},
setCurrentPath(pathArr) {
this.currentPath = pathArr;
},
setCurrentPageName(name) {
this.currentPageName = name;
},
setAvatarPath(path) {
localStorage.avatorImgPath = path;
},
switchLang(lang) {
this.lang = lang;
localStorage.lang = lang;
},
clearOpenedSubmenu() {
this.openedSubmenuArr.length = 0;
},
setMessageCount(count) {
this.messageCount = count;
},
increateTag(tagObj) {
if (this.dontCache.indexOf(tagObj.name) < 0) {
this.cachePage.push(tagObj.name);
localStorage.cachePage = JSON.stringify(this.cachePage);
}
this.storeOpenedList.push(tagObj);
localStorage.storeOpenedList = JSON.stringify(this.storeOpenedList);
},
},
});

26
seller/src/stores/dict.js Normal file
View File

@@ -0,0 +1,26 @@
import { defineStore } from "pinia";
export const useDictStore = defineStore("dict", {
state: () => ({
// 经常需要读取的数据字典
sex: [],
messageType: [],
priority: [],
leaveType: [],
}),
actions: {
// 设置值的改变方法
setSex(list) {
this.sex = list;
},
setMessageType(list) {
this.messageType = list;
},
setPriority(list) {
this.priority = list;
},
setLeaveType(list) {
this.leaveType = list;
},
},
});

View File

@@ -0,0 +1,17 @@
import { defineStore } from "pinia";
import config from "@/config/index";
const { setting } = config;
const localSetting = window.localStorage.getItem("setting");
export const useSettingStore = defineStore("setting", {
state: () => ({
setting: localSetting ? JSON.parse(localSetting) : setting,
}),
actions: {
updateSetting(data) {
this.setting = data;
},
},
});

25
seller/src/stores/user.js Normal file
View File

@@ -0,0 +1,25 @@
import { defineStore } from "pinia";
import Cookies from "js-cookie";
import { removeStore } from "@/libs/storage";
// 退出登录时需要清除的会话/店铺数据
// 保留 uuid(设备标识)、setting(主题)、currNav、category(缓存)、saveLogin(记住登录状态)等无关键
const LOGOUT_KEYS = [
"accessToken",
"refreshToken",
"menuData",
"seller_expiration_time",
"sellersiteName",
"sellerlogoImg",
"sellerIconImg",
];
export const useUserStore = defineStore("user", {
actions: {
logout() {
Cookies.remove("userInfoSeller");
// 精确清除会话相关键,不再 localStorage.clear() 误删无关数据
LOGOUT_KEYS.forEach((key) => removeStore(key));
},
},
});

View File

@@ -1,13 +1,13 @@
<template>
<div class="forget-password" @click="$refs.verify.show = false">
<div class="forget-password" @click="hideVerify">
<div style="height: 50px"></div>
<div class="logo-box">
<img :src="$store.state.logoImg" width="150" @click="$router.push('/')" />
<img :src="logoImg" width="150" @click="$router.push('/')" />
<div>修改密码</div>
</div>
<div class="login-container">
<el-form
ref="formFirst"
ref="formFirstRef"
:model="formFirst"
:rules="ruleInline"
style="width: 300px"
@@ -46,7 +46,7 @@
</el-form-item>
</el-form>
<el-form
ref="form"
ref="formRef"
:model="form"
:rules="ruleInline"
style="width: 300px"
@@ -91,7 +91,7 @@
</el-form-item>
</el-form>
<verify
ref="verify"
ref="verifyRef"
class="verify-con"
:verifyType="verifyType"
@change="verifyChange"
@@ -115,157 +115,171 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { Iphone, Message, Lock } from "@element-plus/icons-vue";
import { Message as $Message } from "@/utils/message";
import { md5 } from "@/utils/md5.js";
import * as RegExp from "@/utils/regular.js";
import * as apiLogin from "@/api/index.js";
import { sendSms } from "@/api/common.js";
import config from "@/config/index";
import verify from "@/views/my-components/verify";
import { useAppStore } from "@/stores/app";
export default {
name: "ForgetPassword",
components: { verify, Iphone, Message, Lock },
data() {
return {
config,
loading: false,
loading1: false,
formFirst: {
mobile: "",
code: "",
},
form: {
password: "",
oncePasd: "",
},
year: new Date().getFullYear(),
step: 0,
ruleInline: {
mobile: [
{ required: true, message: "请输入手机号码" },
{
pattern: RegExp.mobile,
trigger: "blur",
message: "请输入正确的手机号",
},
],
code: [{ required: true, message: "请输入手机验证码" }],
password: [
{ required: true, message: "密码不能为空" },
{ pattern: RegExp.password, message: "密码不能少于6位" },
],
},
verifyStatus: false,
verifyType: "FIND_USER",
codeMsg: "发送验证码",
interval: "",
time: 60,
};
},
methods: {
next() {
this.$refs.formFirst.validate((valid) => {
if (valid) {
this.loading = true;
const data = JSON.parse(JSON.stringify(this.formFirst));
apiLogin
.validateCode(data)
.then((res) => {
this.loading = false;
if (res.success) {
this.step = 1;
} else {
this.$Message.warning(res.message);
}
})
.catch(() => {
this.loading = false;
});
}
});
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
const params = JSON.parse(JSON.stringify(this.form));
if (params.password !== params.oncePasd) {
this.$Message.warning("两次输入密码不一致");
return;
}
params.mobile = this.formFirst.mobile;
params.password = this.md5(params.password);
delete params.oncePasd;
this.loading1 = true;
defineOptions({ name: "ForgetPassword" });
apiLogin
.forgetAndModify(params)
.then((res) => {
this.loading1 = false;
if (res.success) {
this.$Message.success("修改密码成功");
this.$router.push("login");
}
})
.catch(() => {
this.loading1 = false;
});
}
});
const router = useRouter();
const appStore = useAppStore();
const logoImg = computed(() => appStore.logoImg);
const loading = ref(false);
const loading1 = ref(false);
const formFirst = reactive({
mobile: "",
code: "",
});
const form = reactive({
password: "",
oncePasd: "",
});
const year = new Date().getFullYear();
const step = ref(0);
const ruleInline = {
mobile: [
{ required: true, message: "请输入手机号码" },
{
pattern: RegExp.mobile,
trigger: "blur",
message: "请输入正确的手机号",
},
sendCode() {
if (this.time === 60) {
if (this.formFirst.mobile === "") {
this.$Message.warning("请先填写手机号");
return;
}
if (!this.verifyStatus) {
this.$Message.warning("请先完成安全验证");
return;
}
const params = {
mobile: this.formFirst.mobile,
verificationEnums: "FIND_USER",
};
sendSms(params)
.then((res) => {
if (res.success) {
this.$Message.success("验证码发送成功");
const that = this;
this.interval = setInterval(() => {
that.time--;
if (that.time === 0) {
that.time = 60;
that.codeMsg = "重新发送";
that.verifyStatus = false;
clearInterval(that.interval);
} else {
that.codeMsg = that.time;
}
}, 1000);
} else {
this.$Message.warning(res.message);
}
})
.catch(() => {});
}
},
verifyChange(con) {
if (!con.status) return;
this.$refs.verify.show = false;
this.verifyStatus = true;
},
verifyBtnClick() {
if (!this.verifyStatus) {
this.$refs.verify.init();
}
},
},
mounted() {
document.querySelector(".forget-password").style.height = window.innerHeight + "px";
this.$refs.formFirst.resetFields();
},
],
code: [{ required: true, message: "请输入手机验证码" }],
password: [
{ required: true, message: "密码不能为空" },
{ pattern: RegExp.password, message: "密码不能少于6位" },
],
};
const verifyStatus = ref(false);
const verifyType = ref("FIND_USER");
const codeMsg = ref("发送验证码");
const interval = ref("");
const time = ref(60);
const formFirstRef = ref(null);
const formRef = ref(null);
const verifyRef = ref(null);
function hideVerify() {
verifyRef.value.show = false;
}
function next() {
formFirstRef.value.validate((valid) => {
if (valid) {
loading.value = true;
const data = JSON.parse(JSON.stringify(formFirst));
apiLogin
.validateCode(data)
.then((res) => {
loading.value = false;
if (res.success) {
step.value = 1;
} else {
$Message.warning(res.message);
}
})
.catch(() => {
loading.value = false;
});
}
});
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (valid) {
const params = JSON.parse(JSON.stringify(form));
if (params.password !== params.oncePasd) {
$Message.warning("两次输入密码不一致");
return;
}
params.mobile = formFirst.mobile;
params.password = md5(params.password);
delete params.oncePasd;
loading1.value = true;
apiLogin
.forgetAndModify(params)
.then((res) => {
loading1.value = false;
if (res.success) {
$Message.success("修改密码成功");
router.push("login");
}
})
.catch(() => {
loading1.value = false;
});
}
});
}
function sendCode() {
if (time.value === 60) {
if (formFirst.mobile === "") {
$Message.warning("请先填写手机号");
return;
}
if (!verifyStatus.value) {
$Message.warning("请先完成安全验证");
return;
}
const params = {
mobile: formFirst.mobile,
verificationEnums: "FIND_USER",
};
sendSms(params)
.then((res) => {
if (res.success) {
$Message.success("验证码发送成功");
interval.value = setInterval(() => {
time.value--;
if (time.value === 0) {
time.value = 60;
codeMsg.value = "重新发送";
verifyStatus.value = false;
clearInterval(interval.value);
} else {
codeMsg.value = time.value;
}
}, 1000);
} else {
$Message.warning(res.message);
}
})
.catch(() => {});
}
}
function verifyChange(con) {
if (!con.status) return;
hideVerify();
verifyStatus.value = true;
}
function verifyBtnClick() {
if (!verifyStatus.value) {
verifyRef.value.init();
}
}
onMounted(() => {
document.querySelector(".forget-password").style.height = window.innerHeight + "px";
formFirstRef.value.resetFields();
});
</script>
<style scoped lang="scss">

View File

@@ -75,11 +75,13 @@
</div>
</div>
<circleLoading class="loading-position" v-show="loading" />
<configDrawer ref="config" />
<configDrawer ref="configRef" />
</div>
</template>
<script>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ChatDotRound, Setting, UserFilled } from "@element-plus/icons-vue";
import shrinkableMenu from "./main-components/shrinkable-menu/shrinkable-menu.vue";
import tagsPageOpened from "./main-components/tags-page-opened.vue";
@@ -92,147 +94,134 @@ import { logout } from "@/api/index";
import { getIMDetail } from "@/api/common";
import { userMsg } from "@/api/index";
import config from "@/config/index.js";
import { Message } from "@/utils/message";
import { getStore, setStore } from "@/libs/storage";
import { useAppStore } from "@/stores/app";
import { useUserStore } from "@/stores/user";
import { useSettingStore } from "@/stores/setting";
export default {
components: {
ChatDotRound,
Setting,
UserFilled,
shrinkableMenu,
tagsPageOpened,
circleLoading,
configDrawer,
storeQrcode,
},
data() {
return {
config,
sliceNum: 5,
userInfo: {},
storeSideLogo: "",
IMLink: "",
load: false,
};
},
computed: {
setting() {
return this.$store.state.setting.setting;
},
loading() {
return this.$store.state.app.loading;
},
pageTagsList() {
return this.$store.state.app.storeOpenedList;
},
showSubMenu() {
return this.$store.state.app.menuList.length > 0;
},
lang() {
return this.$store.state.app.lang;
},
},
methods: {
handleClickSetting() {
this.$refs.config.open();
},
async im() {
const accessToken = this.getStore("accessToken");
this.load = true;
await this.getIMDetailMethods();
const userInfo = await userMsg();
this.load = false;
if (userInfo.success && this.IMLink) {
window.open(`${this.IMLink}?token=` + accessToken);
} else {
this.$Message.error("请登录后再联系客服");
}
},
async getIMDetailMethods() {
const res = await getIMDetail();
if (res.success) {
this.IMLink = res.result;
}
},
init() {
const pathArr = util.setCurrentPath(this, this.$route.name);
if (pathArr.length >= 2) {
this.$store.commit("addOpenSubmenu", pathArr[1].name);
}
this.storeSideLogo = localStorage.getItem("sellerlogoImg");
window.document.title = localStorage.getItem("sellersiteName");
const link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
link.type = "image/x-icon";
link.href = localStorage.getItem("sellerIconImg");
link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link);
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const userStore = useUserStore();
const settingStore = useSettingStore();
const userInfo = JSON.parse(Cookies.get("userInfoSeller"));
this.userInfo = userInfo;
this.checkTag(this.$route.name);
const configRef = ref(null);
const sliceNum = ref(5);
const userInfo = ref({});
const storeSideLogo = ref("");
const IMLink = ref("");
const load = ref(false);
const currWidth = document.body.clientWidth;
if (currWidth <= 1200) {
this.sliceNum = 2;
}
},
handleClickUserDropdown(name) {
if (name === "changePass") {
util.openNewPage(this, "change_pass");
this.$router.push({ name: "change_pass" });
} else if (name === "loginOut") {
logout().then(() => {
Cookies.set("accessToken", "");
this.$store.commit("logout", this);
this.$store.commit("clearOpenedSubmenu");
this.setStore("accessToken", "");
this.setStore("refreshToken", "");
this.$router.push({ path: "/login" });
});
}
},
checkTag(name) {
const openpageHasTag = this.pageTagsList.some((item) => item.name === name);
if (!openpageHasTag) {
util.openNewPage(
this,
name,
this.$route.params || {},
this.$route.query || {}
);
}
},
resize() {
const currWidth = document.body.clientWidth;
const count = currWidth / 300;
this.sliceNum = count > 6 ? 6 : count;
},
},
watch: {
$route(to) {
this.$store.commit("setCurrentPageName", to.name);
const pathArr = util.setCurrentPath(this, to.name);
if (pathArr.length > 2) {
this.$store.commit("addOpenSubmenu", pathArr[1].name);
}
this.checkTag(to.name);
localStorage.currentPageName = to.name;
},
lang() {
util.setCurrentPath(this, this.$route.name);
},
},
created() {
this.$store.commit("setOpenedList");
},
mounted() {
this.init();
this.resize();
window.addEventListener("resize", this.resize);
},
beforeUnmount() {
window.removeEventListener("resize", this.resize);
},
};
const setting = computed(() => settingStore.setting);
const loading = computed(() => appStore.loading);
const pageTagsList = computed(() => appStore.storeOpenedList);
const showSubMenu = computed(() => appStore.menuList.length > 0);
const lang = computed(() => appStore.lang);
function handleClickSetting() {
configRef.value.open();
}
async function im() {
const accessToken = getStore("accessToken");
load.value = true;
await getIMDetailMethods();
const userInfo = await userMsg();
load.value = false;
if (userInfo.success && IMLink.value) {
window.open(`${IMLink.value}?token=` + accessToken);
} else {
Message.error("请登录后再联系客服");
}
}
async function getIMDetailMethods() {
const res = await getIMDetail();
if (res.success) {
IMLink.value = res.result;
}
}
function init() {
const pathArr = util.setCurrentPath(route.name);
if (pathArr.length >= 2) {
appStore.addOpenSubmenu(pathArr[1].name);
}
storeSideLogo.value = localStorage.getItem("sellerlogoImg");
window.document.title = localStorage.getItem("sellersiteName");
const link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
link.type = "image/x-icon";
link.href = localStorage.getItem("sellerIconImg");
link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link);
userInfo.value = JSON.parse(Cookies.get("userInfoSeller"));
checkTag(route.name);
const currWidth = document.body.clientWidth;
if (currWidth <= 1200) {
sliceNum.value = 2;
}
}
function handleClickUserDropdown(name) {
if (name === "changePass") {
util.openNewPage("change_pass");
router.push({ name: "change_pass" });
} else if (name === "loginOut") {
logout().then(() => {
Cookies.set("accessToken", "");
userStore.logout();
appStore.clearOpenedSubmenu();
setStore("accessToken", "");
setStore("refreshToken", "");
router.push({ path: "/login" });
});
}
}
function checkTag(name) {
const openpageHasTag = pageTagsList.value.some((item) => item.name === name);
if (!openpageHasTag) {
util.openNewPage(name, route.params || {}, route.query || {});
}
}
function resize() {
const currWidth = document.body.clientWidth;
const count = currWidth / 300;
sliceNum.value = count > 6 ? 6 : count;
}
watch(
() => route.name,
(name) => {
appStore.setCurrentPageName(name);
const pathArr = util.setCurrentPath(name);
if (pathArr.length > 2) {
appStore.addOpenSubmenu(pathArr[1].name);
}
checkTag(name);
localStorage.currentPageName = name;
}
);
watch(lang, () => {
util.setCurrentPath(route.name);
});
appStore.setOpenedList();
onMounted(() => {
init();
resize();
window.addEventListener("resize", resize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resize);
});
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -88,7 +88,15 @@
</div>
</template>
<script>
<script setup>
import { computed } from "vue";
defineOptions({ name: "SystemFormFieldPreview" });
const props = defineProps({
field: { type: Object, required: true },
});
function pad(n) {
return String(n).padStart(2, "0");
}
@@ -103,88 +111,80 @@ function nowTimeText() {
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
export default {
name: "SystemFormFieldPreview",
props: {
field: { type: Object, required: true },
},
computed: {
isInlineOptions() {
return [
"CHECKBOX",
"RADIO",
"CITY",
"DATE",
"DATE_RANGE",
"SELECT",
"TEXT",
"TIME",
"TIME_RANGE",
].includes(this.field.field_type);
},
imageSlotCount() {
const n = Number(this.field.max_upload_count);
return Math.min(10, Math.max(1, Number.isFinite(n) ? n : 1));
},
optionListStacked() {
// 一行放不下两个选项时改为纵向堆叠
const labels = (this.field.options || []).map((item) => item.label || "");
if (labels.length < 2) return false;
const itemWidth = (text) => 24 + Array.from(text).length * 14;
const available = 230;
for (let i = 0; i < labels.length; i += 2) {
const next = labels[i + 1];
if (next == null) break;
if (itemWidth(labels[i]) + 12 + itemWidth(next) > available) {
return true;
}
}
return false;
},
showDefault() {
return this.field.default_visibility === "SHOW";
},
singleDefault() {
if (!this.showDefault) return "";
if (this.field.field_type === "DATE") {
return this.field.default_date_mode === "SPECIFIED_DATE"
? this.field.default_date
: todayText();
}
return this.field.default_time_mode === "SPECIFIED_TIME"
? this.field.default_time
: nowTimeText();
},
dateRangeText() {
if (this.rangeStart && this.rangeEnd) {
return `${this.rangeStart} - ${this.rangeEnd}`;
}
return this.field.hint_text || "请选择";
},
rangeStart() {
if (!this.showDefault) return "";
if (this.field.field_type === "DATE_RANGE") {
return this.field.default_date_mode === "SPECIFIED_DATE"
? this.field.default_date_start
: todayText();
}
return this.field.default_time_mode === "SPECIFIED_TIME"
? this.field.default_time_start
: nowTimeText();
},
rangeEnd() {
if (!this.showDefault) return "";
if (this.field.field_type === "DATE_RANGE") {
return this.field.default_date_mode === "SPECIFIED_DATE"
? this.field.default_date_end
: todayText();
}
return this.field.default_time_mode === "SPECIFIED_TIME"
? this.field.default_time_end
: nowTimeText();
},
},
};
const isInlineOptions = computed(() => {
return [
"CHECKBOX",
"RADIO",
"CITY",
"DATE",
"DATE_RANGE",
"SELECT",
"TEXT",
"TIME",
"TIME_RANGE",
].includes(props.field.field_type);
});
const imageSlotCount = computed(() => {
const n = Number(props.field.max_upload_count);
return Math.min(10, Math.max(1, Number.isFinite(n) ? n : 1));
});
const optionListStacked = computed(() => {
// 一行放不下两个选项时改为纵向堆叠
const labels = (props.field.options || []).map((item) => item.label || "");
if (labels.length < 2) return false;
const itemWidth = (text) => 24 + Array.from(text).length * 14;
const available = 230;
for (let i = 0; i < labels.length; i += 2) {
const next = labels[i + 1];
if (next == null) break;
if (itemWidth(labels[i]) + 12 + itemWidth(next) > available) {
return true;
}
}
return false;
});
const showDefault = computed(() => {
return props.field.default_visibility === "SHOW";
});
const singleDefault = computed(() => {
if (!showDefault.value) return "";
if (props.field.field_type === "DATE") {
return props.field.default_date_mode === "SPECIFIED_DATE"
? props.field.default_date
: todayText();
}
return props.field.default_time_mode === "SPECIFIED_TIME"
? props.field.default_time
: nowTimeText();
});
const dateRangeText = computed(() => {
if (rangeStart.value && rangeEnd.value) {
return `${rangeStart.value} - ${rangeEnd.value}`;
}
return props.field.hint_text || "请选择";
});
const rangeStart = computed(() => {
if (!showDefault.value) return "";
if (props.field.field_type === "DATE_RANGE") {
return props.field.default_date_mode === "SPECIFIED_DATE"
? props.field.default_date_start
: todayText();
}
return props.field.default_time_mode === "SPECIFIED_TIME"
? props.field.default_time_start
: nowTimeText();
});
const rangeEnd = computed(() => {
if (!showDefault.value) return "";
if (props.field.field_type === "DATE_RANGE") {
return props.field.default_date_mode === "SPECIFIED_DATE"
? props.field.default_date_end
: todayText();
}
return props.field.default_time_mode === "SPECIFIED_TIME"
? props.field.default_time_end
: nowTimeText();
});
</script>
<style scoped>

View File

@@ -23,52 +23,50 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { getInStoreSettings, saveInStoreSettings } from "@/api/appointment";
import { Message } from "@/utils/message";
export default {
name: "appointmentInStoreSettings",
data() {
return {
loading: false,
saving: false,
form: {
userBooking: true,
workOrder: true,
endService: true,
},
};
},
mounted() {
this.loadData();
},
methods: {
loadData() {
this.loading = true;
getInStoreSettings()
.then((res) => {
if (res.success && res.result) {
this.form = { ...this.form, ...res.result };
}
})
.finally(() => {
this.loading = false;
});
},
handleSave() {
this.saving = true;
saveInStoreSettings(this.form)
.then((res) => {
if (res.success) {
this.$message.success("保存成功");
}
})
.finally(() => {
this.saving = false;
});
},
},
};
defineOptions({ name: "appointmentInStoreSettings" });
const loading = ref(false);
const saving = ref(false);
const form = ref({
userBooking: true,
workOrder: true,
endService: true,
});
onMounted(() => {
loadData();
});
function loadData() {
loading.value = true;
getInStoreSettings()
.then((res) => {
if (res.success && res.result) {
form.value = { ...form.value, ...res.result };
}
})
.finally(() => {
loading.value = false;
});
}
function handleSave() {
saving.value = true;
saveInStoreSettings(form.value)
.then((res) => {
if (res.success) {
Message.success("保存成功");
}
})
.finally(() => {
saving.value = false;
});
}
</script>
<style scoped>

View File

@@ -50,59 +50,57 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { getOnSiteSettings, saveOnSiteSettings } from "@/api/appointment";
import { Message } from "@/utils/message";
import ServiceAreaFence from "./components/ServiceAreaFence.vue";
export default {
name: "appointmentOnSiteSettings",
components: { ServiceAreaFence },
data() {
return {
activeTab: "flow",
loading: false,
saving: false,
form: {
userBooking: true,
acceptOrder: true,
onSiteCheckin: true,
serviceEvidence: false,
endService: true,
serviceAreaEnabled: false,
serviceAreaGeo: "",
},
};
},
mounted() {
this.loadData();
},
methods: {
loadData() {
this.loading = true;
getOnSiteSettings()
.then((res) => {
if (res.success && res.result) {
this.form = { ...this.form, ...res.result };
}
})
.finally(() => {
this.loading = false;
});
},
handleSave() {
this.saving = true;
saveOnSiteSettings(this.form)
.then((res) => {
if (res.success) {
this.$message.success("保存成功");
}
})
.finally(() => {
this.saving = false;
});
},
},
};
defineOptions({ name: "appointmentOnSiteSettings" });
const activeTab = ref("flow");
const loading = ref(false);
const saving = ref(false);
const form = ref({
userBooking: true,
acceptOrder: true,
onSiteCheckin: true,
serviceEvidence: false,
endService: true,
serviceAreaEnabled: false,
serviceAreaGeo: "",
});
onMounted(() => {
loadData();
});
function loadData() {
loading.value = true;
getOnSiteSettings()
.then((res) => {
if (res.success && res.result) {
form.value = { ...form.value, ...res.result };
}
})
.finally(() => {
loading.value = false;
});
}
function handleSave() {
saving.value = true;
saveOnSiteSettings(form.value)
.then((res) => {
if (res.success) {
// 原代码为 this.$message(小写),全局只注册了 $Message,按作者意图修正
Message.success("保存成功");
}
})
.finally(() => {
saving.value = false;
});
}
</script>
<style scoped>

View File

@@ -35,81 +35,74 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { getServiceCalendarMonth, getServiceCalendarDay } from "@/api/appointment";
defineOptions({ name: "appointmentServiceCalendar" });
function formatMonth(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
return `${y}-${m}`;
}
export default {
name: "appointmentServiceCalendar",
data() {
return {
loading: false,
currentMonth: formatMonth(new Date()),
monthDays: [],
dayVisible: false,
dayLoading: false,
activeDate: "",
dayStatus: "ALL",
dayRows: [],
};
},
computed: {
monthLabel() {
return this.currentMonth;
},
},
mounted() {
this.loadMonth();
},
methods: {
goToday() {
this.currentMonth = formatMonth(new Date());
this.loadMonth();
},
changeMonth(delta) {
const [y, m] = this.currentMonth.split("-").map(Number);
const date = new Date(y, m - 1 + delta, 1);
this.currentMonth = formatMonth(date);
this.loadMonth();
},
loadMonth() {
this.loading = true;
getServiceCalendarMonth(this.currentMonth)
.then((res) => {
if (res.success) {
this.monthDays = res.result?.days || res.result || [];
}
})
.finally(() => {
this.loading = false;
});
},
openDay(date) {
this.activeDate = date;
this.dayStatus = "ALL";
this.dayVisible = true;
this.loadDay();
},
loadDay() {
if (!this.activeDate) return;
this.dayLoading = true;
getServiceCalendarDay({ date: this.activeDate, status: this.dayStatus })
.then((res) => {
if (res.success) {
this.dayRows = res.result?.records || res.result || [];
}
})
.finally(() => {
this.dayLoading = false;
});
},
},
};
const loading = ref(false);
const currentMonth = ref(formatMonth(new Date()));
const monthDays = ref([]);
const dayVisible = ref(false);
const dayLoading = ref(false);
const activeDate = ref("");
const dayStatus = ref("ALL");
const dayRows = ref([]);
const monthLabel = computed(() => currentMonth.value);
onMounted(() => {
loadMonth();
});
function goToday() {
currentMonth.value = formatMonth(new Date());
loadMonth();
}
function changeMonth(delta) {
const [y, m] = currentMonth.value.split("-").map(Number);
const date = new Date(y, m - 1 + delta, 1);
currentMonth.value = formatMonth(date);
loadMonth();
}
function loadMonth() {
loading.value = true;
getServiceCalendarMonth(currentMonth.value)
.then((res) => {
if (res.success) {
monthDays.value = res.result?.days || res.result || [];
}
})
.finally(() => {
loading.value = false;
});
}
function openDay(date) {
activeDate.value = date;
dayStatus.value = "ALL";
dayVisible.value = true;
loadDay();
}
function loadDay() {
if (!activeDate.value) return;
dayLoading.value = true;
getServiceCalendarDay({ date: activeDate.value, status: dayStatus.value })
.then((res) => {
if (res.success) {
dayRows.value = res.result?.records || res.result || [];
}
})
.finally(() => {
dayLoading.value = false;
});
}
</script>
<style scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="80px" @keyup.enter="handleSearch">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="80px" @keyup.enter="handleSearch">
<el-form-item label="员工姓名" prop="staffName">
<el-input v-model="searchForm.staffName" clearable placeholder="模糊搜索" style="width: 220px" />
</el-form-item>
@@ -90,8 +90,10 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { ElMessageBox } from "element-plus";
import { Message } from "@/utils/message";
import { getUserListData } from "@/api/index";
import {
getServiceStaffPage,
@@ -101,163 +103,172 @@ import {
deleteServiceStaff,
} from "@/api/appointment";
export default {
name: "appointmentServiceStaffList",
data() {
return {
loading: false,
data: [],
total: 0,
searchForm: {
pageNumber: 1,
pageSize: 20,
staffName: "",
status: "",
},
createVisible: false,
createLoading: false,
clerkLoading: false,
clerkOptions: [],
createForm: {
id: "",
clerkUserId: "",
staffName: "",
contactPhone: "",
sortOrder: 0,
status: "ENABLED",
},
};
},
mounted() {
this.loadData();
},
methods: {
handleSearch() {
this.searchForm.pageNumber = 1;
this.loadData();
},
loadData() {
this.loading = true;
getServiceStaffPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result?.records || [];
this.total = res.result?.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
emptyForm() {
return {
id: "",
clerkUserId: "",
staffName: "",
contactPhone: "",
sortOrder: 0,
status: "ENABLED",
};
},
openCreate() {
this.createForm = this.emptyForm();
this.createVisible = true;
this.loadClerks();
},
openEdit(row) {
this.createForm = {
id: row.id,
clerkUserId: row.clerkUserId || "",
staffName: row.staffName || "",
contactPhone: row.contactPhone || "",
sortOrder: row.sortOrder == null ? 0 : row.sortOrder,
status: row.status || "ENABLED",
};
this.createVisible = true;
this.loadClerks();
},
loadClerks() {
this.clerkLoading = true;
getUserListData({ pageNumber: 1, pageSize: 200 })
.then((res) => {
if (res.success) {
const records = res.result?.records || [];
const usedIds = new Set(
(this.data || [])
.filter((staff) => staff.id !== this.createForm.id)
.map((staff) => staff.clerkUserId)
.filter(Boolean)
);
const options = records.filter(
(clerk) => clerk.memberId && clerk.status !== false && !usedIds.has(clerk.memberId)
);
const currentId = this.createForm.clerkUserId;
if (currentId && !options.some((clerk) => clerk.memberId === currentId)) {
options.unshift({
memberId: currentId,
clerkName: this.createForm.staffName || "当前店员",
});
}
this.clerkOptions = options;
}
})
.finally(() => {
this.clerkLoading = false;
});
},
clerkOptionLabel(clerk) {
return clerk.clerkName || "未命名店员";
},
onClerkChange(memberId) {
const clerk = this.clerkOptions.find((item) => item.memberId === memberId);
if (!clerk) {
return;
defineOptions({ name: "appointmentServiceStaffList" });
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
staffName: "",
status: "",
});
const createVisible = ref(false);
const createLoading = ref(false);
const clerkLoading = ref(false);
const clerkOptions = ref([]);
const createForm = ref({
id: "",
clerkUserId: "",
staffName: "",
contactPhone: "",
sortOrder: 0,
status: "ENABLED",
});
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖(原 ref="searchForm" 在 script 中未被使用)
const searchFormRef = ref(null);
onMounted(() => {
loadData();
});
function handleSearch() {
searchForm.pageNumber = 1;
loadData();
}
function loadData() {
loading.value = true;
getServiceStaffPage(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result?.records || [];
total.value = res.result?.total || 0;
}
this.createForm.staffName = clerk.clerkName || "";
this.createForm.contactPhone = clerk.mobile || "";
},
submitCreate() {
if (!this.createForm.clerkUserId) {
this.$message.warning("请选择店员");
return;
}
if (!this.createForm.staffName) {
this.$message.warning("请填写员工姓名");
return;
}
const editing = !!this.createForm.id;
const request = editing ? updateServiceStaff(this.createForm.id, this.createForm) : createServiceStaff(this.createForm);
this.createLoading = true;
request
.then((res) => {
if (res.success) {
this.$message.success(editing ? "已保存" : "创建成功");
this.createVisible = false;
this.loadData();
}
})
.finally(() => {
this.createLoading = false;
});
},
handleDelete(row) {
ElMessageBox.confirm(`确认删除服务人员「${row.staffName || ""}」?`, "提示", { type: "warning" })
.then(() => deleteServiceStaff(row.id))
.then((res) => {
if (res && res.success) {
this.$message.success("已删除");
this.loadData();
}
})
.catch(() => {});
},
toggleStatus(row, enabled) {
updateServiceStaffStatus(row.id, enabled ? "ENABLED" : "DISABLED").then((res) => {
if (res.success) {
row.status = enabled ? "ENABLED" : "DISABLED";
})
.finally(() => {
loading.value = false;
});
}
function emptyForm() {
return {
id: "",
clerkUserId: "",
staffName: "",
contactPhone: "",
sortOrder: 0,
status: "ENABLED",
};
}
function openCreate() {
createForm.value = emptyForm();
createVisible.value = true;
loadClerks();
}
function openEdit(row) {
createForm.value = {
id: row.id,
clerkUserId: row.clerkUserId || "",
staffName: row.staffName || "",
contactPhone: row.contactPhone || "",
sortOrder: row.sortOrder == null ? 0 : row.sortOrder,
status: row.status || "ENABLED",
};
createVisible.value = true;
loadClerks();
}
function loadClerks() {
clerkLoading.value = true;
getUserListData({ pageNumber: 1, pageSize: 200 })
.then((res) => {
if (res.success) {
const records = res.result?.records || [];
const usedIds = new Set(
(data.value || [])
.filter((staff) => staff.id !== createForm.value.id)
.map((staff) => staff.clerkUserId)
.filter(Boolean)
);
const options = records.filter(
(clerk) => clerk.memberId && clerk.status !== false && !usedIds.has(clerk.memberId)
);
const currentId = createForm.value.clerkUserId;
if (currentId && !options.some((clerk) => clerk.memberId === currentId)) {
options.unshift({
memberId: currentId,
clerkName: createForm.value.staffName || "当前店员",
});
}
});
},
},
};
clerkOptions.value = options;
}
})
.finally(() => {
clerkLoading.value = false;
});
}
function clerkOptionLabel(clerk) {
return clerk.clerkName || "未命名店员";
}
function onClerkChange(memberId) {
const clerk = clerkOptions.value.find((item) => item.memberId === memberId);
if (!clerk) {
return;
}
createForm.value.staffName = clerk.clerkName || "";
createForm.value.contactPhone = clerk.mobile || "";
}
function submitCreate() {
if (!createForm.value.clerkUserId) {
// 原代码 this.$message 小写,全局仅注册了 $Message,此处按作者意图修正
Message.warning("请选择店员");
return;
}
if (!createForm.value.staffName) {
Message.warning("请填写员工姓名");
return;
}
const editing = !!createForm.value.id;
const request = editing ? updateServiceStaff(createForm.value.id, createForm.value) : createServiceStaff(createForm.value);
createLoading.value = true;
request
.then((res) => {
if (res.success) {
Message.success(editing ? "已保存" : "创建成功");
createVisible.value = false;
loadData();
}
})
.finally(() => {
createLoading.value = false;
});
}
function handleDelete(row) {
ElMessageBox.confirm(`确认删除服务人员「${row.staffName || ""}」?`, "提示", { type: "warning" })
.then(() => deleteServiceStaff(row.id))
.then((res) => {
if (res && res.success) {
Message.success("已删除");
loadData();
}
})
.catch(() => {});
}
function toggleStatus(row, enabled) {
updateServiceStaffStatus(row.id, enabled ? "ENABLED" : "DISABLED").then((res) => {
if (res.success) {
row.status = enabled ? "ENABLED" : "DISABLED";
}
});
}
</script>

View File

@@ -260,7 +260,9 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import {
Delete,
@@ -299,282 +301,268 @@ import {
hasOptions,
} from "@/constants/systemForm";
export default {
name: "appointmentSystemFormDesigner",
components: { editor, SystemFormFieldPreview, Delete, CopyDocument, ArrowUp, ArrowDown },
data() {
return {
loading: false,
saving: false,
formId: "",
formName: "",
fields: [],
selectedKey: "",
baseline: "",
batchVisible: false,
batchText: "",
palette: FORM_FIELD_PALETTE,
paletteIcons: {
CHECKBOX: Checked,
CITY: Location,
RADIO: CircleCheck,
DATE: Calendar,
DATE_RANGE: Tickets,
SELECT: ArrowDown,
TEXT: EditPen,
TIME: Clock,
TIME_RANGE: Timer,
RICH_TEXT: Document,
IMAGE: Picture,
},
OPTION_MAX,
FIELD_LABEL_MAX,
OPTION_LABEL_MAX,
HINT_TEXT_MAX,
Delete,
};
},
computed: {
selected() {
return this.fields.find((item) => item.field_key === this.selectedKey) || null;
},
dateRangeValue: {
get() {
const field = this.selected;
if (!field) return null;
return field.default_date_start && field.default_date_end
? [field.default_date_start, field.default_date_end]
: null;
},
set(val) {
if (!this.selected) return;
this.selected.default_date_start = val?.[0] || null;
this.selected.default_date_end = val?.[1] || null;
},
},
timeRangeValue: {
get() {
const field = this.selected;
if (!field) return null;
return field.default_time_start && field.default_time_end
? [field.default_time_start, field.default_time_end]
: null;
},
set(val) {
if (!this.selected) return;
this.selected.default_time_start = val?.[0] || null;
this.selected.default_time_end = val?.[1] || null;
},
},
},
watch: {
selectedKey() {
this.closeBatch();
},
},
mounted() {
this.formId = this.$route.query.id || "";
if (this.formId) {
this.loadForm();
} else {
this.captureBaseline();
}
},
methods: {
addOption,
removeOption,
hasRequiredSwitch,
hasOptions,
fieldTypeLabel(type) {
return FORM_FIELD_LABEL[type] || type;
},
captureBaseline() {
this.baseline = JSON.stringify({
formName: this.formName,
fields: this.fields,
});
},
loadForm() {
this.loading = true;
getRequest(`/appointment/system-forms/${this.formId}`)
.then((res) => {
if (!res.success || !res.result) {
ElMessage.error("表单不存在");
this.$router.push({ name: "appointment-system-forms" });
return;
}
this.formName = res.result.formName || "";
this.fields = parseFormSchema(res.result.formSchema);
this.selectedKey = this.fields[0]?.field_key || "";
this.captureBaseline();
})
.catch(() => {
ElMessage.error("加载表单失败");
})
.finally(() => {
this.loading = false;
});
},
addField(type) {
const field = createField(type);
this.fields.push(field);
this.selectedKey = field.field_key;
},
removeField(index) {
this.fields.splice(index, 1);
this.selectedKey = this.fields[index]?.field_key || this.fields[index - 1]?.field_key || "";
},
copyField(index) {
const source = this.fields[index];
if (!source) return;
const cloned = JSON.parse(JSON.stringify(source));
cloned.field_key = createFieldKey();
if (Array.isArray(cloned.options)) {
cloned.options = cloned.options.map((opt) => ({
...opt,
value: createFieldKey(), // 避免与原字段共用 option value
}));
}
this.fields.splice(index + 1, 0, cloned);
this.selectedKey = cloned.field_key;
},
moveField(index, step) {
const target = index + step;
if (target < 0 || target >= this.fields.length) return;
const list = [...this.fields];
const [item] = list.splice(index, 1);
list.splice(target, 0, item);
this.fields = list;
},
closeBatch() {
this.batchVisible = false;
this.batchText = "";
},
confirmBatch() {
if (!this.selected) return;
this.selected.options = batchAddOptions(this.selected.options, this.batchText, {
radio: this.selected.field_type === "RADIO",
});
this.closeBatch();
},
handleReset() {
ElMessageBox.confirm("将撤销本次未保存的编辑,恢复进入页面时的内容?", "重置", {
type: "warning",
}).then(() => {
const snap = JSON.parse(this.baseline || "{}");
this.formName = snap.formName || "";
this.fields = JSON.parse(JSON.stringify(snap.fields || []));
this.selectedKey = this.fields[0]?.field_key || "";
}).catch(() => {});
},
validate() {
if (!this.formName.trim()) {
ElMessage.error("请填写表单名称");
return false;
}
if (!this.fields.length) {
ElMessage.error("请至少添加一个表单组件");
return false;
}
for (const field of this.fields) {
if (field.field_type !== "RICH_TEXT") {
const label = (field.field_label || "").trim();
if (!label) {
ElMessage.error("请填写组件标题");
return false;
}
if (label.length > FIELD_LABEL_MAX) {
ElMessage.error(`组件标题最多 ${FIELD_LABEL_MAX} 个字`);
return false;
}
}
if (hasOptions(field.field_type)) {
const labels = (field.options || []).map((item) => item.label?.trim()).filter(Boolean);
if (labels.length < 2) {
ElMessage.error(`${field.field_label || "选项组件"}至少需要 2 个选项`);
return false;
}
if ((field.options || []).some((item) => (item.label || "").length > OPTION_LABEL_MAX)) {
ElMessage.error("选项最多 10 个字");
return false;
}
}
if (field.hint_text && field.hint_text.length > HINT_TEXT_MAX) {
ElMessage.error("提示语最多 10 个字");
return false;
}
if (
field.default_visibility === "SHOW" &&
field.default_date_mode === "SPECIFIED_DATE" &&
field.field_type === "DATE" &&
!field.default_date
) {
ElMessage.error("请为日期组件指定默认日期");
return false;
}
if (
field.default_visibility === "SHOW" &&
field.default_date_mode === "SPECIFIED_DATE" &&
field.field_type === "DATE_RANGE"
) {
if (!field.default_date_start || !field.default_date_end) {
ElMessage.error("请为日期范围指定起止日期");
return false;
}
if (field.default_date_start > field.default_date_end) {
ElMessage.error("日期范围的结束日期不能早于开始日期");
return false;
}
}
if (
field.default_visibility === "SHOW" &&
field.default_time_mode === "SPECIFIED_TIME" &&
field.field_type === "TIME" &&
!field.default_time
) {
ElMessage.error("请为时间组件指定默认时间");
return false;
}
if (
field.default_visibility === "SHOW" &&
field.default_time_mode === "SPECIFIED_TIME" &&
field.field_type === "TIME_RANGE"
) {
if (!field.default_time_start || !field.default_time_end) {
ElMessage.error("请为时间范围指定起止时间");
return false;
}
if (field.default_time_start > field.default_time_end) {
ElMessage.error("时间范围的结束时间不能早于开始时间");
return false;
}
}
}
return true;
},
handleSave() {
if (!this.validate()) return;
this.saving = true;
postRequestWithNoForm("/appointment/system-forms", {
id: this.formId || undefined,
formName: this.formName.trim(),
formSchema: serializeFormSchema(this.fields),
status: "ENABLED",
})
.then((res) => {
if (res.success) {
ElMessage.success("已保存并发布");
this.formId = res.result?.id || this.formId;
this.captureBaseline();
this.$router.push({ name: "appointment-system-forms" });
}
})
.finally(() => {
this.saving = false;
});
},
},
defineOptions({ name: "appointmentSystemFormDesigner" });
const route = useRoute();
const router = useRouter();
const loading = ref(false);
const saving = ref(false);
const formId = ref("");
const formName = ref("");
const fields = ref([]);
const selectedKey = ref("");
const baseline = ref("");
const batchVisible = ref(false);
const batchText = ref("");
const palette = FORM_FIELD_PALETTE;
const paletteIcons = {
CHECKBOX: Checked,
CITY: Location,
RADIO: CircleCheck,
DATE: Calendar,
DATE_RANGE: Tickets,
SELECT: ArrowDown,
TEXT: EditPen,
TIME: Clock,
TIME_RANGE: Timer,
RICH_TEXT: Document,
IMAGE: Picture,
};
const selected = computed(
() => fields.value.find((item) => item.field_key === selectedKey.value) || null
);
const dateRangeValue = computed({
get() {
const field = selected.value;
if (!field) return null;
return field.default_date_start && field.default_date_end
? [field.default_date_start, field.default_date_end]
: null;
},
set(val) {
if (!selected.value) return;
selected.value.default_date_start = val?.[0] || null;
selected.value.default_date_end = val?.[1] || null;
},
});
const timeRangeValue = computed({
get() {
const field = selected.value;
if (!field) return null;
return field.default_time_start && field.default_time_end
? [field.default_time_start, field.default_time_end]
: null;
},
set(val) {
if (!selected.value) return;
selected.value.default_time_start = val?.[0] || null;
selected.value.default_time_end = val?.[1] || null;
},
});
watch(selectedKey, () => {
closeBatch();
});
onMounted(() => {
formId.value = route.query.id || "";
if (formId.value) {
loadForm();
} else {
captureBaseline();
}
});
function fieldTypeLabel(type) {
return FORM_FIELD_LABEL[type] || type;
}
function captureBaseline() {
baseline.value = JSON.stringify({
formName: formName.value,
fields: fields.value,
});
}
function loadForm() {
loading.value = true;
getRequest(`/appointment/system-forms/${formId.value}`)
.then((res) => {
if (!res.success || !res.result) {
ElMessage.error("表单不存在");
router.push({ name: "appointment-system-forms" });
return;
}
formName.value = res.result.formName || "";
fields.value = parseFormSchema(res.result.formSchema);
selectedKey.value = fields.value[0]?.field_key || "";
captureBaseline();
})
.catch(() => {
ElMessage.error("加载表单失败");
})
.finally(() => {
loading.value = false;
});
}
function addField(type) {
const field = createField(type);
fields.value.push(field);
selectedKey.value = field.field_key;
}
function removeField(index) {
fields.value.splice(index, 1);
selectedKey.value = fields.value[index]?.field_key || fields.value[index - 1]?.field_key || "";
}
function copyField(index) {
const source = fields.value[index];
if (!source) return;
const cloned = JSON.parse(JSON.stringify(source));
cloned.field_key = createFieldKey();
if (Array.isArray(cloned.options)) {
cloned.options = cloned.options.map((opt) => ({
...opt,
value: createFieldKey(), // 避免与原字段共用 option value
}));
}
fields.value.splice(index + 1, 0, cloned);
selectedKey.value = cloned.field_key;
}
function moveField(index, step) {
const target = index + step;
if (target < 0 || target >= fields.value.length) return;
const list = [...fields.value];
const [item] = list.splice(index, 1);
list.splice(target, 0, item);
fields.value = list;
}
function closeBatch() {
batchVisible.value = false;
batchText.value = "";
}
function confirmBatch() {
if (!selected.value) return;
selected.value.options = batchAddOptions(selected.value.options, batchText.value, {
radio: selected.value.field_type === "RADIO",
});
closeBatch();
}
function handleReset() {
ElMessageBox.confirm("将撤销本次未保存的编辑,恢复进入页面时的内容?", "重置", {
type: "warning",
}).then(() => {
const snap = JSON.parse(baseline.value || "{}");
formName.value = snap.formName || "";
fields.value = JSON.parse(JSON.stringify(snap.fields || []));
selectedKey.value = fields.value[0]?.field_key || "";
}).catch(() => {});
}
function validate() {
if (!formName.value.trim()) {
ElMessage.error("请填写表单名称");
return false;
}
if (!fields.value.length) {
ElMessage.error("请至少添加一个表单组件");
return false;
}
for (const field of fields.value) {
if (field.field_type !== "RICH_TEXT") {
const label = (field.field_label || "").trim();
if (!label) {
ElMessage.error("请填写组件标题");
return false;
}
if (label.length > FIELD_LABEL_MAX) {
ElMessage.error(`组件标题最多 ${FIELD_LABEL_MAX} 个字`);
return false;
}
}
if (hasOptions(field.field_type)) {
const labels = (field.options || []).map((item) => item.label?.trim()).filter(Boolean);
if (labels.length < 2) {
ElMessage.error(`${field.field_label || "选项组件"}至少需要 2 个选项`);
return false;
}
if ((field.options || []).some((item) => (item.label || "").length > OPTION_LABEL_MAX)) {
ElMessage.error("选项最多 10 个字");
return false;
}
}
if (field.hint_text && field.hint_text.length > HINT_TEXT_MAX) {
ElMessage.error("提示语最多 10 个字");
return false;
}
if (
field.default_visibility === "SHOW" &&
field.default_date_mode === "SPECIFIED_DATE" &&
field.field_type === "DATE" &&
!field.default_date
) {
ElMessage.error("请为日期组件指定默认日期");
return false;
}
if (
field.default_visibility === "SHOW" &&
field.default_date_mode === "SPECIFIED_DATE" &&
field.field_type === "DATE_RANGE"
) {
if (!field.default_date_start || !field.default_date_end) {
ElMessage.error("请为日期范围指定起止日期");
return false;
}
if (field.default_date_start > field.default_date_end) {
ElMessage.error("日期范围的结束日期不能早于开始日期");
return false;
}
}
if (
field.default_visibility === "SHOW" &&
field.default_time_mode === "SPECIFIED_TIME" &&
field.field_type === "TIME" &&
!field.default_time
) {
ElMessage.error("请为时间组件指定默认时间");
return false;
}
if (
field.default_visibility === "SHOW" &&
field.default_time_mode === "SPECIFIED_TIME" &&
field.field_type === "TIME_RANGE"
) {
if (!field.default_time_start || !field.default_time_end) {
ElMessage.error("请为时间范围指定起止时间");
return false;
}
if (field.default_time_start > field.default_time_end) {
ElMessage.error("时间范围的结束时间不能早于开始时间");
return false;
}
}
}
return true;
}
function handleSave() {
if (!validate()) return;
saving.value = true;
postRequestWithNoForm("/appointment/system-forms", {
id: formId.value || undefined,
formName: formName.value.trim(),
formSchema: serializeFormSchema(fields.value),
status: "ENABLED",
})
.then((res) => {
if (res.success) {
ElMessage.success("已保存并发布");
formId.value = res.result?.id || formId.value;
captureBaseline();
router.push({ name: "appointment-system-forms" });
}
})
.finally(() => {
saving.value = false;
});
}
</script>
<style scoped lang="scss">

View File

@@ -54,84 +54,89 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { getRequest, postRequestWithNoForm, deleteRequest } from "@/libs/axios";
import { ElMessage, ElMessageBox } from "element-plus";
import { countFormFields } from "@/constants/systemForm";
export default {
name: "appointmentSystemFormList",
data() {
return {
loading: false,
data: [],
total: 0,
searchForm: {
formName: "",
status: "",
pageNumber: 1,
pageSize: 10,
},
};
},
mounted() {
this.loadData();
},
methods: {
fieldCount: countFormFields,
handleSearch() {
this.searchForm.pageNumber = 1;
this.loadData();
},
handleReset() {
this.searchForm.formName = "";
this.searchForm.status = "";
this.searchForm.pageNumber = 1;
this.loadData();
},
loadData() {
this.loading = true;
getRequest("/appointment/system-forms", this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result?.records || [];
this.total = res.result?.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
goDesigner(id) {
this.$router.push({
name: "appointment-system-form-designer",
query: id ? { id } : {},
});
},
toggleStatus(row, enabled) {
const status = enabled ? "ENABLED" : "DISABLED";
postRequestWithNoForm("/appointment/system-forms", {
id: row.id,
formName: row.formName,
formSchema: row.formSchema || "[]",
status,
}).then((res) => {
if (res.success) {
row.status = status;
ElMessage.success(enabled ? "已启用" : "已停用");
}
});
},
handleDelete(row) {
ElMessageBox.confirm("确认删除该表单?", "提示", { type: "warning" }).then(() => {
deleteRequest(`/appointment/system-forms/${row.id}`).then((res) => {
if (res.success) {
ElMessage.success("已删除");
this.loadData();
}
});
});
},
},
};
defineOptions({ name: "appointmentSystemFormList" });
const router = useRouter();
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const searchForm = reactive({
formName: "",
status: "",
pageNumber: 1,
pageSize: 10,
});
const fieldCount = countFormFields;
onMounted(() => {
loadData();
});
function handleSearch() {
searchForm.pageNumber = 1;
loadData();
}
function handleReset() {
searchForm.formName = "";
searchForm.status = "";
searchForm.pageNumber = 1;
loadData();
}
function loadData() {
loading.value = true;
getRequest("/appointment/system-forms", searchForm)
.then((res) => {
if (res.success) {
data.value = res.result?.records || [];
total.value = res.result?.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
function goDesigner(id) {
router.push({
name: "appointment-system-form-designer",
query: id ? { id } : {},
});
}
function toggleStatus(row, enabled) {
const status = enabled ? "ENABLED" : "DISABLED";
postRequestWithNoForm("/appointment/system-forms", {
id: row.id,
formName: row.formName,
formSchema: row.formSchema || "[]",
status,
}).then((res) => {
if (res.success) {
row.status = status;
ElMessage.success(enabled ? "已启用" : "已停用");
}
});
}
function handleDelete(row) {
ElMessageBox.confirm("确认删除该表单?", "提示", { type: "warning" }).then(() => {
deleteRequest(`/appointment/system-forms/${row.id}`).then((res) => {
if (res.success) {
ElMessage.success("已删除");
loadData();
}
});
});
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="80px" @keyup.enter="handleSearch">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="80px" @keyup.enter="handleSearch">
<el-form-item label="工单编号" prop="workOrderSn">
<el-input v-model="searchForm.workOrderSn" clearable style="width: 200px" />
</el-form-item>
@@ -75,7 +75,9 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import {
getWorkOrderPage,
getWorkOrderStatusCounts,
@@ -83,102 +85,103 @@ import {
endWorkOrderService,
} from "@/api/appointment";
import { SERVICE_MODE_LABEL, WORK_ORDER_STATUS_LABEL } from "@/constants/appointment";
import { Message } from "@/utils/message";
export default {
name: "appointmentWorkOrderList",
data() {
return {
loading: false,
data: [],
total: 0,
statusTab: "ALL",
statusCounts: {},
searchForm: {
pageNumber: 1,
pageSize: 20,
serviceStatus: "",
workOrderSn: "",
orderSn: "",
serviceMode: "",
},
statusTabs: [
{ value: "ALL", label: "全部" },
{ value: "PENDING_CLAIM", label: "待领取" },
{ value: "WAIT_VERIFY", label: "待核销" },
{ value: "IN_SERVICE", label: "服务中" },
{ value: "SERVICE_ENDED", label: "服务结束" },
{ value: "REFUNDED", label: "已退款" },
],
};
},
mounted() {
if (this.$route.query.tab) {
this.statusTab = this.$route.query.tab;
this.searchForm.serviceStatus = this.$route.query.tab === "ALL" ? "" : this.$route.query.tab;
defineOptions({ name: "appointmentWorkOrderList" });
const route = useRoute();
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const statusTab = ref("ALL");
const statusCounts = ref({});
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
serviceStatus: "",
workOrderSn: "",
orderSn: "",
serviceMode: "",
});
const statusTabs = ref([
{ value: "ALL", label: "全部" },
{ value: "PENDING_CLAIM", label: "待领取" },
{ value: "WAIT_VERIFY", label: "待核销" },
{ value: "IN_SERVICE", label: "服务中" },
{ value: "SERVICE_ENDED", label: "服务结束" },
{ value: "REFUNDED", label: "已退款" },
]);
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
onMounted(() => {
if (route.query.tab) {
statusTab.value = route.query.tab;
searchForm.serviceStatus = route.query.tab === "ALL" ? "" : route.query.tab;
}
loadCounts();
loadData();
});
function serviceModeText(v) {
return SERVICE_MODE_LABEL[v] || v || "—";
}
function statusText(v) {
return WORK_ORDER_STATUS_LABEL[v] || v || "—";
}
function tabLabel(item) {
const count = statusCounts.value[item.value];
return count != null ? `${item.label}(${count})` : item.label;
}
function onTabClick(tab) {
searchForm.serviceStatus = tab.paneName === "ALL" ? "" : tab.paneName;
searchForm.pageNumber = 1;
loadData();
}
function handleSearch() {
searchForm.pageNumber = 1;
loadData();
}
function loadCounts() {
getWorkOrderStatusCounts()
.then((res) => {
if (res.success && res.result) {
statusCounts.value = res.result;
}
})
.catch(() => {});
}
function loadData() {
loading.value = true;
getWorkOrderPage(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result?.records || [];
total.value = res.result?.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
function handleClaim(row) {
claimWorkOrder(row.id).then((res) => {
if (res.success) {
Message.success("领取成功");
loadCounts();
loadData();
}
this.loadCounts();
this.loadData();
},
methods: {
serviceModeText(v) {
return SERVICE_MODE_LABEL[v] || v || "—";
},
statusText(v) {
return WORK_ORDER_STATUS_LABEL[v] || v || "—";
},
tabLabel(item) {
const count = this.statusCounts[item.value];
return count != null ? `${item.label}(${count})` : item.label;
},
onTabClick(tab) {
this.searchForm.serviceStatus = tab.paneName === "ALL" ? "" : tab.paneName;
this.searchForm.pageNumber = 1;
this.loadData();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.loadData();
},
loadCounts() {
getWorkOrderStatusCounts()
.then((res) => {
if (res.success && res.result) {
this.statusCounts = res.result;
}
})
.catch(() => {});
},
loadData() {
this.loading = true;
getWorkOrderPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result?.records || [];
this.total = res.result?.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
handleClaim(row) {
claimWorkOrder(row.id).then((res) => {
if (res.success) {
this.$message.success("领取成功");
this.loadCounts();
this.loadData();
}
});
},
handleEnd(row) {
endWorkOrderService(row.id).then((res) => {
if (res.success) {
this.$message.success("已结束服务");
this.loadCounts();
this.loadData();
}
});
},
},
};
});
}
function handleEnd(row) {
endWorkOrderService(row.id).then((res) => {
if (res.success) {
Message.success("已结束服务");
loadCounts();
loadData();
}
});
}
</script>

View File

@@ -3,7 +3,7 @@
<el-card class="change-pass">
<template #header>修改密码</template>
<el-form
ref="editPasswordForm"
ref="editPasswordFormRef"
:model="editPasswordForm"
label-width="100px"
label-position="right"
@@ -41,84 +41,83 @@
</div>
</template>
<script>
<script setup>
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import SetPassword from "@/components/lili/set-password";
import { changePass } from "@/api/index";
import { ElMessageBox } from "element-plus";
import { md5 } from "@/utils/md5.js";
import { useAppStore } from "@/stores/app";
import { useUserStore } from "@/stores/user";
export default {
name: "change_pass",
components: {
SetPassword,
},
data() {
const valideRePassword = (rule, value, callback) => {
if (value !== this.editPasswordForm.newPassword) {
callback(new Error("两次输入密码不一致"));
} else {
callback();
}
};
return {
savePassLoading: false,
editPasswordForm: {
oldPass: "",
newPassword: "",
rePass: "",
},
strength: "",
passwordValidate: {
oldPass: [{ required: true, message: "请输入原密码", trigger: "blur" }],
newPassword: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, message: "请至少输入6个字符", trigger: "blur" },
{ max: 32, message: "最多输入32个字符", trigger: "blur" },
],
rePass: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{ validator: valideRePassword, trigger: "blur" },
],
},
};
},
methods: {
changeInputPass(v, grade, strength) {
this.strength = strength;
},
editPassword() {
const params = {
password: this.md5(this.editPasswordForm.oldPass),
newPassword: this.md5(this.editPasswordForm.newPassword),
};
this.$refs.editPasswordForm.validate((valid) => {
if (!valid) return;
this.savePassLoading = true;
changePass(params)
.then((res) => {
if (res?.success) {
ElMessageBox.alert("修改密码成功,需重新登录", "修改密码成功", {
confirmButtonText: "确定",
}).then(() => {
this.$store.commit("logout", this);
this.$store.commit("clearOpenedSubmenu");
this.$router.push({ name: "login" });
});
}
})
.finally(() => {
this.savePassLoading = false;
});
});
},
cancelEditPass() {
this.$store.commit("removeTag", "change_pass");
localStorage.storeOpenedList = JSON.stringify(
this.$store.state.app.storeOpenedList
);
const list = this.$store.state.app.storeOpenedList;
const lastPageName = list.length > 1 ? list[list.length - 1].name : list[0].name;
this.$router.push({ name: lastPageName });
},
},
defineOptions({ name: "change_pass" });
const router = useRouter();
const appStore = useAppStore();
const userStore = useUserStore();
const editPasswordFormRef = ref(null);
const savePassLoading = ref(false);
const editPasswordForm = reactive({
oldPass: "",
newPassword: "",
rePass: "",
});
const strength = ref("");
const valideRePassword = (rule, value, callback) => {
if (value !== editPasswordForm.newPassword) {
callback(new Error("两次输入密码不一致"));
} else {
callback();
}
};
const passwordValidate = {
oldPass: [{ required: true, message: "请输入原密码", trigger: "blur" }],
newPassword: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, message: "请至少输入6个字符", trigger: "blur" },
{ max: 32, message: "最多输入32个字符", trigger: "blur" },
],
rePass: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{ validator: valideRePassword, trigger: "blur" },
],
};
function changeInputPass(v, grade, strengthVal) {
strength.value = strengthVal;
}
function editPassword() {
const params = {
password: md5(editPasswordForm.oldPass),
newPassword: md5(editPasswordForm.newPassword),
};
editPasswordFormRef.value.validate((valid) => {
if (!valid) return;
savePassLoading.value = true;
changePass(params)
.then((res) => {
if (res?.success) {
ElMessageBox.alert("修改密码成功,需重新登录", "修改密码成功", {
confirmButtonText: "确定",
}).then(() => {
userStore.logout();
appStore.clearOpenedSubmenu();
router.push({ name: "login" });
});
}
})
.finally(() => {
savePassLoading.value = false;
});
});
}
function cancelEditPass() {
appStore.removeTag("change_pass");
localStorage.storeOpenedList = JSON.stringify(appStore.storeOpenedList);
const list = appStore.storeOpenedList;
const lastPageName = list.length > 1 ? list[list.length - 1].name : list[0].name;
router.push({ name: lastPageName });
}
</script>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -132,7 +132,7 @@
</div>
</el-card>
<liliDialog ref="liliDialog" @selectedGoodsData="selectedGoodsData" />
<liliDialog ref="liliDialogRef" @selectedGoodsData="selectedGoodsData" />
<el-dialog
v-model="modalVisible"
@@ -222,7 +222,8 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import {
getDistributionGoods,
distributionGoodsCancel,
@@ -230,242 +231,259 @@ import {
updateDistributionGoodsCommission,
} from "@/api/distribution";
import { getShopGoodsLabelListSeller } from "@/api/goods";
import liliDialog from "@/views/lili-dialog";
import { Message, Modal } from "@/utils/message";
export default {
name: "distributionGoods",
components: { liliDialog },
data() {
return {
modalVisible: false,
modalTitle: "添加分销商品",
editVisible: false,
editLoading: false,
submitLoading: false,
loading: true,
currentAuthStatus: "ALL",
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
goodsName: "",
storeCategoryPath: "",
goodsType: "",
authStatus: "",
},
shopCategoryList: [],
shopCategoryProps: {
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: false,
},
selectedGoodsList: [],
editForm: {
id: "",
goodsName: "",
commission: "",
commissionError: false,
},
commissionPattern: /^[1-9]\d{0,3}(\.\d{1,2})?$/,
data: [],
total: 0,
};
},
methods: {
init() {
this.loadFilterOptions();
this.getDataList();
},
loadFilterOptions() {
getShopGoodsLabelListSeller()
.then((res) => {
if (res?.success) {
this.shopCategoryList = this.formatShopCategory(res.result || []);
}
})
.catch(() => {});
},
formatShopCategory(list) {
return (list || []).map((item) => ({
value: item.id,
label: item.labelName,
children:
item.children && item.children.length
? this.formatShopCategory(item.children)
: undefined,
}));
},
goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
},
authStatusText(value) {
if (value === "TOBEAUDITED") return "待审核";
if (value === "REFUSE") return "审核未通过";
return "已审核";
},
onAuthStatusChange(name) {
this.searchForm.authStatus = name === "ALL" ? "" : name;
this.searchForm.pageNumber = 1;
this.getDataList();
},
add() {
this.$refs.liliDialog.goodsData = [];
this.$refs.liliDialog.open("goods");
},
selectedGoodsData(selected) {
if (!selected?.length) return;
this.selectedGoodsList = selected.map((item) => ({
skuId: item.id,
goodsId: item.goodsId,
thumbnail: item.thumbnail,
goodsName: item.goodsName,
specs: item.simpleSpecs || "-",
price: item.price,
commission: "1",
commissionError: false,
}));
this.modalVisible = true;
this.modalTitle = `添加分销商品(已选${this.selectedGoodsList.length}项)`;
},
validateRowCommission(row) {
const valid = this.commissionPattern.test(String(row.commission || "").trim());
row.commissionError = !valid;
return valid;
},
validateAllCommission() {
let valid = true;
this.selectedGoodsList.forEach((row) => {
if (!this.validateRowCommission(row)) {
valid = false;
defineOptions({ name: "distributionGoods" });
const modalVisible = ref(false);
const modalTitle = ref("添加分销商品");
const editVisible = ref(false);
const editLoading = ref(false);
const submitLoading = ref(false);
const loading = ref(true);
const currentAuthStatus = ref("ALL");
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
goodsName: "",
storeCategoryPath: "",
goodsType: "",
authStatus: "",
});
const shopCategoryList = ref([]);
const shopCategoryProps = {
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: false,
};
const selectedGoodsList = ref([]);
const editForm = ref({
id: "",
goodsName: "",
commission: "",
commissionError: false,
});
const commissionPattern = /^[1-9]\d{0,3}(\.\d{1,2})?$/;
const data = ref([]);
const total = ref(0);
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
const liliDialogRef = ref(null);
const editFormRef = ref(null);
onMounted(() => {
init();
});
function init() {
loadFilterOptions();
getDataList();
}
function loadFilterOptions() {
getShopGoodsLabelListSeller()
.then((res) => {
if (res?.success) {
shopCategoryList.value = formatShopCategory(res.result || []);
}
})
.catch(() => {});
}
function formatShopCategory(list) {
return (list || []).map((item) => ({
value: item.id,
label: item.labelName,
children:
item.children && item.children.length
? formatShopCategory(item.children)
: undefined,
}));
}
function goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
}
function authStatusText(value) {
if (value === "TOBEAUDITED") return "待审核";
if (value === "REFUSE") return "审核未通过";
return "已审核";
}
function onAuthStatusChange(name) {
searchForm.authStatus = name === "ALL" ? "" : name;
searchForm.pageNumber = 1;
getDataList();
}
function add() {
liliDialogRef.value.goodsData = [];
liliDialogRef.value.open("goods");
}
function selectedGoodsData(selected) {
if (!selected?.length) return;
selectedGoodsList.value = selected.map((item) => ({
skuId: item.id,
goodsId: item.goodsId,
thumbnail: item.thumbnail,
goodsName: item.goodsName,
specs: item.simpleSpecs || "-",
price: item.price,
commission: "1",
commissionError: false,
}));
modalVisible.value = true;
modalTitle.value = `添加分销商品(已选${selectedGoodsList.value.length}项)`;
}
function validateRowCommission(row) {
const valid = commissionPattern.test(String(row.commission || "").trim());
row.commissionError = !valid;
return valid;
}
function validateAllCommission() {
let valid = true;
selectedGoodsList.value.forEach((row) => {
if (!validateRowCommission(row)) {
valid = false;
}
});
return valid;
}
function removeSelectedGoods(index) {
selectedGoodsList.value.splice(index, 1);
if (!selectedGoodsList.value.length) {
modalVisible.value = false;
return;
}
modalTitle.value = `添加分销商品(已选${selectedGoodsList.value.length}项)`;
}
function handleSubmit() {
if (!selectedGoodsList.value.length) return;
if (!validateAllCommission()) {
Message.error("请为所有商品填写大于1且小于9999的合法佣金金额");
return;
}
submitLoading.value = true;
const items = selectedGoodsList.value.map((item) => ({
skuId: item.skuId,
commission: Number(item.commission),
}));
distributionGoodsBatchCheck(items)
.then((res) => {
if (res?.success || res?.message === "success") {
const addedCount = Array.isArray(res?.result) ? res.result.length : 0;
if (addedCount > 0) {
Message.success(`成功添加${addedCount}个分销商品`);
} else {
Message.info("所选商品均已是分销商品,已跳过");
}
}
modalVisible.value = false;
selectedGoodsList.value = [];
getDataList();
})
.finally(() => {
submitLoading.value = false;
});
}
function changePage(v) {
searchForm.pageNumber = v;
getDataList();
}
function changePageSize(v) {
searchForm.pageSize = v;
searchForm.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
getDistributionGoods(searchForm)
.then((res) => {
if (res?.success) {
const page = res.result || {};
data.value = page.records || [];
total.value = page.total || 0;
} else {
data.value = [];
total.value = 0;
}
})
.finally(() => {
loading.value = false;
});
}
function remove(row) {
Modal.confirm({
title: "确认删除",
content: "您确认要删除此分销商品吗?",
loading: true,
onOk: () => {
distributionGoodsCancel(row.id).then((res) => {
Modal.remove();
if (res?.success) {
Message.success("删除成功");
getDataList();
}
});
return valid;
},
removeSelectedGoods(index) {
this.selectedGoodsList.splice(index, 1);
if (!this.selectedGoodsList.length) {
this.modalVisible = false;
return;
});
}
function edit(row) {
editForm.value = {
id: row.id,
goodsName: row.goodsName,
commission: row.commission != null ? String(row.commission) : "1",
commissionError: false,
};
editVisible.value = true;
}
function handleEditSubmit() {
const valid = commissionPattern.test(String(editForm.value.commission || "").trim());
editForm.value.commissionError = !valid;
if (!valid) {
Message.error("请输入大于1且小于9999的合法佣金金额");
return;
}
editLoading.value = true;
updateDistributionGoodsCommission(editForm.value.id, {
commission: Number(editForm.value.commission),
})
.then((res) => {
if (res?.success || res?.message === "success") {
Message.success("修改成功");
editVisible.value = false;
getDataList();
}
this.modalTitle = `添加分销商品(已选${this.selectedGoodsList.length}项)`;
},
handleSubmit() {
if (!this.selectedGoodsList.length) return;
if (!this.validateAllCommission()) {
this.$Message.error("请为所有商品填写大于1且小于9999的合法佣金金额");
return;
}
this.submitLoading = true;
const items = this.selectedGoodsList.map((item) => ({
skuId: item.skuId,
commission: Number(item.commission),
}));
distributionGoodsBatchCheck(items)
.then((res) => {
if (res?.success || res?.message === "success") {
const addedCount = Array.isArray(res?.result) ? res.result.length : 0;
if (addedCount > 0) {
this.$Message.success(`成功添加${addedCount}个分销商品`);
} else {
this.$Message.info("所选商品均已是分销商品,已跳过");
}
}
this.modalVisible = false;
this.selectedGoodsList = [];
this.getDataList();
})
.finally(() => {
this.submitLoading = false;
});
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
},
changePageSize(v) {
this.searchForm.pageSize = v;
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getDistributionGoods(this.searchForm)
.then((res) => {
if (res?.success) {
const page = res.result || {};
this.data = page.records || [];
this.total = page.total || 0;
} else {
this.data = [];
this.total = 0;
}
})
.finally(() => {
this.loading = false;
});
},
remove(row) {
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除此分销商品吗?",
loading: true,
onOk: () => {
distributionGoodsCancel(row.id).then((res) => {
this.$Modal.remove();
if (res?.success) {
this.$Message.success("删除成功");
this.getDataList();
}
});
},
});
},
edit(row) {
this.editForm = {
id: row.id,
goodsName: row.goodsName,
commission: row.commission != null ? String(row.commission) : "1",
commissionError: false,
};
this.editVisible = true;
},
handleEditSubmit() {
const valid = this.commissionPattern.test(String(this.editForm.commission || "").trim());
this.editForm.commissionError = !valid;
if (!valid) {
this.$Message.error("请输入大于1且小于9999的合法佣金金额");
return;
}
this.editLoading = true;
updateDistributionGoodsCommission(this.editForm.id, {
commission: Number(this.editForm.commission),
})
.then((res) => {
if (res?.success || res?.message === "success") {
this.$Message.success("修改成功");
this.editVisible = false;
this.getDataList();
}
})
.finally(() => {
this.editLoading = false;
});
},
},
mounted() {
this.init();
},
};
})
.finally(() => {
editLoading.value = false;
});
}
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<div>
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -122,110 +122,119 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { getDistributionOrder } from "@/api/distribution";
import { orderStatusList } from "./dataJson";
import vueQr from "vue-qr";
import * as filters from "@/utils/filters";
export default {
name: "distributionOrder",
components: { vueQr },
data() {
return {
timeRange: [],
orderStatusList,
distributionId: this.$route.query.id,
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
},
data: [],
total: 0,
};
},
watch: {
$route(e) {
this.distributionId = e.query.id ? e.query.id : undefined;
this.getDataList();
},
},
methods: {
init() {
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
getDataList() {
this.searchForm.distributionId = this.distributionId;
this.loading = true;
if (this.timeRange && this.timeRange[0] && this.timeRange[1]) {
const startTime = new Date(this.timeRange[0]).getTime();
const endTime = new Date(this.timeRange[1]).getTime();
this.searchForm.startTime = this.$filters.unixToDate(startTime / 1000);
this.searchForm.endTime = this.$filters.unixToDate(endTime / 1000);
defineOptions({ name: "distributionOrder" });
const route = useRoute();
const timeRange = ref([]);
const distributionId = ref(route.query.id);
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
});
const data = ref([]);
const total = ref(0);
// searchFormRef:模板 ref 改名避免与响应式变量 searchForm 互相覆盖
const searchFormRef = ref(null);
const table = ref(null);
watch(
() => route.query,
(e) => {
distributionId.value = e.id ? e.id : undefined;
getDataList();
}
);
onMounted(() => {
init();
});
function init() {
getDataList();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
}
function getDataList() {
searchForm.distributionId = distributionId.value;
loading.value = true;
if (timeRange.value && timeRange.value[0] && timeRange.value[1]) {
const startTime = new Date(timeRange.value[0]).getTime();
const endTime = new Date(timeRange.value[1]).getTime();
searchForm.startTime = filters.unixToDate(startTime / 1000);
searchForm.endTime = filters.unixToDate(endTime / 1000);
} else {
searchForm.startTime = null;
searchForm.endTime = null;
}
getDistributionOrder(searchForm)
.then((res) => {
if (res?.success) {
const page = res.result || {};
data.value = page.records || [];
total.value = page.total || 0;
} else {
this.searchForm.startTime = null;
this.searchForm.endTime = null;
data.value = [];
total.value = 0;
}
getDistributionOrder(this.searchForm)
.then((res) => {
if (res?.success) {
const page = res.result || {};
this.data = page.records || [];
this.total = page.total || 0;
} else {
this.data = [];
this.total = 0;
}
})
.finally(() => {
this.loading = false;
});
},
filterStatus(status) {
const arr = [
{ status: "NO_COMPLETED", title: "未完成" },
{ status: "COMPLETE", title: "完成" },
{ status: "REFUND", title: "退款" },
];
for (let i = 0; i < arr.length; i++) {
if (arr[i].status === status) {
return arr[i].title;
}
}
return "未完成";
},
filterStatusTagType(status) {
const arr = [
{ status: "NO_COMPLETED", type: "warning" },
{ status: "COMPLETE", type: "success" },
{ status: "REFUND", type: "danger" },
];
for (let i = 0; i < arr.length; i++) {
if (arr[i].status === status) {
return arr[i].type;
}
}
return "warning";
},
},
mounted() {
this.init();
},
};
})
.finally(() => {
loading.value = false;
});
}
function filterStatus(status) {
const arr = [
{ status: "NO_COMPLETED", title: "未完成" },
{ status: "COMPLETE", title: "完成" },
{ status: "REFUND", title: "退款" },
];
for (let i = 0; i < arr.length; i++) {
if (arr[i].status === status) {
return arr[i].title;
}
}
return "未完成";
}
function filterStatusTagType(status) {
const arr = [
{ status: "NO_COMPLETED", type: "warning" },
{ status: "COMPLETE", type: "success" },
{ status: "REFUND", type: "danger" },
];
for (let i = 0; i < arr.length; i++) {
if (arr[i].status === status) {
return arr[i].type;
}
}
return "warning";
}
</script>
<style lang="scss">

View File

@@ -17,20 +17,21 @@
</div>
</template>
<script>
export default {
name: "Error403",
methods: {
backPage() {
this.$router.go(-1);
},
goHome() {
this.$router.push({
name: "home_index",
});
},
},
};
<script setup>
import { useRouter } from "vue-router";
defineOptions({ name: "Error403" });
const router = useRouter();
function backPage() {
router.go(-1);
}
function goHome() {
router.push({
name: "home_index",
});
}
</script>
<style lang="scss" scoped>

View File

@@ -17,20 +17,22 @@
</div>
</template>
<script>
export default {
name: "Error404",
methods: {
backPage() {
this.$router.go(-1);
},
goHome() {
this.$router.push({
name: "home_index",
});
},
},
};
<script setup>
import { useRouter } from "vue-router";
defineOptions({ name: "Error404" });
const router = useRouter();
function backPage() {
router.go(-1);
}
function goHome() {
router.push({
name: "home_index",
});
}
</script>
<style lang="scss" scoped>

View File

@@ -17,20 +17,22 @@
</div>
</template>
<script>
export default {
name: "Error500",
methods: {
backPage() {
this.$router.go(-1);
},
goHome() {
this.$router.push({
name: "home_index",
});
},
},
};
<script setup>
import { useRouter } from "vue-router";
defineOptions({ name: "Error500" });
const router = useRouter();
function backPage() {
router.go(-1);
}
function goHome() {
router.push({
name: "home_index",
});
}
</script>
<style lang="scss" scoped>

View File

@@ -220,7 +220,9 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
importCardKey,
addCardKey,
@@ -235,6 +237,7 @@ import {
formatCardKeyStatus,
CARD_KEY_STATUS_TAG,
} from "@/constants/cardKey";
import { Message, Modal } from "@/utils/message";
/**
* 商家卡池管理页:导入/新增/作废/导出,列表展示明文卡密(仅商家授权上下文)。
@@ -243,332 +246,355 @@ import {
* @author Mike
* @date 2026-07-31
*/
export default {
name: "cardKeyPool",
data() {
return {
skuId: "",
goodsId: "",
goodsName: "",
skuOptions: [],
skuLoading: false,
statusTab: "ALL",
stats: null,
poolLocked: false,
loading: false,
exportLoading: false,
templateLoading: false,
data: [],
total: 0,
importRange: [],
searchForm: {
cardNo: "",
pageNumber: 1,
pageSize: 20,
},
importModal: false,
importResultVisible: false,
importResult: {},
addVisible: false,
addLoading: false,
addForm: {
cardNo: "",
cardSecret: "",
},
addRules: {
cardNo: [{ required: true, message: "卡号不能为空", trigger: "blur" }],
cardSecret: [{ required: true, message: "卡密不能为空", trigger: "blur" }],
},
};
},
computed: {
statusTabsWithCount() {
const s = this.stats || {};
const withCount = (label, count) =>
count != null && this.stats ? `${label}(${count})` : label;
const total =
(s.unusedCount || 0) +
(s.reservedCount || 0) +
(s.allocatedCount || 0) +
(s.voidedCount || 0);
return [
{ title: withCount("全部", total), value: "ALL" },
{ title: withCount("未使用", s.unusedCount || 0), value: "UNUSED" },
{ title: withCount("已预占", s.reservedCount || 0), value: "RESERVED" },
{ title: withCount("已分配", s.allocatedCount || 0), value: "ALLOCATED" },
{ title: withCount("已作废", s.voidedCount || 0), value: "VOIDED" },
];
},
},
watch: {
"$route.query": {
handler() {
const prevSkuId = this.skuId;
this.initFromRoute();
if (this.skuId && this.skuId !== prevSkuId) {
this.resetPoolViewState();
this.loadStats();
this.getList();
}
},
deep: true,
},
},
methods: {
formatStatus: formatCardKeyStatus,
statusTag(status) {
return CARD_KEY_STATUS_TAG[status] || "info";
},
goBack() {
this.$router.back();
},
initFromRoute() {
const q = this.$route.query;
this.skuId = q.skuId || "";
this.goodsId = q.goodsId || "";
this.goodsName = q.goodsName || "";
},
formatSkuOptionLabel(sku) {
if (!sku) return "";
const spec = sku.simpleSpecs || sku.sn || sku.id;
const qty =
sku.poolStock != null && sku.poolStock !== ""
? sku.poolStock
: sku.quantity != null
? sku.quantity
: "—";
return `${spec}(库存 ${qty})`;
},
resetPoolViewState() {
this.statusTab = "ALL";
this.searchForm.pageNumber = 1;
this.searchForm.cardNo = "";
this.importRange = [];
this.poolLocked = false;
this.stats = null;
this.data = [];
this.total = 0;
},
/** 卡池变更后刷新 SKU 选择器中的可售库存(后端 syncSkuStock → quantity) */
refreshSkuOptionStock() {
if (!this.goodsId) return Promise.resolve();
return this.loadSkuOptions();
},
async loadSkuOptions() {
if (!this.goodsId) {
this.skuOptions = [];
return;
}
this.skuLoading = true;
try {
const res = await getQueryGoodsIdGoodsList(this.goodsId);
if (res.success) {
this.skuOptions = (res.result || []).filter((s) => !s.deleteFlag);
}
} catch {
this.skuOptions = [];
} finally {
this.skuLoading = false;
}
},
onSkuChange(newSkuId) {
if (!newSkuId) return;
this.skuId = newSkuId;
this.resetPoolViewState();
this.$router.replace({
path: this.$route.path,
query: {
...this.$route.query,
skuId: newSkuId,
goodsId: this.goodsId,
goodsName: this.goodsName,
},
});
this.loadStats();
this.getList();
},
onStatusTabClick(tab) {
this.statusTab = tab.paneName;
this.searchForm.pageNumber = 1;
this.getList();
},
buildListParams() {
const params = {
skuId: this.skuId,
pageNumber: this.searchForm.pageNumber,
pageSize: this.searchForm.pageSize,
};
if (this.goodsId) params.goodsId = this.goodsId;
if (this.searchForm.cardNo) params.cardNo = this.searchForm.cardNo;
if (this.statusTab && this.statusTab !== "ALL") {
params.status = this.statusTab;
}
if (this.importRange && this.importRange.length === 2) {
params.createTimeStart = this.importRange[0];
params.createTimeEnd = this.importRange[1];
}
return params;
},
loadStats() {
if (!this.skuId) return;
getCardKeyStats(this.skuId).then((res) => {
if (res.success) {
this.stats = res.result;
}
});
},
getList() {
if (!this.skuId) return;
this.loading = true;
getCardKeyList(this.buildListParams())
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
this.poolLocked = false;
} else {
this.checkPoolLocked(res);
}
})
.catch(() => {
this.loading = false;
});
},
/** 审核拒绝 / SKU 删除 / 关店时禁用卡池操作(S-06 / S-07 / EC-21~23) */
checkPoolLocked(res) {
const lockedCodes = [
"CARD_KEY_GOODS_AUTH_REFUSE",
"CARD_KEY_SKU_DELETED",
"CARD_KEY_STORE_CLOSED",
];
if (res.code && lockedCodes.includes(String(res.code))) {
this.poolLocked = true;
}
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getList();
},
handleReset() {
this.searchForm.cardNo = "";
this.importRange = [];
this.searchForm.pageNumber = 1;
this.getList();
},
onPageSizeChange() {
this.searchForm.pageNumber = 1;
this.getList();
},
handleImportUpload(file) {
if (!/\.xlsx$/i.test(file.name)) {
this.$Message.error("请上传 .xlsx 文件");
return false;
}
importCardKey(this.skuId, file).then((res) => {
if (res.success) {
this.importModal = false;
this.importResult = res.result || {};
this.importResultVisible = true;
this.loadStats();
this.getList();
this.refreshSkuOptionStock();
} else {
this.checkPoolLocked(res);
}
});
return false;
},
handleDownloadTemplate() {
this.templateLoading = true;
downloadImportTemplate()
.then(() => {
this.$Message.success("模板下载成功");
})
.catch(() => {})
.finally(() => {
this.templateLoading = false;
});
},
openAddDialog() {
this.addForm = { cardNo: "", cardSecret: "" };
this.addVisible = true;
},
submitAdd() {
this.$refs.addFormRef.validate((valid) => {
if (!valid) return;
this.addLoading = true;
addCardKey({
skuId: this.skuId,
cardNo: this.addForm.cardNo.trim(),
cardSecret: this.addForm.cardSecret.trim(),
})
.then((res) => {
this.addLoading = false;
if (res.success) {
this.$Message.success("新增成功");
this.addVisible = false;
this.loadStats();
this.getList();
this.refreshSkuOptionStock();
} else {
this.checkPoolLocked(res);
}
})
.catch(() => {
this.addLoading = false;
});
});
},
handleVoid(row) {
this.$Modal.confirm({
title: "确认作废",
content: `确定作废卡号「${row.cardNo}」?作废后不可恢复。`,
onOk: () => {
voidCardKey(row.id).then((res) => {
if (res.success) {
this.$Message.success("作废成功");
this.loadStats();
this.getList();
this.refreshSkuOptionStock();
}
});
},
});
},
handleExport() {
this.exportLoading = true;
const params = { ...this.buildListParams() };
delete params.pageNumber;
delete params.pageSize;
exportCardKey(params, this.skuId)
.then(() => {
this.$Message.success("导出成功");
})
.catch(() => {})
.finally(() => {
this.exportLoading = false;
});
},
async init() {
this.initFromRoute();
if (this.goodsId) {
await this.loadSkuOptions();
}
if (!this.skuId && this.skuOptions.length) {
this.onSkuChange(this.skuOptions[0].id);
return;
}
if (this.skuId) {
this.loadStats();
this.getList();
}
},
},
mounted() {
this.init();
},
defineOptions({ name: "cardKeyPool" });
const route = useRoute();
const router = useRouter();
const skuId = ref("");
const goodsId = ref("");
const goodsName = ref("");
const skuOptions = ref([]);
const skuLoading = ref(false);
const statusTab = ref("ALL");
const stats = ref(null);
const poolLocked = ref(false);
const loading = ref(false);
const exportLoading = ref(false);
const templateLoading = ref(false);
const data = ref([]);
const total = ref(0);
const importRange = ref([]);
const searchForm = reactive({
cardNo: "",
pageNumber: 1,
pageSize: 20,
});
const importModal = ref(false);
const importResultVisible = ref(false);
const importResult = ref({});
const addVisible = ref(false);
const addLoading = ref(false);
const addForm = ref({
cardNo: "",
cardSecret: "",
});
const addRules = {
cardNo: [{ required: true, message: "卡号不能为空", trigger: "blur" }],
cardSecret: [{ required: true, message: "卡密不能为空", trigger: "blur" }],
};
const searchFormRef = ref(null);
const addFormRef = ref(null);
const statusTabsWithCount = computed(() => {
const s = stats.value || {};
const withCount = (label, count) =>
count != null && stats.value ? `${label}(${count})` : label;
const totalCount =
(s.unusedCount || 0) +
(s.reservedCount || 0) +
(s.allocatedCount || 0) +
(s.voidedCount || 0);
return [
{ title: withCount("全部", totalCount), value: "ALL" },
{ title: withCount("未使用", s.unusedCount || 0), value: "UNUSED" },
{ title: withCount("已预占", s.reservedCount || 0), value: "RESERVED" },
{ title: withCount("已分配", s.allocatedCount || 0), value: "ALLOCATED" },
{ title: withCount("已作废", s.voidedCount || 0), value: "VOIDED" },
];
});
watch(
() => route.query,
() => {
const prevSkuId = skuId.value;
initFromRoute();
if (skuId.value && skuId.value !== prevSkuId) {
resetPoolViewState();
loadStats();
getList();
}
},
{ deep: true }
);
const formatStatus = formatCardKeyStatus;
function statusTag(status) {
return CARD_KEY_STATUS_TAG[status] || "info";
}
function goBack() {
router.back();
}
function initFromRoute() {
const q = route.query;
skuId.value = q.skuId || "";
goodsId.value = q.goodsId || "";
goodsName.value = q.goodsName || "";
}
function formatSkuOptionLabel(sku) {
if (!sku) return "";
const spec = sku.simpleSpecs || sku.sn || sku.id;
const qty =
sku.poolStock != null && sku.poolStock !== ""
? sku.poolStock
: sku.quantity != null
? sku.quantity
: "—";
return `${spec}(库存 ${qty})`;
}
function resetPoolViewState() {
statusTab.value = "ALL";
searchForm.pageNumber = 1;
searchForm.cardNo = "";
importRange.value = [];
poolLocked.value = false;
stats.value = null;
data.value = [];
total.value = 0;
}
/** 卡池变更后刷新 SKU 选择器中的可售库存(后端 syncSkuStock → quantity) */
function refreshSkuOptionStock() {
if (!goodsId.value) return Promise.resolve();
return loadSkuOptions();
}
async function loadSkuOptions() {
if (!goodsId.value) {
skuOptions.value = [];
return;
}
skuLoading.value = true;
try {
const res = await getQueryGoodsIdGoodsList(goodsId.value);
if (res.success) {
skuOptions.value = (res.result || []).filter((s) => !s.deleteFlag);
}
} catch {
skuOptions.value = [];
} finally {
skuLoading.value = false;
}
}
function onSkuChange(newSkuId) {
if (!newSkuId) return;
skuId.value = newSkuId;
resetPoolViewState();
router.replace({
path: route.path,
query: {
...route.query,
skuId: newSkuId,
goodsId: goodsId.value,
goodsName: goodsName.value,
},
});
loadStats();
getList();
}
function onStatusTabClick(tab) {
statusTab.value = tab.paneName;
searchForm.pageNumber = 1;
getList();
}
function buildListParams() {
const params = {
skuId: skuId.value,
pageNumber: searchForm.pageNumber,
pageSize: searchForm.pageSize,
};
if (goodsId.value) params.goodsId = goodsId.value;
if (searchForm.cardNo) params.cardNo = searchForm.cardNo;
if (statusTab.value && statusTab.value !== "ALL") {
params.status = statusTab.value;
}
if (importRange.value && importRange.value.length === 2) {
params.createTimeStart = importRange.value[0];
params.createTimeEnd = importRange.value[1];
}
return params;
}
function loadStats() {
if (!skuId.value) return;
getCardKeyStats(skuId.value).then((res) => {
if (res.success) {
stats.value = res.result;
}
});
}
function getList() {
if (!skuId.value) return;
loading.value = true;
getCardKeyList(buildListParams())
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
poolLocked.value = false;
} else {
checkPoolLocked(res);
}
})
.catch(() => {
loading.value = false;
});
}
/** 审核拒绝 / SKU 删除 / 关店时禁用卡池操作(S-06 / S-07 / EC-21~23) */
function checkPoolLocked(res) {
const lockedCodes = [
"CARD_KEY_GOODS_AUTH_REFUSE",
"CARD_KEY_SKU_DELETED",
"CARD_KEY_STORE_CLOSED",
];
if (res.code && lockedCodes.includes(String(res.code))) {
poolLocked.value = true;
}
}
function handleSearch() {
searchForm.pageNumber = 1;
getList();
}
function handleReset() {
searchForm.cardNo = "";
importRange.value = [];
searchForm.pageNumber = 1;
getList();
}
function onPageSizeChange() {
searchForm.pageNumber = 1;
getList();
}
function handleImportUpload(file) {
if (!/\.xlsx$/i.test(file.name)) {
Message.error("请上传 .xlsx 文件");
return false;
}
importCardKey(skuId.value, file).then((res) => {
if (res.success) {
importModal.value = false;
importResult.value = res.result || {};
importResultVisible.value = true;
loadStats();
getList();
refreshSkuOptionStock();
} else {
checkPoolLocked(res);
}
});
return false;
}
function handleDownloadTemplate() {
templateLoading.value = true;
downloadImportTemplate()
.then(() => {
Message.success("模板下载成功");
})
.catch(() => {})
.finally(() => {
templateLoading.value = false;
});
}
function openAddDialog() {
addForm.value = { cardNo: "", cardSecret: "" };
addVisible.value = true;
}
function submitAdd() {
addFormRef.value.validate((valid) => {
if (!valid) return;
addLoading.value = true;
addCardKey({
skuId: skuId.value,
cardNo: addForm.value.cardNo.trim(),
cardSecret: addForm.value.cardSecret.trim(),
})
.then((res) => {
addLoading.value = false;
if (res.success) {
Message.success("新增成功");
addVisible.value = false;
loadStats();
getList();
refreshSkuOptionStock();
} else {
checkPoolLocked(res);
}
})
.catch(() => {
addLoading.value = false;
});
});
}
function handleVoid(row) {
Modal.confirm({
title: "确认作废",
content: `确定作废卡号「${row.cardNo}」?作废后不可恢复。`,
onOk: () => {
voidCardKey(row.id).then((res) => {
if (res.success) {
Message.success("作废成功");
loadStats();
getList();
refreshSkuOptionStock();
}
});
},
});
}
function handleExport() {
exportLoading.value = true;
const params = { ...buildListParams() };
delete params.pageNumber;
delete params.pageSize;
exportCardKey(params, skuId.value)
.then(() => {
Message.success("导出成功");
})
.catch(() => {})
.finally(() => {
exportLoading.value = false;
});
}
async function init() {
initFromRoute();
if (goodsId.value) {
await loadSkuOptions();
}
if (!skuId.value && skuOptions.value.length) {
onSkuChange(skuOptions.value[0].id);
return;
}
if (skuId.value) {
loadStats();
getList();
}
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -37,7 +37,7 @@
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="form" :model="formAdd" label-width="120px" :rules="formValidate">
<el-form ref="formRef" :model="formAdd" label-width="120px" :rules="formValidate">
<el-form-item v-if="showParent" label="上级分类" prop="parentId">
{{ parentTitle }}
<el-input v-model="formAdd.parentId" style="display: none" />
@@ -65,7 +65,8 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import {
addShopGoodsLabel,
delCategdelShopGoodsLabel,
@@ -74,170 +75,166 @@ import {
} from "@/api/goods";
import uploadPicInput from "@/views/my-components/lili/upload-pic-input";
import { regular } from "@/utils";
import { Message, Modal } from "@/utils/message";
export default {
name: "goods-category",
components: {
uploadPicInput,
},
data() {
return {
submitLoading: false,
categoryList: [],
loading: false,
modalType: 0,
modalVisible: false,
modalTitle: "",
showParent: false,
parentTitle: "",
formAdd: this.createDefaultForm(),
formValidate: {
name: [regular.REQUIRED, regular.VARCHAR20],
sortOrder: [regular.REQUIRED, regular.INTEGER],
},
tableData: [],
};
},
methods: {
createDefaultForm() {
return {
parentId: "",
name: "",
image: "",
sortOrder: 0,
level: 0,
};
},
normalizeCategoryTree(list, parentId = 0, level = 0) {
if (!Array.isArray(list) || list.length === 0) return;
list.forEach((item) => {
if (!item || typeof item !== "object") return;
item.name = item.name || item.labelName;
item.parentId = item.parentId ?? parentId;
item.level = item.level ?? level;
if (Array.isArray(item.children) && item.children.length) {
this.normalizeCategoryTree(item.children, item.id, Number(item.level) + 1);
}
});
},
init() {
this.getAllList();
},
addChildren(v) {
this.modalType = 0;
this.modalTitle = "添加子分类";
this.parentTitle = v.name;
this.formAdd = this.createDefaultForm();
this.formAdd.level = Number(v.level) + 1;
this.showParent = true;
delete this.formAdd.id;
this.formAdd.parentId = v.id;
this.modalVisible = true;
},
edit(v) {
this.modalType = 1;
this.modalTitle = "编辑";
this.formAdd.id = v.id;
this.formAdd.name = v.name;
this.formAdd.level = v.level;
this.formAdd.parentId = v.parentId ?? 0;
this.formAdd.sortOrder = v.sortOrder;
this.formAdd.image = v.image;
this.showParent = false;
this.modalVisible = true;
},
addParent() {
this.modalType = 0;
this.modalTitle = "添加一级分类";
this.parentTitle = "顶级分类";
this.showParent = true;
this.formAdd = this.createDefaultForm();
delete this.formAdd.id;
this.formAdd.parentId = 0;
this.formAdd.level = 0;
this.modalVisible = true;
},
Submit() {
this.$refs.form.validate((valid) => {
if (!valid) return;
this.submitLoading = true;
const params = this.buildLabelParams(this.formAdd);
if (this.modalType === 0) {
delete params.id;
addShopGoodsLabel(params).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("添加成功");
this.getAllList();
this.modalVisible = false;
this.$refs.form.resetFields();
}
});
} else {
editShopGoodsLabel(params).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("修改成功");
this.getAllList();
this.modalVisible = false;
this.$refs.form.resetFields();
}
});
}
});
},
remove(v) {
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除 " + v.name + " ?",
loading: true,
onOk: () => {
delCategdelShopGoodsLabel(v.id).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.getAllList();
}
});
},
});
},
getAllList() {
this.loading = true;
getShopGoodsLabelList()
.then((res) => {
this.loading = false;
if (res.success) {
this.normalizeCategoryTree(res.result);
this.categoryList = JSON.parse(JSON.stringify(res.result));
this.tableData = JSON.parse(JSON.stringify(res.result));
}
})
.catch(() => {
this.loading = false;
});
},
buildLabelParams(form) {
const params = {
id: form.id,
parentId: form.parentId,
labelName: form.name,
image: form.image,
sortOrder: form.sortOrder,
level: form.level,
};
Object.keys(params).forEach((key) => {
if (params[key] === undefined || params[key] === "") {
delete params[key];
}
});
return params;
},
},
mounted() {
this.init();
},
defineOptions({ name: "goods-category" });
const submitLoading = ref(false);
const categoryList = ref([]);
const loading = ref(false);
const modalType = ref(0);
const modalVisible = ref(false);
const modalTitle = ref("");
const showParent = ref(false);
const parentTitle = ref("");
const formAdd = ref(createDefaultForm());
const formValidate = {
name: [regular.REQUIRED, regular.VARCHAR20],
sortOrder: [regular.REQUIRED, regular.INTEGER],
};
const tableData = ref([]);
// formRef:模板 ref 改名避免与顶层变量互相覆盖
const formRef = ref(null);
onMounted(() => {
init();
});
function createDefaultForm() {
return {
parentId: "",
name: "",
image: "",
sortOrder: 0,
level: 0,
};
}
function normalizeCategoryTree(list, parentId = 0, level = 0) {
if (!Array.isArray(list) || list.length === 0) return;
list.forEach((item) => {
if (!item || typeof item !== "object") return;
item.name = item.name || item.labelName;
item.parentId = item.parentId ?? parentId;
item.level = item.level ?? level;
if (Array.isArray(item.children) && item.children.length) {
normalizeCategoryTree(item.children, item.id, Number(item.level) + 1);
}
});
}
function init() {
getAllList();
}
function addChildren(v) {
modalType.value = 0;
modalTitle.value = "添加子分类";
parentTitle.value = v.name;
formAdd.value = createDefaultForm();
formAdd.value.level = Number(v.level) + 1;
showParent.value = true;
delete formAdd.value.id;
formAdd.value.parentId = v.id;
modalVisible.value = true;
}
function edit(v) {
modalType.value = 1;
modalTitle.value = "编辑";
formAdd.value.id = v.id;
formAdd.value.name = v.name;
formAdd.value.level = v.level;
formAdd.value.parentId = v.parentId ?? 0;
formAdd.value.sortOrder = v.sortOrder;
formAdd.value.image = v.image;
showParent.value = false;
modalVisible.value = true;
}
function addParent() {
modalType.value = 0;
modalTitle.value = "添加一级分类";
parentTitle.value = "顶级分类";
showParent.value = true;
formAdd.value = createDefaultForm();
delete formAdd.value.id;
formAdd.value.parentId = 0;
formAdd.value.level = 0;
modalVisible.value = true;
}
function Submit() {
formRef.value.validate((valid) => {
if (!valid) return;
submitLoading.value = true;
const params = buildLabelParams(formAdd.value);
if (modalType.value === 0) {
delete params.id;
addShopGoodsLabel(params).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("添加成功");
getAllList();
modalVisible.value = false;
formRef.value.resetFields();
}
});
} else {
editShopGoodsLabel(params).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("修改成功");
getAllList();
modalVisible.value = false;
formRef.value.resetFields();
}
});
}
});
}
function remove(v) {
Modal.confirm({
title: "确认删除",
content: "您确认要删除 " + v.name + " ?",
loading: true,
onOk: () => {
delCategdelShopGoodsLabel(v.id).then((res) => {
Modal.remove();
if (res.success) {
Message.success("操作成功");
getAllList();
}
});
},
});
}
function getAllList() {
loading.value = true;
getShopGoodsLabelList()
.then((res) => {
loading.value = false;
if (res.success) {
normalizeCategoryTree(res.result);
categoryList.value = JSON.parse(JSON.stringify(res.result));
tableData.value = JSON.parse(JSON.stringify(res.result));
}
})
.catch(() => {
loading.value = false;
});
}
function buildLabelParams(form) {
const params = {
id: form.id,
parentId: form.parentId,
labelName: form.name,
image: form.image,
sortOrder: form.sortOrder,
level: form.level,
};
Object.keys(params).forEach((key) => {
if (params[key] === undefined || params[key] === "") {
delete params[key];
}
});
return params;
}
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<div @keyup.enter="handleSearch">
<el-form ref="searchForm" :model="searchForm" inline label-width="70px" class="search-form">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="70px" class="search-form">
<el-form-item label="商品名称" prop="goodsName">
<el-input v-model="searchForm.goodsName" placeholder="请输入商品名称" clearable style="width: 200px" />
</el-form-item>
@@ -39,7 +39,7 @@
<el-button type="primary" class="export" @click="exportStock">批量导出</el-button>
<el-button class="export" @click="openImportStock">批量导入</el-button>
</div>
<el-table ref="table" v-loading="loading" border :data="stockAllData" class="mt_10" style="width: 100%">
<el-table ref="tableRef" v-loading="loading" border :data="stockAllData" class="mt_10" style="width: 100%">
<el-table-column label="商品信息" min-width="400">
<template #default="{ row }">
<div style="margin-top: 5px; height: 90px; display: flex">
@@ -115,139 +115,139 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import {
getGoodsCategoryAll,
getGoodsSkuListDataSeller,
importStockExcel,
queryExportStock,
} from "@/api/goods";
import { Message } from "@/utils/message";
export default {
name: "alertQuantity",
data() {
return {
importModal: false,
spinShow: false,
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
},
stockAllData: [],
total: 0,
categoryList: [],
category: [],
file: null,
};
},
methods: {
init() {
this.getDataList();
this.deepGroup();
},
changePageSize(v) {
this.searchForm.pageSize = v;
this.getDataList();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.categoryPath = this.category?.length ? this.category.join(",") : null;
this.getDataList();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
};
this.category = [];
this.getDataList();
},
getDataList() {
this.loading = true;
getGoodsSkuListDataSeller(this.searchForm).then((res) => {
if (res.success) {
this.stockAllData = res.result.records;
this.total = res.result.total;
}
this.loading = false;
});
},
deepGroup() {
getGoodsCategoryAll().then((res) => {
if (res.success) {
this.categoryList = res.result.map((item) => ({
value: item.id,
label: item.name,
children: (item.children || []).map((child) => ({
value: child.id,
label: child.name,
children: (child.children || []).map((grandson) => ({
value: grandson.id,
label: grandson.name,
})),
})),
}));
}
});
},
async exportStock() {
let randomNumber = "";
for (let i = 0; i < 10; i++) {
randomNumber += Math.floor(Math.random() * 10);
defineOptions({ name: "alertQuantity" });
const searchFormRef = ref(null);
const tableRef = ref(null);
const importModal = ref(false);
const spinShow = ref(false);
const loading = ref(true);
const searchForm = ref({
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
});
const stockAllData = ref([]);
const total = ref(0);
const categoryList = ref([]);
const category = ref([]);
const file = ref(null);
function init() {
getDataList();
deepGroup();
}
function changePageSize(v) {
searchForm.value.pageSize = v;
getDataList();
}
function changePage(v) {
searchForm.value.pageNumber = v;
getDataList();
}
function handleSearch() {
searchForm.value.pageNumber = 1;
searchForm.value.categoryPath = category.value?.length ? category.value.join(",") : null;
getDataList();
}
function handleReset() {
searchForm.value = {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
};
category.value = [];
getDataList();
}
function getDataList() {
loading.value = true;
getGoodsSkuListDataSeller(searchForm.value).then((res) => {
if (res.success) {
stockAllData.value = res.result.records;
total.value = res.result.total;
}
loading.value = false;
});
}
function deepGroup() {
getGoodsCategoryAll().then((res) => {
if (res.success) {
categoryList.value = res.result.map((item) => ({
value: item.id,
label: item.name,
children: (item.children || []).map((child) => ({
value: child.id,
label: child.name,
children: (child.children || []).map((grandson) => ({
value: grandson.id,
label: grandson.name,
})),
})),
}));
}
});
}
async function exportStock() {
let randomNumber = "";
for (let i = 0; i < 10; i++) {
randomNumber += Math.floor(Math.random() * 10);
}
queryExportStock(searchForm.value)
.then((res) => {
const blob = new Blob([res], { type: "application/vnd.ms-excel;charset=utf-8" });
if ("download" in document.createElement("a")) {
const link = document.createElement("a");
link.download = randomNumber + ".xlsx";
link.style.display = "none";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} else {
navigator.msSaveBlob(blob, randomNumber + ".xlsx");
}
queryExportStock(this.searchForm)
.then((res) => {
const blob = new Blob([res], { type: "application/vnd.ms-excel;charset=utf-8" });
if ("download" in document.createElement("a")) {
const link = document.createElement("a");
link.download = randomNumber + ".xlsx";
link.style.display = "none";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} else {
navigator.msSaveBlob(blob, randomNumber + ".xlsx");
}
})
.catch((err) => console.log(err));
},
openImportStock() {
this.importModal = true;
},
handleUpload(file) {
this.file = file;
this.upload();
return false;
},
upload() {
const fd = new FormData();
fd.append("files", this.file);
this.spinShow = true;
importStockExcel(fd).then((res) => {
this.spinShow = false;
if (res.success) {
this.$Message.success("导入成功");
this.importModal = false;
this.getDataList();
}
});
},
},
mounted() {
this.init();
},
};
})
.catch((err) => console.log(err));
}
function openImportStock() {
importModal.value = true;
}
function handleUpload(f) {
file.value = f;
upload();
return false;
}
function upload() {
const fd = new FormData();
fd.append("files", file.value);
spinShow.value = true;
importStockExcel(fd).then((res) => {
spinShow.value = false;
if (res.success) {
Message.success("导入成功");
importModal.value = false;
getDataList();
}
});
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
@import "@/styles/table-common.scss";

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<div @keyup.enter="handleSearch">
<el-form ref="searchForm" :model="searchForm" inline label-width="70px" class="search-form">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="70px" class="search-form">
<el-form-item label="商品名称" prop="goodsName">
<el-input v-model="searchForm.goodsName" placeholder="请输入商品名称" clearable style="width: 200px" />
</el-form-item>
@@ -120,7 +120,9 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import {
getGoodsCategoryAll,
getGoodsListDataByStockSeller,
@@ -128,6 +130,9 @@ import {
updateGoodsAlertStocks,
updateGoodsSkuStocks,
} from "@/api/goods";
import { Message } from "@/utils/message";
defineOptions({ name: "alertQuantityWarn" });
const ROUTE_TAB_MAP = {
alertQuantityWarn: "warnList",
@@ -135,154 +140,165 @@ const ROUTE_TAB_MAP = {
alertQuantityWarnSetting: "warnSetting",
};
export default {
name: "alertQuantityWarn",
data() {
return {
loading: true,
updateStockModalVisible: false,
activeTab: "warnList",
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
},
checkFlag: false,
stockList: [],
warnData: [],
skuAllData: [],
total: 0,
categoryList: [],
category: [],
selectedSku: {},
};
},
watch: {
"$route.name"(name) {
this.applyRouteTab(name);
this.handleReset();
},
},
mounted() {
this.applyRouteTab();
this.init();
},
methods: {
applyRouteTab(name = this.$route.name) {
this.activeTab = ROUTE_TAB_MAP[name] || "warnList";
},
init() {
this.getDataList();
this.deepGroup();
},
openUpdataStockModal(row) {
this.stockList = [];
this.selectedSku = JSON.parse(JSON.stringify(row));
this.stockList.push(this.selectedSku);
this.updateStockModalVisible = true;
},
updateStock() {
const updateStockList = this.stockList.map((i) => ({
skuId: i.id,
quantity: i.quantity,
const route = useRoute();
const loading = ref(true);
const updateStockModalVisible = ref(false);
const activeTab = ref("warnList");
const searchForm = ref({
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
});
const checkFlag = ref(false);
const stockList = ref([]);
const warnData = ref([]);
const skuAllData = ref([]);
const total = ref(0);
const categoryList = ref([]);
const category = ref([]);
const selectedSku = ref({});
const searchFormRef = ref(null);
watch(
() => route.name,
(name) => {
applyRouteTab(name);
handleReset();
}
);
onMounted(() => {
applyRouteTab();
init();
});
function applyRouteTab(name = route.name) {
activeTab.value = ROUTE_TAB_MAP[name] || "warnList";
}
function init() {
getDataList();
deepGroup();
}
function openUpdataStockModal(row) {
stockList.value = [];
selectedSku.value = JSON.parse(JSON.stringify(row));
stockList.value.push(selectedSku.value);
updateStockModalVisible.value = true;
}
function updateStock() {
const updateStockList = stockList.value.map((i) => ({
skuId: i.id,
quantity: i.quantity,
}));
updateGoodsSkuStocks(updateStockList).then((res) => {
if (res.success) {
updateStockModalVisible.value = false;
Message.success("更新库存成功");
getDataList();
}
});
}
function updateWarnStock(row) {
if (checkFlag.value) {
updateGoodsAlertStocks({ skuId: row.id, alertQuantity: row.alertQuantity }).then((res) => {
if (res.success) {
Message.success("更新成功");
}
});
}
}
function checkVal(row) {
if (
!/^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d+)$/.test(row.alertQuantity) ||
parseInt(row.alertQuantity) < 0 ||
parseInt(row.alertQuantity) > 99999999
) {
Message.error("请输入0~99999999之间的数字值");
row.alertQuantity = 0;
checkFlag.value = false;
return;
}
checkFlag.value = true;
}
function switchTabs() {
handleReset();
}
function changePageSize(v) {
searchForm.value.pageSize = v;
getDataList();
}
function changePage(v) {
searchForm.value.pageNumber = v;
getDataList();
}
function handleSearch() {
searchForm.value.pageNumber = 1;
searchForm.value.categoryPath = category.value?.length ? category.value.join(",") : null;
getDataList();
}
function handleReset() {
searchForm.value = {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
};
category.value = [];
getDataList();
}
function getDataList() {
loading.value = true;
if (activeTab.value === "warnList") {
getGoodsListDataByStockSeller(searchForm.value).then((res) => {
if (res.success) {
warnData.value = res.result.records;
total.value = res.result.total;
}
loading.value = false;
});
} else {
getGoodsSkuListDataSeller(searchForm.value).then((res) => {
if (res.success) {
skuAllData.value = res.result.records;
total.value = res.result.total;
}
loading.value = false;
});
}
}
function deepGroup() {
getGoodsCategoryAll().then((res) => {
if (res.success) {
categoryList.value = res.result.map((item) => ({
value: item.id,
label: item.name,
children: (item.children || []).map((child) => ({
value: child.id,
label: child.name,
children: (child.children || []).map((grandson) => ({
value: grandson.id,
label: grandson.name,
})),
})),
}));
updateGoodsSkuStocks(updateStockList).then((res) => {
if (res.success) {
this.updateStockModalVisible = false;
this.$Message.success("更新库存成功");
this.getDataList();
}
});
},
updateWarnStock(row) {
if (this.checkFlag) {
updateGoodsAlertStocks({ skuId: row.id, alertQuantity: row.alertQuantity }).then((res) => {
if (res.success) {
this.$Message.success("更新成功");
}
});
}
},
checkVal(row) {
if (
!/^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d+)$/.test(row.alertQuantity) ||
parseInt(row.alertQuantity) < 0 ||
parseInt(row.alertQuantity) > 99999999
) {
this.$Message.error("请输入0~99999999之间的数字值");
row.alertQuantity = 0;
this.checkFlag = false;
return;
}
this.checkFlag = true;
},
switchTabs() {
this.handleReset();
},
changePageSize(v) {
this.searchForm.pageSize = v;
this.getDataList();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.categoryPath = this.category?.length ? this.category.join(",") : null;
this.getDataList();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
};
this.category = [];
this.getDataList();
},
getDataList() {
this.loading = true;
if (this.activeTab === "warnList") {
getGoodsListDataByStockSeller(this.searchForm).then((res) => {
if (res.success) {
this.warnData = res.result.records;
this.total = res.result.total;
}
this.loading = false;
});
} else {
getGoodsSkuListDataSeller(this.searchForm).then((res) => {
if (res.success) {
this.skuAllData = res.result.records;
this.total = res.result.total;
}
this.loading = false;
});
}
},
deepGroup() {
getGoodsCategoryAll().then((res) => {
if (res.success) {
this.categoryList = res.result.map((item) => ({
value: item.id,
label: item.name,
children: (item.children || []).map((child) => ({
value: child.id,
label: child.name,
children: (child.children || []).map((grandson) => ({
value: grandson.id,
label: grandson.name,
})),
})),
}));
}
});
},
},
};
}
});
}
</script>
<style lang="scss" scoped>

View File

@@ -292,8 +292,11 @@
</div>
</template>
<script>
<script setup>
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { getRequest } from "@/libs/axios";
import { Message } from "@/utils/message";
import {
SERVICE_MODE,
BOOKABLE_DATE_MODE,
@@ -307,6 +310,35 @@ import {
slotKey,
} from "@/constants/appointment";
defineOptions({ name: "AppointmentPublishPanel" });
const props = defineProps({
settings: { type: Object, default: () => defaultAppointmentSettings() },
division: { type: Object, default: () => defaultSlotDivision() },
skuCapacities: { type: Object, default: () => ({}) },
skuInfo: { type: Array, default: () => [] },
divisionTarget: {
type: String,
default: "",
},
});
const emit = defineEmits(["update:settings", "update:division", "update:skuCapacities"]);
const router = useRouter();
const localSettings = ref(defaultAppointmentSettings());
const localDivision = ref(defaultSlotDivision());
const enabledSlotKeys = ref([]);
const systemFormOptions = ref([]);
const capacityDialogVisible = ref(false);
const capacityDraft = ref({});
const activeSkuRowKey = ref("");
const syncing = ref(false);
const divisionReady = ref(false);
// 历史遗留:原代码读取未声明的 this.part,恒为 undefined,行为原样保留
const part = undefined;
function parseMinutes(timeStr) {
if (!timeStr) return 0;
const [h, m] = timeStr.split(":").map(Number);
@@ -319,402 +351,391 @@ function formatMinutes(total) {
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}
export default {
name: "AppointmentPublishPanel",
props: {
settings: { type: Object, default: () => defaultAppointmentSettings() },
division: { type: Object, default: () => defaultSlotDivision() },
skuCapacities: { type: Object, default: () => ({}) },
skuInfo: { type: Array, default: () => [] },
divisionTarget: {
type: String,
default: "",
},
const bookableDateHint = computed(() => {
return "设置预约服务的可预约日期。例如:周六、周天不选中,则在可预约日期中的周一到周五可用。";
});
const customDateRange = computed({
get() {
const { bookableCustomStartDate, bookableCustomEndDate } = localSettings.value;
if (bookableCustomStartDate && bookableCustomEndDate) {
return [bookableCustomStartDate, bookableCustomEndDate];
}
return null;
},
emits: ["update:settings", "update:division", "update:skuCapacities"],
data() {
return {
SERVICE_MODE,
BOOKABLE_DATE_MODE,
ADVANCE_MODE,
CANCEL_MODE,
FORM_SUBMIT_MODE,
SLOT_DIVISION_MODE,
WEEKDAY_OPTIONS,
localSettings: defaultAppointmentSettings(),
localDivision: defaultSlotDivision(),
enabledSlotKeys: [],
systemFormOptions: [],
capacityDialogVisible: false,
capacityDraft: {},
activeSkuRowKey: "",
syncing: false,
divisionReady: false,
set(val) {
if (val && val.length === 2) {
localSettings.value.bookableCustomStartDate = val[0];
localSettings.value.bookableCustomEndDate = val[1];
} else {
localSettings.value.bookableCustomStartDate = "";
localSettings.value.bookableCustomEndDate = "";
}
},
});
const autoTimeRange = computed({
get() {
if (localDivision.value.autoStartTime && localDivision.value.autoEndTime) {
return [localDivision.value.autoStartTime, localDivision.value.autoEndTime];
}
return null;
},
set(val) {
if (val && val.length === 2) {
localDivision.value.autoStartTime = val[0];
localDivision.value.autoEndTime = val[1];
}
},
});
const activeCapacityRows = computed(() => {
return localDivision.value.confirmedSlots
.filter((slot) => isSlotEnabled(slot))
.map((slot) => ({
key: slotKey(slot),
label: `${slot.startTime}-${slot.endTime}`,
}));
});
const allSlotsChecked = computed(() => {
const enabled = localDivision.value.confirmedSlots.filter((s) => isSlotEnabled(s));
return enabled.length > 0 && enabled.length === enabledSlotKeys.value.length;
});
const slotIndeterminate = computed(() => {
const enabled = localDivision.value.confirmedSlots.filter((s) => isSlotEnabled(s));
return enabledSlotKeys.value.length > 0 && enabledSlotKeys.value.length < enabled.length;
});
watch(
() => props.settings,
(val) => {
if (syncing.value) return;
localSettings.value = { ...defaultAppointmentSettings(), ...(val || {}) };
},
{ immediate: true, deep: true }
);
watch(
() => props.division,
(val) => {
if (syncing.value) return;
localDivision.value = { ...defaultSlotDivision(), ...(val || {}) };
enabledSlotKeys.value = (localDivision.value.confirmedSlots || [])
.filter((s) => s.slotEnabled !== false)
.map((s) => slotKey(s));
},
{ immediate: true, deep: true }
);
watch(
localSettings,
(val) => {
syncing.value = true;
emit("update:settings", { ...val });
nextTick(() => {
syncing.value = false;
});
},
{ deep: true }
);
watch(
localDivision,
(val) => {
syncing.value = true;
emit("update:division", { ...val });
nextTick(() => {
syncing.value = false;
});
},
{ deep: true }
);
watch(enabledSlotKeys, (val) => {
localDivision.value.confirmedSlots = (localDivision.value.confirmedSlots || []).map((slot) => ({
...slot,
slotEnabled: val.includes(slotKey(slot)),
}));
});
watch(
() => props.divisionTarget,
() => {
prepareDivisionTarget();
},
{ immediate: true }
);
function prepareDivisionTarget() {
divisionReady.value = false;
if (!props.divisionTarget) {
return;
}
nextTick(() => {
divisionReady.value = !!document.querySelector(props.divisionTarget);
});
}
function isSlotEnabled(slot) {
return slot.slotEnabled !== false;
}
function toggleAllSlots(checked) {
if (checked) {
enabledSlotKeys.value = localDivision.value.confirmedSlots.map((s) => slotKey(s));
} else {
enabledSlotKeys.value = [];
}
}
function loadSystemForms() {
getRequest("/appointment/system-forms/enabled")
.then((res) => {
if (res.success) {
systemFormOptions.value = res.result || [];
}
})
.catch(() => {});
}
function goAddSystemForm() {
// 主布局无 keep-alive,新开页避免发品表单被销毁
const route = router.resolve({ name: "appointment-system-form-designer" });
window.open(route.href, "_blank");
}
function addCustomDraft() {
const last = localDivision.value.customDrafts[localDivision.value.customDrafts.length - 1];
const start = last?.endTime || "08:00";
const endMinutes = parseMinutes(start) + 60;
const end = endMinutes >= 24 * 60 ? "23:59" : formatMinutes(endMinutes);
localDivision.value.customDrafts.push({ startTime: start, endTime: end });
}
function removeCustomDraft(index) {
localDivision.value.customDrafts.splice(index, 1);
}
function confirmSlots() {
let slots = [];
if (localDivision.value.slotDivisionMode === SLOT_DIVISION_MODE.AUTO) {
const duration = Number(localDivision.value.serviceDurationMinutes);
if (!duration || duration < 15) {
Message.error("请填写有效的服务时长(15-480 分钟)");
return false;
}
const start = parseMinutes(localDivision.value.autoStartTime);
const end = parseMinutes(localDivision.value.autoEndTime);
if (!localDivision.value.autoStartTime || !localDivision.value.autoEndTime || end <= start) {
Message.error("请选择有效的起止时间(同一天内,结束大于开始)");
return false;
}
for (let t = start; t + duration <= end; t += duration) {
slots.push({
startTime: formatMinutes(t),
endTime: formatMinutes(t + duration),
slotEnabled: true,
});
}
if (!slots.length) {
Message.error("未生成有效时段,请检查起止时间与服务时长");
return false;
}
} else {
const drafts = localDivision.value.customDrafts || [];
if (!drafts.length) {
Message.error("请至少添加一条自定义时段");
return false;
}
let prevEnd = -1;
for (const row of drafts) {
if (!row.startTime || !row.endTime) {
Message.error("请完整填写每条时段的开始和结束时间");
return false;
}
const s = parseMinutes(row.startTime);
const e = parseMinutes(row.endTime);
if (e <= s) {
Message.error("结束时间须大于开始时间,且不支持跨天");
return false;
}
if (s < prevEnd) {
Message.error("自定义时段不得重叠,须按时间顺序添加");
return false;
}
prevEnd = e;
slots.push({ startTime: row.startTime, endTime: row.endTime, slotEnabled: true });
}
}
localDivision.value.confirmedSlots = slots;
enabledSlotKeys.value = slots.map((s) => slotKey(s));
Message.success(`已确认 ${slots.length} 个时段`);
return true;
}
function openSkuCapacityDialog(skuRow) {
activeSkuRowKey.value = getSkuRowKey(skuRow);
const draft = {};
activeCapacityRows.value.forEach((row) => {
draft[row.key] = props.skuCapacities?.[activeSkuRowKey.value]?.[row.key] ?? 0;
});
capacityDraft.value = draft;
capacityDialogVisible.value = true;
}
function saveSkuCapacities() {
const next = { ...(props.skuCapacities || {}) };
next[activeSkuRowKey.value] = { ...capacityDraft.value };
emit("update:skuCapacities", next);
capacityDialogVisible.value = false;
}
function validateRules() {
const s = localSettings.value;
if (!s.serviceModes?.length) {
Message.error("预约规则:请至少选择一种服务模式");
return false;
}
if (s.bookableDateMode === BOOKABLE_DATE_MODE.CUSTOM) {
if (!s.bookableCustomStartDate || !s.bookableCustomEndDate) {
Message.error("预约规则:请选择自定义可预约日期范围");
return false;
}
if (!s.bookableWeekdays?.length) {
Message.error("预约规则:请至少勾选一天可约星期");
return false;
}
}
if (!s.bookableDays || s.bookableDays < 1) {
Message.error("预约规则:预约日期范围须为正整数");
return false;
}
if (s.advanceMode === ADVANCE_MODE.REQUIRE_HOURS && (!s.advanceHours || s.advanceHours < 1)) {
Message.error("预约规则:请填写提前预约小时数");
return false;
}
if (
s.cancelMode === CANCEL_MODE.ALLOW_BEFORE_HOURS &&
(!s.cancelAheadHours || s.cancelAheadHours < 1)
) {
Message.error("预约规则:请填写取消政策小时数");
return false;
}
return true;
}
function validateDivision() {
const enabledSlots = (localDivision.value.confirmedSlots || []).filter((slot) =>
isSlotEnabled(slot)
);
if (!enabledSlots.length) {
Message.error("时段划分:请确认时间段并至少启用 1 个时段");
return false;
}
return true;
}
function validate() {
if (part === "rules") {
return validateRules();
}
if (part === "division") {
return validateDivision();
}
return validateRules() && validateDivision();
}
function buildSettingsPayload() {
return {
appointmentSettings: { ...localSettings.value },
};
}
function buildDivisionPayload() {
const enabledSlots = (localDivision.value.confirmedSlots || []).filter((slot) =>
isSlotEnabled(slot)
);
return {
appointmentSlotTemplate: {
slotDivisionMode: localDivision.value.slotDivisionMode,
serviceDurationMinutes: localDivision.value.serviceDurationMinutes,
autoStartTime: localDivision.value.autoStartTime,
autoEndTime: localDivision.value.autoEndTime,
slots: enabledSlots.map((slot) => ({
startTime: slot.startTime,
endTime: slot.endTime,
slotEnabled: true,
})),
},
};
}
function buildSubmitPayload() {
return {
...buildSettingsPayload(),
...buildDivisionPayload(),
};
}
function buildSkuSlotCapacities(skuRow) {
const rowKey = getSkuRowKey(skuRow);
const map = props.skuCapacities?.[rowKey] || {};
return (localDivision.value.confirmedSlots || [])
.filter((slot) => isSlotEnabled(slot))
.map((slot) => ({
startTime: slot.startTime,
endTime: slot.endTime,
slotCapacity: map[slotKey(slot)] ?? 0,
}));
}
function getSkuRowKey(row) {
return props.skuInfo
.map((info) => row[info.name])
.filter(Boolean)
.join("::");
}
function hydrateFromGoods(result) {
if (result.appointmentSettings) {
localSettings.value = { ...defaultAppointmentSettings(), ...result.appointmentSettings };
}
if (result.appointmentSlotTemplate) {
const tpl = result.appointmentSlotTemplate;
localDivision.value = {
...defaultSlotDivision(),
slotDivisionMode: tpl.slotDivisionMode || SLOT_DIVISION_MODE.AUTO,
serviceDurationMinutes: tpl.serviceDurationMinutes || 60,
autoStartTime: tpl.autoStartTime || "08:00",
autoEndTime: tpl.autoEndTime || "18:00",
confirmedSlots: tpl.slots || [],
customDrafts: tpl.slots?.length
? tpl.slots.map((s) => ({ startTime: s.startTime, endTime: s.endTime }))
: defaultSlotDivision().customDrafts,
};
},
computed: {
bookableDateHint() {
return "设置预约服务的可预约日期。例如:周六、周天不选中,则在可预约日期中的周一到周五可用。";
},
customDateRange: {
get() {
const { bookableCustomStartDate, bookableCustomEndDate } = this.localSettings;
if (bookableCustomStartDate && bookableCustomEndDate) {
return [bookableCustomStartDate, bookableCustomEndDate];
}
return null;
},
set(val) {
if (val && val.length === 2) {
this.localSettings.bookableCustomStartDate = val[0];
this.localSettings.bookableCustomEndDate = val[1];
} else {
this.localSettings.bookableCustomStartDate = "";
this.localSettings.bookableCustomEndDate = "";
}
},
},
autoTimeRange: {
get() {
if (this.localDivision.autoStartTime && this.localDivision.autoEndTime) {
return [this.localDivision.autoStartTime, this.localDivision.autoEndTime];
}
return null;
},
set(val) {
if (val && val.length === 2) {
this.localDivision.autoStartTime = val[0];
this.localDivision.autoEndTime = val[1];
}
},
},
activeCapacityRows() {
return this.localDivision.confirmedSlots
.filter((slot) => this.isSlotEnabled(slot))
.map((slot) => ({
key: slotKey(slot),
label: `${slot.startTime}-${slot.endTime}`,
}));
},
allSlotsChecked() {
const enabled = this.localDivision.confirmedSlots.filter((s) => this.isSlotEnabled(s));
return enabled.length > 0 && enabled.length === this.enabledSlotKeys.length;
},
slotIndeterminate() {
const enabled = this.localDivision.confirmedSlots.filter((s) => this.isSlotEnabled(s));
return this.enabledSlotKeys.length > 0 && this.enabledSlotKeys.length < enabled.length;
},
},
watch: {
settings: {
immediate: true,
deep: true,
handler(val) {
if (this.syncing) return;
this.localSettings = { ...defaultAppointmentSettings(), ...(val || {}) };
},
},
division: {
immediate: true,
deep: true,
handler(val) {
if (this.syncing) return;
this.localDivision = { ...defaultSlotDivision(), ...(val || {}) };
this.enabledSlotKeys = (this.localDivision.confirmedSlots || [])
.filter((s) => s.slotEnabled !== false)
.map((s) => slotKey(s));
},
},
localSettings: {
deep: true,
handler(val) {
this.syncing = true;
this.$emit("update:settings", { ...val });
this.$nextTick(() => {
this.syncing = false;
});
},
},
localDivision: {
deep: true,
handler(val) {
this.syncing = true;
this.$emit("update:division", { ...val });
this.$nextTick(() => {
this.syncing = false;
});
},
},
enabledSlotKeys(val) {
this.localDivision.confirmedSlots = (this.localDivision.confirmedSlots || []).map((slot) => ({
...slot,
slotEnabled: val.includes(slotKey(slot)),
}));
},
divisionTarget: {
immediate: true,
handler() {
this.prepareDivisionTarget();
},
},
},
mounted() {
this.loadSystemForms();
this.prepareDivisionTarget();
},
methods: {
prepareDivisionTarget() {
this.divisionReady = false;
if (!this.divisionTarget) {
return;
}
this.$nextTick(() => {
this.divisionReady = !!document.querySelector(this.divisionTarget);
enabledSlotKeys.value = (tpl.slots || [])
.filter((s) => s.slotEnabled !== false)
.map((s) => slotKey(s));
}
if (result.skuList?.length) {
const cap = {};
result.skuList.forEach((sku) => {
const specKey = (sku.specList || []).map((s) => s.specValue).filter(Boolean).join("::");
const keys = [specKey, sku.id, sku.sn].filter(Boolean);
const slotMap = {};
(sku.slotCapacities || []).forEach((item) => {
slotMap[slotKey(item)] = item.slotCapacity ?? 0;
});
},
slotKey,
isSlotEnabled(slot) {
return slot.slotEnabled !== false;
},
toggleAllSlots(checked) {
if (checked) {
this.enabledSlotKeys = this.localDivision.confirmedSlots.map((s) => slotKey(s));
} else {
this.enabledSlotKeys = [];
}
},
loadSystemForms() {
getRequest("/appointment/system-forms/enabled")
.then((res) => {
if (res.success) {
this.systemFormOptions = res.result || [];
}
})
.catch(() => {});
},
goAddSystemForm() {
// 主布局无 keep-alive,新开页避免发品表单被销毁
const route = this.$router.resolve({ name: "appointment-system-form-designer" });
window.open(route.href, "_blank");
},
addCustomDraft() {
const last = this.localDivision.customDrafts[this.localDivision.customDrafts.length - 1];
const start = last?.endTime || "08:00";
const endMinutes = parseMinutes(start) + 60;
const end = endMinutes >= 24 * 60 ? "23:59" : formatMinutes(endMinutes);
this.localDivision.customDrafts.push({ startTime: start, endTime: end });
},
removeCustomDraft(index) {
this.localDivision.customDrafts.splice(index, 1);
},
confirmSlots() {
let slots = [];
if (this.localDivision.slotDivisionMode === SLOT_DIVISION_MODE.AUTO) {
const duration = Number(this.localDivision.serviceDurationMinutes);
if (!duration || duration < 15) {
this.$Message.error("请填写有效的服务时长(15-480 分钟)");
return false;
}
const start = parseMinutes(this.localDivision.autoStartTime);
const end = parseMinutes(this.localDivision.autoEndTime);
if (!this.localDivision.autoStartTime || !this.localDivision.autoEndTime || end <= start) {
this.$Message.error("请选择有效的起止时间(同一天内,结束大于开始)");
return false;
}
for (let t = start; t + duration <= end; t += duration) {
slots.push({
startTime: formatMinutes(t),
endTime: formatMinutes(t + duration),
slotEnabled: true,
});
}
if (!slots.length) {
this.$Message.error("未生成有效时段,请检查起止时间与服务时长");
return false;
}
} else {
const drafts = this.localDivision.customDrafts || [];
if (!drafts.length) {
this.$Message.error("请至少添加一条自定义时段");
return false;
}
let prevEnd = -1;
for (const row of drafts) {
if (!row.startTime || !row.endTime) {
this.$Message.error("请完整填写每条时段的开始和结束时间");
return false;
}
const s = parseMinutes(row.startTime);
const e = parseMinutes(row.endTime);
if (e <= s) {
this.$Message.error("结束时间须大于开始时间,且不支持跨天");
return false;
}
if (s < prevEnd) {
this.$Message.error("自定义时段不得重叠,须按时间顺序添加");
return false;
}
prevEnd = e;
slots.push({ startTime: row.startTime, endTime: row.endTime, slotEnabled: true });
}
}
this.localDivision.confirmedSlots = slots;
this.enabledSlotKeys = slots.map((s) => slotKey(s));
this.$Message.success(`已确认 ${slots.length} 个时段`);
return true;
},
openSkuCapacityDialog(skuRow) {
this.activeSkuRowKey = this.getSkuRowKey(skuRow);
const draft = {};
this.activeCapacityRows.forEach((row) => {
draft[row.key] = this.skuCapacities?.[this.activeSkuRowKey]?.[row.key] ?? 0;
keys.forEach((key) => {
cap[key] = { ...slotMap };
});
this.capacityDraft = draft;
this.capacityDialogVisible = true;
},
saveSkuCapacities() {
const next = { ...(this.skuCapacities || {}) };
next[this.activeSkuRowKey] = { ...this.capacityDraft };
this.$emit("update:skuCapacities", next);
this.capacityDialogVisible = false;
},
validateRules() {
const s = this.localSettings;
if (!s.serviceModes?.length) {
this.$Message.error("预约规则:请至少选择一种服务模式");
return false;
}
if (s.bookableDateMode === BOOKABLE_DATE_MODE.CUSTOM) {
if (!s.bookableCustomStartDate || !s.bookableCustomEndDate) {
this.$Message.error("预约规则:请选择自定义可预约日期范围");
return false;
}
if (!s.bookableWeekdays?.length) {
this.$Message.error("预约规则:请至少勾选一天可约星期");
return false;
}
}
if (!s.bookableDays || s.bookableDays < 1) {
this.$Message.error("预约规则:预约日期范围须为正整数");
return false;
}
if (s.advanceMode === ADVANCE_MODE.REQUIRE_HOURS && (!s.advanceHours || s.advanceHours < 1)) {
this.$Message.error("预约规则:请填写提前预约小时数");
return false;
}
if (
s.cancelMode === CANCEL_MODE.ALLOW_BEFORE_HOURS &&
(!s.cancelAheadHours || s.cancelAheadHours < 1)
) {
this.$Message.error("预约规则:请填写取消政策小时数");
return false;
}
return true;
},
validateDivision() {
const enabledSlots = (this.localDivision.confirmedSlots || []).filter((slot) =>
this.isSlotEnabled(slot)
);
if (!enabledSlots.length) {
this.$Message.error("时段划分:请确认时间段并至少启用 1 个时段");
return false;
}
return true;
},
validate() {
if (this.part === "rules") {
return this.validateRules();
}
if (this.part === "division") {
return this.validateDivision();
}
return this.validateRules() && this.validateDivision();
},
buildSettingsPayload() {
return {
appointmentSettings: { ...this.localSettings },
};
},
buildDivisionPayload() {
const enabledSlots = (this.localDivision.confirmedSlots || []).filter((slot) =>
this.isSlotEnabled(slot)
);
return {
appointmentSlotTemplate: {
slotDivisionMode: this.localDivision.slotDivisionMode,
serviceDurationMinutes: this.localDivision.serviceDurationMinutes,
autoStartTime: this.localDivision.autoStartTime,
autoEndTime: this.localDivision.autoEndTime,
slots: enabledSlots.map((slot) => ({
startTime: slot.startTime,
endTime: slot.endTime,
slotEnabled: true,
})),
},
};
},
buildSubmitPayload() {
return {
...this.buildSettingsPayload(),
...this.buildDivisionPayload(),
};
},
buildSkuSlotCapacities(skuRow) {
const rowKey = this.getSkuRowKey(skuRow);
const map = this.skuCapacities?.[rowKey] || {};
return (this.localDivision.confirmedSlots || [])
.filter((slot) => this.isSlotEnabled(slot))
.map((slot) => ({
startTime: slot.startTime,
endTime: slot.endTime,
slotCapacity: map[slotKey(slot)] ?? 0,
}));
},
getSkuRowKey(row) {
return this.skuInfo
.map((info) => row[info.name])
.filter(Boolean)
.join("::");
},
hydrateFromGoods(result) {
if (result.appointmentSettings) {
this.localSettings = { ...defaultAppointmentSettings(), ...result.appointmentSettings };
}
if (result.appointmentSlotTemplate) {
const tpl = result.appointmentSlotTemplate;
this.localDivision = {
...defaultSlotDivision(),
slotDivisionMode: tpl.slotDivisionMode || SLOT_DIVISION_MODE.AUTO,
serviceDurationMinutes: tpl.serviceDurationMinutes || 60,
autoStartTime: tpl.autoStartTime || "08:00",
autoEndTime: tpl.autoEndTime || "18:00",
confirmedSlots: tpl.slots || [],
customDrafts: tpl.slots?.length
? tpl.slots.map((s) => ({ startTime: s.startTime, endTime: s.endTime }))
: defaultSlotDivision().customDrafts,
};
this.enabledSlotKeys = (tpl.slots || [])
.filter((s) => s.slotEnabled !== false)
.map((s) => slotKey(s));
}
if (result.skuList?.length) {
const cap = {};
result.skuList.forEach((sku) => {
const specKey = (sku.specList || []).map((s) => s.specValue).filter(Boolean).join("::");
const keys = [specKey, sku.id, sku.sn].filter(Boolean);
const slotMap = {};
(sku.slotCapacities || []).forEach((item) => {
slotMap[slotKey(item)] = item.slotCapacity ?? 0;
});
keys.forEach((key) => {
cap[key] = { ...slotMap };
});
});
this.$emit("update:skuCapacities", cap);
}
},
},
};
});
emit("update:skuCapacities", cap);
}
}
onMounted(() => {
loadSystemForms();
prepareDivisionTarget();
});
// 父组件 goodsOperationSec.vue 通过 ref 调用以下方法,保持公开接口不变
defineExpose({
prepareDivisionTarget,
slotKey,
isSlotEnabled,
toggleAllSlots,
loadSystemForms,
goAddSystemForm,
addCustomDraft,
removeCustomDraft,
confirmSlots,
openSkuCapacityDialog,
saveSkuCapacities,
validateRules,
validateDivision,
validate,
buildSettingsPayload,
buildDivisionPayload,
buildSubmitPayload,
buildSkuSlotCapacities,
getSkuRowKey,
hydrateFromGoods,
});
</script>
<style scoped lang="scss">

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -23,7 +23,7 @@
</el-card>
<el-card>
<el-table v-loading="loading" border :data="data" ref="table" class="mt_10" style="width: 100%">
<el-table v-loading="loading" border :data="data" ref="tableRef" class="mt_10" style="width: 100%">
<el-table-column prop="id" label="编号" min-width="120" />
<el-table-column label="商品原图" width="120" align="center">
<template #default="{ row }">
@@ -66,88 +66,94 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getDraftGoodsListData, deleteDraftGoods } from "@/api/goods";
import { Message, Modal } from "@/utils/message";
export default {
name: "goods",
data() {
return {
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 10,
sort: "create_time",
order: "desc",
saveType: "TEMPLATE",
},
data: [],
total: 0,
};
},
methods: {
init() {
this.getDataList();
},
editGoods(v) {
this.$router.push({
name: "goods-template-operation-edit",
query: { draftId: v.id },
});
},
removeDraft(id) {
this.$Modal.confirm({
title: "确认审核",
content: "您确认要删除id为 " + id + " 的模版吗?",
loading: true,
onOk: () => {
deleteDraftGoods(id).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("删除成功");
this.getDataList();
}
});
},
});
},
changePage() {
this.getDataList();
},
changePageSize() {
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 10;
this.getDataList();
},
handleReset() {
this.$refs.searchForm.resetFields();
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 10;
this.getDataList();
},
getDataList() {
this.loading = true;
getDraftGoodsListData(this.searchForm).then((res) => {
this.loading = false;
defineOptions({ name: "goods" });
const route = useRoute();
const router = useRouter();
const searchFormRef = ref(null);
const tableRef = ref(null);
const loading = ref(true);
const searchForm = ref({
pageNumber: 1,
pageSize: 10,
sort: "create_time",
order: "desc",
saveType: "TEMPLATE",
});
const data = ref([]);
const total = ref(0);
function init() {
getDataList();
}
function editGoods(v) {
router.push({
name: "goods-template-operation-edit",
query: { draftId: v.id },
});
}
function removeDraft(id) {
Modal.confirm({
title: "确认审核",
content: "您确认要删除id为 " + id + " 的模版吗?",
loading: true,
onOk: () => {
deleteDraftGoods(id).then((res) => {
Modal.remove();
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
Message.success("删除成功");
getDataList();
}
});
},
},
mounted() {
this.init();
},
watch: {
$route() {
this.init();
},
},
};
});
}
function changePage() {
getDataList();
}
function changePageSize() {
getDataList();
}
function handleSearch() {
searchForm.value.pageNumber = 1;
searchForm.value.pageSize = 10;
getDataList();
}
function handleReset() {
searchFormRef.value.resetFields();
searchForm.value.pageNumber = 1;
searchForm.value.pageSize = 10;
getDataList();
}
function getDataList() {
loading.value = true;
getDraftGoodsListData(searchForm.value).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
onMounted(() => {
init();
});
watch(
() => route.query,
() => {
init();
}
);
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -164,7 +164,7 @@
</div>
<el-table
ref="table"
ref="tableRef"
v-loading="loading"
:data="data"
class="mt_10"
@@ -257,7 +257,7 @@
</el-card>
<el-dialog v-model="modalVisible" title="下架操作" width="500px" :close-on-click-modal="false">
<el-form ref="underForm" :model="underForm" label-width="100px">
<el-form ref="underFormRef" :model="underForm" label-width="100px">
<el-form-item label="下架原因" prop="reason">
<el-input v-model="underForm.reason" clearable />
</el-form-item>
@@ -270,7 +270,9 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
getCategoryTree,
getGoodsListData,
@@ -280,411 +282,415 @@ import {
upGoods,
lowGoods,
} from "@/api/goods";
export default {
name: "goods",
data() {
return {
id: "", //要操作的id
loading: true, // 表单加载状态
modalVisible: false, // 添加或编辑显示
createRange: [],
searchForm: {
// 搜索框初始化对象
pageNumber: 1, // 当前页数
pageSize: 20, // 页面大小
sort: "create_time", // 默认排序字段
order: "desc", // 默认排序方式
storeCategoryPath: "",
categoryPath: "",
skuName: "",
skuSn: "",
stockStatus: "",
stockMin: "",
stockMax: "",
priceMin: "",
priceMax: "",
},
platformCategoryList: [],
shopCategoryList: [],
platformCategoryProps: {
value: "id",
label: "name",
children: "children",
checkStrictly: true,
emitPath: false,
},
shopCategoryProps: {
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: false,
},
underForm: {
// 下架原因
reason: "",
},
submitLoading: false, // 添加或编辑提交状态
data: [], // 表单数据
total: 0, // 表单数据总数
currentStatus: "ALL",
goodsNumerData: {},
selectedRows: [], // 选中的行数据
selectAll: false, // 全选状态
};
},
computed: {
goodsStatusWithCount() {
return [
{title: '全部', value: 'ALL'},
{title: `出售中${this.goodsNumerData.upperGoodsNum ? '(' + this.goodsNumerData.upperGoodsNum + ')' : ''}`, value: 'UPPER'},
{title: `仓库中${this.goodsNumerData.downGoodsNum ? '(' + this.goodsNumerData.downGoodsNum + ')' : ''}`, value: 'DOWN'},
{title: `待审核${this.goodsNumerData.auditGoodsNum ? '(' + this.goodsNumerData.auditGoodsNum + ')' : ''}`, value: 'TOBEAUDITED'},
{title: `审核未通过${this.goodsNumerData.refuseGoodsNum ? '(' + this.goodsNumerData.refuseGoodsNum + ')' : ''}`, value: 'REFUSE'}
];
}
},
methods: {
clearTableSelection() {
this.$refs.table?.clearSelection?.();
},
onStatusTabClick(tab) {
this.goodsStatusClick(tab.paneName);
},
goodsTypeText(v) {
if (v === "PHYSICAL_GOODS") return "实物商品";
if (v === "VIRTUAL_GOODS") return "虚拟商品";
if (v === "E_COUPON") return "电子卡券"; // 卡密商品
if (v === "APPOINTMENT_GOODS") return "预约商品";
return v || "—";
},
findTreeNodeLabel(tree, targetId, valueKey, labelKey) {
for (const node of tree || []) {
if (String(node[valueKey]) === String(targetId)) {
return node[labelKey];
}
const childLabel = this.findTreeNodeLabel(node.children, targetId, valueKey, labelKey);
if (childLabel) return childLabel;
}
return "";
},
platformCategoryText(path) {
if (!path) return "—";
const ids = String(path).split(",").filter(Boolean);
if (!ids.length) return "—";
const labels = ids
.map((id) => this.findTreeNodeLabel(this.platformCategoryList, id, "id", "name"))
.filter(Boolean);
return labels.length ? labels.join(" / ") : "—";
},
/** 跳转卡池管理;单 SKU 商品默认取第一个 SKU(card-key-pool 需 skuId) */
async goCardKeyPool(row) {
let skuId = row.skuId;
if (!skuId) {
try {
const res = await getQueryGoodsIdGoodsList(row.id);
if (res.success && res.result?.length) {
skuId = res.result[0].id;
} else {
this.$message.warning("该商品暂无 SKU,请先完善商品规格");
return;
}
} catch {
this.$message.error("获取商品规格失败");
return;
}
}
this.$router.push({
path: "/card-key-pool",
query: {
skuId,
goodsId: row.id,
goodsName: row.goodsName,
},
});
},
marketEnableText(v) {
if (v === "DOWN") return "下架";
if (v === "UPPER") return "上架";
return "";
},
authFlagText(v) {
if (v === "TOBEAUDITED") return "待审核";
if (v === "PASS") return "通过";
if (v === "REFUSE") return "拒绝";
return "";
},
// 初始化数据
init() {
this.loadFilterOptions();
this.getDataList();
this.getNumberData();
},
loadFilterOptions() {
getShopGoodsLabelListSeller()
.then((res) => {
if (res?.success) {
this.shopCategoryList = this.formatShopCategory(res.result || []);
}
})
.catch(() => {});
getCategoryTree()
.then((res) => {
if (res?.success) {
this.platformCategoryList = res.result || [];
}
})
.catch(() => {});
},
onStockStatusChange(value) {
if (value !== "IN_STOCK") {
this.searchForm.stockMin = "";
this.searchForm.stockMax = "";
}
},
buildSearchParams(includeGoodsStatus = true) {
const { stockStatus, stockMin, stockMax, priceMin, priceMax, ...rest } = this.searchForm;
const params = { ...rest };
import { Message, Modal } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
if (!includeGoodsStatus) {
delete params.goodsStatus;
}
defineOptions({ name: "goods" });
if (this.createRange && this.createRange.length === 2) {
params.startDate = this.createRange[0];
params.endDate = this.createRange[1];
}
const route = useRoute();
const router = useRouter();
if (stockStatus === "IN_STOCK") {
params.geQuantity = stockMin !== "" && stockMin != null ? Number(stockMin) : 1;
if (stockMax !== "" && stockMax != null) {
params.leQuantity = Number(stockMax);
}
} else if (stockStatus === "OUT_OF_STOCK") {
params.geQuantity = 0;
params.leQuantity = 0;
} else {
if (stockMin !== "" && stockMin != null) params.geQuantity = Number(stockMin);
if (stockMax !== "" && stockMax != null) params.leQuantity = Number(stockMax);
}
const searchFormRef = ref(null);
const tableRef = ref(null);
const underFormRef = ref(null);
if (priceMin !== "" && priceMax !== "") {
params.price = `${priceMin}_${priceMax}`;
} else if (priceMin !== "") {
params.price = String(priceMin);
} else if (priceMax !== "") {
params.price = `0_${priceMax}`;
}
return params;
},
formatShopCategory(list) {
return (list || []).map((item) => ({
value: item.id,
label: item.labelName,
children:
item.children && item.children.length
? this.formatShopCategory(item.children)
: undefined,
}));
},
// 分页 改变页码
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
},
// 分页 改变页数
changePageSize(v) {
this.searchForm.pageSize = v;
this.getDataList();
},
// 搜索
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
this.getNumberData();
},
// 获取数据
getDataList() {
this.loading = true;
getGoodsListData(this.buildSearchParams()).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
getNumberData() {
getGoodsNumerData(this.buildSearchParams(false)).then((res) => {
if (res.success) {
this.goodsNumerData = res.result;
}
})
},
// 编辑
edit(v) {
this.id = v.id;
if (v.underMessage) {
this.underForm.reason = v.underMessage;
} else {
this.underForm.reason = "";
}
this.modalVisible = true;
},
// 下架
lower() {
let params = {
goodsId: this.id,
reason:this.underForm.reason
};
lowGoods(params).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.modalVisible = false;
this.getDataList();
this.getNumberData(); // 添加这行
}
});
},
// 上架
upper(v) {
this.$Modal.confirm({
title: "确认上架",
content: "您确认要上架 " + v.goodsName + " ?",
loading: true,
onOk: () => {
let params = {
goodsId: v.id
};
upGoods(params).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("上架成功");
this.getDataList();
this.getNumberData(); // 添加这行
}
});
},
});
},
// 编辑商品
editGoods(v) {
this.$filters.customRouterPush({
name: "goods-operation-edit",
query: { id: v.id },
});
},
// 复制商品
copyGoods(v) {
this.$filters.customRouterPush({
name: "goods-operation",
query: { copyId: v.id },
});
},
// 商品状态筛选
goodsStatusClick(name) {
if (name === "ALL" || name === "" || name === undefined || name === null) {
delete this.searchForm.goodsStatus;
this.currentStatus = "ALL";
} else {
this.searchForm.goodsStatus = name;
this.currentStatus = name;
}
this.selectedRows = [];
this.clearTableSelection();
this.getDataList();
},
onSelectionChange(selection) {
this.selectedRows = selection;
},
// 批量上架
batchUpper() {
if (this.selectedRows.length === 0) {
this.$Message.warning('请先选择要上架的商品');
return;
}
const goodsNames = this.selectedRows.map(item => item.goodsName).join('、');
this.$Modal.confirm({
title: '确认批量上架',
content: `您确认要上架以下商品吗?\n${goodsNames}`,
loading: true,
onOk: () => {
// 提取所有选中商品的ID
const goodsIds = this.selectedRows.map(item => item.id);
const params = {
goodsId: goodsIds // 传递ID数组
};
upGoods(params).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success('批量上架成功');
this.selectedRows = [];
this.selectAll = false;
this.getDataList();
this.getNumberData();
}
}).catch(() => {
this.$Modal.remove();
});
}
});
},
// 批量下架
batchLower() {
if (this.selectedRows.length === 0) {
this.$Message.warning('请先选择要下架的商品');
return;
}
const goodsNames = this.selectedRows.map(item => item.goodsName).join('、');
this.$Modal.confirm({
title: '确认批量下架',
content: `您确认要下架以下商品吗?\n${goodsNames}`,
loading: true,
onOk: () => {
// 提取所有选中商品的ID
const goodsIds = this.selectedRows.map(item => item.id);
const params = {
goodsId: goodsIds, // 传递ID数组
reason: '批量下架操作' // 可以设置默认下架原因
};
lowGoods(params).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success('批量下架成功');
this.selectedRows = [];
this.selectAll = false;
this.getDataList();
this.getNumberData();
}
}).catch(() => {
this.$Modal.remove();
});
}
});
},
},
mounted() {
const goodsStatus = this.$route.query.goodsStatus;
if (goodsStatus) {
this.currentStatus = goodsStatus;
this.searchForm.goodsStatus = goodsStatus;
}
this.init();
},
const id = ref(""); //要操作的id
const loading = ref(true); // 表单加载状态
const modalVisible = ref(false); // 添加或编辑显示
const createRange = ref([]);
const searchForm = ref({
// 搜索框初始化对象
pageNumber: 1, // 当前页数
pageSize: 20, // 页面大小
sort: "create_time", // 默认排序字段
order: "desc", // 默认排序方式
storeCategoryPath: "",
categoryPath: "",
skuName: "",
skuSn: "",
stockStatus: "",
stockMin: "",
stockMax: "",
priceMin: "",
priceMax: "",
});
const platformCategoryList = ref([]);
const shopCategoryList = ref([]);
const platformCategoryProps = {
value: "id",
label: "name",
children: "children",
checkStrictly: true,
emitPath: false,
};
const shopCategoryProps = {
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: false,
};
const underForm = ref({
// 下架原因
reason: "",
});
const submitLoading = ref(false); // 添加或编辑提交状态
const data = ref([]); // 表单数据
const total = ref(0); // 表单数据总数
const currentStatus = ref("ALL");
const goodsNumerData = ref({});
const selectedRows = ref([]); // 选中的行数据
const selectAll = ref(false); // 全选状态
const goodsStatusWithCount = computed(() => {
return [
{title: '全部', value: 'ALL'},
{title: `出售中${goodsNumerData.value.upperGoodsNum ? '(' + goodsNumerData.value.upperGoodsNum + ')' : ''}`, value: 'UPPER'},
{title: `仓库中${goodsNumerData.value.downGoodsNum ? '(' + goodsNumerData.value.downGoodsNum + ')' : ''}`, value: 'DOWN'},
{title: `待审核${goodsNumerData.value.auditGoodsNum ? '(' + goodsNumerData.value.auditGoodsNum + ')' : ''}`, value: 'TOBEAUDITED'},
{title: `审核未通过${goodsNumerData.value.refuseGoodsNum ? '(' + goodsNumerData.value.refuseGoodsNum + ')' : ''}`, value: 'REFUSE'}
];
});
function clearTableSelection() {
tableRef.value?.clearSelection?.();
}
function onStatusTabClick(tab) {
goodsStatusClick(tab.paneName);
}
function goodsTypeText(v) {
if (v === "PHYSICAL_GOODS") return "实物商品";
if (v === "VIRTUAL_GOODS") return "虚拟商品";
if (v === "E_COUPON") return "电子卡券"; // 卡密商品
if (v === "APPOINTMENT_GOODS") return "预约商品";
return v || "—";
}
function findTreeNodeLabel(tree, targetId, valueKey, labelKey) {
for (const node of tree || []) {
if (String(node[valueKey]) === String(targetId)) {
return node[labelKey];
}
const childLabel = findTreeNodeLabel(node.children, targetId, valueKey, labelKey);
if (childLabel) return childLabel;
}
return "";
}
function platformCategoryText(path) {
if (!path) return "—";
const ids = String(path).split(",").filter(Boolean);
if (!ids.length) return "—";
const labels = ids
.map((id) => findTreeNodeLabel(platformCategoryList.value, id, "id", "name"))
.filter(Boolean);
return labels.length ? labels.join(" / ") : "—";
}
/** 跳转卡池管理;单 SKU 商品默认取第一个 SKU(card-key-pool 需 skuId) */
async function goCardKeyPool(row) {
let skuId = row.skuId;
if (!skuId) {
try {
const res = await getQueryGoodsIdGoodsList(row.id);
if (res.success && res.result?.length) {
skuId = res.result[0].id;
} else {
Message.warning("该商品暂无 SKU,请先完善商品规格");
return;
}
} catch {
Message.error("获取商品规格失败");
return;
}
}
router.push({
path: "/card-key-pool",
query: {
skuId,
goodsId: row.id,
goodsName: row.goodsName,
},
});
}
function marketEnableText(v) {
if (v === "DOWN") return "下架";
if (v === "UPPER") return "上架";
return "";
}
function authFlagText(v) {
if (v === "TOBEAUDITED") return "待审核";
if (v === "PASS") return "通过";
if (v === "REFUSE") return "拒绝";
return "";
}
// 初始化数据
function init() {
loadFilterOptions();
getDataList();
getNumberData();
}
function loadFilterOptions() {
getShopGoodsLabelListSeller()
.then((res) => {
if (res?.success) {
shopCategoryList.value = formatShopCategory(res.result || []);
}
})
.catch(() => {});
getCategoryTree()
.then((res) => {
if (res?.success) {
platformCategoryList.value = res.result || [];
}
})
.catch(() => {});
}
function onStockStatusChange(value) {
if (value !== "IN_STOCK") {
searchForm.value.stockMin = "";
searchForm.value.stockMax = "";
}
}
function buildSearchParams(includeGoodsStatus = true) {
const { stockStatus, stockMin, stockMax, priceMin, priceMax, ...rest } = searchForm.value;
const params = { ...rest };
if (!includeGoodsStatus) {
delete params.goodsStatus;
}
if (createRange.value && createRange.value.length === 2) {
params.startDate = createRange.value[0];
params.endDate = createRange.value[1];
}
if (stockStatus === "IN_STOCK") {
params.geQuantity = stockMin !== "" && stockMin != null ? Number(stockMin) : 1;
if (stockMax !== "" && stockMax != null) {
params.leQuantity = Number(stockMax);
}
} else if (stockStatus === "OUT_OF_STOCK") {
params.geQuantity = 0;
params.leQuantity = 0;
} else {
if (stockMin !== "" && stockMin != null) params.geQuantity = Number(stockMin);
if (stockMax !== "" && stockMax != null) params.leQuantity = Number(stockMax);
}
if (priceMin !== "" && priceMax !== "") {
params.price = `${priceMin}_${priceMax}`;
} else if (priceMin !== "") {
params.price = String(priceMin);
} else if (priceMax !== "") {
params.price = `0_${priceMax}`;
}
return params;
}
function formatShopCategory(list) {
return (list || []).map((item) => ({
value: item.id,
label: item.labelName,
children:
item.children && item.children.length
? formatShopCategory(item.children)
: undefined,
}));
}
// 分页 改变页码
function changePage(v) {
searchForm.value.pageNumber = v;
getDataList();
}
// 分页 改变页数
function changePageSize(v) {
searchForm.value.pageSize = v;
getDataList();
}
// 搜索
function handleSearch() {
searchForm.value.pageNumber = 1;
searchForm.value.pageSize = 20;
getDataList();
getNumberData();
}
// 获取数据
function getDataList() {
loading.value = true;
getGoodsListData(buildSearchParams()).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function getNumberData() {
getGoodsNumerData(buildSearchParams(false)).then((res) => {
if (res.success) {
goodsNumerData.value = res.result;
}
})
}
// 编辑
function edit(v) {
id.value = v.id;
if (v.underMessage) {
underForm.value.reason = v.underMessage;
} else {
underForm.value.reason = "";
}
modalVisible.value = true;
}
// 下架
function lower() {
let params = {
goodsId: id.value,
reason: underForm.value.reason
};
lowGoods(params).then((res) => {
Modal.remove();
if (res.success) {
Message.success("操作成功");
modalVisible.value = false;
getDataList();
getNumberData(); // 添加这行
}
});
}
// 上架
function upper(v) {
Modal.confirm({
title: "确认上架",
content: "您确认要上架 " + v.goodsName + " ?",
loading: true,
onOk: () => {
let params = {
goodsId: v.id
};
upGoods(params).then((res) => {
Modal.remove();
if (res.success) {
Message.success("上架成功");
getDataList();
getNumberData(); // 添加这行
}
});
},
});
}
// 编辑商品
function editGoods(v) {
customRouterPush({
name: "goods-operation-edit",
query: { id: v.id },
});
}
// 复制商品
function copyGoods(v) {
customRouterPush({
name: "goods-operation",
query: { copyId: v.id },
});
}
// 商品状态筛选
function goodsStatusClick(name) {
if (name === "ALL" || name === "" || name === undefined || name === null) {
delete searchForm.value.goodsStatus;
currentStatus.value = "ALL";
} else {
searchForm.value.goodsStatus = name;
currentStatus.value = name;
}
selectedRows.value = [];
clearTableSelection();
getDataList();
}
function onSelectionChange(selection) {
selectedRows.value = selection;
}
// 批量上架
function batchUpper() {
if (selectedRows.value.length === 0) {
Message.warning('请先选择要上架的商品');
return;
}
const goodsNames = selectedRows.value.map(item => item.goodsName).join('、');
Modal.confirm({
title: '确认批量上架',
content: `您确认要上架以下商品吗?\n${goodsNames}`,
loading: true,
onOk: () => {
// 提取所有选中商品的ID
const goodsIds = selectedRows.value.map(item => item.id);
const params = {
goodsId: goodsIds // 传递ID数组
};
upGoods(params).then((res) => {
Modal.remove();
if (res.success) {
Message.success('批量上架成功');
selectedRows.value = [];
selectAll.value = false;
getDataList();
getNumberData();
}
}).catch(() => {
Modal.remove();
});
}
});
}
// 批量下架
function batchLower() {
if (selectedRows.value.length === 0) {
Message.warning('请先选择要下架的商品');
return;
}
const goodsNames = selectedRows.value.map(item => item.goodsName).join('、');
Modal.confirm({
title: '确认批量下架',
content: `您确认要下架以下商品吗?\n${goodsNames}`,
loading: true,
onOk: () => {
// 提取所有选中商品的ID
const goodsIds = selectedRows.value.map(item => item.id);
const params = {
goodsId: goodsIds, // 传递ID数组
reason: '批量下架操作' // 可以设置默认下架原因
};
lowGoods(params).then((res) => {
Modal.remove();
if (res.success) {
Message.success('批量下架成功');
selectedRows.value = [];
selectAll.value = false;
getDataList();
getNumberData();
}
}).catch(() => {
Modal.remove();
});
}
});
}
onMounted(() => {
const goodsStatus = route.query.goodsStatus;
if (goodsStatus) {
currentStatus.value = goodsStatus;
searchForm.value.goodsStatus = goodsStatus;
}
init();
});
</script>
<style lang="scss" scoped>
.search-form-row {

View File

@@ -9,7 +9,7 @@
</el-steps>
</div>
<!-- 第一步 选择分类 -->
<first-step ref='first' v-show="activestep === 0" @change="getFirstData"></first-step>
<first-step ref='firstRef' v-show="activestep === 0" @change="getFirstData"></first-step>
<!-- 第二步 商品详细信息 -->
<second-step ref='second' :firstData="firstData" v-if="activestep === 1"></second-step>
<!-- 第三步 发布完成 -->
@@ -18,43 +18,41 @@
</div>
</template>
<script>
import firstStep from './goodsOperationFirst'
import secondStep from './goodsOperationSec'
import thirdStep from './goodsOperationThird'
export default {
name: "addGoods",
components: {
firstStep,
secondStep,
thirdStep
},
<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import firstStep from './goodsOperationFirst'
import secondStep from './goodsOperationSec'
import thirdStep from './goodsOperationThird'
data() {
return {
/** 当前激活步骤*/
activestep: 0,
firstData: {}, // 第一步传递的数据
};
},
methods: {
// 选择商品分类回调
getFirstData (item) {
this.firstData = item;
this.activestep = 1;
}
},
mounted() {
// 编辑商品、模板、复制商品
if (this.$route.query.id || this.$route.query.draftId || this.$route.query.copyId) {
this.activestep = 1;
} else {
this.activestep = 0
this.$refs.first.selectGoodsType = true;
}
defineOptions({ name: "addGoods" });
const route = useRoute();
const firstRef = ref(null);
/** 当前激活步骤*/
const activestep = ref(0);
const firstData = ref({}); // 第一步传递的数据
// 子组件 goodsOperationSec 通过 $parent.activestep-- 回退一步
defineExpose({ activestep });
// 选择商品分类回调
function getFirstData (item) {
firstData.value = item;
activestep.value = 1;
}
onMounted(() => {
// 编辑商品、模板、复制商品
if (route.query.id || route.query.draftId || route.query.copyId) {
activestep.value = 1;
} else {
activestep.value = 0
firstRef.value.selectGoodsType = true;
}
};
});
</script>
<style lang="scss" scoped>
@import "./addGoods.scss";

View File

@@ -85,143 +85,142 @@
</div>
</div>
</template>
<script>
<script setup>
import { computed, markRaw, onMounted, ref, watch } from "vue";
import * as API_GOODS from "@/api/goods";
import { Box, Calendar, CreditCard, Ticket } from "@element-plus/icons-vue";
import { Message } from "@/utils/message";
export default {
components: { Box, Calendar, CreditCard, Ticket },
data() {
return {
selectGoodsType: false,
goodsTypeWay: [
{
title: "实物商品",
desc: "零售批发,物流配送",
type: "PHYSICAL_GOODS",
icon: "Box",
tone: "tone-physical",
},
{
title: "虚拟商品",
desc: "虚拟核验,无需物流",
type: "VIRTUAL_GOODS",
icon: "Ticket",
tone: "tone-virtual",
},
{
title: "电子卡券",
desc: "卡密自动发卡,无需物流",
type: "E_COUPON",
icon: "CreditCard",
tone: "tone-coupon",
},
{
title: "预约商品",
desc: "时段预约,到店或上门",
type: "APPOINTMENT_GOODS",
icon: "Calendar",
tone: "tone-appointment",
},
],
category: [
{ name: "", id: "" },
{ name: "", id: "" },
{ name: "", id: "" },
],
goodsType: "",
pendingGoodsType: "",
categoryListLevel1: [],
categoryListLevel2: [],
categoryListLevel3: [],
};
const emit = defineEmits(["change"]);
const selectGoodsType = ref(false);
// icon 直接存组件对象:<script setup> 下无 components 选项,字符串名无法在 <component :is> 中解析
const goodsTypeWay = ref([
{
title: "实物商品",
desc: "零售批发,物流配送",
type: "PHYSICAL_GOODS",
icon: markRaw(Box),
tone: "tone-physical",
},
computed: {
selectedTypeTitle() {
const hit = this.goodsTypeWay.find((item) => item.type === this.goodsType);
return hit ? hit.title : "";
},
{
title: "虚拟商品",
desc: "虚拟核验,无需物流",
type: "VIRTUAL_GOODS",
icon: markRaw(Ticket),
tone: "tone-virtual",
},
watch: {
selectGoodsType(val) {
if (val) {
this.syncGoodsTypeSelection();
}
},
{
title: "电子卡券",
desc: "卡密自动发卡,无需物流",
type: "E_COUPON",
icon: markRaw(CreditCard),
tone: "tone-coupon",
},
methods: {
syncGoodsTypeSelection() {
this.pendingGoodsType = this.goodsType;
},
openGoodsTypeDialog() {
this.selectGoodsType = true;
},
handleClickGoodsType(item) {
this.pendingGoodsType = item.type;
},
cancelGoodsType() {
this.syncGoodsTypeSelection();
this.selectGoodsType = false;
},
confirmGoodsType() {
if (!this.pendingGoodsType) {
this.$Message.error("请选择商品类型");
return;
}
this.goodsType = this.pendingGoodsType;
this.selectGoodsType = false;
},
handleSelectCategory(row, index, level) {
if (level === 1) {
this.category.forEach((cate) => {
(cate.name = ""), (cate.id = "");
});
this.category[0].name = row.name;
this.category[0].id = row.id;
this.categoryListLevel2 = this.categoryListLevel1[index].children;
this.categoryListLevel3 = [];
} else if (level === 2) {
this.category[1].name = row.name;
this.category[1].id = row.id;
this.category[2].name = "";
this.category[2].id = "";
this.categoryListLevel3 = this.categoryListLevel2[index].children;
} else {
this.category[2].name = row.name;
this.category[2].id = row.id;
}
},
GET_NextLevelCategory() {
API_GOODS.getGoodsCategoryAll().then((res) => {
if (res.success && res.result) {
this.categoryListLevel1 = res.result;
}
});
},
next() {
window.scrollTo(0, 0);
if (!this.goodsType) {
this.$Message.error("请选择商品类型");
this.openGoodsTypeDialog();
return;
}
if (!this.category[0].name) {
this.$Message.error("请选择商品分类");
return;
} else if (!this.category[2].name) {
this.$Message.error("必须选择到三级分类");
return;
}
this.$emit("change", {
category: this.category,
goodsType: this.goodsType,
});
},
{
title: "预约商品",
desc: "时段预约,到店或上门",
type: "APPOINTMENT_GOODS",
icon: markRaw(Calendar),
tone: "tone-appointment",
},
mounted() {
this.GET_NextLevelCategory();
},
};
]);
const category = ref([
{ name: "", id: "" },
{ name: "", id: "" },
{ name: "", id: "" },
]);
const goodsType = ref("");
const pendingGoodsType = ref("");
const categoryListLevel1 = ref([]);
const categoryListLevel2 = ref([]);
const categoryListLevel3 = ref([]);
const selectedTypeTitle = computed(() => {
const hit = goodsTypeWay.value.find((item) => item.type === goodsType.value);
return hit ? hit.title : "";
});
watch(selectGoodsType, (val) => {
if (val) {
syncGoodsTypeSelection();
}
});
function syncGoodsTypeSelection() {
pendingGoodsType.value = goodsType.value;
}
function openGoodsTypeDialog() {
selectGoodsType.value = true;
}
function handleClickGoodsType(item) {
pendingGoodsType.value = item.type;
}
function cancelGoodsType() {
syncGoodsTypeSelection();
selectGoodsType.value = false;
}
function confirmGoodsType() {
if (!pendingGoodsType.value) {
Message.error("请选择商品类型");
return;
}
goodsType.value = pendingGoodsType.value;
selectGoodsType.value = false;
}
function handleSelectCategory(row, index, level) {
if (level === 1) {
category.value.forEach((cate) => {
(cate.name = ""), (cate.id = "");
});
category.value[0].name = row.name;
category.value[0].id = row.id;
categoryListLevel2.value = categoryListLevel1.value[index].children;
categoryListLevel3.value = [];
} else if (level === 2) {
category.value[1].name = row.name;
category.value[1].id = row.id;
category.value[2].name = "";
category.value[2].id = "";
categoryListLevel3.value = categoryListLevel2.value[index].children;
} else {
category.value[2].name = row.name;
category.value[2].id = row.id;
}
}
function GET_NextLevelCategory() {
API_GOODS.getGoodsCategoryAll().then((res) => {
if (res.success && res.result) {
categoryListLevel1.value = res.result;
}
});
}
function next() {
window.scrollTo(0, 0);
if (!goodsType.value) {
Message.error("请选择商品类型");
openGoodsTypeDialog();
return;
}
if (!category.value[0].name) {
Message.error("请选择商品分类");
return;
} else if (!category.value[2].name) {
Message.error("必须选择到三级分类");
return;
}
emit("change", {
category: category.value,
goodsType: goodsType.value,
});
}
onMounted(() => {
GET_NextLevelCategory();
});
// 父组件 goodsOperation.vue 通过 ref 直接赋值 selectGoodsType 打开弹窗
defineExpose({ selectGoodsType });
</script>
<style lang="scss" scoped>
@import "./addGoods.scss";

File diff suppressed because it is too large Load Diff

View File

@@ -16,18 +16,19 @@
</div>
</div>
</template>
<script>
export default {
methods: {
// 跳转商品列表
gotoGoodsList() {
this.$router.push({name: "goods"});
},
// 刷新页面
gotoBack() {
this.$router.go();
},
}
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
// 跳转商品列表
function gotoGoodsList() {
router.push({name: "goods"});
}
// 刷新页面
function gotoBack() {
router.go();
}
</script>
<style lang="scss" scoped>

View File

@@ -207,7 +207,9 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import defaultLogo from "@/assets/logo1.png";
import { getSellerHomeData, getHomeNotice } from "@/api/index";
import { getOrderOverView, goodsStatistics } from "@/api/goods";
@@ -217,6 +219,12 @@ import { getIMDetail } from "@/api/common";
import { seeArticle } from "@/api/pages";
import Cookies from "js-cookie";
import { userMsg } from "@/api/index";
import { Message } from "@/utils/message";
import { getStore } from "@/libs/storage";
defineOptions({ name: "home" });
const router = useRouter();
const HOME_ROUTE_TARGETS = {
orderList: { name: "orderList", path: "/order/orderList" },
@@ -237,161 +245,153 @@ const HOME_ROUTE_TARGETS = {
const AFTER_SALE_RETURN_GOODS_STATUSES = ["PASS", "BUYER_RETURN"];
export default {
name: "home",
data() {
return {
defaultLogo,
noticeFlage: false,
homeData: {},
overviewToday: {},
overviewYesterday: {},
topGoodsData: [],
afterSaleNumData: {},
procurementStatusCount: {},
userData: {},
notices: "",
noticesDetail: {
title: "",
},
IMLink: "",
load: false,
};
},
methods: {
navigateAfterSale(serviceStatus) {
const routeKey = AFTER_SALE_RETURN_GOODS_STATUSES.includes(serviceStatus)
? "returnGoodsOrder"
: "returnMoneyOrder";
this.navigateTo(routeKey, { serviceStatus });
},
navigateToProcurementAudit() {
this.navigateTo("procurementPurchaseOrderList", { status: "SUBMITTED" });
},
navigateTo(key, query) {
const target = HOME_ROUTE_TARGETS[key] || { name: key };
if (target.name && this.$router.hasRoute(target.name)) {
this.$router.push({ name: target.name, query });
return;
}
if (target.path) {
const resolved = this.$router.resolve({ path: target.path, query });
if (resolved.matched.length > 0) {
this.$router.push({ path: target.path, query });
return;
}
}
this.$Message.warning("暂无权限访问该页面");
},
compareText(today, yesterday) {
const t = Number(today) || 0;
const y = Number(yesterday) || 0;
if (y === 0) {
return t > 0 ? "↑ --" : "—";
}
const rate = (((t - y) / y) * 100).toFixed(1);
if (t > y) return `↑ ${rate}%`;
if (t < y) return `↓ ${Math.abs(rate)}%`;
return "—";
},
compareClass(today, yesterday) {
const t = Number(today) || 0;
const y = Number(yesterday) || 0;
if (t > y) return "compare-up";
if (t < y) return "compare-down";
return "compare-flat";
},
// ElProgress 的 percentage 必须是 0-100 的数字,评分缺失/异常时兜底为 0
scorePercent(score) {
const n = Number(score) * 20;
return Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : 0;
},
async init() {
const userInfo = JSON.parse(Cookies.get("userInfoSeller"));
this.userData = userInfo;
const noticeFlage = ref(false);
const homeData = ref({});
const overviewToday = ref({});
const overviewYesterday = ref({});
const topGoodsData = ref([]);
const afterSaleNumData = ref({});
const procurementStatusCount = ref({});
const userData = ref({});
const notices = ref("");
const noticesDetail = ref({
title: "",
});
const IMLink = ref("");
const load = ref(false);
const res = await getHomeNotice();
if (res.success) {
this.notices = res.result.records;
}
},
async clickLinkNotices(val) {
const res = await seeArticle(val.id);
if (res.success) {
this.noticesDetail = res.result;
this.noticeFlage = true;
}
},
async im() {
const accessToken = this.getStore("accessToken");
this.load = true;
await this.getIMDetailMethods();
const userInfo = await userMsg();
this.load = false;
if (userInfo.success && this.IMLink) {
window.open(`${this.IMLink}?token=` + accessToken);
} else {
this.$Message.error("请登录后再联系客服");
}
},
async getIMDetailMethods() {
const res = await getIMDetail();
if (res.success) {
this.IMLink = res.result;
}
},
async getHomeData() {
const res = await getSellerHomeData();
if (res.success) {
this.homeData = res.result;
}
},
async loadOverview() {
const year = new Date().getFullYear();
const [todayRes, yesterdayRes] = await Promise.all([
getOrderOverView({ searchType: "TODAY", year }),
getOrderOverView({ searchType: "YESTERDAY", year }),
]);
if (todayRes.success) {
this.overviewToday = todayRes.result || {};
}
if (yesterdayRes.success) {
this.overviewYesterday = yesterdayRes.result || {};
}
},
async loadSalesRank() {
const year = new Date().getFullYear();
const res = await goodsStatistics({
searchType: "LAST_SEVEN",
year,
type: "NUM",
});
if (res.success && res.result) {
this.topGoodsData = res.result.slice(0, 10);
}
},
async loadAfterSaleNum() {
const res = await getAfterSaleNumVO({});
if (res.success) {
this.afterSaleNumData = res.result || {};
}
},
async loadProcurementStatusCount() {
const res = await getProcurementOrderStatusCount({});
if (res.success) {
this.procurementStatusCount = res.result || {};
}
},
},
mounted() {
this.init();
this.getHomeData();
this.loadOverview();
this.loadSalesRank();
this.loadAfterSaleNum();
this.loadProcurementStatusCount();
},
};
function navigateAfterSale(serviceStatus) {
const routeKey = AFTER_SALE_RETURN_GOODS_STATUSES.includes(serviceStatus)
? "returnGoodsOrder"
: "returnMoneyOrder";
navigateTo(routeKey, { serviceStatus });
}
function navigateToProcurementAudit() {
navigateTo("procurementPurchaseOrderList", { status: "SUBMITTED" });
}
function navigateTo(key, query) {
const target = HOME_ROUTE_TARGETS[key] || { name: key };
if (target.name && router.hasRoute(target.name)) {
router.push({ name: target.name, query });
return;
}
if (target.path) {
const resolved = router.resolve({ path: target.path, query });
if (resolved.matched.length > 0) {
router.push({ path: target.path, query });
return;
}
}
Message.warning("暂无权限访问该页面");
}
function compareText(today, yesterday) {
const t = Number(today) || 0;
const y = Number(yesterday) || 0;
if (y === 0) {
return t > 0 ? "↑ --" : "—";
}
const rate = (((t - y) / y) * 100).toFixed(1);
if (t > y) return `↑ ${rate}%`;
if (t < y) return `↓ ${Math.abs(rate)}%`;
return "—";
}
function compareClass(today, yesterday) {
const t = Number(today) || 0;
const y = Number(yesterday) || 0;
if (t > y) return "compare-up";
if (t < y) return "compare-down";
return "compare-flat";
}
// ElProgress 的 percentage 必须是 0-100 的数字,评分缺失/异常时兜底为 0
function scorePercent(score) {
const n = Number(score) * 20;
return Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : 0;
}
async function init() {
const userInfo = JSON.parse(Cookies.get("userInfoSeller"));
userData.value = userInfo;
const res = await getHomeNotice();
if (res.success) {
notices.value = res.result.records;
}
}
async function clickLinkNotices(val) {
const res = await seeArticle(val.id);
if (res.success) {
noticesDetail.value = res.result;
noticeFlage.value = true;
}
}
async function im() {
const accessToken = getStore("accessToken");
load.value = true;
await getIMDetailMethods();
const userInfo = await userMsg();
load.value = false;
if (userInfo.success && IMLink.value) {
window.open(`${IMLink.value}?token=` + accessToken);
} else {
Message.error("请登录后再联系客服");
}
}
async function getIMDetailMethods() {
const res = await getIMDetail();
if (res.success) {
IMLink.value = res.result;
}
}
async function getHomeData() {
const res = await getSellerHomeData();
if (res.success) {
homeData.value = res.result;
}
}
async function loadOverview() {
const year = new Date().getFullYear();
const [todayRes, yesterdayRes] = await Promise.all([
getOrderOverView({ searchType: "TODAY", year }),
getOrderOverView({ searchType: "YESTERDAY", year }),
]);
if (todayRes.success) {
overviewToday.value = todayRes.result || {};
}
if (yesterdayRes.success) {
overviewYesterday.value = yesterdayRes.result || {};
}
}
async function loadSalesRank() {
const year = new Date().getFullYear();
const res = await goodsStatistics({
searchType: "LAST_SEVEN",
year,
type: "NUM",
});
if (res.success && res.result) {
topGoodsData.value = res.result.slice(0, 10);
}
}
async function loadAfterSaleNum() {
const res = await getAfterSaleNumVO({});
if (res.success) {
afterSaleNumData.value = res.result || {};
}
}
async function loadProcurementStatusCount() {
const res = await getProcurementOrderStatusCount({});
if (res.success) {
procurementStatusCount.value = res.result || {};
}
}
onMounted(() => {
init();
getHomeData();
loadOverview();
loadSalesRank();
loadAfterSaleNum();
loadProcurementStatusCount();
});
</script>
<style lang="scss" scoped>

View File

@@ -46,120 +46,117 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { getShopListData } from "@/api/shops.js";
export default {
props: ["closeShop"],
data() {
return {
month: "",
selectedWay: {
title: "过去7天",
selected: true,
searchType: "LAST_SEVEN",
},
storeId: "",
dates: [],
params: {
pageNumber: 1,
pageSize: 100,
storeName: "",
},
dateList: [
{ title: "今天", selected: false, searchType: "TODAY" },
{ title: "昨天", selected: false, searchType: "YESTERDAY" },
{ title: "过去7天", selected: true, searchType: "LAST_SEVEN" },
{ title: "过去30天", selected: false, searchType: "LAST_THIRTY" },
],
originDateList: [
{ title: "今天", selected: false, searchType: "TODAY" },
{ title: "昨天", selected: false, searchType: "YESTERDAY" },
{ title: "过去7天", selected: true, searchType: "LAST_SEVEN" },
{ title: "过去30天", selected: false, searchType: "LAST_THIRTY" },
],
shopTotal: 0,
shopsData: [],
const props = defineProps(["closeShop"]);
const emit = defineEmits(["selected"]);
const month = ref("");
const selectedWay = ref({
title: "过去7天",
selected: true,
searchType: "LAST_SEVEN",
});
const storeId = ref("");
const dates = ref([]);
const params = reactive({
pageNumber: 1,
pageSize: 100,
storeName: "",
});
const dateList = ref([
{ title: "今天", selected: false, searchType: "TODAY" },
{ title: "昨天", selected: false, searchType: "YESTERDAY" },
{ title: "过去7天", selected: true, searchType: "LAST_SEVEN" },
{ title: "过去30天", selected: false, searchType: "LAST_THIRTY" },
]);
const originDateList = ref([
{ title: "今天", selected: false, searchType: "TODAY" },
{ title: "昨天", selected: false, searchType: "YESTERDAY" },
{ title: "过去7天", selected: true, searchType: "LAST_SEVEN" },
{ title: "过去30天", selected: false, searchType: "LAST_THIRTY" },
]);
const shopTotal = ref(0);
const shopsData = ref([]);
function getShopList() {
getShopListData(params).then((res) => {
if (res.success) {
shopTotal.value = res.result.total;
shopsData.value = res.result.records || [];
}
});
}
function changeshop() {
selectedWay.value.storeId = storeId.value;
emit("selected", selectedWay.value);
}
function getFiveYears() {
const getYear = new Date().getFullYear();
const lastFiveYear = getYear - 5;
const maxMonth = new Date().getMonth() + 1;
const dateArr = [];
for (let year = lastFiveYear; year <= getYear; year++) {
for (let month = 1; month <= 12; month++) {
if (year === getYear && month > maxMonth) {
continue;
}
dateArr.push({ year, month });
}
}
dates.value = dateArr.reverse();
}
function changeSelect(e) {
month.value = e;
if (month.value) {
dateList.value.forEach((res) => {
res.selected = false;
});
const parts = String(month.value).split("-");
selectedWay.value = {
...selectedWay.value,
year: parts[0],
month: parts[1],
searchType: "",
storeId: storeId.value,
};
},
mounted() {
this.getFiveYears();
if (!this.closeShop) this.getShopList();
},
methods: {
getShopList() {
getShopListData(this.params).then((res) => {
if (res.success) {
this.shopTotal = res.result.total;
this.shopsData = res.result.records || [];
}
});
},
changeshop() {
this.selectedWay.storeId = this.storeId;
this.$emit("selected", this.selectedWay);
},
getFiveYears() {
const getYear = new Date().getFullYear();
const lastFiveYear = getYear - 5;
const maxMonth = new Date().getMonth() + 1;
const dates = [];
for (let year = lastFiveYear; year <= getYear; year++) {
for (let month = 1; month <= 12; month++) {
if (year === getYear && month > maxMonth) {
continue;
}
dates.push({ year, month });
}
}
this.dates = dates.reverse();
},
changeSelect(e) {
this.month = e;
if (this.month) {
this.dateList.forEach((res) => {
res.selected = false;
});
const parts = String(this.month).split("-");
this.selectedWay = {
...this.selectedWay,
year: parts[0],
month: parts[1],
searchType: "",
storeId: this.storeId,
};
this.$emit("selected", this.selectedWay);
} else {
const current =
this.dateList.find((item) => item.selected) ||
this.dateList.find((item) => item.title === this.selectedWay.title) ||
this.dateList.find((item) => item.searchType === "LAST_SEVEN");
this.clickBreadcrumb(current);
}
},
clickBreadcrumb(item) {
if (!item) return;
let currentIndex;
this.dateList.forEach((res, index) => {
res.selected = false;
if (res.title === item.title) {
currentIndex = index;
}
});
item.selected = true;
item.storeId = this.storeId;
this.month = "";
if (item.searchType === "") {
const origin = this.originDateList[currentIndex];
item.searchType = (origin && origin.searchType) || "LAST_SEVEN";
}
this.selectedWay = item;
this.selectedWay.year = new Date().getFullYear();
this.selectedWay.month = "";
this.$emit("selected", this.selectedWay);
},
},
};
emit("selected", selectedWay.value);
} else {
const current =
dateList.value.find((item) => item.selected) ||
dateList.value.find((item) => item.title === selectedWay.value.title) ||
dateList.value.find((item) => item.searchType === "LAST_SEVEN");
clickBreadcrumb(current);
}
}
function clickBreadcrumb(item) {
if (!item) return;
let currentIndex;
dateList.value.forEach((res, index) => {
res.selected = false;
if (res.title === item.title) {
currentIndex = index;
}
});
item.selected = true;
item.storeId = storeId.value;
month.value = "";
if (item.searchType === "") {
const origin = originDateList.value[currentIndex];
item.searchType = (origin && origin.searchType) || "LAST_SEVEN";
}
selectedWay.value = item;
selectedWay.value.year = new Date().getFullYear();
selectedWay.value.month = "";
emit("selected", selectedWay.value);
}
onMounted(() => {
getFiveYears();
if (!props.closeShop) getShopList();
});
</script>
<style lang="scss" scoped>

View File

@@ -8,172 +8,174 @@
</div>
</div>
</template>
<script>
<script setup>
import {
computed,
onActivated,
onDeactivated,
onMounted,
onUnmounted,
ref,
watch,
} from "vue";
import { initEditor } from "@/views/lili-components/editor/config";
import { loadTinymce } from "@/views/lili-components/editor/load-tinymce";
import uploadImage from "@/views/lili-components/editor/upload-image.vue";
export default {
components:{uploadImage},
name: "Tinymce",
props: {
modelValue: {
type: String,
default: "",
},
value: {
type: String,
default: "",
},
height:{
type:String,
default:'500px'
}
},
emits: ["update:modelValue", "input"],
data() {
return {
// 引入编辑器的配置
initEditor,
hasChange: false, // 标记内容是否有更改
hasInit: false, // 标记编辑器是否已初始化
pendingContent: null, // 编辑器未就绪时暂存待回填内容
tinymceId:
"tinymce-" + +new Date() + ((Math.random() * 1000).toFixed(0) + ""), // 生成唯一的编辑器 ID
fullscreen: false, // 标记编辑器是否处于全屏模式
toolbar: [], // 工具栏配置
content: "", // 编辑器内容
};
},
computed: {
bindValue() {
return this.modelValue ?? this.value ?? "";
},
},
watch: {
bindValue: {
handler(val) {
if (!this.hasChange) {
this.setContent(val || "");
}
},
},
},
methods: {
emitContent(content) {
this.$emit("update:modelValue", content);
this.$emit("input", content);
},
getEditor() {
return window.tinymce?.get(this.tinymceId);
},
isEditorReady() {
const editor = this.getEditor();
return !!(editor && editor.initialized && editor.dom);
},
// 数据返回并给富文本框插入图片
insertImage(arr){
const editor = this.getEditor();
if (!editor || !this.isEditorReady()) {
return;
}
arr.forEach(v => editor.insertContent(`<img src="${v}" >`))
},
init() {
// 初始化编辑器
this.initTinymce();
},
initTinymce() {
if (this.getEditor()) {
return;
}
const _this = this;
window.tinymce.init({
selector: `#${this.tinymceId}`,
convert_urls: false,
init_instance_callback: (editor) => {
_this.hasInit = true;
const initial = _this.pendingContent ?? _this.bindValue;
_this.pendingContent = null;
if (initial) {
_this.applyContent(editor, initial);
}
// 不监听 NodeChange/SetContent:初始化或回填时会把空内容写回 v-model,覆盖已加载的商品描述
editor.on("input change undo redo KeyUp", () => {
_this.hasChange = true;
_this.emitContent(editor.getContent());
});
},
setup(editor) {
// 监听全屏状态变化
editor.on("FullscreenStateChanged", (e) => {
_this.fullscreen = e.state;
});
},
..._this.initEditor,
height:this.height
defineOptions({ name: "Tinymce" });
const props = defineProps({
modelValue: {
type: String,
default: "",
},
value: {
type: String,
default: "",
},
height: {
type: String,
default: "500px",
},
});
const emit = defineEmits(["update:modelValue", "input"]);
// 引入编辑器的配置(data 中的 initEditor 与 import 同名,直接使用 import)
const hasChange = ref(false); // 标记内容是否有更改
const hasInit = ref(false); // 标记编辑器是否已初始化
const pendingContent = ref(null); // 编辑器未就绪时暂存待回填内容
const tinymceId =
"tinymce-" + +new Date() + ((Math.random() * 1000).toFixed(0) + ""); // 生成唯一的编辑器 ID
const fullscreen = ref(false); // 标记编辑器是否处于全屏模式
const toolbar = ref([]); // 工具栏配置
const content = ref(""); // 编辑器内容
const bindValue = computed(() => {
return props.modelValue ?? props.value ?? "";
});
watch(bindValue, (val) => {
if (!hasChange.value) {
setContent(val || "");
}
});
function emitContent(val) {
emit("update:modelValue", val);
emit("input", val);
}
function getEditor() {
return window.tinymce?.get(tinymceId);
}
function isEditorReady() {
const editor = getEditor();
return !!(editor && editor.initialized && editor.dom);
}
// 数据返回并给富文本框插入图片
function insertImage(arr) {
const editor = getEditor();
if (!editor || !isEditorReady()) {
return;
}
arr.forEach((v) => editor.insertContent(`<img src="${v}" >`));
}
function init() {
// 初始化编辑器
initTinymce();
}
function initTinymce() {
if (getEditor()) {
return;
}
window.tinymce.init({
selector: `#${tinymceId}`,
convert_urls: false,
init_instance_callback: (editor) => {
hasInit.value = true;
const initial = pendingContent.value ?? bindValue.value;
pendingContent.value = null;
if (initial) {
applyContent(editor, initial);
}
// 不监听 NodeChange/SetContent:初始化或回填时会把空内容写回 v-model,覆盖已加载的商品描述
editor.on("input change undo redo KeyUp", () => {
hasChange.value = true;
emitContent(editor.getContent());
});
},
applyContent(editor, content) {
if (!editor || !editor.initialized || !editor.dom) {
return false;
}
try {
editor.setContent(content);
return true;
} catch (e) {
console.warn("tinymce setContent failed", e);
return false;
}
setup(editor) {
// 监听全屏状态变化
editor.on("FullscreenStateChanged", (e) => {
fullscreen.value = e.state;
});
},
setContent(value) {
const content = value || "";
this.hasChange = false;
if (!this.isEditorReady()) {
this.pendingContent = content;
return;
}
this.pendingContent = null;
this.applyContent(this.getEditor(), content);
this.emitContent(content);
},
getContent() {
const editor = this.getEditor();
if (!editor || !editor.initialized) {
return this.pendingContent ?? this.bindValue ?? "";
}
return editor.getContent();
},
destroyTinymce() {
const tinymce = this.getEditor();
...initEditor,
if (tinymce) {
// 销毁编辑器实例
tinymce.destroy();
}
},
},
mounted() {
// 编辑器脚本改为按需加载,加载完成后再初始化
height: props.height,
});
}
function applyContent(editor, val) {
if (!editor || !editor.initialized || !editor.dom) {
return false;
}
try {
editor.setContent(val);
return true;
} catch (e) {
console.warn("tinymce setContent failed", e);
return false;
}
}
function setContent(value) {
const val = value || "";
hasChange.value = false;
if (!isEditorReady()) {
pendingContent.value = val;
return;
}
pendingContent.value = null;
applyContent(getEditor(), val);
emitContent(val);
}
function getContent() {
const editor = getEditor();
if (!editor || !editor.initialized) {
return pendingContent.value ?? bindValue.value ?? "";
}
return editor.getContent();
}
function destroyTinymce() {
const tinymce = getEditor();
if (tinymce) {
// 销毁编辑器实例
tinymce.destroy();
}
}
onMounted(() => {
// 编辑器脚本改为按需加载,加载完成后再初始化
loadTinymce().then(() => {
init();
});
});
onActivated(() => {
if (window.tinymce) {
initTinymce();
} else {
loadTinymce().then(() => {
this.init();
initTinymce();
});
},
activated() {
if (window.tinymce) {
this.initTinymce();
} else {
loadTinymce().then(() => {
this.initTinymce();
});
}
},
deactivated() {
this.destroyTinymce();
},
unmounted() {
this.destroyTinymce();
},
};
}
});
onDeactivated(() => {
destroyTinymce();
});
onUnmounted(() => {
destroyTinymce();
});
// 父组件通过 ref 调用:goodsOperationSec 使用 getEditor/setContent/getContent
defineExpose({ getEditor, setContent, getContent });
</script>

View File

@@ -32,7 +32,7 @@
</vuedraggable>
<div class="upload-box">
<el-upload
ref="upload"
ref="uploadRef"
:action="uploadFileUrl"
:headers="accessToken"
:show-file-list="false"
@@ -64,7 +64,7 @@
@closed="resetOssSelection"
>
<OssManage
ref="ossManage"
ref="ossManageRef"
:is-component="true"
:initialize="showOssManager"
:hide-select-footer="true"
@@ -84,123 +84,120 @@
</el-dialog>
</div>
</template>
<script>
<script setup>
import { nextTick, onMounted, ref } from "vue";
import { Delete, Plus, ZoomIn } from "@element-plus/icons-vue";
import vuedraggable from "vuedraggable";
import { uploadFile } from "@/libs/axios";
import { getStore } from "@/libs/storage";
import { Message } from "@/utils/message";
import OssManage from "@/views/sys/oss-manage/ossManage";
export default {
name: "upload-image",
components: {
OssManage,
vuedraggable,
Delete,
Plus,
ZoomIn,
},
data() {
return {
show: false,
uploadFileUrl: uploadFile,
accessToken: {},
showOssManager: false,
images: [],
selectedImage: [],
viewImage: false,
previewUrl: "",
};
},
mounted() {
this.accessToken = {
accessToken: this.getStore("accessToken"),
};
},
methods: {
draggableItemKey(item) {
return item?.url || item;
},
handleClickUploadImage() {
this.show = true;
},
handleView(url) {
this.previewUrl = url;
this.viewImage = true;
},
callback() {
const formatImages = this.images.map((item) => item.url);
this.$emit("callback", formatImages);
this.show = false;
},
handleRemoveGoodsPicture(__index) {
this.images.splice(__index, 1);
},
handleBeforeUpload(file) {
const okType = ["image/jpeg", "image/png", "image/jpg"].includes(file.type);
if (!okType) {
this.$Message.warning("文件 " + file.name + " 的格式不正确,请选择 jpg/jpeg/png");
return false;
}
if (file.size / 1024 / 1024 > 10) {
this.$Message.warning("图片大小不能超过10MB");
return false;
}
return true;
},
handleSuccessGoodsPicture(res) {
const url = res?.result ?? res?.data?.result;
if (url) {
this.images.push({ url });
} else {
this.$Message.error(res?.message || "上传失败");
}
},
handleUploadError(err) {
this.$Message.error(err?.message || String(err));
},
confirmUrls() {
this.applySelectedImages();
this.showOssManager = false;
},
cancelOssImport() {
this.selectedImage = [];
this.showOssManager = false;
},
resetOssSelection() {
this.selectedImage = [];
},
handleOssSelected(list) {
this.selectedImage = Array.isArray(list) ? list : [];
},
parseOssSelectionUrl(item) {
if (!item) {
return "";
}
if (typeof item === "string") {
const index = item.indexOf(",");
return index >= 0 ? item.slice(index + 1) : item;
}
return item.url || "";
},
applySelectedImages() {
(this.selectedImage || []).forEach((item) => {
const url = this.parseOssSelectionUrl(item);
if (url) {
this.images.push({ url });
}
});
},
importOSS() {
this.selectedImage = [];
this.showOssManager = true;
this.$nextTick(() => {
if (this.$refs.ossManage) {
this.$refs.ossManage.selectImage = true;
}
});
},
},
};
defineOptions({ name: "upload-image" });
const emit = defineEmits(["callback"]);
const show = ref(false);
const uploadFileUrl = uploadFile;
const accessToken = ref({});
const showOssManager = ref(false);
const images = ref([]);
const selectedImage = ref([]);
const viewImage = ref(false);
const previewUrl = ref("");
// 模板 ref:upload/ossManage 原名与顶层变量无冲突,但统一改名避免与后续变量互相覆盖
const uploadRef = ref(null);
const ossManageRef = ref(null);
onMounted(() => {
accessToken.value = {
accessToken: getStore("accessToken"),
};
});
function draggableItemKey(item) {
return item?.url || item;
}
function handleClickUploadImage() {
show.value = true;
}
function handleView(url) {
previewUrl.value = url;
viewImage.value = true;
}
function callback() {
const formatImages = images.value.map((item) => item.url);
emit("callback", formatImages);
show.value = false;
}
function handleRemoveGoodsPicture(__index) {
images.value.splice(__index, 1);
}
function handleBeforeUpload(file) {
const okType = ["image/jpeg", "image/png", "image/jpg"].includes(file.type);
if (!okType) {
Message.warning("文件 " + file.name + " 的格式不正确,请选择 jpg/jpeg/png");
return false;
}
if (file.size / 1024 / 1024 > 10) {
Message.warning("图片大小不能超过10MB");
return false;
}
return true;
}
function handleSuccessGoodsPicture(res) {
const url = res?.result ?? res?.data?.result;
if (url) {
images.value.push({ url });
} else {
Message.error(res?.message || "上传失败");
}
}
function handleUploadError(err) {
Message.error(err?.message || String(err));
}
function confirmUrls() {
applySelectedImages();
showOssManager.value = false;
}
function cancelOssImport() {
selectedImage.value = [];
showOssManager.value = false;
}
function resetOssSelection() {
selectedImage.value = [];
}
function handleOssSelected(list) {
selectedImage.value = Array.isArray(list) ? list : [];
}
function parseOssSelectionUrl(item) {
if (!item) {
return "";
}
if (typeof item === "string") {
const index = item.indexOf(",");
return index >= 0 ? item.slice(index + 1) : item;
}
return item.url || "";
}
function applySelectedImages() {
(selectedImage.value || []).forEach((item) => {
const url = parseOssSelectionUrl(item);
if (url) {
images.value.push({ url });
}
});
}
function importOSS() {
selectedImage.value = [];
showOssManager.value = true;
nextTick(() => {
if (ossManageRef.value) {
ossManageRef.value.selectImage = true;
}
});
}
</script>
<style scoped lang="scss">

View File

@@ -9,7 +9,7 @@
>
<div v-loading="spinShow" class="flex">
<el-tree
ref="tree"
ref="treeRef"
class="tree"
:data="data"
:props="treeProps"
@@ -23,175 +23,184 @@
</template>
</el-dialog>
</template>
<script>
<script setup>
import { ref, reactive, nextTick, onMounted } from "vue";
import { getAllCity } from "@/api/index";
export default {
data() {
return {
switched: false,
spinShow: false,
data: [],
selectedWay: [],
callBackData: "",
treeProps: {
label: "title",
children: "children",
},
};
},
mounted() {
this.init();
},
methods: {
clear() {
this.data = [];
this.selectedWay = [];
this.init();
},
cancel() {
this.switched = false;
this.data.forEach((item) => {
item.disabled = false;
item.children.forEach((child) => {
child.disabled = false;
});
});
},
open(val, index) {
if (val) {
let checkedData = this.$store.state.shipTemplate;
let checkData = [];
let disabledData = checkedData.filter((item, i) => {
if (i != index) {
return i != index;
} else {
checkData.push(item);
}
});
checkData.forEach((check) => {
check.areaId.split(",").forEach((ids) => {
this.data.forEach((item) => {
if (check.selectedAll) {
check.area.split(",").forEach((area) => {
if (area == item.name) {
item.checked = true;
}
});
}
item.children.forEach((child) => {
if (item.checked) {
child.checked = true;
}
if (child.id == ids) {
child.checked = true;
}
});
});
});
});
disabledData.forEach((dis) => {
dis.areaId.split(",").forEach((ids) => {
this.data.forEach((item) => {
if (dis.selectedAll) {
dis.area.split(",").forEach((area) => {
if (area == item.name) {
item.disabled = true;
}
});
}
item.children.forEach((child) => {
if (item.disabled) {
child.disabled = true;
} else if (child.id == ids) {
child.disabled = true;
}
});
});
});
});
this.syncTreeCheckedKeys();
import { useAppStore } from "@/stores/app";
const emit = defineEmits(["selected"]);
const appStore = useAppStore();
const treeRef = ref(null);
const switched = ref(false);
const spinShow = ref(false);
const data = ref([]);
const selectedWay = ref([]);
const callBackData = ref("");
const treeProps = reactive({
label: "title",
children: "children",
});
onMounted(() => {
init();
});
function clear() {
data.value = [];
selectedWay.value = [];
init();
}
function cancel() {
switched.value = false;
data.value.forEach((item) => {
item.disabled = false;
item.children.forEach((child) => {
child.disabled = false;
});
});
}
function open(val, index) {
if (val) {
let checkedData = appStore.shipTemplates;
let checkData = [];
let disabledData = checkedData.filter((item, i) => {
if (i != index) {
return i != index;
} else {
checkData.push(item);
}
this.switched = true;
},
syncTreeCheckedKeys() {
this.$nextTick(() => {
const keys = [];
this.data.forEach((item) => {
if (item.checked) {
keys.push(item.id);
});
checkData.forEach((check) => {
check.areaId.split(",").forEach((ids) => {
data.value.forEach((item) => {
if (check.selectedAll) {
check.area.split(",").forEach((area) => {
if (area == item.name) {
item.checked = true;
}
});
}
item.children.forEach((child) => {
if (child.checked) {
keys.push(child.id);
if (item.checked) {
child.checked = true;
}
if (child.id == ids) {
child.checked = true;
}
});
});
if (this.$refs.tree) {
this.$refs.tree.setCheckedKeys(keys);
});
});
disabledData.forEach((dis) => {
dis.areaId.split(",").forEach((ids) => {
data.value.forEach((item) => {
if (dis.selectedAll) {
dis.area.split(",").forEach((area) => {
if (area == item.name) {
item.disabled = true;
}
});
}
item.children.forEach((child) => {
if (item.disabled) {
child.disabled = true;
} else if (child.id == ids) {
child.disabled = true;
}
});
});
});
});
syncTreeCheckedKeys();
}
switched.value = true;
}
function syncTreeCheckedKeys() {
nextTick(() => {
const keys = [];
data.value.forEach((item) => {
if (item.checked) {
keys.push(item.id);
}
item.children.forEach((child) => {
if (child.checked) {
keys.push(child.id);
}
});
},
submit() {
const checkedNodes = this.$refs.tree.getCheckedNodes(false, true);
const halfCheckedNodes = this.$refs.tree.getHalfCheckedNodes();
let list = [...checkedNodes, ...halfCheckedNodes];
});
if (treeRef.value) {
treeRef.value.setCheckedKeys(keys);
}
});
}
let sort = [];
list.forEach((item) => {
item.selectedList = [];
item.selectedAll = false;
if (item.level == "province" && !item.disabled) {
sort.push({ ...item });
}
sort.forEach((sortItem) => {
if (item.level != "province" && sortItem.id == item.parentId && !item.disabled) {
sortItem.selectedList.push({ ...item });
}
});
});
function submit() {
const checkedNodes = treeRef.value.getCheckedNodes(false, true);
const halfCheckedNodes = treeRef.value.getHalfCheckedNodes();
let list = [...checkedNodes, ...halfCheckedNodes];
this.data.forEach((whether) => {
sort.forEach((item) => {
if (
item.id == whether.id &&
item.selectedList.length == whether.children.length
) {
item.selectedList.forEach((child) => {
child.selectedAll = true;
});
item.selectedAll = true;
}
});
});
let sort = [];
list.forEach((item) => {
item.selectedList = [];
item.selectedAll = false;
if (item.level == "province" && !item.disabled) {
sort.push({ ...item });
}
sort.forEach((sortItem) => {
if (item.level != "province" && sortItem.id == item.parentId && !item.disabled) {
sortItem.selectedList.push({ ...item });
}
});
});
this.$emit("selected", sort);
this.cancel();
},
init() {
this.spinShow = true;
getAllCity()
.then((res) => {
if (res.result) {
this.data = [];
res.result.forEach((item) => {
item.children.forEach((child) => {
child.title = child.name;
});
this.data.push({
title: item.name,
...item,
});
this.selectedWay.push({ name: item.name, id: item.id });
});
this.$store.state.regions = this.data;
}
})
.finally(() => {
this.spinShow = false;
data.value.forEach((whether) => {
sort.forEach((item) => {
if (
item.id == whether.id &&
item.selectedList.length == whether.children.length
) {
item.selectedList.forEach((child) => {
child.selectedAll = true;
});
},
},
};
item.selectedAll = true;
}
});
});
emit("selected", sort);
cancel();
}
function init() {
spinShow.value = true;
getAllCity()
.then((res) => {
if (res.result) {
data.value = [];
res.result.forEach((item) => {
item.children.forEach((child) => {
child.title = child.name;
});
data.value.push({
title: item.name,
...item,
});
selectedWay.value.push({ name: item.name, id: item.id });
});
appStore.regions = data.value;
}
})
.finally(() => {
spinShow.value = false;
});
}
defineExpose({ clear, open });
</script>
<style scoped lang="scss">
.flex {

View File

@@ -109,9 +109,9 @@
</el-form>
</div>
<div ref="tableWrap" class="goods-table-wrap">
<div ref="tableWrapRef" class="goods-table-wrap">
<el-table
ref="goodsTable"
ref="goodsTableRef"
v-loading="loading"
:data="goodsData"
border
@@ -194,7 +194,8 @@
</div>
</div>
</template>
<script>
<script setup>
import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
import * as API_Goods from "@/api/goods";
const defaultFilterForm = () => ({
@@ -212,270 +213,273 @@ const defaultFilterForm = () => ({
stockMax: "",
});
export default {
emits: ["selected"],
props: {
selectedWay: {
type: Array,
default: () => [],
},
const props = defineProps({
selectedWay: {
type: Array,
default: () => [],
},
data() {
return {
type: "multiple",
selectedList: [],
platformCategoryList: [],
shopCategoryList: [],
brandList: [],
platformCategoryProps: {
value: "id",
label: "name",
children: "children",
checkStrictly: true,
emitPath: false,
},
shopCategoryProps: {
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: false,
},
filterForm: defaultFilterForm(),
total: 0,
goodsParams: {
pageNumber: 1,
pageSize: 20,
order: "desc",
marketEnable: "UPPER",
authFlag: "PASS",
sort: "createTime",
},
goodsData: [],
loading: false,
tableHeight: 400,
syncingSelection: false,
};
},
watch: {
selectedWay: {
handler(val) {
this.selectedList = Array.isArray(val) ? val.slice() : [];
},
deep: true,
immediate: true,
},
},
mounted() {
this.init();
this.$nextTick(this.calcTableHeight);
window.addEventListener("resize", this.calcTableHeight);
},
beforeUnmount() {
window.removeEventListener("resize", this.calcTableHeight);
},
methods: {
calcTableHeight() {
this.$nextTick(() => {
const wrap = this.$refs.tableWrap;
if (wrap) {
this.tableHeight = Math.max(wrap.clientHeight, 320);
}
});
},
onSearchGoods() {
this.goodsParams.pageNumber = 1;
this.getQueryGoodsList();
},
resetFilter() {
this.filterForm = defaultFilterForm();
this.onSearchGoods();
},
changePageSize(v) {
this.goodsParams.pageNumber = v;
this.getQueryGoodsList();
},
buildQueryParams() {
const params = {
...this.goodsParams,
goodsName: this.filterForm.goodsName || undefined,
goodsId: this.filterForm.goodsId || undefined,
goodsType: this.filterForm.goodsType || undefined,
brandId: this.filterForm.brandId || undefined,
storeCategoryPath: this.filterForm.storeCategoryPath || undefined,
categoryPath: this.filterForm.categoryPath || undefined,
geQuantity: undefined,
leQuantity: undefined,
geBuyCount: undefined,
leBuyCount: undefined,
price: undefined,
};
});
const emit = defineEmits(["selected"]);
const { priceMin, priceMax, salesMin, salesMax, stockMin, stockMax } = this.filterForm;
if (priceMin !== "" && priceMax !== "") {
params.price = `${priceMin}_${priceMax}`;
} else if (priceMin !== "") {
params.price = String(priceMin);
} else if (priceMax !== "") {
params.price = `0_${priceMax}`;
}
if (stockMin !== "" && stockMin != null) params.geQuantity = Number(stockMin);
if (stockMax !== "" && stockMax != null) params.leQuantity = Number(stockMax);
if (salesMin !== "" && salesMin != null) params.geBuyCount = Number(salesMin);
if (salesMax !== "" && salesMax != null) params.leBuyCount = Number(salesMax);
return params;
},
getQueryGoodsList() {
this.loading = true;
API_Goods.getGoodsSkuData(this.buildQueryParams())
.then((res) => {
this.initGoods(res);
})
.finally(() => {
this.loading = false;
this.calcTableHeight();
});
},
initGoods(res) {
const records = res?.result?.records || [];
this.total = res?.result?.total || 0;
const selectedIds = new Set(
(this.selectedList || [])
.map((e) => e.id || e.skuId)
.filter((id) => id != null)
.map(String)
);
records.forEach((item) => {
item.___type = "goods";
item.selected = selectedIds.has(String(item.id));
});
this.goodsData = records;
this.syncTableSelection();
},
syncTableSelection() {
this.$nextTick(() => {
const table = this.$refs.goodsTable;
if (!table) return;
if (this.type === "multiple") {
// clearSelection/toggleRowSelection 都会触发 selection-change,
// 同步期间忽略,避免把已选状态清掉
this.syncingSelection = true;
table.clearSelection();
this.goodsData.forEach((row) => {
if (row.selected) {
table.toggleRowSelection(row, true);
}
});
this.$nextTick(() => {
this.syncingSelection = false;
});
return;
}
const selected = this.goodsData.find((item) => item.selected);
table.setCurrentRow(selected || null);
});
},
emitSelected(list) {
this.selectedList = list;
this.$emit("selected", this.selectedList);
},
handleRowClick(row) {
if (this.type === "multiple") return;
this.goodsData.forEach((item) => {
item.selected = false;
});
row.selected = true;
this.emitSelected([row]);
this.$refs.goodsTable?.setCurrentRow(row);
},
handleSelectionChange(selection) {
if (this.type !== "multiple" || this.syncingSelection) return;
const selectedIds = new Set(selection.map((item) => String(item.id)));
this.goodsData.forEach((item) => {
item.selected = selectedIds.has(String(item.id));
});
const currentPageIds = new Set(this.goodsData.map((item) => String(item.id)));
const otherPages = this.selectedList.filter((item) => {
const id = item.id || item.skuId;
return id != null && !currentPageIds.has(String(id));
});
this.emitSelected([...otherPages, ...selection]);
},
formatShopCategory(list) {
return (list || []).map((item) => ({
value: item.id,
label: item.labelName,
children:
item.children && item.children.length ? this.formatShopCategory(item.children) : undefined,
}));
},
goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
},
brandText(brandId) {
if (!brandId) return "-";
const brand = this.brandList.find((item) => String(item.id) === String(brandId));
return brand?.name || "-";
},
findTreeNodeLabel(tree, targetId, valueKey, labelKey) {
for (const node of tree || []) {
if (String(node[valueKey]) === String(targetId)) {
return node[labelKey];
}
const childLabel = this.findTreeNodeLabel(node.children, targetId, valueKey, labelKey);
if (childLabel) return childLabel;
}
return "";
},
categoryPathText(path, tree, valueKey, labelKey) {
if (!path) return "-";
const ids = String(path).split(",").filter(Boolean);
if (!ids.length) return "-";
const labels = ids
.map((id) => this.findTreeNodeLabel(tree, id, valueKey, labelKey))
.filter(Boolean);
return labels.length ? labels.join(" / ") : "-";
},
storeCategoryText(path) {
return this.categoryPathText(path, this.shopCategoryList, "value", "label");
},
platformCategoryText(path) {
if (!path) return "-";
const ids = String(path).split(",").filter(Boolean);
if (!ids.length) return "-";
const targetId = ids.length >= 3 ? ids[2] : ids[ids.length - 1];
const label = this.findTreeNodeLabel(this.platformCategoryList, targetId, "id", "name");
return label || "-";
},
init() {
this.getQueryGoodsList();
API_Goods.getCategoryTree()
.then((res) => {
if (res?.success) {
this.platformCategoryList = res.result || [];
}
})
.catch(() => {});
API_Goods.getShopGoodsLabelListSeller()
.then((res) => {
if (res?.success) {
this.shopCategoryList = this.formatShopCategory(res.result || []);
}
})
.catch(() => {});
API_Goods.getBrandListData()
.then((res) => {
this.brandList = Array.isArray(res) ? res : res?.result || [];
})
.catch(() => {});
},
},
const type = ref("multiple");
const selectedList = ref([]);
const platformCategoryList = ref([]);
const shopCategoryList = ref([]);
const brandList = ref([]);
// 静态配置对象,仅模板读取、从不修改,无需响应式
const platformCategoryProps = {
value: "id",
label: "name",
children: "children",
checkStrictly: true,
emitPath: false,
};
const shopCategoryProps = {
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: false,
};
const filterForm = ref(defaultFilterForm());
const total = ref(0);
const goodsParams = reactive({
pageNumber: 1,
pageSize: 20,
order: "desc",
marketEnable: "UPPER",
authFlag: "PASS",
sort: "createTime",
});
const goodsData = ref([]);
const loading = ref(false);
const tableHeight = ref(400);
const syncingSelection = ref(false);
// 模板 ref:原名 tableWrap/goodsTable,统一改名 xxxRef 避免与变量同名互相覆盖
const tableWrapRef = ref(null);
const goodsTableRef = ref(null);
watch(
() => props.selectedWay,
(val) => {
selectedList.value = Array.isArray(val) ? val.slice() : [];
},
{ deep: true, immediate: true }
);
function calcTableHeight() {
nextTick(() => {
const wrap = tableWrapRef.value;
if (wrap) {
tableHeight.value = Math.max(wrap.clientHeight, 320);
}
});
}
function onSearchGoods() {
goodsParams.pageNumber = 1;
getQueryGoodsList();
}
function resetFilter() {
filterForm.value = defaultFilterForm();
onSearchGoods();
}
function changePageSize(v) {
goodsParams.pageNumber = v;
getQueryGoodsList();
}
function buildQueryParams() {
const params = {
...goodsParams,
goodsName: filterForm.value.goodsName || undefined,
goodsId: filterForm.value.goodsId || undefined,
goodsType: filterForm.value.goodsType || undefined,
brandId: filterForm.value.brandId || undefined,
storeCategoryPath: filterForm.value.storeCategoryPath || undefined,
categoryPath: filterForm.value.categoryPath || undefined,
geQuantity: undefined,
leQuantity: undefined,
geBuyCount: undefined,
leBuyCount: undefined,
price: undefined,
};
const { priceMin, priceMax, salesMin, salesMax, stockMin, stockMax } = filterForm.value;
if (priceMin !== "" && priceMax !== "") {
params.price = `${priceMin}_${priceMax}`;
} else if (priceMin !== "") {
params.price = String(priceMin);
} else if (priceMax !== "") {
params.price = `0_${priceMax}`;
}
if (stockMin !== "" && stockMin != null) params.geQuantity = Number(stockMin);
if (stockMax !== "" && stockMax != null) params.leQuantity = Number(stockMax);
if (salesMin !== "" && salesMin != null) params.geBuyCount = Number(salesMin);
if (salesMax !== "" && salesMax != null) params.leBuyCount = Number(salesMax);
return params;
}
function getQueryGoodsList() {
loading.value = true;
API_Goods.getGoodsSkuData(buildQueryParams())
.then((res) => {
initGoods(res);
})
.finally(() => {
loading.value = false;
calcTableHeight();
});
}
function initGoods(res) {
const records = res?.result?.records || [];
total.value = res?.result?.total || 0;
const selectedIds = new Set(
(selectedList.value || [])
.map((e) => e.id || e.skuId)
.filter((id) => id != null)
.map(String)
);
records.forEach((item) => {
item.___type = "goods";
item.selected = selectedIds.has(String(item.id));
});
goodsData.value = records;
syncTableSelection();
}
function syncTableSelection() {
nextTick(() => {
const table = goodsTableRef.value;
if (!table) return;
if (type.value === "multiple") {
// clearSelection/toggleRowSelection 都会触发 selection-change,
// 同步期间忽略,避免把已选状态清掉
syncingSelection.value = true;
table.clearSelection();
goodsData.value.forEach((row) => {
if (row.selected) {
table.toggleRowSelection(row, true);
}
});
nextTick(() => {
syncingSelection.value = false;
});
return;
}
const selected = goodsData.value.find((item) => item.selected);
table.setCurrentRow(selected || null);
});
}
function emitSelected(list) {
selectedList.value = list;
emit("selected", selectedList.value);
}
function handleRowClick(row) {
if (type.value === "multiple") return;
goodsData.value.forEach((item) => {
item.selected = false;
});
row.selected = true;
emitSelected([row]);
goodsTableRef.value?.setCurrentRow(row);
}
function handleSelectionChange(selection) {
if (type.value !== "multiple" || syncingSelection.value) return;
const selectedIds = new Set(selection.map((item) => String(item.id)));
goodsData.value.forEach((item) => {
item.selected = selectedIds.has(String(item.id));
});
const currentPageIds = new Set(goodsData.value.map((item) => String(item.id)));
const otherPages = selectedList.value.filter((item) => {
const id = item.id || item.skuId;
return id != null && !currentPageIds.has(String(id));
});
emitSelected([...otherPages, ...selection]);
}
function formatShopCategory(list) {
return (list || []).map((item) => ({
value: item.id,
label: item.labelName,
children:
item.children && item.children.length ? formatShopCategory(item.children) : undefined,
}));
}
function goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
}
function brandText(brandId) {
if (!brandId) return "-";
const brand = brandList.value.find((item) => String(item.id) === String(brandId));
return brand?.name || "-";
}
function findTreeNodeLabel(tree, targetId, valueKey, labelKey) {
for (const node of tree || []) {
if (String(node[valueKey]) === String(targetId)) {
return node[labelKey];
}
const childLabel = findTreeNodeLabel(node.children, targetId, valueKey, labelKey);
if (childLabel) return childLabel;
}
return "";
}
function categoryPathText(path, tree, valueKey, labelKey) {
if (!path) return "-";
const ids = String(path).split(",").filter(Boolean);
if (!ids.length) return "-";
const labels = ids
.map((id) => findTreeNodeLabel(tree, id, valueKey, labelKey))
.filter(Boolean);
return labels.length ? labels.join(" / ") : "-";
}
function storeCategoryText(path) {
return categoryPathText(path, shopCategoryList.value, "value", "label");
}
function platformCategoryText(path) {
if (!path) return "-";
const ids = String(path).split(",").filter(Boolean);
if (!ids.length) return "-";
const targetId = ids.length >= 3 ? ids[2] : ids[ids.length - 1];
const label = findTreeNodeLabel(platformCategoryList.value, targetId, "id", "name");
return label || "-";
}
function init() {
getQueryGoodsList();
API_Goods.getCategoryTree()
.then((res) => {
if (res?.success) {
platformCategoryList.value = res.result || [];
}
})
.catch(() => {});
API_Goods.getShopGoodsLabelListSeller()
.then((res) => {
if (res?.success) {
shopCategoryList.value = formatShopCategory(res.result || []);
}
})
.catch(() => {});
API_Goods.getBrandListData()
.then((res) => {
brandList.value = Array.isArray(res) ? res : res?.result || [];
})
.catch(() => {});
}
onMounted(() => {
init();
nextTick(calcTableHeight);
window.addEventListener("resize", calcTableHeight);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", calcTableHeight);
});
// 父组件(lili-dialog/index.vue 的 $refs.goodsDialog、link-dialog.vue 的 $refs["lili-component"])
// 通过实例写入 type 切换单选/多选;expose 的 ref 外部读写等价于原 data 字段
defineExpose({ type });
</script>
<style scoped lang="scss">
@import "./style.scss";

View File

@@ -12,7 +12,7 @@
>
<goodsDialog
v-if="goodsVisible"
ref="goodsDialog"
ref="goodsDialogRef"
:selectedWay="goodsData"
@selected="
(val) => {
@@ -51,86 +51,96 @@
</template>
</el-dialog>
</template>
<script>
<script setup>
import { computed, ref } from "vue";
import goodsDialog from "./goods-dialog";
import linkDialog from "./link-dialog";
export default {
components: {
goodsDialog,
linkDialog,
const emit = defineEmits(["closeFlag", "selectedGoodsData", "selectedLink"]);
const goodsVisible = ref(false);
const linkVisible = ref(false);
const goodsFlag = ref(false);
const goodsData = ref([]);
const linkData = ref("");
// 模板 ref:原 ref="goodsDialog" 与 import 的组件同名,改名避免互相覆盖
const goodsDialogRef = ref(null);
const flag = computed({
get() {
return goodsVisible.value || linkVisible.value;
},
emits: ["closeFlag", "selectedGoodsData", "selectedLink"],
data() {
return {
goodsVisible: false,
linkVisible: false,
goodsFlag: false,
goodsData: [],
linkData: "",
};
},
computed: {
flag: {
get() {
return this.goodsVisible || this.linkVisible;
},
set(val) {
if (val) {
if (this.goodsFlag) {
this.goodsVisible = true;
} else {
this.linkVisible = true;
}
} else {
this.goodsVisible = false;
this.linkVisible = false;
}
},
},
},
methods: {
clearGoodsSelected() {
this.goodsData = [];
},
clickClose() {
this.goodsVisible = false;
this.linkVisible = false;
this.goodsFlag = false;
this.$emit("closeFlag", false);
},
singleGoods() {
const timer = setInterval(() => {
if (this.$refs.goodsDialog) {
this.$refs.goodsDialog.type = "single";
clearInterval(timer);
}
}, 100);
},
clickOK() {
if (this.goodsVisible) {
this.$emit("selectedGoodsData", this.goodsData);
} else if (this.linkVisible) {
this.$emit("selectedLink", this.linkData);
}
this.clickClose();
},
open(type, mutiple) {
if (type == "goods") {
this.goodsFlag = true;
this.goodsVisible = true;
if (mutiple) {
this.singleGoods();
}
set(val) {
if (val) {
if (goodsFlag.value) {
goodsVisible.value = true;
} else {
this.goodsFlag = false;
this.linkVisible = true;
linkVisible.value = true;
}
},
close() {
this.clickClose();
},
} else {
goodsVisible.value = false;
linkVisible.value = false;
}
},
};
});
function clearGoodsSelected() {
goodsData.value = [];
}
function clickClose() {
goodsVisible.value = false;
linkVisible.value = false;
goodsFlag.value = false;
emit("closeFlag", false);
}
function singleGoods() {
const timer = setInterval(() => {
if (goodsDialogRef.value) {
goodsDialogRef.value.type = "single";
clearInterval(timer);
}
}, 100);
}
function clickOK() {
if (goodsVisible.value) {
emit("selectedGoodsData", goodsData.value);
} else if (linkVisible.value) {
emit("selectedLink", linkData.value);
}
clickClose();
}
function open(type, mutiple) {
if (type == "goods") {
goodsFlag.value = true;
goodsVisible.value = true;
if (mutiple) {
singleGoods();
}
} else {
goodsFlag.value = false;
linkVisible.value = true;
}
}
function close() {
clickClose();
}
// 外部通过 ref 访问的成员(grep seller/src 全量确认):
// - open:所有调用方打开弹窗(goods/link)
// - singleGoods:shop/modelList/forYour.vue、mixModel.vue 直接调用
// - clearGoodsSelected:shop/wap/decorate.vue、modelList/goodsAndType.vue 调用
// - goodsData:促销/采购/装修多处回写已选商品
// - goodsFlag:shop/wap/decorate.vue 直接写入
// - flag(可写 computed):shop/wap/decorate.vue 写入触发弹层开关
defineExpose({
open,
singleGoods,
clearGoodsSelected,
goodsData,
goodsFlag,
flag,
});
</script>
<style scoped lang="scss">
.lili-goods-drawer {

View File

@@ -8,7 +8,7 @@
:key="i"
>
<component
ref="lili-component"
ref="liliComponentRef"
:is="templateWay[item.name]"
@selected="
(val) => {
@@ -20,48 +20,43 @@
</el-tabs>
</div>
</template>
<script>
import wap from "./wap.js";
<script setup>
import { nextTick, onMounted, ref, watch } from "vue";
import wapList from "./wap.js";
import goodsDialog from "./goods-dialog";
import templateWay from "./template/index";
export default {
emits: ["selectedLink"],
components: {
goodsDialog,
const emit = defineEmits(["selectedLink"]);
const changed = ref("");
const selected = ref(0);
const selectedLink = ref("");
// wap 为共享的模块级数组,保持原 data 引用同一数组的行为
const wap = ref(wapList);
const activeTab = ref(wapList[0]?.title || "");
// v-for 内模板 ref 收集为数组
const liliComponentRef = ref([]);
watch(
changed,
(val) => {
emit("selectedLink", val[0]);
},
setup() {
return { templateWay };
},
data() {
return {
changed: "",
selected: 0,
selectedLink: "",
wap,
activeTab: wap[0]?.title || "",
};
},
watch: {
changed: {
handler(val) {
this.$emit("selectedLink", val[0]);
},
deep: true,
},
},
mounted() {
this.$nextTick(() => {
if (this.$refs["lili-component"]?.[0]) {
this.$refs["lili-component"][0].type = "single";
}
});
this.wap.forEach((item) => {
if (item) {
item.selected = false;
}
});
},
};
{ deep: true }
);
onMounted(() => {
nextTick(() => {
if (liliComponentRef.value?.[0]) {
liliComponentRef.value[0].type = "single";
}
});
wap.value.forEach((item) => {
if (item) {
item.selected = false;
}
});
});
</script>
<style scoped lang="scss">
@import "./style.scss";

View File

@@ -26,91 +26,91 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { getCategoryTree } from "@/api/goods.js";
export default {
data() {
return {
parentIndex: '', // 分类父级下标
secondIndex: '', // 分类二级下标
thirdIndex: '', // 分类三级下标
categoryList: [], // 分类列表一级数据
secondLevel: [], // 分类列表二级数据
thirdLevel: [], // 分类列表三级数据
selectedData: "", // 已选分类数据
};
},
mounted() {
this.init();
},
methods: {
loadCategoryFromCache() {
try {
const raw = localStorage.getItem("category");
if (!raw) {
return null;
}
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : null;
} catch {
return null;
}
},
applyCategoryList(category) {
if (!Array.isArray(category)) {
this.categoryList = [];
return;
}
category.forEach((item) => {
item.___type = "category";
});
this.categoryList = category;
},
// 点击一级
handleClickChild(item, index) {
this.parentIndex = index;
this.secondLevel = item.children || [];
item.___type = "category";
item.allId = item.id;
const emit = defineEmits(["selected"]);
this.secondIndex = '';
this.thirdIndex = '';
this.thirdLevel = []
this.$emit("selected", [item]);
},
// 点击二级
handleClickSecondChild(second, index) {
second.___type = "category";
second.allId = `${second.parentId},${second.id}`
// 无 defineExpose:grep seller/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关;
// shop/wap/decorate.vue 仅通过 @selected 事件交互)
const parentIndex = ref(''); // 分类父级下标
const secondIndex = ref(''); // 分类二级下标
const thirdIndex = ref(''); // 分类三级下标
const categoryList = ref([]); // 分类列表一级数据
const secondLevel = ref([]); // 分类列表二级数据
const thirdLevel = ref([]); // 分类列表三级数据
const selectedData = ref(""); // 已选分类数据
this.secondIndex = index;
this.thirdLevel = second.children || [];
this.thirdIndex = '';
this.$emit("selected", [second]);
},
// 点击三级
handleClickthirdChild(item, index) {
item.___type = "category";
item.allId = `${this.categoryList[this.parentIndex].id},${item.parentId},${item.id}`
this.$emit("selected", [item]);
this.thirdIndex = index;
},
init() {
const cached = this.loadCategoryFromCache();
if (cached) {
this.applyCategoryList(cached);
return;
}
getCategoryTree().then((res) => {
if (res.success && Array.isArray(res.result)) {
localStorage.setItem("category", JSON.stringify(res.result));
this.applyCategoryList(res.result);
}
});
},
},
};
function loadCategoryFromCache() {
try {
const raw = localStorage.getItem("category");
if (!raw) {
return null;
}
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : null;
} catch {
return null;
}
}
function applyCategoryList(category) {
if (!Array.isArray(category)) {
categoryList.value = [];
return;
}
category.forEach((item) => {
item.___type = "category";
});
categoryList.value = category;
}
// 点击一级
function handleClickChild(item, index) {
parentIndex.value = index;
secondLevel.value = item.children || [];
item.___type = "category";
item.allId = item.id;
secondIndex.value = '';
thirdIndex.value = '';
thirdLevel.value = []
emit("selected", [item]);
}
// 点击二级
function handleClickSecondChild(second, index) {
second.___type = "category";
second.allId = `${second.parentId},${second.id}`
secondIndex.value = index;
thirdLevel.value = second.children || [];
thirdIndex.value = '';
emit("selected", [second]);
}
// 点击三级
function handleClickthirdChild(item, index) {
item.___type = "category";
item.allId = `${categoryList.value[parentIndex.value].id},${item.parentId},${item.id}`
emit("selected", [item]);
thirdIndex.value = index;
}
function init() {
const cached = loadCategoryFromCache();
if (cached) {
applyCategoryList(cached);
return;
}
getCategoryTree().then((res) => {
if (res.success && Array.isArray(res.result)) {
localStorage.setItem("category", JSON.stringify(res.result));
applyCategoryList(res.result);
}
});
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
.list {

View File

@@ -44,8 +44,9 @@
</el-row>
</div>
</template>
<script>
import { markRaw } from "vue";
<script setup>
import { markRaw, ref } from "vue";
import { useRoute } from "vue-router";
import {
House,
ShoppingCart,
@@ -60,52 +61,52 @@ import {
Link,
} from "@element-plus/icons-vue";
export default {
data() {
return {
linkList: [
{ title: "首页", icon: markRaw(House), ___type: "home" },
{ title: "购物车", icon: markRaw(ShoppingCart), ___type: "cart" },
{ title: "收藏商品", icon: markRaw(Star), ___type: "collection" },
{ title: "我的订单", icon: markRaw(Document), ___type: "order" },
{ title: "个人中心", icon: markRaw(User), ___type: "user" },
{ title: "拼团频道", icon: markRaw(Promotion), ___type: "group" },
{ title: "秒杀频道", icon: markRaw(Promotion), ___type: "seckill" },
{ title: "领券中心", icon: markRaw(PriceTag), ___type: "coupon" },
{ title: "签到", icon: markRaw(Sunny), ___type: "sign" },
{ title: "砍价", icon: markRaw(Share), ___type: "kanjia" },
{ title: "积分商城", icon: markRaw(ShoppingBag), ___type: "point" },
],
linkItem: {
title: "外部链接",
icon: markRaw(Link),
___type: "link",
url: "",
},
linkVisible: false,
linkPopoverVisible: false,
selectedIndex: 9999999,
};
},
methods: {
showLinkItem(item) {
return (
(item.title !== "拼团频道" && item.title !== "签到") ||
this.$route.name !== "renovation"
);
},
handleLink(val, index) {
val = { ...val, ___type: "other" };
this.selectedIndex = index;
if (index === this.linkList.length) {
this.linkPopoverVisible = true;
} else {
this.linkPopoverVisible = false;
}
this.$emit("selected", [val]);
},
},
};
const emit = defineEmits(["selected"]);
const route = useRoute();
const linkList = ref([
{ title: "首页", icon: markRaw(House), ___type: "home" },
{ title: "购物车", icon: markRaw(ShoppingCart), ___type: "cart" },
{ title: "收藏商品", icon: markRaw(Star), ___type: "collection" },
{ title: "我的订单", icon: markRaw(Document), ___type: "order" },
{ title: "个人中心", icon: markRaw(User), ___type: "user" },
{ title: "拼团频道", icon: markRaw(Promotion), ___type: "group" },
{ title: "秒杀频道", icon: markRaw(Promotion), ___type: "seckill" },
{ title: "领券中心", icon: markRaw(PriceTag), ___type: "coupon" },
{ title: "签到", icon: markRaw(Sunny), ___type: "sign" },
{ title: "砍价", icon: markRaw(Share), ___type: "kanjia" },
{ title: "积分商城", icon: markRaw(ShoppingBag), ___type: "point" },
]);
const linkItem = ref({
title: "外部链接",
icon: markRaw(Link),
___type: "link",
url: "",
});
const linkVisible = ref(false);
const linkPopoverVisible = ref(false);
const selectedIndex = ref(9999999);
function showLinkItem(item) {
return (
(item.title !== "拼团频道" && item.title !== "签到") ||
route.name !== "renovation"
);
}
function handleLink(val, index) {
val = { ...val, ___type: "other" };
selectedIndex.value = index;
if (index === linkList.value.length) {
linkPopoverVisible.value = true;
} else {
linkPopoverVisible.value = false;
}
emit("selected", [val]);
}
// 无 defineExpose:grep seller/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关)
</script>
<style lang="scss" scoped>
@import "../style.scss";

View File

@@ -12,25 +12,20 @@
</div>
</template>
<script>
<script setup>
// import Article from "@/views/page/article-manage/articleList.vue";
export default {
components: {
// Article,
},
data() {
return {};
},
methods: {
callbackArticle(val) {
val.___type = "pages";
val.___path = "/pages/passport/article";
const emit = defineEmits(["selected"]);
this.$emit("selected", [val]);
},
},
};
// 无 defineExpose:grep seller/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关)
function callbackArticle(val) {
val.___type = "pages";
val.___path = "/pages/passport/article";
emit("selected", [val]);
}
</script>
<style lang="scss" scoped>
:deep(.el-card__body) {

View File

@@ -56,53 +56,50 @@
</div>
</div>
</template>
<script>
<script setup>
import { reactive, ref } from "vue";
import { getShopListData } from "@/api/shops.js";
export default {
data() {
return {
loading: false,
total: 0,
params: {
pageNumber: 1,
pageSize: 12,
storeDisable: "OPEN",
storeName: "",
},
shopsData: [],
selected: 9999999999,
};
},
created() {
this.init();
},
methods: {
resetSearch() {
this.shopsData = [];
this.params.pageNumber = 1;
this.init();
},
changePageSize(v) {
this.params.pageNumber = v;
this.init();
},
init() {
this.loading = true;
getShopListData(this.params).then((res) => {
if (res.success) {
this.total = res.result.total;
this.shopsData = res.result.records;
}
this.loading = false;
});
},
clickShop(val, i) {
this.selected = i;
val = { ...val, ___type: "shops" };
this.$emit("selected", [val]);
},
},
};
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";

View File

@@ -1,5 +1,5 @@
<template>
<div class="login" @click="$refs.verify.show = false">
<div class="login" @click="hideVerify">
<div class="login-hero">
<img class="login-bg" :src="loginBg" alt="" />
<div class="login-panel" @keyup.enter="submitLogin">
@@ -10,7 +10,7 @@
<el-tabs v-model="loginType" class="login-tabs">
<el-tab-pane label="账号登录" name="passwordLogin">
<el-form
ref="usernameLoginForm"
ref="usernameLoginFormRef"
:model="form"
:rules="rules"
class="form"
@@ -46,11 +46,11 @@
</el-tab-pane>
<el-tab-pane label="手机登录" name="mobileLogin">
<el-form
ref="formSms"
ref="formSmsRef"
:model="formSms"
:rules="ruleInline"
class="form"
@click.self="$refs.verify.show = false"
@click.self="hideVerify"
>
<el-form-item prop="mobile">
<el-input
@@ -105,7 +105,7 @@
</el-button>
<verify
ref="verify"
ref="verifyRef"
class="verify-con"
verifyType="LOGIN"
@change="verifyChange"
@@ -117,215 +117,220 @@
</div>
</template>
<script>
<script setup>
import { computed, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { User, Lock, Iphone, Message } from "@element-plus/icons-vue";
import * as RegExp from "@/utils/regular.js";
import { sendSms } from "@/api/common.js";
import { login, storeSmsLogin, userMsg } from "@/api/index";
import util from "@/libs/util.js";
import { setStore } from "@/libs/storage";
import { Message as $Message } from "@/utils/message";
import { md5 } from "@/utils/md5.js";
import Footer from "@/views/main-components/footer";
import Header from "@/views/main-components/header";
import verify from "@/views/my-components/verify";
import Cookies from "js-cookie";
import { useAppStore } from "@/stores/app";
export default {
components: {
Header,
Footer,
verify,
User,
Lock,
Iphone,
Message,
},
data() {
return {
loginBg: require("@/assets/login-bg.jpg"),
loginLogo: require("@/assets/logo-lilishop.png"),
saveLogin: true,
sendCodeLoading: false,
loading: false,
verifyStatus: false,
smsCodeSent: false,
time: 60,
loginType: "passwordLogin",
form: {
username: "",
password: "",
mobile: "",
code: "",
},
formSms: {
mobile: "",
code: "",
},
rules: {
username: [{ required: true, message: "账号不能为空", trigger: "blur" }],
password: [{ required: true, message: "密码不能为空", trigger: "blur" }],
},
ruleInline: {
mobile: [
{ required: true, message: "请输入手机号码" },
{
pattern: RegExp.mobile,
message: "请输入正确的手机号",
},
],
code: [{ required: true, message: "请输入手机验证码" }],
},
codeMsg: "发送验证码",
};
},
computed: {
isMobileValid() {
return RegExp.mobile.test(this.formSms.mobile);
},
canSubmitLogin() {
if (this.loginType === "passwordLogin") {
return !!(this.form.username.trim() && this.form.password.trim());
}
return this.smsCodeSent;
},
},
watch: {
"formSms.mobile"() {
this.smsCodeSent = false;
this.verifyStatus = false;
this.formSms.code = "";
},
},
created() {
window.localStorage.setItem("menuData", "");
},
methods: {
afterLogin(res) {
const accessToken = res.result.accessToken;
this.setStore("accessToken", accessToken);
this.setStore("refreshToken", res.result.refreshToken);
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
userMsg()
.then((res) => {
if (res.success) {
this.setStore("saveLogin", this.saveLogin);
if (this.saveLogin) {
Cookies.set("userInfoSeller", JSON.stringify(res.result), {
expires: 7,
});
} else {
Cookies.set("userInfoSeller", JSON.stringify(res.result));
}
util.initRouter(this);
this.$store.commit("setAvatarPath", res.result.storeLogo);
const redirectRouter = this.$route.query.redirect;
this.$router.push({
path: redirectRouter || "/home",
});
} else {
this.loading = false;
}
})
.catch(() => {
this.loading = false;
});
},
sendCode() {
if (!this.isMobileValid) {
this.$Message.warning("请输入正确的手机号");
return;
}
if (!this.verifyStatus) {
this.$refs.verify.init();
return;
}
if (this.time === 60) {
this.sendCodeLoading = true;
const params = {
mobile: this.formSms.mobile,
verificationEnums: "LOGIN",
};
sendSms(params)
.then((res) => {
if (res.success) {
this.smsCodeSent = true;
this.$Message.success("验证码发送成功");
const that = this;
this.interval = setInterval(() => {
that.time--;
if (that.time === 0) {
this.sendCodeLoading = false;
that.time = 60;
that.codeMsg = "重新发送";
that.verifyStatus = false;
clearInterval(that.interval);
} else {
that.codeMsg = that.time;
}
}, 1000);
} else {
this.$Message.warning(res.message);
}
})
.catch(() => {
this.sendCodeLoading = false;
});
}
},
submitLogin() {
if (!this.canSubmitLogin) return;
if (this.loginType === "passwordLogin") {
this.$refs.usernameLoginForm.validate((valid) => {
if (valid) {
this.$refs.verify.init();
}
});
} else if (this.loginType === "mobileLogin") {
this.$refs.formSms.validate((valid) => {
if (valid) {
this.loading = true;
storeSmsLogin(this.formSms)
.then((res) => {
this.loading = false;
if (res.success) {
this.afterLogin(res);
}
})
.catch(() => {
this.loading = false;
});
}
});
}
},
verifyChange(con) {
if (!con.status) return;
if (this.loginType === "passwordLogin") {
this.loading = true;
const fd = new FormData();
fd.append("username", this.form.username);
fd.append("password", this.md5(this.form.password));
login(fd)
.then((res) => {
this.loading = false;
if (res && res.success) {
this.afterLogin(res);
}
})
.catch(() => {
this.loading = false;
});
} else {
this.verifyStatus = true;
this.sendCode();
}
this.$refs.verify.show = false;
},
},
const loginBg = require("@/assets/login-bg.jpg");
const loginLogo = require("@/assets/logo-lilishop.png");
const saveLogin = ref(true);
const sendCodeLoading = ref(false);
const loading = ref(false);
const verifyStatus = ref(false);
const smsCodeSent = ref(false);
const time = ref(60);
const loginType = ref("passwordLogin");
const form = reactive({
username: "",
password: "",
mobile: "",
code: "",
});
const formSms = reactive({
mobile: "",
code: "",
});
const rules = {
username: [{ required: true, message: "账号不能为空", trigger: "blur" }],
password: [{ required: true, message: "密码不能为空", trigger: "blur" }],
};
const ruleInline = {
mobile: [
{ required: true, message: "请输入手机号码" },
{
pattern: RegExp.mobile,
message: "请输入正确的手机号",
},
],
code: [{ required: true, message: "请输入手机验证码" }],
};
const codeMsg = ref("发送验证码");
let interval = null;
const usernameLoginFormRef = ref(null);
const formSmsRef = ref(null);
const verifyRef = ref(null);
const isMobileValid = computed(() => {
return RegExp.mobile.test(formSms.mobile);
});
const canSubmitLogin = computed(() => {
if (loginType.value === "passwordLogin") {
return !!(form.username.trim() && form.password.trim());
}
return smsCodeSent.value;
});
watch(
() => formSms.mobile,
() => {
smsCodeSent.value = false;
verifyStatus.value = false;
formSms.code = "";
}
);
window.localStorage.setItem("menuData", "");
function hideVerify() {
verifyRef.value.show = false;
}
function afterLogin(res) {
const accessToken = res.result.accessToken;
setStore("accessToken", accessToken);
setStore("refreshToken", res.result.refreshToken);
userMsg()
.then((res) => {
if (res.success) {
setStore("saveLogin", saveLogin.value);
if (saveLogin.value) {
Cookies.set("userInfoSeller", JSON.stringify(res.result), {
expires: 7,
});
} else {
Cookies.set("userInfoSeller", JSON.stringify(res.result));
}
util.initRouter();
appStore.setAvatarPath(res.result.storeLogo);
const redirectRouter = route.query.redirect;
router.push({
path: redirectRouter || "/home",
});
} else {
loading.value = false;
}
})
.catch(() => {
loading.value = false;
});
}
function sendCode() {
if (!isMobileValid.value) {
$Message.warning("请输入正确的手机号");
return;
}
if (!verifyStatus.value) {
verifyRef.value.init();
return;
}
if (time.value === 60) {
sendCodeLoading.value = true;
const params = {
mobile: formSms.mobile,
verificationEnums: "LOGIN",
};
sendSms(params)
.then((res) => {
if (res.success) {
smsCodeSent.value = true;
$Message.success("验证码发送成功");
interval = setInterval(() => {
time.value--;
if (time.value === 0) {
sendCodeLoading.value = false;
time.value = 60;
codeMsg.value = "重新发送";
verifyStatus.value = false;
clearInterval(interval);
} else {
codeMsg.value = time.value;
}
}, 1000);
} else {
$Message.warning(res.message);
}
})
.catch(() => {
sendCodeLoading.value = false;
});
}
}
function submitLogin() {
if (!canSubmitLogin.value) return;
if (loginType.value === "passwordLogin") {
usernameLoginFormRef.value.validate((valid) => {
if (valid) {
verifyRef.value.init();
}
});
} else if (loginType.value === "mobileLogin") {
formSmsRef.value.validate((valid) => {
if (valid) {
loading.value = true;
storeSmsLogin(formSms)
.then((res) => {
loading.value = false;
if (res.success) {
afterLogin(res);
}
})
.catch(() => {
loading.value = false;
});
}
});
}
}
function verifyChange(con) {
if (!con.status) return;
if (loginType.value === "passwordLogin") {
loading.value = true;
const fd = new FormData();
fd.append("username", form.username);
fd.append("password", md5(form.password));
login(fd)
.then((res) => {
loading.value = false;
if (res && res.success) {
afterLogin(res);
}
})
.catch(() => {
loading.value = false;
});
} else {
verifyStatus.value = true;
sendCode();
}
hideVerify();
}
</script>
<style lang="scss" scoped>

View File

@@ -17,39 +17,37 @@
</div>
</template>
<script>
import { mapState } from "vuex";
<script setup>
import { computed, ref, watch } from "vue";
import { useSettingStore } from "@/stores/setting";
import { setStore } from "@/libs/storage";
export default {
name: "configDrawer",
data() {
return {
drawer: false,
};
defineOptions({ name: "configDrawer" });
const settingStore = useSettingStore();
const drawer = ref(false);
const setting = computed(() => settingStore.setting);
watch(
setting,
(val) => {
setStore("setting", val);
settingStore.updateSetting(val);
},
computed: {
...mapState({
setting: (state) => state.setting.setting,
}),
},
watch: {
setting: {
handler(val) {
this.setStore("setting", val);
this.$store.commit("updateSetting", val);
},
deep: true,
},
},
methods: {
open() {
this.drawer = true;
},
close() {
this.drawer = false;
},
},
};
{ deep: true }
);
function open() {
drawer.value = true;
}
function close() {
drawer.value = false;
}
defineExpose({ open, close });
</script>
<style lang="scss" scoped>

View File

@@ -32,18 +32,11 @@
</div>
</template>
<script>
const config = require("@/config/index");
<script setup>
import config from "@/config/index";
import zhizhaoImg from "@/assets/images/zhizhao.jpg";
export default {
data() {
return {
config,
zhizhaoImg: require("@/assets/images/zhizhao.jpg"),
year: new Date().getFullYear(),
};
},
};
const year = new Date().getFullYear();
</script>
<style lang="scss" scoped>

View File

@@ -9,93 +9,90 @@
</div>
</template>
<script>
<script setup>
import { computed, onBeforeUnmount } from "vue";
import { FullScreen, ScaleToOriginal } from "@element-plus/icons-vue";
export default {
name: "fullScreen",
components: { FullScreen, ScaleToOriginal },
props: {
modelValue: {
type: Boolean,
default: false,
},
value: {
type: Boolean,
default: false,
},
defineOptions({ name: "fullScreen" });
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
emits: ["update:modelValue", "input", "on-change"],
computed: {
showFullScreenBtn() {
return window.navigator.userAgent.indexOf("MSIE") < 0;
},
value: {
type: Boolean,
default: false,
},
methods: {
handleFullscreen() {
const main = document.body;
if (this.modelValue) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
if (main.requestFullscreen) {
main.requestFullscreen();
} else if (main.mozRequestFullScreen) {
main.mozRequestFullScreen();
} else if (main.webkitRequestFullScreen) {
main.webkitRequestFullScreen();
} else if (main.msRequestFullscreen) {
main.msRequestFullscreen();
}
}
},
handleChange() {
this.handleFullscreen();
},
emitFullscreenState(isFullscreen) {
this.$emit("update:modelValue", isFullscreen);
this.$emit("input", isFullscreen);
this.$emit("on-change", isFullscreen);
},
onFullscreenChange() {
const isFullscreen = !!(
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.fullScreen ||
document.mozFullScreen ||
document.webkitIsFullScreen
);
this.emitFullscreenState(isFullscreen);
},
},
created() {
const isFullscreen = !!(
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.fullScreen ||
document.mozFullScreen ||
document.webkitIsFullScreen
);
this.emitFullscreenState(isFullscreen);
document.addEventListener("fullscreenchange", this.onFullscreenChange);
document.addEventListener("mozfullscreenchange", this.onFullscreenChange);
document.addEventListener("webkitfullscreenchange", this.onFullscreenChange);
document.addEventListener("msfullscreenchange", this.onFullscreenChange);
},
beforeUnmount() {
document.removeEventListener("fullscreenchange", this.onFullscreenChange);
document.removeEventListener("mozfullscreenchange", this.onFullscreenChange);
document.removeEventListener("webkitfullscreenchange", this.onFullscreenChange);
document.removeEventListener("msfullscreenchange", this.onFullscreenChange);
},
};
});
const emit = defineEmits(["update:modelValue", "input", "on-change"]);
const showFullScreenBtn = computed(() => {
return window.navigator.userAgent.indexOf("MSIE") < 0;
});
function handleFullscreen() {
const main = document.body;
if (props.modelValue) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
if (main.requestFullscreen) {
main.requestFullscreen();
} else if (main.mozRequestFullScreen) {
main.mozRequestFullScreen();
} else if (main.webkitRequestFullScreen) {
main.webkitRequestFullScreen();
} else if (main.msRequestFullscreen) {
main.msRequestFullscreen();
}
}
}
function handleChange() {
handleFullscreen();
}
function emitFullscreenState(isFullscreen) {
emit("update:modelValue", isFullscreen);
emit("input", isFullscreen);
emit("on-change", isFullscreen);
}
function onFullscreenChange() {
const isFullscreen = !!(
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.fullScreen ||
document.mozFullScreen ||
document.webkitIsFullScreen
);
emitFullscreenState(isFullscreen);
}
const isFullscreen = !!(
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.fullScreen ||
document.mozFullScreen ||
document.webkitIsFullScreen
);
emitFullscreenState(isFullscreen);
document.addEventListener("fullscreenchange", onFullscreenChange);
document.addEventListener("mozfullscreenchange", onFullscreenChange);
document.addEventListener("webkitfullscreenchange", onFullscreenChange);
document.addEventListener("msfullscreenchange", onFullscreenChange);
onBeforeUnmount(() => {
document.removeEventListener("fullscreenchange", onFullscreenChange);
document.removeEventListener("mozfullscreenchange", onFullscreenChange);
document.removeEventListener("webkitfullscreenchange", onFullscreenChange);
document.removeEventListener("msfullscreenchange", onFullscreenChange);
});
</script>

View File

@@ -6,63 +6,58 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { getBaseSite } from "@/api/common.js";
export default {
data() {
return {
storeSideLogo: require("@/assets/logo-lilishop.png"),
};
},
methods: {
init() {
if (
!localStorage.getItem("sellersiteName") ||
!localStorage.getItem("sellerlogoImg") ||
!localStorage.getItem("sellerIconImg") ||
!localStorage.getItem("seller_expiration_time")
) {
this.getSite();
} else if (new Date() > localStorage.getItem("seller_expiration_time")) {
this.getSite();
} else {
this.storeSideLogo = localStorage.getItem("sellerlogoImg");
window.document.title = localStorage.getItem("sellersiteName");
this.applyFavicon(localStorage.getItem("sellerIconImg"));
const storeSideLogo = ref(require("@/assets/logo-lilishop.png"));
function init() {
if (
!localStorage.getItem("sellersiteName") ||
!localStorage.getItem("sellerlogoImg") ||
!localStorage.getItem("sellerIconImg") ||
!localStorage.getItem("seller_expiration_time")
) {
getSite();
} else if (new Date() > localStorage.getItem("seller_expiration_time")) {
getSite();
} else {
storeSideLogo.value = localStorage.getItem("sellerlogoImg");
window.document.title = localStorage.getItem("sellersiteName");
applyFavicon(localStorage.getItem("sellerIconImg"));
}
}
function applyFavicon(href) {
const link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
link.type = "image/x-icon";
link.href = href;
link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link);
}
function getSite() {
getBaseSite()
.then((res) => {
if (res.success && res.result.settingValue) {
const data = JSON.parse(res.result.settingValue);
const expirationTime = new Date().setHours(new Date().getHours() + 1);
localStorage.setItem("seller_expiration_time", expirationTime);
localStorage.setItem("sellersiteName", data.siteName);
localStorage.setItem("sellerlogoImg", data.storeSideLogo);
localStorage.setItem("sellerIconImg", data.storeSideIcon);
storeSideLogo.value = data.storeSideLogo;
window.document.title = data.siteName;
applyFavicon(data.storeSideIcon);
}
},
applyFavicon(href) {
const link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
link.type = "image/x-icon";
link.href = href;
link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link);
},
getSite() {
getBaseSite()
.then((res) => {
if (res.success && res.result.settingValue) {
const data = JSON.parse(res.result.settingValue);
const expirationTime = new Date().setHours(new Date().getHours() + 1);
localStorage.setItem("seller_expiration_time", expirationTime);
localStorage.setItem("sellersiteName", data.siteName);
localStorage.setItem("sellerlogoImg", data.storeSideLogo);
localStorage.setItem("sellerIconImg", data.storeSideIcon);
this.storeSideLogo = data.storeSideLogo;
window.document.title = data.siteName;
this.applyFavicon(data.storeSideIcon);
}
})
.catch(() => {});
},
},
mounted() {
this.init();
},
};
})
.catch(() => {});
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -12,30 +12,30 @@
</div>
</template>
<script>
<script setup>
import { useRouter } from "vue-router";
import { Bell } from "@element-plus/icons-vue";
import util from "@/libs/util.js";
export default {
name: "messageTip",
components: { Bell },
props: {
value: {
type: Number,
default: 0,
},
message: {
type: String,
default: "",
},
defineOptions({ name: "messageTip" });
const props = defineProps({
value: {
type: Number,
default: 0,
},
methods: {
showMessage() {
util.openNewPage(this, "message_index");
this.$router.push({
name: "message_index",
});
},
message: {
type: String,
default: "",
},
};
});
const router = useRouter();
function showMessage() {
util.openNewPage("message_index");
router.push({
name: "message_index",
});
}
</script>

View File

@@ -33,71 +33,77 @@
</div>
</template>
<script>
<script setup>
import { computed, ref, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import util, { HOME_NAV_NAME, HOME_NAV_ITEM } from "@/libs/util.js";
import { setStore } from "@/libs/storage";
import { useAppStore } from "@/stores/app";
export default {
name: "shrinkableMenu",
computed: {
menuList() {
return this.$store.state.app.menuList;
},
navList() {
return this.$store.state.app.navList;
},
currNav() {
return this.$store.state.app.currNav;
},
},
watch: {
$route(val) {
this.syncRouteMenu(val);
},
},
methods: {
syncRouteMenu(route) {
if (!route?.name) {
return;
}
if (route.name === "home_index") {
this.activateHomeNav();
return;
}
const firstRouterName =
route.meta?.firstRouterName || util.getFirstRouterName(route.name);
if (firstRouterName && firstRouterName !== this.currNav) {
this.selectNav(firstRouterName);
}
},
activateHomeNav() {
this.$store.commit("setCurrNav", HOME_NAV_NAME);
this.$store.commit("setCurrNavTitle", HOME_NAV_ITEM.title);
this.setStore("currNav", HOME_NAV_NAME);
this.$store.commit("updateMenulist", []);
},
changeMenu(name) {
if (!name) return;
this.$router.push({ name });
},
selectNav(name) {
this.$store.commit("childrenMenu", this.$refs.childrenMenu);
this.$store.commit("setCurrNav", name);
this.setStore("currNav", name);
if (name === HOME_NAV_NAME) {
this.$store.commit("updateMenulist", []);
this.$store.commit("setCurrNavTitle", HOME_NAV_ITEM.title);
if (this.$route.name !== "home_index") {
this.$router.push({ name: "home_index" });
}
return;
}
util.initRouter(this);
},
},
mounted() {
this.syncRouteMenu(this.$route);
},
};
defineOptions({ name: "shrinkableMenu" });
const appStore = useAppStore();
const route = useRoute();
const router = useRouter();
const childrenMenu = ref(null);
const menuList = computed(() => appStore.menuList);
const navList = computed(() => appStore.navList);
const currNav = computed(() => appStore.currNav);
watch(
() => route.name,
() => {
syncRouteMenu(route);
}
);
function syncRouteMenu(route) {
if (!route?.name) {
return;
}
if (route.name === "home_index") {
activateHomeNav();
return;
}
const firstRouterName =
route.meta?.firstRouterName || util.getFirstRouterName(route.name);
if (firstRouterName && firstRouterName !== currNav.value) {
selectNav(firstRouterName);
}
}
function activateHomeNav() {
appStore.setCurrNav(HOME_NAV_NAME);
appStore.setCurrNavTitle(HOME_NAV_ITEM.title);
setStore("currNav", HOME_NAV_NAME);
appStore.updateMenulist([]);
}
function changeMenu(name) {
if (!name) return;
router.push({ name });
}
function selectNav(name) {
appStore.childrenMenu(childrenMenu.value);
appStore.setCurrNav(name);
setStore("currNav", name);
if (name === HOME_NAV_NAME) {
appStore.updateMenulist([]);
appStore.setCurrNavTitle(HOME_NAV_ITEM.title);
if (route.name !== "home_index") {
router.push({ name: "home_index" });
}
return;
}
util.initRouter();
}
onMounted(() => {
syncRouteMenu(route);
});
</script>
<style lang="scss" scoped>

View File

@@ -36,129 +36,126 @@
</div>
</template>
<script>
<script setup>
import { computed, ref } from "vue";
import Cookies from "js-cookie";
import vueQr from "vue-qr";
import { getStoreMpQrcode } from "@/api/index";
import { getBaseSite } from "@/api/common.js";
import { Message } from "@/utils/message";
export default {
name: "store-qrcode",
components: { "vue-qr": vueQr },
data() {
return {
visible: false,
storeId: "",
wapUrl: "",
pcUrlBase: "",
mpQrcode: "",
mpError: "",
mpLoaded: false,
};
},
computed: {
h5Url() {
if (!this.storeId || !this.wapUrl) return "";
return this.joinUrl(this.wapUrl, `/pages/product/shopPage?id=${this.storeId}`);
},
pcUrl() {
if (!this.storeId || !this.pcUrlBase) return "";
return this.joinUrl(this.pcUrlBase, `/merchant?id=${this.storeId}`);
},
mpQrSrc() {
if (!this.mpQrcode) return "";
const mime = String(this.mpQrcode).startsWith("/9j/")
? "image/jpeg"
: "image/png";
return `data:${mime};base64,${this.mpQrcode}`;
},
},
methods: {
joinUrl(base, path) {
return String(base || "").replace(/\/$/, "") + path;
},
parseSettingValue(raw) {
if (!raw) return {};
if (typeof raw === "object") return raw;
try {
return JSON.parse(raw);
} catch (e) {
return {};
defineOptions({ name: "store-qrcode" });
const visible = ref(false);
const storeId = ref("");
const wapUrl = ref("");
const pcUrlBase = ref("");
const mpQrcode = ref("");
const mpError = ref("");
const mpLoaded = ref(false);
const h5Url = computed(() => {
if (!storeId.value || !wapUrl.value) return "";
return joinUrl(wapUrl.value, `/pages/product/shopPage?id=${storeId.value}`);
});
const pcUrl = computed(() => {
if (!storeId.value || !pcUrlBase.value) return "";
return joinUrl(pcUrlBase.value, `/merchant?id=${storeId.value}`);
});
const mpQrSrc = computed(() => {
if (!mpQrcode.value) return "";
const mime = String(mpQrcode.value).startsWith("/9j/")
? "image/jpeg"
: "image/png";
return `data:${mime};base64,${mpQrcode.value}`;
});
function joinUrl(base, path) {
return String(base || "").replace(/\/$/, "") + path;
}
function parseSettingValue(raw) {
if (!raw) return {};
if (typeof raw === "object") return raw;
try {
return JSON.parse(raw);
} catch (e) {
return {};
}
}
function initStore() {
try {
const userInfo = JSON.parse(Cookies.get("userInfoSeller") || "{}") || {};
storeId.value = userInfo.id || userInfo.storeId || "";
} catch (e) {
storeId.value = "";
}
}
function open() {
initStore();
visible.value = true;
}
function loadCodes() {
initStore();
getBaseSite()
.then((res) => {
const data = parseSettingValue(res && res.result && res.result.settingValue);
const fallback = typeof window !== "undefined" ? window.BASE || {} : {};
// wap/站点地址常配官网,H5、PC 店铺页用买家端域名
wapUrl.value = fallback.WAP_URL || data.staticPageWapAddress || "";
pcUrlBase.value = fallback.PC_URL || data.staticPageAddress || "";
})
.catch(() => {
const fallback = typeof window !== "undefined" ? window.BASE || {} : {};
wapUrl.value = fallback.WAP_URL || "";
pcUrlBase.value = fallback.PC_URL || "";
});
loadMpQrcode();
}
function loadMpQrcode() {
if (!storeId.value || mpLoaded.value) return;
mpError.value = "";
getStoreMpQrcode()
.then((res) => {
if (res && res.success && res.result) {
if (String(res.result).startsWith("eyJ")) {
mpError.value = "请先配置微信小程序";
return;
}
mpQrcode.value = res.result;
mpLoaded.value = true;
mpError.value = "";
} else {
mpError.value = "请先配置微信小程序";
}
},
initStore() {
try {
const userInfo = JSON.parse(Cookies.get("userInfoSeller") || "{}") || {};
this.storeId = userInfo.id || userInfo.storeId || "";
} catch (e) {
this.storeId = "";
}
},
open() {
this.initStore();
this.visible = true;
},
loadCodes() {
this.initStore();
getBaseSite()
.then((res) => {
const data = this.parseSettingValue(res && res.result && res.result.settingValue);
const fallback = typeof window !== "undefined" ? window.BASE || {} : {};
// wap/站点地址常配官网,H5、PC 店铺页用买家端域名
this.wapUrl = fallback.WAP_URL || data.staticPageWapAddress || "";
this.pcUrlBase = fallback.PC_URL || data.staticPageAddress || "";
})
.catch(() => {
const fallback = typeof window !== "undefined" ? window.BASE || {} : {};
this.wapUrl = fallback.WAP_URL || "";
this.pcUrlBase = fallback.PC_URL || "";
});
this.loadMpQrcode();
},
loadMpQrcode() {
if (!this.storeId || this.mpLoaded) return;
this.mpError = "";
getStoreMpQrcode()
.then((res) => {
if (res && res.success && res.result) {
if (String(res.result).startsWith("eyJ")) {
this.mpError = "请先配置微信小程序";
return;
}
this.mpQrcode = res.result;
this.mpLoaded = true;
this.mpError = "";
} else {
this.mpError = "请先配置微信小程序";
}
})
.catch(() => {
this.mpError = "请先配置微信小程序";
});
},
copyText(text) {
if (!text) return;
const ok = () => this.$Message.success("复制成功");
const fail = () => this.$Message.error("复制失败");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(ok).catch(fail);
return;
}
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
ok();
} catch (e) {
fail();
}
document.body.removeChild(textArea);
},
},
};
})
.catch(() => {
mpError.value = "请先配置微信小程序";
});
}
function copyText(text) {
if (!text) return;
const ok = () => Message.success("复制成功");
const fail = () => Message.error("复制失败");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(ok).catch(fail);
return;
}
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
ok();
} catch (e) {
fail();
}
document.body.removeChild(textArea);
}
// 外部无 ref/$refs/$parent 访问(仅 Main.vue 以 <store-qrcode /> 使用),无需暴露成员
defineExpose({});
</script>
<style scoped lang="scss">

View File

@@ -41,148 +41,163 @@
</div>
</template>
<script>
export default {
name: "tagsPageOpened",
props: {
pageTagsList: Array,
beforePush: {
type: Function,
default: () => true,
},
},
data() {
return {
currentPageName: this.$route.name,
tagBodyLeft: 0,
visible: false,
contextMenuLeft: 0,
contextMenuTop: 0,
actionList: {
others: "关闭其他",
clearAll: "关闭所有",
},
};
},
computed: {
tagsList() {
return this.$store.state.app.storeOpenedList;
},
},
methods: {
isActive(item) {
return item.children
? item.children[0].name === this.currentPageName
: item.name === this.currentPageName;
},
tagType(item) {
return this.isActive(item) ? "primary" : "info";
},
itemTitle(item) {
if (typeof item.title === "object") {
return item.title;
}
return item.title;
},
closePage(event, name) {
let storeOpenedList = this.$store.state.app.storeOpenedList;
let lastPageObj = storeOpenedList[0];
if (this.currentPageName === name) {
const len = storeOpenedList.length;
for (let i = 1; i < len; i++) {
if (storeOpenedList[i].name === name) {
lastPageObj =
i < len - 1 ? storeOpenedList[i + 1] : storeOpenedList[i - 1];
break;
}
}
} else if (event && event.target) {
const tagWidth = event.target.parentNode?.offsetWidth || 0;
this.tagBodyLeft = Math.min(this.tagBodyLeft + tagWidth, 0);
}
this.$store.commit("removeTag", name);
this.$store.commit("closePage", name);
storeOpenedList = this.$store.state.app.storeOpenedList;
localStorage.storeOpenedList = JSON.stringify(storeOpenedList);
if (this.currentPageName === name) {
this.linkTo(lastPageObj);
}
},
linkTo(item) {
if (this.$route.name === item.name) return;
const routerObj = { name: item.name };
if (item.argu) routerObj.params = item.argu;
if (item.query) routerObj.query = item.query;
if (this.beforePush(item)) {
this.$router.push(routerObj);
}
},
handlescroll(e) {
const type = e.type;
let delta = 0;
if (type === "DOMMouseScroll" || type === "mousewheel") {
delta = e.wheelDelta ? e.wheelDelta : -(e.detail || 0) * 40;
}
let left = 0;
if (delta > 0) {
left = Math.min(0, this.tagBodyLeft + delta);
} else if (
this.$refs.scrollCon.offsetWidth - 100 <
this.$refs.scrollBody.offsetWidth
) {
if (
this.tagBodyLeft <
-(this.$refs.scrollBody.offsetWidth - this.$refs.scrollCon.offsetWidth + 100)
) {
left = this.tagBodyLeft;
} else {
left = Math.max(
this.tagBodyLeft + delta,
this.$refs.scrollCon.offsetWidth -
this.$refs.scrollBody.offsetWidth -
100
);
}
} else {
this.tagBodyLeft = 0;
}
this.tagBodyLeft = left;
},
handleTagsOption(type) {
if (type === "clearAll") {
this.$store.commit("clearAllTags");
this.$router.push({ name: "home_index" });
} else {
this.$store.commit("clearOtherTags", this);
}
this.tagBodyLeft = 0;
},
contextMenu(item, e) {
this.visible = true;
const offsetLeft = this.$el.getBoundingClientRect().left;
this.contextMenuLeft = e.clientX - offsetLeft + 10;
this.contextMenuTop = e.clientY - 64;
},
closeMenu() {
this.visible = false;
},
},
watch: {
$route(to) {
this.currentPageName = to.name;
},
visible(value) {
if (value) {
document.body.addEventListener("click", this.closeMenu);
} else {
document.body.removeEventListener("click", this.closeMenu);
}
},
},
beforeUnmount() {
document.body.removeEventListener("click", this.closeMenu);
<script setup>
import { ref, computed, watch, onBeforeUnmount } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useAppStore } from "@/stores/app";
defineOptions({ name: "tagsPageOpened" });
const props = defineProps({
pageTagsList: Array,
beforePush: {
type: Function,
default: () => true,
},
});
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const currentPageName = ref(route.name);
const tagBodyLeft = ref(0);
const visible = ref(false);
const contextMenuLeft = ref(0);
const contextMenuTop = ref(0);
const actionList = {
others: "关闭其他",
clearAll: "关闭所有",
};
const scrollCon = ref(null);
const scrollBody = ref(null);
const tagsList = computed(() => appStore.storeOpenedList);
const isActive = (item) => {
return item.children
? item.children[0].name === currentPageName.value
: item.name === currentPageName.value;
};
const tagType = (item) => {
return isActive(item) ? "primary" : "info";
};
const itemTitle = (item) => {
if (typeof item.title === "object") {
return item.title;
}
return item.title;
};
const closePage = (event, name) => {
let storeOpenedList = appStore.storeOpenedList;
let lastPageObj = storeOpenedList[0];
if (currentPageName.value === name) {
const len = storeOpenedList.length;
for (let i = 1; i < len; i++) {
if (storeOpenedList[i].name === name) {
lastPageObj =
i < len - 1 ? storeOpenedList[i + 1] : storeOpenedList[i - 1];
break;
}
}
} else if (event && event.target) {
const tagWidth = event.target.parentNode?.offsetWidth || 0;
tagBodyLeft.value = Math.min(tagBodyLeft.value + tagWidth, 0);
}
appStore.removeTag(name);
appStore.closePage(name);
storeOpenedList = appStore.storeOpenedList;
localStorage.storeOpenedList = JSON.stringify(storeOpenedList);
if (currentPageName.value === name) {
linkTo(lastPageObj);
}
};
const linkTo = (item) => {
if (route.name === item.name) return;
const routerObj = { name: item.name };
if (item.argu) routerObj.params = item.argu;
if (item.query) routerObj.query = item.query;
if (props.beforePush(item)) {
router.push(routerObj);
}
};
const handlescroll = (e) => {
const type = e.type;
let delta = 0;
if (type === "DOMMouseScroll" || type === "mousewheel") {
delta = e.wheelDelta ? e.wheelDelta : -(e.detail || 0) * 40;
}
let left = 0;
if (delta > 0) {
left = Math.min(0, tagBodyLeft.value + delta);
} else if (
scrollCon.value.offsetWidth - 100 <
scrollBody.value.offsetWidth
) {
if (
tagBodyLeft.value <
-(scrollBody.value.offsetWidth - scrollCon.value.offsetWidth + 100)
) {
left = tagBodyLeft.value;
} else {
left = Math.max(
tagBodyLeft.value + delta,
scrollCon.value.offsetWidth -
scrollBody.value.offsetWidth -
100
);
}
} else {
tagBodyLeft.value = 0;
}
tagBodyLeft.value = left;
};
const handleTagsOption = (type) => {
if (type === "clearAll") {
appStore.clearAllTags();
router.push({ name: "home_index" });
} else {
appStore.clearOtherTags(route.name);
}
tagBodyLeft.value = 0;
};
const contextMenu = (item, e) => {
visible.value = true;
const offsetLeft = scrollCon.value.getBoundingClientRect().left;
contextMenuLeft.value = e.clientX - offsetLeft + 10;
contextMenuTop.value = e.clientY - 64;
};
const closeMenu = () => {
visible.value = false;
};
watch(
() => route.name,
(name) => {
currentPageName.value = name;
}
);
watch(visible, (value) => {
if (value) {
document.body.addEventListener("click", closeMenu);
} else {
document.body.removeEventListener("click", closeMenu);
}
});
onBeforeUnmount(() => {
document.body.removeEventListener("click", closeMenu);
});
</script>
<style lang="scss">

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -42,7 +42,7 @@
</el-card>
<el-card>
<el-table v-loading="loading" border :data="data" ref="table" class="mt_10" style="width: 100%">
<el-table v-loading="loading" border :data="data" ref="tableRef" class="mt_10" style="width: 100%">
<el-table-column prop="memberName" label="会员名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="goodsName" label="商品名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="content" label="评价内容" min-width="300" show-overflow-tooltip />
@@ -88,7 +88,7 @@
</el-card>
<el-dialog v-model="modalVisible" :title="modalTitle" width="500px" :close-on-click-modal="false">
<el-form ref="form" :model="form" label-width="100px" :rules="formValidate">
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
<el-form-item label="评价内容">
<span v-if="!content">暂无评价</span>
<el-input v-else v-model="content" type="textarea" maxlength="200" disabled :rows="4" style="width: 90%" />
@@ -126,127 +126,129 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import * as API_Member from "@/api/member";
import { Message } from "@/utils/message";
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
export default {
name: "memberComment",
components: { uploadPicThumb },
data() {
return {
detailInfo: {},
image: [],
replyStatus: false,
modalVisible: false,
modalTitle: "",
loading: true,
content: "",
searchForm: {
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
startTime: "",
endTime: "",
},
selectDate: null,
form: {
replyImage: [],
reply: "",
},
formValidate: {
reply: [{ required: true, message: "请输入回复内容", trigger: "blur" }],
},
submitLoading: false,
data: [],
total: 0,
};
},
methods: {
init() {
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 10;
this.getDataList();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
startTime: "",
endTime: "",
};
this.selectDate = null;
this.getDataList();
},
selectDateRange(v) {
if (v && v.length === 2) {
this.searchForm.startTime = v[0];
this.searchForm.endTime = v[1];
} else {
this.searchForm.startTime = "";
this.searchForm.endTime = "";
}
},
getDataList() {
this.loading = true;
API_Member.getMemberReview(this.searchForm).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
API_Member.replyMemberReview(this.form.id, this.form).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("回复成功");
this.getDataList();
this.modalVisible = false;
}
});
}
});
},
detail(v) {
this.form.replyImage = [];
this.loading = true;
API_Member.getMemberInfoReview(v.id).then((res) => {
this.loading = false;
if (res.success) {
this.form.id = res.result.id;
this.content = res.result.content;
this.form.reply = res.result.reply;
this.replyStatus = res.result.replyStatus;
this.image = res.result.images ? (res.result.images || "").split(",") : [];
this.form.replyImage = res.result.replyImage ? (res.result.replyImage || "").split(",") : [];
this.detailInfo = res.result;
this.modalVisible = true;
this.modalTitle = "详细";
}
});
},
},
mounted() {
this.init();
},
defineOptions({ name: "memberComment" });
const detailInfo = ref({});
const image = ref([]);
const replyStatus = ref(false);
const modalVisible = ref(false);
const modalTitle = ref("");
const loading = ref(true);
const content = ref("");
const searchForm = ref({
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
startTime: "",
endTime: "",
});
const selectDate = ref(null);
const form = reactive({
replyImage: [],
reply: "",
});
const formValidate = {
reply: [{ required: true, message: "请输入回复内容", trigger: "blur" }],
};
const submitLoading = ref(false);
const data = ref([]);
const total = ref(0);
// searchFormRef/tableRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
const tableRef = ref(null);
// formRef:模板 ref 改名避免与顶层变量 form 互相覆盖
const formRef = ref(null);
onMounted(() => {
init();
});
function init() {
getDataList();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.value.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.value.pageNumber = 1;
searchForm.value.pageSize = 10;
getDataList();
}
function handleReset() {
searchForm.value = {
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
startTime: "",
endTime: "",
};
selectDate.value = null;
getDataList();
}
function selectDateRange(v) {
if (v && v.length === 2) {
searchForm.value.startTime = v[0];
searchForm.value.endTime = v[1];
} else {
searchForm.value.startTime = "";
searchForm.value.endTime = "";
}
}
function getDataList() {
loading.value = true;
API_Member.getMemberReview(searchForm.value).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (valid) {
API_Member.replyMemberReview(form.id, form).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("回复成功");
getDataList();
modalVisible.value = false;
}
});
}
});
}
function detail(v) {
form.replyImage = [];
loading.value = true;
API_Member.getMemberInfoReview(v.id).then((res) => {
loading.value = false;
if (res.success) {
form.id = res.result.id;
content.value = res.result.content;
form.reply = res.result.reply;
replyStatus.value = res.result.replyStatus;
image.value = res.result.images ? (res.result.images || "").split(",") : [];
form.replyImage = res.result.replyImage ? (res.result.replyImage || "").split(",") : [];
detailInfo.value = res.result;
modalVisible.value = true;
modalTitle.value = "详细";
}
});
}
</script>
<style lang="scss">

View File

@@ -84,137 +84,137 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { Clock, ArrowLeft } from "@element-plus/icons-vue";
import * as API_Index from "@/api/index";
export default {
name: "message_index",
components: { Clock, ArrowLeft },
data() {
return {
loading: true,
mesTypeList: [
{ type: "unread", title: "未读消息" },
{ type: "read", title: "已读消息" },
{ type: "recycleBin", title: "回收站" },
],
params: {
status: "UN_READY",
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
},
total: 0,
currentMesList: [],
currentMessageType: "unread",
showMesTitleList: true,
unReadCount: 0,
noDataText: "暂无未读消息",
mes: {
title: "",
time: "",
content: "",
},
};
},
methods: {
changePage() {
this.refreshMessage();
},
changePageSize() {
this.refreshMessage();
},
refreshMessage() {
let status;
const type = this.currentMessageType;
if (type == "unread") status = "UN_READY";
else if (type == "read") status = "ALREADY_READY";
else status = "ALREADY_REMOVE";
this.params.status = status;
this.loading = true;
API_Index.getMessageSendData(this.params).then((res) => {
this.loading = false;
if (res.success) {
this.currentMesList = res.result.records;
this.total = res.result.total;
}
});
},
getAll() {
API_Index.getAllMessage(this.params).then((res) => {
this.loading = false;
if (res.success) {
this.unReadCount = res.result.UN_READY.total;
this.currentMesList = res.result.UN_READY.records;
}
});
},
backMesTitleList() {
this.showMesTitleList = true;
},
setCurrentMesType(type) {
if (this.currentMessageType !== type) {
this.showMesTitleList = true;
}
this.currentMessageType = type;
if (type == "unread") this.noDataText = "暂无未读消息";
else if (type == "read") this.noDataText = "暂无已读消息";
else this.noDataText = "回收站无消息";
this.params.pageNumber = 1;
this.refreshMessage();
},
openMessage(row) {
this.showMesTitleList = false;
this.mes.title = row.title;
this.mes.time = row.createTime;
this.getContent(row);
},
markAsRead(v) {
this.loading = true;
API_Index.read(v.id).then((res) => {
this.loading = false;
if (res.success) this.getAll();
});
},
deleteMes(v) {
this.loading = true;
API_Index.deleteMessage(v.id).then((res) => {
this.loading = false;
if (res.success) this.refreshMessage();
});
},
restoreMes(v) {
API_Index.reductionMessage(v.id).then((res) => {
this.loading = false;
if (res.success) this.refreshMessage();
});
},
deleteReal(v) {
this.loading = true;
API_Index.clearMessage(v.id).then((res) => {
this.loading = false;
if (res.success) this.refreshMessage();
});
},
getContent(v) {
this.mes.content = v.content;
API_Index.read(v.id).then((res) => {
this.loading = false;
if (res.success) this.getAll();
});
},
},
mounted() {
this.getAll();
},
watch: {
$route(to) {
if (to.name == "message_index") this.getAll();
},
},
};
defineOptions({ name: "message_index" });
const route = useRoute();
const loading = ref(true);
const mesTypeList = ref([
{ type: "unread", title: "未读消息" },
{ type: "read", title: "已读消息" },
{ type: "recycleBin", title: "回收站" },
]);
const params = reactive({
status: "UN_READY",
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
});
const total = ref(0);
const currentMesList = ref([]);
const currentMessageType = ref("unread");
const showMesTitleList = ref(true);
const unReadCount = ref(0);
const noDataText = ref("暂无未读消息");
const mes = reactive({
title: "",
time: "",
content: "",
});
// messageList:模板 ref 对应声明(原 Options API 中未在 script 内使用,仅保留绑定)
const messageList = ref(null);
onMounted(() => {
getAll();
});
watch(() => route, (to) => {
if (to.name == "message_index") getAll();
});
function changePage() {
refreshMessage();
}
function changePageSize() {
refreshMessage();
}
function refreshMessage() {
let status;
const type = currentMessageType.value;
if (type == "unread") status = "UN_READY";
else if (type == "read") status = "ALREADY_READY";
else status = "ALREADY_REMOVE";
params.status = status;
loading.value = true;
API_Index.getMessageSendData(params).then((res) => {
loading.value = false;
if (res.success) {
currentMesList.value = res.result.records;
total.value = res.result.total;
}
});
}
function getAll() {
API_Index.getAllMessage(params).then((res) => {
loading.value = false;
if (res.success) {
unReadCount.value = res.result.UN_READY.total;
currentMesList.value = res.result.UN_READY.records;
}
});
}
function backMesTitleList() {
showMesTitleList.value = true;
}
function setCurrentMesType(type) {
if (currentMessageType.value !== type) {
showMesTitleList.value = true;
}
currentMessageType.value = type;
if (type == "unread") noDataText.value = "暂无未读消息";
else if (type == "read") noDataText.value = "暂无已读消息";
else noDataText.value = "回收站无消息";
params.pageNumber = 1;
refreshMessage();
}
function openMessage(row) {
showMesTitleList.value = false;
mes.title = row.title;
mes.time = row.createTime;
getContent(row);
}
function markAsRead(v) {
loading.value = true;
API_Index.read(v.id).then((res) => {
loading.value = false;
if (res.success) getAll();
});
}
function deleteMes(v) {
loading.value = true;
API_Index.deleteMessage(v.id).then((res) => {
loading.value = false;
if (res.success) refreshMessage();
});
}
function restoreMes(v) {
API_Index.reductionMessage(v.id).then((res) => {
loading.value = false;
if (res.success) refreshMessage();
});
}
function deleteReal(v) {
loading.value = true;
API_Index.clearMessage(v.id).then((res) => {
loading.value = false;
if (res.success) refreshMessage();
});
}
function getContent(v) {
mes.content = v.content;
API_Index.read(v.id).then((res) => {
loading.value = false;
if (res.success) getAll();
});
}
</script>
<style lang="scss" scoped>

View File

@@ -6,13 +6,10 @@
</div>
</template>
<script>
<script setup>
import { Loading } from "@element-plus/icons-vue";
export default {
name: "circleLoading",
components: { Loading },
};
defineOptions({ name: "circleLoading" });
</script>
<style lang="scss" scoped>

View File

@@ -13,56 +13,55 @@
</div>
</template>
<script>
<script setup>
import { ref } from "vue";
import { initDepartment } from "@/api/index";
export default {
name: "departmentChoose",
data() {
return {
selectDep: [],
department: [],
cascaderProps: {
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: true,
},
};
},
methods: {
initDepartmentData() {
initDepartment().then((res) => {
if (res.success) {
const arr = Array.isArray(res.result) ? res.result : [];
this.filterData(arr);
this.department = arr;
}
});
},
handleChangeDep(value) {
let departmentId = "";
if (value && value.length > 0) {
departmentId = value[value.length - 1];
}
this.$emit("on-change", departmentId);
},
clearSelect() {
this.selectDep = [];
},
filterData(data) {
data.forEach((e) => {
e.value = e.id;
e.label = e.title;
if (e.children) {
this.filterData(e.children);
}
});
},
},
created() {
this.initDepartmentData();
},
};
defineOptions({ name: "departmentChoose" });
const emit = defineEmits(["on-change"]);
const selectDep = ref([]);
const department = ref([]);
const cascaderProps = ref({
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: true,
});
function initDepartmentData() {
initDepartment().then((res) => {
if (res.success) {
const arr = Array.isArray(res.result) ? res.result : [];
filterData(arr);
department.value = arr;
}
});
}
function handleChangeDep(value) {
let departmentId = "";
if (value && value.length > 0) {
departmentId = value[value.length - 1];
}
emit("on-change", departmentId);
}
function clearSelect() {
selectDep.value = [];
}
function filterData(data) {
data.forEach((e) => {
e.value = e.id;
e.label = e.title;
if (e.children) {
filterData(e.children);
}
});
}
initDepartmentData();
// 供父组件通过 ref 清空选择/读取选中项(当前引用方 clerkManage 未使用,作为公共组件 API 保留)
defineExpose({ clearSelect, selectDep });
</script>

View File

@@ -35,114 +35,113 @@
</div>
</template>
<script>
<script setup>
import { ref } from "vue";
import { initDepartment, searchDepartment } from "@/api/index";
export default {
name: "departmentTreeChoose",
props: {
multiple: {
type: Boolean,
default: false,
},
clearable: {
type: Boolean,
default: true,
},
placeholder: {
type: String,
default: "点击选择部门",
},
defineOptions({ name: "departmentTreeChoose" });
const props = defineProps({
multiple: {
type: Boolean,
default: false,
},
data() {
return {
depLoading: false,
departmentTitle: "",
searchKey: "",
dataDep: [],
cloneDep: [],
departmentId: [],
treeProps: {
label: "title",
children: "children",
},
};
clearable: {
type: Boolean,
default: true,
},
methods: {
initDepartmentData() {
this.depLoading = true;
initDepartment()
.then((res) => {
if (res.success) {
this.dataDep = res.result;
this.cloneDep = JSON.parse(JSON.stringify(this.dataDep));
}
})
.finally(() => {
this.depLoading = false;
});
},
searchDep() {
if (this.searchKey) {
this.depLoading = true;
searchDepartment({ title: this.searchKey })
.then((res) => {
if (res.success) {
res.result.forEach((e) => {
if (e.status == -1) {
e.title = "[已禁用] " + e.title;
e.disabled = true;
}
});
this.dataDep = res.result;
}
})
.finally(() => {
this.depLoading = false;
});
} else {
this.dataDep = JSON.parse(JSON.stringify(this.cloneDep));
}
},
selectTree(node) {
if (!node) {
this.$emit("on-change", null);
this.departmentId = "";
this.departmentTitle = "";
return;
}
this.departmentId = node.id;
this.departmentTitle = node.title;
this.$emit("on-change", {
departmentId: this.departmentId,
departmentTitle: this.departmentTitle,
});
},
clearSelect() {
this.departmentId = [];
this.departmentTitle = "";
this.initDepartmentData();
if (this.multiple) {
this.$emit("on-change", []);
} else {
this.$emit("on-change", "");
}
this.$emit("on-clear");
},
setData(ids, title) {
this.departmentTitle = title;
if (this.multiple) {
this.departmentId = ids;
} else {
this.departmentId = [];
this.departmentId.push(ids);
}
},
},
created() {
this.initDepartmentData();
placeholder: {
type: String,
default: "点击选择部门",
},
});
const emit = defineEmits(["on-change", "on-clear"]);
const depLoading = ref(false);
const departmentTitle = ref("");
const searchKey = ref("");
const dataDep = ref([]);
const cloneDep = ref([]);
const departmentId = ref([]);
const treeProps = {
label: "title",
children: "children",
};
function initDepartmentData() {
depLoading.value = true;
initDepartment()
.then((res) => {
if (res.success) {
dataDep.value = res.result;
cloneDep.value = JSON.parse(JSON.stringify(dataDep.value));
}
})
.finally(() => {
depLoading.value = false;
});
}
function searchDep() {
if (searchKey.value) {
depLoading.value = true;
searchDepartment({ title: searchKey.value })
.then((res) => {
if (res.success) {
res.result.forEach((e) => {
if (e.status == -1) {
e.title = "[已禁用] " + e.title;
e.disabled = true;
}
});
dataDep.value = res.result;
}
})
.finally(() => {
depLoading.value = false;
});
} else {
dataDep.value = JSON.parse(JSON.stringify(cloneDep.value));
}
}
function selectTree(node) {
if (!node) {
emit("on-change", null);
departmentId.value = "";
departmentTitle.value = "";
return;
}
departmentId.value = node.id;
departmentTitle.value = node.title;
emit("on-change", {
departmentId: departmentId.value,
departmentTitle: departmentTitle.value,
});
}
function clearSelect() {
departmentId.value = [];
departmentTitle.value = "";
initDepartmentData();
if (props.multiple) {
emit("on-change", []);
} else {
emit("on-change", "");
}
emit("on-clear");
}
function setData(ids, title) {
departmentTitle.value = title;
if (props.multiple) {
departmentId.value = ids;
} else {
departmentId.value = [];
departmentId.value.push(ids);
}
}
initDepartmentData();
// 父组件(clerkManage)通过 ref 调用 setData / clearSelect
defineExpose({ setData, clearSelect });
</script>
<style lang="scss" scoped>

View File

@@ -29,99 +29,101 @@
</div>
</template>
<script>
export default {
name: "setPassword",
props: {
modelValue: String,
value: String,
size: String,
placeholder: {
type: String,
default: "请输入密码,长度为6-20个字符",
},
disabled: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
maxlength: {
type: Number,
default: 20,
},
<script setup>
import { ref, watch } from "vue";
defineOptions({ name: "setPassword" });
const props = defineProps({
modelValue: String,
value: String,
size: String,
placeholder: {
type: String,
default: "请输入密码,长度为6-20个字符",
},
emits: ["update:modelValue", "input", "on-change"],
data() {
return {
currentValue: this.modelValue ?? this.value ?? "",
tipStyle: "password-tip-none",
strengthValue: 0,
progressStatus: "",
strength: "无",
grade: 0,
};
disabled: {
type: Boolean,
default: false,
},
watch: {
modelValue(val) {
this.setCurrentValue(val);
},
value(val) {
this.setCurrentValue(val);
},
readonly: {
type: Boolean,
default: false,
},
methods: {
checkStrengthValue(v) {
let grade = 0;
if (/\d/.test(v)) grade++;
if (/[a-z]/.test(v)) grade++;
if (/[A-Z]/.test(v)) grade++;
if (/\W/.test(v)) grade++;
if (v.length >= 10) grade++;
this.grade = grade;
return grade;
},
strengthChange() {
if (!this.currentValue) {
this.tipStyle = "password-tip-none";
this.strength = "无";
this.strengthValue = 0;
return;
}
const grade = this.checkStrengthValue(this.currentValue);
if (grade <= 1) {
this.progressStatus = "exception";
this.tipStyle = "password-tip-weak";
this.strength = "弱";
this.strengthValue = 33;
} else if (grade >= 2 && grade <= 4) {
this.progressStatus = "";
this.tipStyle = "password-tip-middle";
this.strength = "中";
this.strengthValue = 66;
} else {
this.progressStatus = "success";
this.tipStyle = "password-tip-strong";
this.strength = "强";
this.strengthValue = 100;
}
},
handleChange() {
this.strengthChange();
this.$emit("update:modelValue", this.currentValue);
this.$emit("input", this.currentValue);
this.$emit("on-change", this.currentValue, this.grade, this.strength);
},
setCurrentValue(value) {
if (value === this.currentValue) return;
this.currentValue = value ?? "";
this.strengthChange();
this.$emit("on-change", this.currentValue, this.grade, this.strength);
},
maxlength: {
type: Number,
default: 20,
},
};
});
const emit = defineEmits(["update:modelValue", "input", "on-change"]);
const currentValue = ref(props.modelValue ?? props.value ?? "");
const tipStyle = ref("password-tip-none");
const strengthValue = ref(0);
const progressStatus = ref("");
const strength = ref("无");
const grade = ref(0);
watch(
() => props.modelValue,
(val) => {
setCurrentValue(val);
}
);
watch(
() => props.value,
(val) => {
setCurrentValue(val);
}
);
function checkStrengthValue(v) {
let g = 0;
if (/\d/.test(v)) g++;
if (/[a-z]/.test(v)) g++;
if (/[A-Z]/.test(v)) g++;
if (/\W/.test(v)) g++;
if (v.length >= 10) g++;
grade.value = g;
return g;
}
function strengthChange() {
if (!currentValue.value) {
tipStyle.value = "password-tip-none";
strength.value = "无";
strengthValue.value = 0;
return;
}
const g = checkStrengthValue(currentValue.value);
if (g <= 1) {
progressStatus.value = "exception";
tipStyle.value = "password-tip-weak";
strength.value = "弱";
strengthValue.value = 33;
} else if (g >= 2 && g <= 4) {
progressStatus.value = "";
tipStyle.value = "password-tip-middle";
strength.value = "中";
strengthValue.value = 66;
} else {
progressStatus.value = "success";
tipStyle.value = "password-tip-strong";
strength.value = "强";
strengthValue.value = 100;
}
}
function handleChange() {
strengthChange();
emit("update:modelValue", currentValue.value);
emit("input", currentValue.value);
emit("on-change", currentValue.value, grade.value, strength.value);
}
function setCurrentValue(value) {
if (value === currentValue.value) return;
currentValue.value = value ?? "";
strengthChange();
emit("on-change", currentValue.value, grade.value, strength.value);
}
</script>
<style lang="scss" scoped>

View File

@@ -53,7 +53,7 @@
<el-dialog v-model="picModalFlag" width="1200px" append-to-body :z-index="3500" destroy-on-close>
<ossManage
ref="ossManage"
ref="ossManageRef"
:is-component="true"
:initialize="picModalFlag"
:max-select="1"
@@ -69,119 +69,122 @@
</div>
</template>
<script>
<script setup>
import { nextTick, ref, useAttrs, watch } from "vue";
import { View } from "@element-plus/icons-vue";
import { uploadFile } from "@/libs/axios";
import { getStore } from "@/libs/storage";
import { Message } from "@/utils/message";
import ossManage from "@/views/sys/oss-manage/ossManage";
export default {
name: "uploadPicInput",
components: {
ossManage,
View,
defineOptions({ name: "uploadPicInput" });
const props = defineProps({
modelValue: String,
value: String,
size: {
default: "default",
type: String,
},
props: {
modelValue: String,
value: String,
size: {
default: "default",
type: String,
},
placeholder: {
type: String,
default: "图片链接",
},
showInput: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
maxlength: Number,
placeholder: {
type: String,
default: "图片链接",
},
emits: ["update:modelValue", "input", "on-change"],
data() {
return {
accessToken: {},
currentValue: this.modelValue ?? this.value ?? "",
viewImage: false,
uploadFileUrl: uploadFile,
picModalFlag: false,
selectedFormBtnName: "",
picIndex: "",
selectedImage: [],
};
showInput: {
type: Boolean,
default: true,
},
methods: {
handleCLickImg(val, index) {
this.picModalFlag = true;
this.selectedFormBtnName = val;
this.picIndex = index;
this.selectedImage = [];
this.$nextTick(() => {
if (this.$refs.ossManage) {
this.$refs.ossManage.selectImage = true;
}
});
},
callbackSelected(val) {
if (val?.url) {
this.selectedImage = [val.url];
}
},
handleOssSelected(list) {
this.selectedImage = (list || []).map((item) => item.split(",")[1]).filter(Boolean);
},
confirmSelectedImage() {
if (!this.selectedImage.length) {
this.$Message.warning("请选择图片");
return;
}
this.currentValue = this.selectedImage[this.selectedImage.length - 1];
this.picModalFlag = false;
this.picIndex = "";
this.emitValue(this.currentValue);
},
init() {
this.accessToken = {
accessToken: this.getStore("accessToken"),
};
},
emitValue(val) {
this.$emit("update:modelValue", val);
this.$emit("input", val);
this.$emit("on-change", val);
},
handleChange() {
this.emitValue(this.currentValue);
this.$attrs.rollback && this.$attrs.rollback();
},
setCurrentValue(value) {
if (value === this.currentValue) {
return;
}
this.currentValue = value ?? "";
this.emitValue(this.currentValue);
},
disabled: {
type: Boolean,
default: false,
},
watch: {
modelValue(val) {
this.setCurrentValue(val);
},
value(val) {
this.setCurrentValue(val);
},
readonly: {
type: Boolean,
default: false,
},
created() {
this.init();
},
};
maxlength: Number,
});
const emit = defineEmits(["update:modelValue", "input", "on-change"]);
const attrs = useAttrs();
const accessToken = ref({});
const currentValue = ref(props.modelValue ?? props.value ?? "");
const viewImage = ref(false);
const uploadFileUrl = ref(uploadFile);
const picModalFlag = ref(false);
const selectedFormBtnName = ref("");
const picIndex = ref("");
const selectedImage = ref([]);
// 模板 ref:ossManage 与 import 组件同名,改名避免互相覆盖
const ossManageRef = ref(null);
function handleCLickImg(val, index) {
picModalFlag.value = true;
selectedFormBtnName.value = val;
picIndex.value = index;
selectedImage.value = [];
nextTick(() => {
if (ossManageRef.value) {
ossManageRef.value.selectImage = true;
}
});
}
function callbackSelected(val) {
if (val?.url) {
selectedImage.value = [val.url];
}
}
function handleOssSelected(list) {
selectedImage.value = (list || []).map((item) => item.split(",")[1]).filter(Boolean);
}
function confirmSelectedImage() {
if (!selectedImage.value.length) {
Message.warning("请选择图片");
return;
}
currentValue.value = selectedImage.value[selectedImage.value.length - 1];
picModalFlag.value = false;
picIndex.value = "";
emitValue(currentValue.value);
}
function init() {
accessToken.value = {
accessToken: getStore("accessToken"),
};
}
function emitValue(val) {
emit("update:modelValue", val);
emit("input", val);
emit("on-change", val);
}
function handleChange() {
emitValue(currentValue.value);
attrs.rollback && attrs.rollback();
}
function setCurrentValue(value) {
if (value === currentValue.value) {
return;
}
currentValue.value = value ?? "";
emitValue(currentValue.value);
}
watch(
() => props.modelValue,
(val) => {
setCurrentValue(val);
}
);
watch(
() => props.value,
(val) => {
setCurrentValue(val);
}
);
// 原 created 钩子
init();
</script>
<style lang="scss" scoped>

View File

@@ -66,7 +66,7 @@
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
<ossManage
ref="ossManage"
ref="ossManageRef"
:is-component="true"
:initialize="picModelFlag"
:hide-select-footer="true"
@@ -81,204 +81,203 @@
</div>
</template>
<script>
<script setup>
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { Camera, Delete, View } from "@element-plus/icons-vue";
import { uploadFile } from "@/libs/axios";
import { getStore } from "@/libs/storage";
import { Message } from "@/utils/message";
import vuedraggable from "vuedraggable";
import ossManage from "@/views/sys/oss-manage/ossManage";
export default {
name: "uploadPicThumb",
components: {
vuedraggable,
ossManage,
Camera,
Delete,
View,
defineOptions({ name: "uploadPicThumb" });
const props = defineProps({
modelValue: { type: null },
value: { type: null },
draggable: {
type: Boolean,
default: true,
},
props: {
modelValue: { type: null },
value: { type: null },
draggable: {
type: Boolean,
default: true,
},
multiple: {
type: Boolean,
default: true,
},
disable: {
type: Boolean,
default: false,
},
remove: {
type: Boolean,
default: true,
},
limit: {
type: Number,
default: 10,
},
isView: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: true,
},
emits: ["update:modelValue", "input", "on-change", "uploadchange"],
data() {
return {
accessToken: {},
uploadFileUrl: uploadFile,
uploadList: [],
viewImage: false,
imgUrl: "",
picModelFlag: false,
selectedFormBtnName: "",
selectedImage: [],
};
disable: {
type: Boolean,
default: false,
},
computed: {
bindValue() {
return this.modelValue !== undefined ? this.modelValue : this.value;
},
remove: {
type: Boolean,
default: true,
},
methods: {
handleCLickImg(val) {
this.picModelFlag = true;
this.selectedFormBtnName = val;
this.selectedImage = [];
this.$nextTick(() => {
if (this.$refs.ossManage) {
this.$refs.ossManage.selectImage = true;
}
limit: {
type: Number,
default: 10,
},
isView: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "input", "on-change", "uploadchange"]);
const accessToken = ref({});
const uploadFileUrl = uploadFile;
const uploadList = ref([]);
const viewImage = ref(false);
const imgUrl = ref("");
const picModelFlag = ref(false);
const selectedFormBtnName = ref("");
const selectedImage = ref([]);
// 模板 ref:与 import 名 ossManage 重名,改名避免互相覆盖
const ossManageRef = ref(null);
const bindValue = computed(() => {
return props.modelValue !== undefined ? props.modelValue : props.value;
});
watch(
() => props.modelValue,
(val) => {
setData(val);
}
);
watch(
() => props.value,
(val) => {
setData(val);
}
);
function handleCLickImg(val) {
picModelFlag.value = true;
selectedFormBtnName.value = val;
selectedImage.value = [];
nextTick(() => {
if (ossManageRef.value) {
ossManageRef.value.selectImage = true;
}
});
}
function parseOssSelectionUrl(item) {
if (!item) {
return "";
}
if (typeof item === "string") {
const index = item.indexOf(",");
return index >= 0 ? item.slice(index + 1) : item;
}
return item.url || "";
}
function callbackSelected(val) {
if (props.multiple) {
return;
}
if (!val?.url) {
return;
}
picModelFlag.value = false;
if (uploadList.value.length > 0) {
uploadList.value.splice(0, 1);
}
uploadList.value.push({ ...val, status: "finished" });
returnValue();
}
function confirmUrls() {
if (selectedImage.value.length) {
selectedImage.value.forEach((element) => {
const url = parseOssSelectionUrl(element);
if (url) {
uploadList.value.push({ url, status: "finished" });
}
});
}
selectedImage.value = [];
picModelFlag.value = false;
returnValue();
}
function onEnd() {
returnValue();
}
function init() {
setData(bindValue.value, true);
accessToken.value = {
accessToken: getStore("accessToken"),
};
}
function handleView(url) {
imgUrl.value = url;
viewImage.value = true;
}
function handleRemove(file) {
uploadList.value = uploadList.value.filter((i) => i.url !== file.url);
returnValue();
}
function emitValue(val) {
emit("update:modelValue", val);
emit("input", val);
emit("on-change", val);
}
function returnValue() {
if (!uploadList.value || uploadList.value.length < 1) {
const empty = props.multiple ? [] : "";
emitValue(empty);
return;
}
if (!props.multiple) {
emitValue(uploadList.value[0].url);
} else {
emitValue(uploadList.value.map((e) => e.url));
}
}
function setData(v, init) {
if (typeof v == "string") {
if (props.multiple) {
if (!v) {
uploadList.value = [];
return;
}
setData(
v.split(",").map((s) => s.trim()).filter(Boolean),
init
);
return;
}
if (!v) {
return;
}
uploadList.value = [{ url: v, status: "finished" }];
emit("uploadchange", v);
emitValue(v);
} else if (typeof v == "object" && v) {
if (!props.multiple) {
Message.warning("单张上传仅支持字符串数据类型");
return;
}
uploadList.value = [];
const list = v.length > props.limit ? v.slice(0, props.limit) : v;
if (v.length > props.limit) {
Message.warning("最多只能上传" + props.limit + "张图片");
}
list.forEach((e) => {
uploadList.value.push({
status: "finished",
...(typeof e === "string" ? { url: e } : e),
});
},
parseOssSelectionUrl(item) {
if (!item) {
return "";
}
if (typeof item === "string") {
const index = item.indexOf(",");
return index >= 0 ? item.slice(index + 1) : item;
}
return item.url || "";
},
callbackSelected(val) {
if (this.multiple) {
return;
}
if (!val?.url) {
return;
}
this.picModelFlag = false;
if (this.uploadList.length > 0) {
this.uploadList.splice(0, 1);
}
this.uploadList.push({ ...val, status: "finished" });
this.returnValue();
},
confirmUrls() {
if (this.selectedImage.length) {
this.selectedImage.forEach((element) => {
const url = this.parseOssSelectionUrl(element);
if (url) {
this.uploadList.push({ url, status: "finished" });
}
});
}
this.selectedImage = [];
this.picModelFlag = false;
this.returnValue();
},
onEnd() {
this.returnValue();
},
init() {
this.setData(this.bindValue, true);
this.accessToken = {
accessToken: this.getStore("accessToken"),
};
},
handleView(imgUrl) {
this.imgUrl = imgUrl;
this.viewImage = true;
},
handleRemove(file) {
this.uploadList = this.uploadList.filter((i) => i.url !== file.url);
this.returnValue();
},
emitValue(val) {
this.$emit("update:modelValue", val);
this.$emit("input", val);
this.$emit("on-change", val);
},
returnValue() {
if (!this.uploadList || this.uploadList.length < 1) {
const empty = this.multiple ? [] : "";
this.emitValue(empty);
return;
}
if (!this.multiple) {
this.emitValue(this.uploadList[0].url);
} else {
this.emitValue(this.uploadList.map((e) => e.url));
}
},
setData(v, init) {
if (typeof v == "string") {
if (this.multiple) {
if (!v) {
this.uploadList = [];
return;
}
this.setData(
v.split(",").map((s) => s.trim()).filter(Boolean),
init
);
return;
}
if (!v) {
return;
}
this.uploadList = [{ url: v, status: "finished" }];
this.$emit("uploadchange", v);
this.emitValue(v);
} else if (typeof v == "object" && v) {
if (!this.multiple) {
this.$Message.warning("单张上传仅支持字符串数据类型");
return;
}
this.uploadList = [];
const list = v.length > this.limit ? v.slice(0, this.limit) : v;
if (v.length > this.limit) {
this.$Message.warning("最多只能上传" + this.limit + "张图片");
}
list.forEach((e) => {
this.uploadList.push({
status: "finished",
...(typeof e === "string" ? { url: e } : e),
});
});
if (init) {
this.emitValue(list);
} else {
this.$emit("on-change", list);
}
}
},
},
watch: {
modelValue(val) {
this.setData(val);
},
value(val) {
this.setData(val);
},
},
mounted() {
this.init();
},
};
});
if (init) {
emitValue(list);
} else {
emit("on-change", list);
}
}
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -17,116 +17,114 @@
</div>
</div>
</template>
<script>
<script setup>
import { onMounted, ref, watch } from "vue";
import AMapLoader from "@amap/amap-jsapi-loader";
import { getRegion } from "@/api/common.js";
import config from "@/config/index";
export default {
name: "map",
data() {
return {
config,
showMap: false,
mapSearch: "",
map: null,
autoComplete: null,
geocoder: null,
positionPicker: null,
tips: [],
addrContent: {},
loading: false,
import { Message } from "@/utils/message";
defineOptions({ name: "map" });
const emit = defineEmits(["getAddress"]);
const showMap = ref(false);
const mapSearch = ref("");
const map = ref(null);
const autoComplete = ref(null);
const geocoder = ref(null);
const positionPicker = ref(null);
const tips = ref([]);
const addrContent = ref({});
const loading = ref(false);
watch(mapSearch, (val) => {
searchOfMap(val);
});
function ok() {
if (addrContent.value && addrContent.value.regeocode) {
const params = {
cityCode: addrContent.value.regeocode.addressComponent.citycode,
townName: addrContent.value.regeocode.addressComponent.township,
};
},
watch: {
mapSearch(val) {
this.searchOfMap(val);
},
},
methods: {
ok() {
if (this.addrContent && this.addrContent.regeocode) {
const params = {
cityCode: this.addrContent.regeocode.addressComponent.citycode,
townName: this.addrContent.regeocode.addressComponent.township,
};
getRegion(params).then((res) => {
if (res.success) {
this.addrContent.addr = res.result.name.replace(/,/g, " ");
this.addrContent.addrId = res.result.id;
this.loading = false;
this.$emit("getAddress", this.addrContent);
}
});
} else {
this.$Message.error("未获取到坐标信息!请查看高德API配置是否正确");
getRegion(params).then((res) => {
if (res.success) {
addrContent.value.addr = res.result.name.replace(/,/g, " ");
addrContent.value.addrId = res.result.id;
loading.value = false;
emit("getAddress", addrContent.value);
}
});
} else {
Message.error("未获取到坐标信息!请查看高德API配置是否正确");
}
}
function init() {
AMapLoader.load({
key: config.aMapKey,
version: "",
plugins: [
"AMap.ToolBar",
"AMap.Autocomplete",
"AMap.PlaceSearch",
"AMap.Geolocation",
"AMap.Geocoder",
],
AMapUI: {
version: "1.1",
plugins: ["misc/PositionPicker"],
},
init() {
AMapLoader.load({
key: this.config.aMapKey,
version: "",
plugins: [
"AMap.ToolBar",
"AMap.Autocomplete",
"AMap.PlaceSearch",
"AMap.Geolocation",
"AMap.Geocoder",
],
AMapUI: {
version: "1.1",
plugins: ["misc/PositionPicker"],
},
})
.then((AMap) => {
let that = this;
this.map = new AMap.Map("map-container", {
zoom: 12,
});
that.map.addControl(new AMap.ToolBar());
that.map.addControl(new AMap.Autocomplete());
that.map.addControl(new AMap.PlaceSearch());
that.map.addControl(new AMap.Geocoder());
let autoOptions = {
city: "全国",
};
that.autoComplete = new AMap.Autocomplete(autoOptions);
that.geocoder = new AMap.Geocoder(autoOptions);
that.positionPicker = new AMapUI.PositionPicker({
mode: "dragMap",
map: that.map,
});
that.positionPicker.start();
that.positionPicker.on("success", function (positionResult) {
that.addrContent = positionResult;
});
})
.catch(() => {});
},
searchOfMap(val) {
let that = this;
this.autoComplete.search(val, function (status, result) {
if (status == "complete" && result.info == "OK") {
that.tips = result.tips;
} else {
that.tips = [];
}
})
.then((AMap) => {
map.value = new AMap.Map("map-container", {
zoom: 12,
});
},
selectAddr(location) {
if (!location) {
this.$Message.warning("请选择正确点位");
return false;
}
const lnglat = [location.lng, location.lat];
this.positionPicker.start(lnglat);
},
},
mounted() {
this.init();
},
};
map.value.addControl(new AMap.ToolBar());
map.value.addControl(new AMap.Autocomplete());
map.value.addControl(new AMap.PlaceSearch());
map.value.addControl(new AMap.Geocoder());
let autoOptions = {
city: "全国",
};
autoComplete.value = new AMap.Autocomplete(autoOptions);
geocoder.value = new AMap.Geocoder(autoOptions);
positionPicker.value = new AMapUI.PositionPicker({
mode: "dragMap",
map: map.value,
});
positionPicker.value.start();
positionPicker.value.on("success", function (positionResult) {
addrContent.value = positionResult;
});
})
.catch(() => {});
}
function searchOfMap(val) {
autoComplete.value.search(val, function (status, result) {
if (status == "complete" && result.info == "OK") {
tips.value = result.tips;
} else {
tips.value = [];
}
});
}
function selectAddr(location) {
if (!location) {
Message.warning("请选择正确点位");
return false;
}
const lnglat = [location.lng, location.lat];
positionPicker.value.start(lnglat);
}
onMounted(() => {
init();
});
// 唯一引用方 multiple-map.vue 通过 ref 引用本组件但从不访问任何成员(仅监听 getAddress 事件),无需暴露成员
</script>
<style lang="scss" scoped>
#map-container {

View File

@@ -40,85 +40,87 @@
</el-dialog>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { aMapSwitch } from "@/config/index";
import mapping from "@/views/my-components/map/index.vue";
import * as API_Setup from "@/api/common.js";
import { Message } from "@/utils/message";
export default {
components: { mapping },
data() {
return {
aMapSwitch,
enableMap: false,
mapDefault: "select",
data: {
province: [],
city: [],
area: [],
street: [],
},
chiosend: [],
};
},
mounted() {
this.chiosend = new Array(4).fill("");
},
methods: {
open() {
this.enableMap = true;
this.init({ id: 0 }, "province");
},
changeMap(val) {
this.mapDefault = val;
},
init(val, level = "province", index) {
if (level == 0) {
this.chiosend.splice(3, 1, val);
} else {
API_Setup.getChildRegion(val.id).then((res) => {
if (res.result.length && val.id !== 0) {
this.chiosend[index] = val;
} else if (!res.result.length) {
this.chiosend[index] = val;
}
this.data[level] = res.result;
if (level == "city") {
this.data.area = [];
this.data.street = [];
this.chiosend.splice(1, 3, "", "", "");
}
if (level == "area") {
this.data.street = [];
this.chiosend.splice(2, 2, "", "");
}
if (level == "street") {
this.chiosend.splice(3, 1, "");
}
});
const emit = defineEmits(["callback"]);
const enableMap = ref(false);
const mapDefault = ref("select");
const data = reactive({
province: [],
city: [],
area: [],
street: [],
});
const chiosend = ref([]);
// 模板 ref,对应高德地图子组件
const map = ref(null);
onMounted(() => {
chiosend.value = new Array(4).fill("");
});
function open() {
enableMap.value = true;
init({ id: 0 }, "province");
}
function changeMap(val) {
mapDefault.value = val;
}
function init(val, level = "province", index) {
if (level == 0) {
chiosend.value.splice(3, 1, val);
} else {
API_Setup.getChildRegion(val.id).then((res) => {
if (res.result.length && val.id !== 0) {
chiosend.value[index] = val;
} else if (!res.result.length) {
chiosend.value[index] = val;
}
},
getAddress(center) {
this.$emit("callback", {
type: this.mapDefault,
data: center,
});
this.enableMap = false;
},
finished() {
if (!this.chiosend[0]) {
this.$Message.error("请选择地址");
return;
data[level] = res.result;
if (level == "city") {
data.area = [];
data.street = [];
chiosend.value.splice(1, 3, "", "", "");
}
const params = this.chiosend.filter((item) => item !== "" && item.value !== "");
this.enableMap = false;
this.$emit("callback", {
type: this.mapDefault,
data: params,
});
},
},
};
if (level == "area") {
data.street = [];
chiosend.value.splice(2, 2, "", "");
}
if (level == "street") {
chiosend.value.splice(3, 1, "");
}
});
}
}
function getAddress(center) {
emit("callback", {
type: mapDefault.value,
data: center,
});
enableMap.value = false;
}
function finished() {
if (!chiosend.value[0]) {
Message.error("请选择地址");
return;
}
const params = chiosend.value.filter((item) => item !== "" && item.value !== "");
enableMap.value = false;
emit("callback", {
type: mapDefault.value,
data: params,
});
}
// 父组件(shopAddress/shopSetting/orderDetail)通过 ref 调用 open() 打开弹窗
defineExpose({ open });
</script>
<style lang="scss" scoped>

View File

@@ -37,111 +37,110 @@
</div>
</template>
<script>
<script setup>
import { ref, watch } from "vue";
import { Refresh, DArrowRight } from "@element-plus/icons-vue";
import { getVerifyImg, postVerifyImg } from "./verify.js";
import { Message } from "@/utils/message";
export default {
components: { Refresh, DArrowRight },
props: {
verifyType: {
default: "LOGIN",
type: String,
},
const props = defineProps({
verifyType: {
default: "LOGIN",
type: String,
},
data() {
return {
show: false,
type: "LOGIN",
data: {
backImage: "",
slidingImage: "",
originalHeight: 150,
originalWidth: 300,
sliderWidth: 60,
sliderHeight: 60,
},
distance: 0,
flag: false,
downX: 0,
bgColor: "#04ad11",
verifyText: "拖动滑块解锁",
};
});
const emit = defineEmits(["change"]);
const show = ref(false);
const type = ref("LOGIN");
const data = ref({
backImage: "",
slidingImage: "",
originalHeight: 150,
originalWidth: 300,
sliderWidth: 60,
sliderHeight: 60,
});
const distance = ref(0);
const flag = ref(false);
const downX = ref(0);
const bgColor = ref("#04ad11");
const verifyText = ref("拖动滑块解锁");
watch(
() => props.verifyType,
(v) => {
type.value = v;
},
methods: {
mouseDown(e) {
this.downX = e.clientX;
this.flag = true;
},
mouseMove(e) {
if (this.flag) {
const offset = e.clientX - this.downX;
if (offset > this.data.originalWidth - 43) {
this.distance = this.data.originalWidth - 43;
} else if (offset < 0) {
this.distance = 0;
{ immediate: true }
);
function mouseDown(e) {
downX.value = e.clientX;
flag.value = true;
}
function mouseMove(e) {
if (flag.value) {
const offset = e.clientX - downX.value;
if (offset > data.value.originalWidth - 43) {
distance.value = data.value.originalWidth - 43;
} else if (offset < 0) {
distance.value = 0;
} else {
distance.value = offset;
}
}
}
function mouseUp() {
if (!flag.value) return false;
flag.value = false;
const params = {
verificationEnums: type.value,
xPos: distance.value,
};
postVerifyImg(params)
.then((res) => {
if (res.success) {
if (res.result) {
bgColor.value = "green";
verifyText.value = "解锁成功";
emit("change", { status: true, distance: distance.value });
} else {
this.distance = offset;
bgColor.value = "red";
verifyText.value = "解锁失败";
setTimeout(() => init(), 1000);
emit("change", { status: false, distance: distance.value });
}
} else {
init();
}
},
mouseUp() {
if (!this.flag) return false;
this.flag = false;
const params = {
verificationEnums: this.type,
xPos: this.distance,
};
postVerifyImg(params)
.then((res) => {
if (res.success) {
if (res.result) {
this.bgColor = "green";
this.verifyText = "解锁成功";
this.$emit("change", { status: true, distance: this.distance });
} else {
this.bgColor = "red";
this.verifyText = "解锁失败";
setTimeout(() => this.init(), 1000);
this.$emit("change", { status: false, distance: this.distance });
}
} else {
this.init();
}
})
.catch(() => {
this.init();
});
},
init() {
this.flag = false;
this.downX = 0;
this.distance = 0;
this.bgColor = "#04ad11";
this.verifyText = "拖动滑块解锁";
getVerifyImg(this.type)
.then((res) => {
if (res.result) {
this.data = res.result;
this.show = true;
} else {
this.$Message.warning("请求失败请重试!");
}
})
.catch(() => {
this.$Message.warning("验证码加载失败,请点击重试");
});
},
},
watch: {
verifyType: {
immediate: true,
handler(v) {
this.type = v;
},
},
},
};
})
.catch(() => {
init();
});
}
function init() {
flag.value = false;
downX.value = 0;
distance.value = 0;
bgColor.value = "#04ad11";
verifyText.value = "拖动滑块解锁";
getVerifyImg(type.value)
.then((res) => {
if (res.result) {
data.value = res.result;
show.value = true;
} else {
Message.warning("请求失败请重试!");
}
})
.catch(() => {
Message.warning("验证码加载失败,请点击重试");
});
}
// login.vue / ForgetPassword.vue 通过 verifyRef.value.show = false 关闭、verifyRef.value.init() 打开
defineExpose({ show, init });
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -104,96 +104,100 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import * as API_Order from "@/api/order";
import vueQr from "vue-qr";
import { customRouterPush } from "@/utils/filters";
export default {
name: "orderComplaint",
components: { vueQr },
data() {
return {
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
},
data: [],
total: 0,
};
},
watch: {
$route() {
this.getDataList();
},
},
methods: {
complainStatusText(v) {
const map = {
NEW: "新投诉",
CANCEL: "已撤销",
WAIT_APPEAL: "待申诉",
COMMUNICATION: "对话中",
WAIT_ARBITRATION: "等待仲裁",
COMPLETE: "已完成",
};
return map[v] || v || "-";
},
complainStatusTagType(v) {
const map = {
NEW: "primary",
CANCEL: "info",
WAIT_APPEAL: "warning",
COMMUNICATION: "warning",
WAIT_ARBITRATION: "",
COMPLETE: "success",
};
return map[v] || "info";
},
init() {
this.getDataList();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
},
changePageSize(v) {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = v;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
getDataList() {
this.loading = true;
API_Order.getOrderComplain(this.searchForm)
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
})
.catch(() => {
this.loading = false;
});
},
detail(v) {
this.$filters.customRouterPush({
name: "order-complaint-detail",
query: { id: v.id },
});
},
},
mounted() {
this.init();
},
};
defineOptions({ name: "orderComplaint" });
const route = useRoute();
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
});
const data = ref([]);
const total = ref(0);
// searchFormRef:模板 ref 改名避免与 searchForm 数据互相覆盖
const searchFormRef = ref(null);
watch(
() => route.query,
() => {
getDataList();
}
);
onMounted(() => {
init();
});
function complainStatusText(v) {
const map = {
NEW: "新投诉",
CANCEL: "已撤销",
WAIT_APPEAL: "待申诉",
COMMUNICATION: "对话中",
WAIT_ARBITRATION: "等待仲裁",
COMPLETE: "已完成",
};
return map[v] || v || "-";
}
function complainStatusTagType(v) {
const map = {
NEW: "primary",
CANCEL: "info",
WAIT_APPEAL: "warning",
COMMUNICATION: "warning",
WAIT_ARBITRATION: "",
COMPLETE: "success",
};
return map[v] || "info";
}
function init() {
getDataList();
}
function changePage(v) {
searchForm.pageNumber = v;
getDataList();
}
function changePageSize(v) {
searchForm.pageNumber = 1;
searchForm.pageSize = v;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
}
function getDataList() {
loading.value = true;
API_Order.getOrderComplain(searchForm)
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
})
.catch(() => {
loading.value = false;
});
}
function detail(v) {
customRouterPush({
name: "order-complaint-detail",
query: { id: v.id },
});
}
</script>
<style scoped>

View File

@@ -188,134 +188,135 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import * as API_Order from "@/api/order";
import { Message } from "@/utils/message";
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
defineOptions({ name: "orderComplaintDetail" });
const route = useRoute();
const router = useRouter();
const emptyComplaint = () => ({
id: "",
orderComplaintCommunications: [],
});
export default {
name: "orderComplaintDetail",
components: {
uploadPicThumb,
},
data() {
return {
loading: false,
id: "",
complaintInfo: emptyComplaint(),
images: [],
appealImages: [],
submitLoading: false,
params: {
content: "",
complainId: "",
},
appeal: {
orderComplaintId: "",
appealContent: "",
appealImages: [],
},
};
},
methods: {
complainStatusText(v) {
const map = {
NEW: "新投诉",
CANCEL: "已撤销",
WAIT_APPEAL: "待申诉",
COMMUNICATION: "对话中",
WAIT_ARBITRATION: "等待仲裁",
COMPLETE: "已完成",
};
return map[v] || v || "-";
},
getDetail() {
if (!this.id) return;
this.loading = true;
API_Order.getOrderComplainDetail(this.id)
.then((res) => {
this.loading = false;
if (res.success) {
this.complaintInfo = res.result || emptyComplaint();
this.images = (res.result.images || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
this.appealImages = (res.result.appealImages || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
})
.catch(() => {
this.loading = false;
});
},
returnDataList() {
this.$router.push({ name: "orderComplaint" });
},
appealSubmit() {
if (!this.appeal.appealContent) {
this.$Message.error("请填写申诉内容");
return;
const loading = ref(false);
const id = ref("");
const complaintInfo = ref(emptyComplaint());
const images = ref([]);
const appealImages = ref([]);
const submitLoading = ref(false);
const params = reactive({
content: "",
complainId: "",
});
const appeal = reactive({
orderComplaintId: "",
appealContent: "",
appealImages: [],
});
onMounted(() => {
id.value = route.query.id;
getDetail();
});
watch(
() => route.query.id,
(val) => {
id.value = val;
getDetail();
}
);
function complainStatusText(v) {
const map = {
NEW: "新投诉",
CANCEL: "已撤销",
WAIT_APPEAL: "待申诉",
COMMUNICATION: "对话中",
WAIT_ARBITRATION: "等待仲裁",
COMPLETE: "已完成",
};
return map[v] || v || "-";
}
function getDetail() {
if (!id.value) return;
loading.value = true;
API_Order.getOrderComplainDetail(id.value)
.then((res) => {
loading.value = false;
if (res.success) {
complaintInfo.value = res.result || emptyComplaint();
images.value = (res.result.images || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
appealImages.value = (res.result.appealImages || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
this.submitLoading = true;
const appealImages = Array.isArray(this.appeal.appealImages)
? this.appeal.appealImages
: [];
API_Order.appeal({
orderComplaintId: this.id,
appealContent: this.appeal.appealContent,
appealImages,
})
.then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("申诉成功");
this.appeal.appealContent = "";
this.appeal.appealImages = [];
this.getDetail();
}
})
.catch(() => {
this.submitLoading = false;
});
},
handleSubmit() {
if (!this.params.content) {
this.$Message.error("请填写对话内容");
return;
})
.catch(() => {
loading.value = false;
});
}
function returnDataList() {
router.push({ name: "orderComplaint" });
}
function appealSubmit() {
if (!appeal.appealContent) {
Message.error("请填写申诉内容");
return;
}
submitLoading.value = true;
const appealImages = Array.isArray(appeal.appealImages)
? appeal.appealImages
: [];
API_Order.appeal({
orderComplaintId: id.value,
appealContent: appeal.appealContent,
appealImages,
})
.then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("申诉成功");
appeal.appealContent = "";
appeal.appealImages = [];
getDetail();
}
this.submitLoading = true;
this.params.complainId = this.id;
API_Order.addOrderCommunication(this.params)
.then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("对话成功");
this.params.content = "";
this.getDetail();
}
})
.catch(() => {
this.submitLoading = false;
});
},
},
mounted() {
this.id = this.$route.query.id;
this.getDetail();
},
watch: {
"$route.query.id"(val) {
this.id = val;
this.getDetail();
},
},
};
})
.catch(() => {
submitLoading.value = false;
});
}
function handleSubmit() {
if (!params.content) {
Message.error("请填写对话内容");
return;
}
submitLoading.value = true;
params.complainId = id.value;
API_Order.addOrderCommunication(params)
.then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("对话成功");
params.content = "";
getDetail();
}
})
.catch(() => {
submitLoading.value = false;
});
}
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -153,182 +153,184 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import * as API_Order from "@/api/order";
import { customRouterPush } from "@/utils/filters";
export default {
name: "after-sale-order",
data() {
return {
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
serviceType: "RETURN_GOODS",
orderSn: "",
memberName: "",
serviceStatus: "",
storeName: "",
sn: "",
keywords: "",
},
selectDate: null,
data: [],
total: 0,
currentStatus: "",
afterSaleNumData: {},
};
},
computed: {
serviceStatusWithCount() {
return [
{ title: "全部", value: "" },
{
title: `申请售后${this.afterSaleNumData.applyNum ? "(" + this.afterSaleNumData.applyNum + ")" : ""}`,
value: "APPLY",
},
{
title: `通过售后${this.afterSaleNumData.passNum ? "(" + this.afterSaleNumData.passNum + ")" : ""}`,
value: "PASS",
},
{
title: `拒绝售后${this.afterSaleNumData.refuseNum ? "(" + this.afterSaleNumData.refuseNum + ")" : ""}`,
value: "REFUSE",
},
{
title: `待收货${this.afterSaleNumData.buyerReturnNum ? "(" + this.afterSaleNumData.buyerReturnNum + ")" : ""}`,
value: "BUYER_RETURN",
},
{
title: `确认收货${this.afterSaleNumData.sellerConfirmNum ? "(" + this.afterSaleNumData.sellerConfirmNum + ")" : ""}`,
value: "SELLER_CONFIRM",
},
{
title: `完成售后${this.afterSaleNumData.completeNum ? "(" + this.afterSaleNumData.completeNum + ")" : ""}`,
value: "COMPLETE",
},
{
title: `卖家终止售后${this.afterSaleNumData.sellerTerminationNum ? "(" + this.afterSaleNumData.sellerTerminationNum + ")" : ""}`,
value: "SELLER_TERMINATION",
},
{
title: `买家取消售后${this.afterSaleNumData.buyerCancelNum ? "(" + this.afterSaleNumData.buyerCancelNum + ")" : ""}`,
value: "BUYER_CANCEL",
},
{
title: `等待平台退款${this.afterSaleNumData.waitRefundNum ? "(" + this.afterSaleNumData.waitRefundNum + ")" : ""}`,
value: "WAIT_REFUND",
},
];
defineOptions({ name: "after-sale-order" });
const route = useRoute();
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
serviceType: "RETURN_GOODS",
orderSn: "",
memberName: "",
serviceStatus: "",
storeName: "",
sn: "",
keywords: "",
});
const selectDate = ref(null);
const data = ref([]);
const total = ref(0);
const currentStatus = ref("");
const afterSaleNumData = ref({});
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖(原 ref="searchForm" 未被使用)
const searchFormRef = ref(null);
const serviceStatusWithCount = computed(() => {
return [
{ title: "全部", value: "" },
{
title: `申请售后${afterSaleNumData.value.applyNum ? "(" + afterSaleNumData.value.applyNum + ")" : ""}`,
value: "APPLY",
},
},
methods: {
serviceStatusText(status) {
const map = {
APPLY: "申请中",
PASS: "通过售后",
REFUSE: "拒绝售后",
BUYER_RETURN: "买家退货,待卖家收货",
SELLER_CONFIRM: "卖家确认收货",
SELLER_TERMINATION: "卖家终止售后",
BUYER_CANCEL: "买家取消售后",
COMPLETE: "完成售后",
WAIT_REFUND: "待平台退款",
};
return map[status] || status || "-";
{
title: `通过售后${afterSaleNumData.value.passNum ? "(" + afterSaleNumData.value.passNum + ")" : ""}`,
value: "PASS",
},
serviceStatusTagType(status) {
const map = {
APPLY: "primary",
PASS: "info",
REFUSE: "warning",
BUYER_RETURN: "warning",
SELLER_CONFIRM: "",
SELLER_TERMINATION: "success",
BUYER_CANCEL: "danger",
COMPLETE: "success",
WAIT_REFUND: "primary",
};
return map[status] || "info";
{
title: `拒绝售后${afterSaleNumData.value.refuseNum ? "(" + afterSaleNumData.value.refuseNum + ")" : ""}`,
value: "REFUSE",
},
init() {
this.getDataList();
this.getAfterSaleNumData();
{
title: `待收货${afterSaleNumData.value.buyerReturnNum ? "(" + afterSaleNumData.value.buyerReturnNum + ")" : ""}`,
value: "BUYER_RETURN",
},
changePage() {
this.getDataList();
{
title: `确认收货${afterSaleNumData.value.sellerConfirmNum ? "(" + afterSaleNumData.value.sellerConfirmNum + ")" : ""}`,
value: "SELLER_CONFIRM",
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
{
title: `完成售后${afterSaleNumData.value.completeNum ? "(" + afterSaleNumData.value.completeNum + ")" : ""}`,
value: "COMPLETE",
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
this.getAfterSaleNumData();
{
title: `卖家终止售后${afterSaleNumData.value.sellerTerminationNum ? "(" + afterSaleNumData.value.sellerTerminationNum + ")" : ""}`,
value: "SELLER_TERMINATION",
},
selectDateRange(v) {
if (v) {
this.searchForm.startDate = v[0];
this.searchForm.endDate = v[1];
} else {
this.searchForm.startDate = "";
this.searchForm.endDate = "";
}
{
title: `买家取消售后${afterSaleNumData.value.buyerCancelNum ? "(" + afterSaleNumData.value.buyerCancelNum + ")" : ""}`,
value: "BUYER_CANCEL",
},
getDataList() {
this.loading = true;
API_Order.getAfterSaleOrderPage(this.searchForm).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
{
title: `等待平台退款${afterSaleNumData.value.waitRefundNum ? "(" + afterSaleNumData.value.waitRefundNum + ")" : ""}`,
value: "WAIT_REFUND",
},
getAfterSaleNumData() {
const { serviceStatus, ...searchParams } = this.searchForm;
API_Order.getAfterSaleNumVO(searchParams).then((res) => {
if (res.success) {
this.afterSaleNumData = res.result;
}
});
},
detail(v) {
const sn = v.sn;
this.$filters.customRouterPush({
name: "return-goods-order-detail",
query: { sn: sn },
});
},
onStatusTabClick(tab) {
this.serviceStatusClick(tab.paneName);
},
serviceStatusClick(item) {
this.currentStatus = item;
if (item === "" || item === undefined) {
delete this.searchForm.serviceStatus;
} else {
this.searchForm.serviceStatus = item;
}
this.getDataList();
this.getAfterSaleNumData();
},
},
mounted() {
const status = this.$route.query.serviceStatus;
if (status) {
this.currentStatus = status;
this.searchForm.serviceStatus = status;
];
});
function serviceStatusText(status) {
const map = {
APPLY: "申请中",
PASS: "通过售后",
REFUSE: "拒绝售后",
BUYER_RETURN: "买家退货,待卖家收货",
SELLER_CONFIRM: "卖家确认收货",
SELLER_TERMINATION: "卖家终止售后",
BUYER_CANCEL: "买家取消售后",
COMPLETE: "完成售后",
WAIT_REFUND: "待平台退款",
};
return map[status] || status || "-";
}
function serviceStatusTagType(status) {
const map = {
APPLY: "primary",
PASS: "info",
REFUSE: "warning",
BUYER_RETURN: "warning",
SELLER_CONFIRM: "",
SELLER_TERMINATION: "success",
BUYER_CANCEL: "danger",
COMPLETE: "success",
WAIT_REFUND: "primary",
};
return map[status] || "info";
}
function init() {
getDataList();
getAfterSaleNumData();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
getAfterSaleNumData();
}
function selectDateRange(v) {
if (v) {
searchForm.startDate = v[0];
searchForm.endDate = v[1];
} else {
searchForm.startDate = "";
searchForm.endDate = "";
}
}
function getDataList() {
loading.value = true;
API_Order.getAfterSaleOrderPage(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
this.init();
},
};
});
}
function getAfterSaleNumData() {
const { serviceStatus, ...searchParams } = searchForm;
API_Order.getAfterSaleNumVO(searchParams).then((res) => {
if (res.success) {
afterSaleNumData.value = res.result;
}
});
}
function detail(v) {
const sn = v.sn;
customRouterPush({
name: "return-goods-order-detail",
query: { sn: sn },
});
}
function onStatusTabClick(tab) {
serviceStatusClick(tab.paneName);
}
function serviceStatusClick(item) {
currentStatus.value = item;
if (item === "" || item === undefined) {
delete searchForm.serviceStatus;
} else {
searchForm.serviceStatus = item;
}
getDataList();
getAfterSaleNumData();
}
onMounted(() => {
const status = route.query.serviceStatus;
if (status) {
currentStatus.value = status;
searchForm.serviceStatus = status;
}
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -73,7 +73,7 @@
</div>
<el-table
ref="table"
ref="tableRef"
v-loading="loading"
:data="data"
class="mt_10"
@@ -145,8 +145,15 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import * as API_Order from "@/api/order";
import { customRouterPush } from "@/utils/filters";
defineOptions({ name: "returnMoneyOrder" });
const route = useRoute();
const createDefaultSearchForm = () => ({
pageNumber: 1,
@@ -163,166 +170,163 @@ const createDefaultSearchForm = () => ({
keywords: "",
});
export default {
name: "returnMoneyOrder",
data() {
return {
loading: true,
searchForm: createDefaultSearchForm(),
selectDate: null,
data: [],
total: 0,
currentStatus: "",
afterSaleNumData: {},
};
},
computed: {
serviceStatusWithCount() {
return [
{ title: "全部", value: "" },
{
title: `申请售后${this.afterSaleNumData.applyNum ? "(" + this.afterSaleNumData.applyNum + ")" : ""}`,
value: "APPLY",
},
{
title: `通过售后${this.afterSaleNumData.passNum ? "(" + this.afterSaleNumData.passNum + ")" : ""}`,
value: "PASS",
},
{
title: `拒绝售后${this.afterSaleNumData.refuseNum ? "(" + this.afterSaleNumData.refuseNum + ")" : ""}`,
value: "REFUSE",
},
{
title: `完成售后${this.afterSaleNumData.completeNum ? "(" + this.afterSaleNumData.completeNum + ")" : ""}`,
value: "COMPLETE",
},
{
title: `卖家终止售后${this.afterSaleNumData.sellerTerminationNum ? "(" + this.afterSaleNumData.sellerTerminationNum + ")" : ""}`,
value: "SELLER_TERMINATION",
},
{
title: `买家取消售后${this.afterSaleNumData.buyerCancelNum ? "(" + this.afterSaleNumData.buyerCancelNum + ")" : ""}`,
value: "BUYER_CANCEL",
},
{
title: `等待平台退款${this.afterSaleNumData.waitRefundNum ? "(" + this.afterSaleNumData.waitRefundNum + ")" : ""}`,
value: "WAIT_REFUND",
},
];
const loading = ref(true);
// searchForm 会被整体替换(handleReset),使用 ref
const searchForm = ref(createDefaultSearchForm());
const selectDate = ref(null);
const data = ref([]);
const total = ref(0);
const currentStatus = ref("");
const afterSaleNumData = ref({});
// searchFormRef / tableRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
const tableRef = ref(null);
const serviceStatusWithCount = computed(() => {
return [
{ title: "全部", value: "" },
{
title: `申请售后${afterSaleNumData.value.applyNum ? "(" + afterSaleNumData.value.applyNum + ")" : ""}`,
value: "APPLY",
},
},
methods: {
serviceStatusText(status) {
const map = {
APPLY: "申请中",
PASS: "通过售后",
REFUSE: "拒绝售后",
BUYER_RETURN: "买家退货,待卖家收货",
SELLER_CONFIRM: "卖家确认收货",
SELLER_TERMINATION: "卖家终止售后",
BUYER_CANCEL: "买家取消售后",
COMPLETE: "完成售后",
WAIT_REFUND: "待平台退款",
};
return map[status] || status || "-";
{
title: `通过售后${afterSaleNumData.value.passNum ? "(" + afterSaleNumData.value.passNum + ")" : ""}`,
value: "PASS",
},
serviceStatusTagType(status) {
const map = {
APPLY: "primary",
PASS: "info",
REFUSE: "warning",
BUYER_RETURN: "warning",
SELLER_CONFIRM: "",
SELLER_TERMINATION: "success",
BUYER_CANCEL: "danger",
COMPLETE: "success",
WAIT_REFUND: "primary",
};
return map[status] || "info";
{
title: `拒绝售后${afterSaleNumData.value.refuseNum ? "(" + afterSaleNumData.value.refuseNum + ")" : ""}`,
value: "REFUSE",
},
init() {
this.getDataList();
this.getAfterSaleNumData();
{
title: `完成售后${afterSaleNumData.value.completeNum ? "(" + afterSaleNumData.value.completeNum + ")" : ""}`,
value: "COMPLETE",
},
changePage() {
this.getDataList();
{
title: `卖家终止售后${afterSaleNumData.value.sellerTerminationNum ? "(" + afterSaleNumData.value.sellerTerminationNum + ")" : ""}`,
value: "SELLER_TERMINATION",
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
{
title: `买家取消售后${afterSaleNumData.value.buyerCancelNum ? "(" + afterSaleNumData.value.buyerCancelNum + ")" : ""}`,
value: "BUYER_CANCEL",
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
this.getAfterSaleNumData();
{
title: `等待平台退款${afterSaleNumData.value.waitRefundNum ? "(" + afterSaleNumData.value.waitRefundNum + ")" : ""}`,
value: "WAIT_REFUND",
},
handleReset() {
this.searchForm = createDefaultSearchForm();
this.selectDate = null;
this.currentStatus = "";
this.getDataList();
this.getAfterSaleNumData();
},
selectDateRange(v) {
if (v) {
this.searchForm.startDate = v[0];
this.searchForm.endDate = v[1];
} else {
this.searchForm.startDate = "";
this.searchForm.endDate = "";
];
});
onMounted(() => {
const status = route.query.serviceStatus;
if (status) {
currentStatus.value = status;
searchForm.value.serviceStatus = status;
}
init();
});
function serviceStatusText(status) {
const map = {
APPLY: "申请中",
PASS: "通过售后",
REFUSE: "拒绝售后",
BUYER_RETURN: "买家退货,待卖家收货",
SELLER_CONFIRM: "卖家确认收货",
SELLER_TERMINATION: "卖家终止售后",
BUYER_CANCEL: "买家取消售后",
COMPLETE: "完成售后",
WAIT_REFUND: "待平台退款",
};
return map[status] || status || "-";
}
function serviceStatusTagType(status) {
const map = {
APPLY: "primary",
PASS: "info",
REFUSE: "warning",
BUYER_RETURN: "warning",
SELLER_CONFIRM: "",
SELLER_TERMINATION: "success",
BUYER_CANCEL: "danger",
COMPLETE: "success",
WAIT_REFUND: "primary",
};
return map[status] || "info";
}
function init() {
getDataList();
getAfterSaleNumData();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.value.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.value.pageNumber = 1;
searchForm.value.pageSize = 20;
getDataList();
getAfterSaleNumData();
}
function handleReset() {
searchForm.value = createDefaultSearchForm();
selectDate.value = null;
currentStatus.value = "";
getDataList();
getAfterSaleNumData();
}
function selectDateRange(v) {
if (v) {
searchForm.value.startDate = v[0];
searchForm.value.endDate = v[1];
} else {
searchForm.value.startDate = "";
searchForm.value.endDate = "";
}
}
function getDataList() {
loading.value = true;
API_Order.getAfterSaleOrderPage(searchForm.value)
.then((res) => {
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
},
getDataList() {
this.loading = true;
API_Order.getAfterSaleOrderPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
})
.finally(() => {
this.loading = false;
});
},
getAfterSaleNumData() {
const { serviceStatus, ...searchParams } = this.searchForm;
API_Order.getAfterSaleNumVO(searchParams).then((res) => {
if (res.success) {
this.afterSaleNumData = res.result;
}
});
},
detail(v) {
this.$filters.customRouterPush({
name: "return-goods-order-detail",
query: { sn: v.sn },
});
},
onStatusTabClick(tab) {
this.serviceStatusClick(tab.paneName);
},
serviceStatusClick(item) {
this.currentStatus = item;
if (item === "" || item === undefined) {
delete this.searchForm.serviceStatus;
} else {
this.searchForm.serviceStatus = item;
}
this.getDataList();
this.getAfterSaleNumData();
},
},
mounted() {
const status = this.$route.query.serviceStatus;
if (status) {
this.currentStatus = status;
this.searchForm.serviceStatus = status;
})
.finally(() => {
loading.value = false;
});
}
function getAfterSaleNumData() {
const { serviceStatus, ...searchParams } = searchForm.value;
API_Order.getAfterSaleNumVO(searchParams).then((res) => {
if (res.success) {
afterSaleNumData.value = res.result;
}
this.init();
},
};
});
}
function detail(v) {
customRouterPush({
name: "return-goods-order-detail",
query: { sn: v.sn },
});
}
function onStatusTabClick(tab) {
serviceStatusClick(tab.paneName);
}
function serviceStatusClick(item) {
currentStatus.value = item;
if (item === "" || item === undefined) {
delete searchForm.value.serviceStatus;
} else {
searchForm.value.serviceStatus = item;
}
getDataList();
getAfterSaleNumData();
}
</script>
<style lang="scss" scoped>

View File

@@ -348,276 +348,278 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import * as API_Order from "@/api/order";
export default {
name: "returnGoodsOrderDetail",
data() {
return {
sn: "", // 订单号
logisticsModal: false, //查询物流模态框
import { Message, Modal } from "@/utils/message";
logisticsInfo: {}, //物流信息
afterSaleInfo: {}, // 售后信息
afterSaleImage: [], //会员申诉图片
submitLoading: false, // 添加或编辑提交状态
confirmLoading: false, // 确认收货提交状态
storeMsg: {}, // 商家地址信息
//商家处理意见
params: {
serviceStatus: "PASS",
remark: "",
},
//平台退款
refundPriceForm: {
remark: "",
},
afterSaleRefundStatusList: [
{
name: "退款申请",
status: "APPLY",
},
{
name: "通过售后",
status: "PASS",
},
{
name: "拒绝售后",
status: "REFUSE",
},
{
name: "卖家终止售后",
status: "SELLER_TERMINATION",
},
{
name: "买家取消售后",
status: "BUYER_CANCEL",
},
{
name: "完成售后",
status: "COMPLETE",
},
{
name: "待平台退款",
status: "WAIT_REFUND",
},
],
afterSaleStatusList: [
// 售后状态列表
{
name: "申请中",
status: "APPLY",
},
{
name: "通过售后",
status: "PASS",
},
{
name: "拒绝售后",
status: "REFUSE",
},
{
name: "买家退货,待卖家收货",
status: "BUYER_RETURN",
},
{
name: "卖家确认收货",
status: "SELLER_CONFIRM",
},
{
name: "卖家终止售后",
status: "SELLER_TERMINATION",
},
{
name: "买家取消售后",
status: "BUYER_CANCEL",
},
{
name: "完成售后",
status: "COMPLETE",
},
{
name: "待平台退款",
status: "WAIT_REFUND",
},
],
};
defineOptions({ name: "returnGoodsOrderDetail" });
const route = useRoute();
const sn = ref(""); // 订单号
const logisticsModal = ref(false); //查询物流模态框
const logisticsInfo = ref({}); //物流信息
const afterSaleInfo = ref({}); // 售后信息
const afterSaleImage = ref([]); //会员申诉图片
const submitLoading = ref(false); // 添加或编辑提交状态
const confirmLoading = ref(false); // 确认收货提交状态
const storeMsg = ref({}); // 商家地址信息
// 原代码未在 data 声明、直接挂 this 上的字段,补声明避免 ReferenceError
let loading = false;
//商家处理意见
const params = reactive({
serviceStatus: "PASS",
remark: "",
});
//平台退款
const refundPriceForm = reactive({
remark: "",
});
const afterSaleRefundStatusList = ref([
{
name: "退款申请",
status: "APPLY",
},
computed: {
isRefundOnly() {
return this.afterSaleInfo.serviceType === "RETURN_MONEY";
},
sectionTitle() {
return this.isRefundOnly ? "退款申请" : "退货申请";
},
buyerLogisticsName() {
const info = this.afterSaleInfo;
return (
info.mLogisticsName ||
info.mlogisticsName ||
this.logisticsInfo.shipper ||
"-"
);
},
buyerLogisticsNo() {
const info = this.afterSaleInfo;
return (
info.mLogisticsNo ||
info.mlogisticsNo ||
this.logisticsInfo.logisticCode ||
"-"
);
},
canConfirmReceipt() {
const vo = this.afterSaleInfo.afterSaleAllowOperationVO;
if (vo && vo.rog) return true;
return this.afterSaleInfo.serviceStatus === "BUYER_RETURN";
},
{
name: "通过售后",
status: "PASS",
},
methods: {
// 获取售后详情
getDetail() {
this.loading = true;
API_Order.getAfterSaleOrderDetail(this.sn).then((res) => {
this.loading = false;
if (res.success) {
this.afterSaleInfo = res.result;
this.afterSaleInfo.showDelivery = this.showDelivery(
this.afterSaleInfo.serviceStatus
);
this.afterSaleInfo.serviceName = this.filterOrderStatus(
this.afterSaleInfo.serviceStatus
);
this.afterSaleImage = (res.result.afterSaleImage || "").split(",");
//退货地址去掉逗号
if (this.afterSaleInfo.mconsigneeAddressPath)
this.afterSaleInfo.mconsigneeAddressPath = this.afterSaleInfo.mconsigneeAddressPath.replaceAll(
{
name: "拒绝售后",
status: "REFUSE",
},
{
name: "卖家终止售后",
status: "SELLER_TERMINATION",
},
{
name: "买家取消售后",
status: "BUYER_CANCEL",
},
{
name: "完成售后",
status: "COMPLETE",
},
{
name: "待平台退款",
status: "WAIT_REFUND",
},
]);
const afterSaleStatusList = ref([
// 售后状态列表
{
name: "申请中",
status: "APPLY",
},
{
name: "通过售后",
status: "PASS",
},
{
name: "拒绝售后",
status: "REFUSE",
},
{
name: "买家退货,待卖家收货",
status: "BUYER_RETURN",
},
{
name: "卖家确认收货",
status: "SELLER_CONFIRM",
},
{
name: "卖家终止售后",
status: "SELLER_TERMINATION",
},
{
name: "买家取消售后",
status: "BUYER_CANCEL",
},
{
name: "完成售后",
status: "COMPLETE",
},
{
name: "待平台退款",
status: "WAIT_REFUND",
},
]);
const isRefundOnly = computed(() => {
return afterSaleInfo.value.serviceType === "RETURN_MONEY";
});
const sectionTitle = computed(() => {
return isRefundOnly.value ? "退款申请" : "退货申请";
});
const buyerLogisticsName = computed(() => {
const info = afterSaleInfo.value;
return (
info.mLogisticsName ||
info.mlogisticsName ||
logisticsInfo.value.shipper ||
"-"
);
});
const buyerLogisticsNo = computed(() => {
const info = afterSaleInfo.value;
return (
info.mLogisticsNo ||
info.mlogisticsNo ||
logisticsInfo.value.logisticCode ||
"-"
);
});
const canConfirmReceipt = computed(() => {
const vo = afterSaleInfo.value.afterSaleAllowOperationVO;
if (vo && vo.rog) return true;
return afterSaleInfo.value.serviceStatus === "BUYER_RETURN";
});
// 获取售后详情
function getDetail() {
loading = true;
API_Order.getAfterSaleOrderDetail(sn.value).then((res) => {
loading = false;
if (res.success) {
afterSaleInfo.value = res.result;
afterSaleInfo.value.showDelivery = showDelivery(
afterSaleInfo.value.serviceStatus
);
afterSaleInfo.value.serviceName = filterOrderStatus(
afterSaleInfo.value.serviceStatus
);
afterSaleImage.value = (res.result.afterSaleImage || "").split(",");
//退货地址去掉逗号
if (afterSaleInfo.value.mconsigneeAddressPath)
afterSaleInfo.value.mconsigneeAddressPath = afterSaleInfo.value.mconsigneeAddressPath.replaceAll(
",",
" "
);
params.actualRefundPrice = afterSaleInfo.value.applyRefundPrice;
// 如果显示物流信息,展示商家地址
if (afterSaleInfo.value.showDelivery) {
API_Order.storeAddress(sn.value).then((resu) => {
if (resu.success) {
const obj = resu.result;
obj.salesConsigneeAddressPath = obj.salesConsigneeAddressPath.replaceAll(
",",
" "
""
);
this.params.actualRefundPrice = this.afterSaleInfo.applyRefundPrice;
// 如果显示物流信息,展示商家地址
if (this.afterSaleInfo.showDelivery) {
API_Order.storeAddress(this.sn).then((resu) => {
if (resu.success) {
const obj = resu.result;
obj.salesConsigneeAddressPath = obj.salesConsigneeAddressPath.replaceAll(
",",
""
);
this.storeMsg = obj;
}
});
storeMsg.value = obj;
}
}
});
},
//平台退款
refundPriceSubmit() {
if (this.refundPriceForm.remark == "") {
this.$Message.error("请输入退款备注");
return;
});
}
this.$Modal.confirm({
title: "确认退款",
content: "请确认退款?",
loading: true,
onOk: () => {
API_Order.refundPrice(this.sn, this.refundPriceForm).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("收款成功");
this.getDetail();
}
});
},
});
},
//查询物流
logisticsSeller() {
this.logisticsModal = true;
this.logisticsInfo = {};
API_Order.getAfterSaleTraces(this.sn).then((res) => {
if (res.success && res.result != null) {
this.logisticsInfo = res.result;
}
});
},
// 商家确认收货 / 拒收
sellerConfirmSubmit(type) {
const isPass = type === "PASS";
this.$Modal.confirm({
title: isPass ? "确认收货" : "确认拒收",
content: isPass ? "请确认已经收到退货货物?" : "确认拒收此货物?",
loading: true,
onOk: () => {
this.confirmLoading = true;
API_Order.afterSaleSellerConfirm(this.sn, {
serviceStatus: type,
remark: this.params.remark || "",
}).then((res) => {
this.$Modal.remove();
this.confirmLoading = false;
if (res.success) {
this.$Message.success(isPass ? "收货成功" : "拒收成功");
this.getDetail();
}
}).catch(() => {
this.$Modal.remove();
this.confirmLoading = false;
});
},
});
},
//关闭物流弹出框
logisticsClose() {
this.logisticsModal = false;
},
//回复
handleSubmit() {
if (this.params.remark == "") {
this.$Message.error("请输入备注信息");
return;
}
if (this.params.serviceStatus === "PASS" && this.params.actualRefundPrice === "") {
this.$Message.error("请输入退款金额");
return;
}
API_Order.afterSaleSellerReview(this.sn, this.params).then((res) => {
this.submitLoading = false;
}
});
}
//平台退款
function refundPriceSubmit() {
if (refundPriceForm.remark == "") {
Message.error("请输入退款备注");
return;
}
Modal.confirm({
title: "确认退款",
content: "请确认退款?",
loading: true,
onOk: () => {
API_Order.refundPrice(sn.value, refundPriceForm).then((res) => {
Modal.remove();
if (res.success) {
this.$Message.success("审核成功");
this.params.remark = "";
this.getDetail();
Message.success("收款成功");
getDetail();
}
});
},
filterOrderStatus(status) {
const list =
this.afterSaleInfo.serviceType === "RETURN_MONEY"
? this.afterSaleRefundStatusList
: this.afterSaleStatusList;
const ob = list.filter((e) => e.status === status);
return ob.length > 0 ? ob[0].name : status;
},
// 根据订单状态判断是否显示物流信息
showDelivery(status) {
let flag = false;
this.afterSaleStatusList.forEach((e, index) => {
// 订单为买家退货,待卖家收货之后的状态,并且不是买家取消售后,展示物流信息
if (e.status === status && index >= 3 && index !== 6) {
flag = true;
});
}
//查询物流
function logisticsSeller() {
logisticsModal.value = true;
logisticsInfo.value = {};
API_Order.getAfterSaleTraces(sn.value).then((res) => {
if (res.success && res.result != null) {
logisticsInfo.value = res.result;
}
});
}
// 商家确认收货 / 拒收
function sellerConfirmSubmit(type) {
const isPass = type === "PASS";
Modal.confirm({
title: isPass ? "确认收货" : "确认拒收",
content: isPass ? "请确认已经收到退货货物?" : "确认拒收此货物?",
loading: true,
onOk: () => {
confirmLoading.value = true;
API_Order.afterSaleSellerConfirm(sn.value, {
serviceStatus: type,
remark: params.remark || "",
}).then((res) => {
Modal.remove();
confirmLoading.value = false;
if (res.success) {
Message.success(isPass ? "收货成功" : "拒收成功");
getDetail();
}
}).catch(() => {
Modal.remove();
confirmLoading.value = false;
});
return flag;
},
},
mounted() {
this.sn = this.$route.query.sn;
this.getDetail();
},
};
});
}
//关闭物流弹出框
function logisticsClose() {
logisticsModal.value = false;
}
//回复
function handleSubmit() {
if (params.remark == "") {
Message.error("请输入备注信息");
return;
}
if (params.serviceStatus === "PASS" && params.actualRefundPrice === "") {
Message.error("请输入退款金额");
return;
}
API_Order.afterSaleSellerReview(sn.value, params).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("审核成功");
params.remark = "";
getDetail();
}
});
}
function filterOrderStatus(status) {
const list =
afterSaleInfo.value.serviceType === "RETURN_MONEY"
? afterSaleRefundStatusList.value
: afterSaleStatusList.value;
const ob = list.filter((e) => e.status === status);
return ob.length > 0 ? ob[0].name : status;
}
// 根据订单状态判断是否显示物流信息
function showDelivery(status) {
let flag = false;
afterSaleStatusList.value.forEach((e, index) => {
// 订单为买家退货,待卖家收货之后的状态,并且不是买家取消售后,展示物流信息
if (e.status === status && index >= 3 && index !== 6) {
flag = true;
}
});
return flag;
}
onMounted(() => {
sn.value = route.query.sn;
getDetail();
});
</script>
<style lang="scss">
.main-content {

View File

@@ -45,101 +45,106 @@
</el-card>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { UploadFilled } from "@element-plus/icons-vue";
import { downLoadDeliverExcel, uploadDeliverExcel } from "@/api/order.js";
import { baseUrl } from "@/libs/axios.js";
import { getStore } from "@/libs/storage";
import { useAppStore } from "@/stores/app";
import downloadImg from "@/assets/download.png";
import uploadImg from "@/assets/upload.png";
import successImg from "@/assets/success.png";
export default {
components: { UploadFilled },
data() {
return {
file: "",
action: baseUrl + "/order/order/batchDeliver",
accessToken: {},
stepList: [
{
img: downloadImg,
title: "1.下载批量发货导入模板",
checked: true,
},
{
img: uploadImg,
title: "2.上传数据",
checked: false,
},
{
img: successImg,
title: "3.完成",
checked: false,
},
],
};
const appStore = useAppStore();
const router = useRouter();
const file = ref("");
const action = baseUrl + "/order/order/batchDeliver";
const accessToken = ref({});
const stepList = ref([
{
img: downloadImg,
title: "1.下载批量发货导入模板",
checked: true,
},
mounted() {
this.accessToken.accessToken = this.getStore("accessToken");
{
img: uploadImg,
title: "2.上传数据",
checked: false,
},
methods: {
handleCheckStep(val) {
if (val.title.search("3") === -1) {
this.stepList.forEach((item) => {
item.checked = false;
});
val.checked = true;
{
img: successImg,
title: "3.完成",
checked: false,
},
]);
onMounted(() => {
accessToken.value.accessToken = getStore("accessToken");
});
function handleCheckStep(val) {
if (val.title.search("3") === -1) {
stepList.value.forEach((item) => {
item.checked = false;
});
val.checked = true;
}
}
function handleUpload(uploadFile) {
file.value = uploadFile;
upload();
return false;
}
function navigationToGoodsOrder() {
router.push({ path: "/order/orderList" });
}
function close() {
appStore.removeTag("export-order-deliver");
localStorage.storeOpenedList = JSON.stringify(appStore.storeOpenedList);
router.go(-1);
}
async function upload() {
const fd = new FormData();
fd.append("files", file.value);
const res = await uploadDeliverExcel(fd);
if (res.success) {
stepList.value.forEach((item) => {
item.checked = false;
});
stepList.value[2].checked = true;
}
}
function downLoad() {
downLoadDeliverExcel()
.then((res) => {
const blob = new Blob([res], {
type: "application/vnd.ms-excel;charset=utf-8",
});
if ("download" in document.createElement("a")) {
const link = document.createElement("a");
link.download = "批量发货导入模板.xls";
link.style.display = "none";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} else {
navigator.msSaveBlob(blob, "批量发货导入模板.xls");
}
},
handleUpload(file) {
this.file = file;
this.upload();
return false;
},
navigationToGoodsOrder() {
this.$router.push({ path: "/order/orderList" });
},
close() {
this.$store.commit("removeTag", "export-order-deliver");
localStorage.storeOpenedList = JSON.stringify(this.$store.state.app.storeOpenedList);
this.$router.go(-1);
},
async upload() {
const fd = new FormData();
fd.append("files", this.file);
const res = await uploadDeliverExcel(fd);
if (res.success) {
this.stepList.forEach((item) => {
item.checked = false;
});
this.stepList[2].checked = true;
}
},
downLoad() {
downLoadDeliverExcel()
.then((res) => {
const blob = new Blob([res], {
type: "application/vnd.ms-excel;charset=utf-8",
});
if ("download" in document.createElement("a")) {
const link = document.createElement("a");
link.download = "批量发货导入模板.xls";
link.style.display = "none";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} else {
navigator.msSaveBlob(blob, "批量发货导入模板.xls");
}
})
.catch((err) => {
console.log(err);
});
},
},
};
})
.catch((err) => {
console.log(err);
});
}
</script>
<style lang="scss" scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
label-width="70px"
class="search-form order-search-form"
@@ -156,322 +156,325 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import { CopyDocument } from "@element-plus/icons-vue";
import * as API_Order from "@/api/order";
import { canShowShipButton, isNonShippableOrder } from "@/utils/orderFulfill";
export default {
name: "orderList",
components: { CopyDocument },
data() {
return {
fields: {
订单编号: "sn",
下单时间: "createTime",
客户名称: "memberName",
支付方式: {
field: "clientType",
callback: (value) => {
if (value == "H5") return "移动端";
if (value == "PC") return "PC端";
if (value == "WECHAT_MP") return "小程序端";
if (value == "APP") return "移动应用端";
return value;
},
},
商品数量: "groupNum",
付款状态: {
field: "payStatus",
callback: (value) =>
value == "UNPAID" ? "未付款" : value == "PAID" ? "已付款" : "",
},
店铺: "storeName",
},
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
orderType: "",
orderSn: "",
keywords: "",
goodsType: "",
orderStatus: "",
paymentMethod: "",
orderPromotionType: "",
},
selectDate: null,
data: [],
total: 0,
orderNumData: {},
currentStatus: "ALL",
};
},
computed: {
orderStatusWithCount() {
return [
{ title: "全部", value: "ALL" },
{
title: `未付款${this.orderNumData.waitPayNum ? "(" + this.orderNumData.waitPayNum + ")" : ""}`,
value: "UNPAID",
},
{
title: `已付款${this.orderNumData.waitDeliveryNum ? "(" + this.orderNumData.waitDeliveryNum + ")" : ""}`,
value: "PAID",
},
{
title: `待发货${this.orderNumData.waitShipNum ? "(" + this.orderNumData.waitShipNum + ")" : ""}`,
value: "UNDELIVERED",
},
{
title: `部分发货${this.orderNumData.partsDeliveredNumNum ? "(" + this.orderNumData.partsDeliveredNumNum + ")" : ""}`,
value: "PARTS_DELIVERED",
},
{
title: `待收货${this.orderNumData.deliveredNum ? "(" + this.orderNumData.deliveredNum + ")" : ""}`,
value: "DELIVERED",
},
{
title: `待核验${this.orderNumData.waitCheckNum ? "(" + this.orderNumData.waitCheckNum + ")" : ""}`,
value: "TAKE",
},
{
title: `待自提${this.orderNumData.waitSelfPickNum ? "(" + this.orderNumData.waitSelfPickNum + ")" : ""}`,
value: "STAY_PICKED_UP",
},
{
title: `已完成${this.orderNumData.finishNum ? "(" + this.orderNumData.finishNum + ")" : ""}`,
value: "COMPLETED",
},
{
title: `已关闭${this.orderNumData.closeNum ? "(" + this.orderNumData.closeNum + ")" : ""}`,
value: "CANCELLED",
},
];
import { Message } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
defineOptions({ name: "orderList" });
const route = useRoute();
const fields = ref({
订单编号: "sn",
下单时间: "createTime",
客户名称: "memberName",
支付方式: {
field: "clientType",
callback: (value) => {
if (value == "H5") return "移动端";
if (value == "PC") return "PC端";
if (value == "WECHAT_MP") return "小程序端";
if (value == "APP") return "移动应用端";
return value;
},
},
methods: {
onStatusTabClick(tab) {
this.orderStatusClick(tab.paneName);
商品数量: "groupNum",
付款状态: {
field: "payStatus",
callback: (value) =>
value == "UNPAID" ? "未付款" : value == "PAID" ? "已付款" : "",
},
店铺: "storeName",
});
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
orderType: "",
orderSn: "",
keywords: "",
goodsType: "",
orderStatus: "",
paymentMethod: "",
orderPromotionType: "",
});
const selectDate = ref(null);
const data = ref([]);
const total = ref(0);
const orderNumData = ref({});
const currentStatus = ref("ALL");
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
const orderStatusWithCount = computed(() => {
return [
{ title: "全部", value: "ALL" },
{
title: `未付款${orderNumData.value.waitPayNum ? "(" + orderNumData.value.waitPayNum + ")" : ""}`,
value: "UNPAID",
},
orderGoods(row) {
const items = row && row.orderItems;
return items && items.length ? items : [{}];
{
title: `已付款${orderNumData.value.waitDeliveryNum ? "(" + orderNumData.value.waitDeliveryNum + ")" : ""}`,
value: "PAID",
},
afterSaleText(status) {
const map = {
NOT_APPLIED: "—",
NEW: "—",
PART_AFTER_SALE: "部分售后",
ALREADY_APPLIED: "售后中",
EXPIRED: "不可售后",
};
return map[status] || "—";
{
title: `待发货${orderNumData.value.waitShipNum ? "(" + orderNumData.value.waitShipNum + ")" : ""}`,
value: "UNDELIVERED",
},
copyText(text) {
if (!text) {
return;
{
title: `部分发货${orderNumData.value.partsDeliveredNumNum ? "(" + orderNumData.value.partsDeliveredNumNum + ")" : ""}`,
value: "PARTS_DELIVERED",
},
{
title: `待收货${orderNumData.value.deliveredNum ? "(" + orderNumData.value.deliveredNum + ")" : ""}`,
value: "DELIVERED",
},
{
title: `待核验${orderNumData.value.waitCheckNum ? "(" + orderNumData.value.waitCheckNum + ")" : ""}`,
value: "TAKE",
},
{
title: `待自提${orderNumData.value.waitSelfPickNum ? "(" + orderNumData.value.waitSelfPickNum + ")" : ""}`,
value: "STAY_PICKED_UP",
},
{
title: `已完成${orderNumData.value.finishNum ? "(" + orderNumData.value.finishNum + ")" : ""}`,
value: "COMPLETED",
},
{
title: `已关闭${orderNumData.value.closeNum ? "(" + orderNumData.value.closeNum + ")" : ""}`,
value: "CANCELLED",
},
];
});
onMounted(() => {
const orderStatus = route.query.orderStatus;
if (orderStatus) {
currentStatus.value = orderStatus;
searchForm.orderStatus = orderStatus;
}
init();
});
function onStatusTabClick(tab) {
orderStatusClick(tab.paneName);
}
function orderGoods(row) {
const items = row && row.orderItems;
return items && items.length ? items : [{}];
}
function afterSaleText(status) {
const map = {
NOT_APPLIED: "—",
NEW: "—",
PART_AFTER_SALE: "部分售后",
ALREADY_APPLIED: "售后中",
EXPIRED: "不可售后",
};
return map[status] || "—";
}
function copyText(text) {
if (!text) {
return;
}
const done = () => Message.success("已复制订单编号");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done).catch(() => copyTextFallback(text, done));
return;
}
copyTextFallback(text, done);
}
function copyTextFallback(text, done) {
const input = document.createElement("textarea");
input.value = text;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
done();
}
function clientTypeText(v) {
const map = { H5: "移动端", PC: "PC端", WECHAT_MP: "小程序端", APP: "移动应用端" };
return map[v] || v || "-";
}
function orderPromotionText(v) {
const map = {
NORMAL: "普通订单",
PINTUAN: "拼团订单",
GIFT: "赠品订单",
POINTS: "积分订单",
KANJIA: "砍价订单",
};
return map[v] || v || "-";
}
function orderPromotionTagType(v) {
const map = {
NORMAL: "primary",
PINTUAN: "danger",
GIFT: "success",
POINTS: "info",
KANJIA: "warning",
};
return map[v] || "info";
}
function paymentMethodText(v) {
const map = {
NOT_ACTUALLY_PAID: "-",
WECHAT: "微信支付",
ALIPAY: "支付宝",
WALLET: "余额支付",
BANK_TRANSFER: "线下转账",
};
return map[v] || v || "-";
}
function orderStatusText(v) {
const map = {
UNPAID: "未付款",
PAID: "已付款",
UNDELIVERED: "待发货",
STAY_PICKED_UP: "待自提",
PARTS_DELIVERED: "部分发货",
DELIVERED: "已发货",
COMPLETED: "已完成",
TAKE: "待核验",
CANCELLED: "已关闭",
};
return map[v] || v || "-";
}
function orderStatusTagType(v) {
const map = {
UNPAID: "danger",
PAID: "primary",
UNDELIVERED: "info",
STAY_PICKED_UP: "info",
PARTS_DELIVERED: "warning",
DELIVERED: "warning",
COMPLETED: "success",
TAKE: "warning",
CANCELLED: "danger",
};
return map[v] || "info";
}
function init() {
getDataList();
getOrderNumData();
}
function changePage(v) {
searchForm.pageNumber = v;
getDataList();
}
function changePageSize(v) {
searchForm.pageNumber = 1;
searchForm.pageSize = v;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
getOrderNumData();
}
function selectDateRange(v) {
if (v && v.length === 2) {
searchForm.startDate = v[0];
searchForm.endDate = v[1];
} else {
searchForm.startDate = "";
searchForm.endDate = "";
}
}
function getDataList() {
loading.value = true;
API_Order.getOrderList(searchForm)
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
const done = () => this.$Message.success("已复制订单编号");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done).catch(() => this.copyTextFallback(text, done));
return;
}
this.copyTextFallback(text, done);
})
.catch(() => {
loading.value = false;
});
}
function detail(v, openFulfill = false) {
customRouterPush({
name: "order-detail",
query: {
sn: v.sn,
...(openFulfill ? { openFulfill: "1" } : {}),
...(openFulfill && v.orderStatus === "PARTS_DELIVERED" ? { fulfillMode: "SPLIT" } : {}),
},
copyTextFallback(text, done) {
const input = document.createElement("textarea");
input.value = text;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
done();
},
clientTypeText(v) {
const map = { H5: "移动端", PC: "PC端", WECHAT_MP: "小程序端", APP: "移动应用端" };
return map[v] || v || "-";
},
orderPromotionText(v) {
const map = {
NORMAL: "普通订单",
PINTUAN: "拼团订单",
GIFT: "赠品订单",
POINTS: "积分订单",
KANJIA: "砍价订单",
};
return map[v] || v || "-";
},
orderPromotionTagType(v) {
const map = {
NORMAL: "primary",
PINTUAN: "danger",
GIFT: "success",
POINTS: "info",
KANJIA: "warning",
};
return map[v] || "info";
},
paymentMethodText(v) {
const map = {
NOT_ACTUALLY_PAID: "-",
WECHAT: "微信支付",
ALIPAY: "支付宝",
WALLET: "余额支付",
BANK_TRANSFER: "线下转账",
};
return map[v] || v || "-";
},
orderStatusText(v) {
const map = {
UNPAID: "未付款",
PAID: "已付款",
UNDELIVERED: "待发货",
STAY_PICKED_UP: "待自提",
PARTS_DELIVERED: "部分发货",
DELIVERED: "已发货",
COMPLETED: "已完成",
TAKE: "待核验",
CANCELLED: "已关闭",
};
return map[v] || v || "-";
},
orderStatusTagType(v) {
const map = {
UNPAID: "danger",
PAID: "primary",
UNDELIVERED: "info",
STAY_PICKED_UP: "info",
PARTS_DELIVERED: "warning",
DELIVERED: "warning",
COMPLETED: "success",
TAKE: "warning",
CANCELLED: "danger",
};
return map[v] || "info";
},
init() {
this.getDataList();
this.getOrderNumData();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
},
changePageSize(v) {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = v;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
this.getOrderNumData();
},
selectDateRange(v) {
if (v && v.length === 2) {
this.searchForm.startDate = v[0];
this.searchForm.endDate = v[1];
} else {
this.searchForm.startDate = "";
this.searchForm.endDate = "";
}
},
getDataList() {
this.loading = true;
API_Order.getOrderList(this.searchForm)
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
})
.catch(() => {
this.loading = false;
});
},
detail(v, openFulfill = false) {
this.$filters.customRouterPush({
name: "order-detail",
query: {
sn: v.sn,
...(openFulfill ? { openFulfill: "1" } : {}),
...(openFulfill && v.orderStatus === "PARTS_DELIVERED" ? { fulfillMode: "SPLIT" } : {}),
},
});
}
function canShip(row) {
if (!row) return false;
if (row.allowOperationVO) {
return canShowShipButton(row.allowOperationVO, row);
}
if (isNonShippableOrder(row)) return false;
return row.orderStatus === "UNDELIVERED" || row.orderStatus === "PARTS_DELIVERED";
}
async function exportOrder() {
if (!searchForm.startDate || !searchForm.endDate) {
Message.error("必须选择时间范围,搜索后进行导出!");
return;
}
API_Order.exportOrder(searchForm)
.then((res) => {
const blob = new Blob([res], {
type: "application/vnd.ms-excel;charset=utf-8",
});
},
canShip(row) {
if (!row) return false;
if (row.allowOperationVO) {
return canShowShipButton(row.allowOperationVO, row);
}
if (isNonShippableOrder(row)) return false;
return row.orderStatus === "UNDELIVERED" || row.orderStatus === "PARTS_DELIVERED";
},
async exportOrder() {
if (!this.searchForm.startDate || !this.searchForm.endDate) {
this.$Message.error("必须选择时间范围,搜索后进行导出!");
return;
}
API_Order.exportOrder(this.searchForm)
.then((res) => {
const blob = new Blob([res], {
type: "application/vnd.ms-excel;charset=utf-8",
});
if ("download" in document.createElement("a")) {
const link = document.createElement("a");
link.download = "订单列表.xlsx";
link.style.display = "none";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} else {
navigator.msSaveBlob(blob, "订单列表.xlsx");
}
})
.catch((err) => {
console.log(err);
});
},
orderStatusClick(name) {
if (name === "ALL" || name === "" || name === undefined) {
this.searchForm.orderStatus = "";
this.currentStatus = "ALL";
if ("download" in document.createElement("a")) {
const link = document.createElement("a");
link.download = "订单列表.xlsx";
link.style.display = "none";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} else {
this.searchForm.orderStatus = name;
this.currentStatus = name;
navigator.msSaveBlob(blob, "订单列表.xlsx");
}
this.getDataList();
},
getOrderNumData() {
const { orderStatus, pageNumber, pageSize, sort, order, ...searchParams } = this.searchForm;
API_Order.getOrderNum(searchParams)
.then((res) => {
if (res.success) {
this.orderNumData = res.result;
}
})
.catch((err) => {
console.error("获取订单数量统计失败:", err);
});
},
},
mounted() {
const orderStatus = this.$route.query.orderStatus;
if (orderStatus) {
this.currentStatus = orderStatus;
this.searchForm.orderStatus = orderStatus;
}
this.init();
},
};
})
.catch((err) => {
console.log(err);
});
}
function orderStatusClick(name) {
if (name === "ALL" || name === "" || name === undefined) {
searchForm.orderStatus = "";
currentStatus.value = "ALL";
} else {
searchForm.orderStatus = name;
currentStatus.value = name;
}
getDataList();
}
function getOrderNumData() {
const { orderStatus, pageNumber, pageSize, sort, order, ...searchParams } = searchForm;
API_Order.getOrderNum(searchParams)
.then((res) => {
if (res.success) {
orderNumData.value = res.result;
}
})
.catch((err) => {
console.error("获取订单数量统计失败:", err);
});
}
</script>
<style lang="scss" scoped>

View File

@@ -113,135 +113,131 @@
</div>
</template>
<script>
<script setup>
import { reactive, ref } from "vue";
import { confirmVerification, getVerificationRecords, previewVerification } from "@/api/order";
import { Message } from "@/utils/message";
export default {
name: "orderVerification",
data() {
return {
activeTab: "verify",
code: "",
lookingUp: false,
submitting: false,
confirmVisible: false,
preview: {
code: "",
orderSn: "",
goods: [],
},
loading: false,
records: [],
total: 0,
pageNumber: 1,
pageSize: 20,
verifiedRange: null,
recordQuery: {
goodsName: "",
verifierName: "",
},
};
},
methods: {
specText(specs) {
if (!specs) return "";
try {
const obj = typeof specs === "string" ? JSON.parse(specs) : specs;
if (!obj || typeof obj !== "object") return "";
return Object.keys(obj)
.filter((key) => key !== "images")
.map((key) => `${key}:${obj[key]}`)
.join(" ");
} catch (e) {
return "";
defineOptions({ name: "orderVerification" });
const activeTab = ref("verify");
const code = ref("");
const lookingUp = ref(false);
const submitting = ref(false);
const confirmVisible = ref(false);
const preview = ref({
code: "",
orderSn: "",
goods: [],
});
const loading = ref(false);
const records = ref([]);
const total = ref(0);
const pageNumber = ref(1);
const pageSize = ref(20);
const verifiedRange = ref(null);
const recordQuery = reactive({
goodsName: "",
verifierName: "",
});
function specText(specs) {
if (!specs) return "";
try {
const obj = typeof specs === "string" ? JSON.parse(specs) : specs;
if (!obj || typeof obj !== "object") return "";
return Object.keys(obj)
.filter((key) => key !== "images")
.map((key) => `${key}:${obj[key]}`)
.join(" ");
} catch (e) {
return "";
}
}
function confirmVerify() {
const inputCode = (code.value || "").trim();
if (!inputCode) {
Message.warning("请输入核销码");
return;
}
lookingUp.value = true;
previewVerification(inputCode)
.then((res) => {
lookingUp.value = false;
if (res.success && res.result) {
preview.value = {
code: res.result.code || inputCode,
orderSn: res.result.orderSn || "",
goods: res.result.goods || [],
};
confirmVisible.value = true;
}
},
confirmVerify() {
const code = (this.code || "").trim();
if (!code) {
this.$Message.warning("请输入核销码");
return;
})
.catch(() => {
lookingUp.value = false;
});
}
function submitVerify() {
const submitCode = (preview.value.code || "").trim();
if (!submitCode) {
Message.warning("请输入核销码");
return;
}
submitting.value = true;
confirmVerification(submitCode)
.then((res) => {
submitting.value = false;
if (res.success) {
Message.success(res.result || "核销成功");
confirmVisible.value = false;
code.value = "";
preview.value = { code: "", orderSn: "", goods: [] };
pageNumber.value = 1;
loadRecords();
}
this.lookingUp = true;
previewVerification(code)
.then((res) => {
this.lookingUp = false;
if (res.success && res.result) {
this.preview = {
code: res.result.code || code,
orderSn: res.result.orderSn || "",
goods: res.result.goods || [],
};
this.confirmVisible = true;
}
})
.catch(() => {
this.lookingUp = false;
});
},
submitVerify() {
const code = (this.preview.code || "").trim();
if (!code) {
this.$Message.warning("请输入核销码");
return;
})
.catch(() => {
submitting.value = false;
});
}
function onTabChange(name) {
if (name === "records") {
loadRecords();
}
}
function onSizeChange() {
pageNumber.value = 1;
loadRecords();
}
function searchRecords() {
pageNumber.value = 1;
loadRecords();
}
function loadRecords() {
loading.value = true;
const range = verifiedRange.value;
const params = {
pageNumber: pageNumber.value,
pageSize: pageSize.value,
goodsName: (recordQuery.goodsName || "").trim(),
verifierName: (recordQuery.verifierName || "").trim(),
};
if (range && range.length === 2) {
params.startTime = range[0];
params.endTime = range[1];
}
getVerificationRecords(params)
.then((res) => {
loading.value = false;
if (res.success) {
records.value = res.result.records || [];
total.value = res.result.total || 0;
}
this.submitting = true;
confirmVerification(code)
.then((res) => {
this.submitting = false;
if (res.success) {
this.$Message.success(res.result || "核销成功");
this.confirmVisible = false;
this.code = "";
this.preview = { code: "", orderSn: "", goods: [] };
this.pageNumber = 1;
this.loadRecords();
}
})
.catch(() => {
this.submitting = false;
});
},
onTabChange(name) {
if (name === "records") {
this.loadRecords();
}
},
onSizeChange() {
this.pageNumber = 1;
this.loadRecords();
},
searchRecords() {
this.pageNumber = 1;
this.loadRecords();
},
loadRecords() {
this.loading = true;
const range = this.verifiedRange;
const params = {
pageNumber: this.pageNumber,
pageSize: this.pageSize,
goodsName: (this.recordQuery.goodsName || "").trim(),
verifierName: (this.recordQuery.verifierName || "").trim(),
};
if (range && range.length === 2) {
params.startTime = range[0];
params.endTime = range[1];
}
getVerificationRecords(params)
.then((res) => {
this.loading = false;
if (res.success) {
this.records = res.result.records || [];
this.total = res.result.total || 0;
}
})
.catch(() => {
this.loading = false;
});
},
},
};
})
.catch(() => {
loading.value = false;
});
}
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -57,7 +57,7 @@
</div>
<el-table
ref="table"
ref="tableRef"
v-loading="loading"
border
:data="data"
@@ -126,110 +126,113 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import * as API_Order from "@/api/order";
export default {
name: "fictitiousOrderList",
data() {
return {
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "",
order: "",
startDate: "",
endDate: "",
orderType: "VIRTUAL",
orderSn: "",
buyerName: "",
orderStatus: "",
},
selectDate: null,
data: [],
total: 0,
orderStatus: [
{ title: "全部", value: "" },
{ title: "未付款", value: "UNPAID" },
{ title: "已付款", value: "PAID" },
{ title: "待核验", value: "TAKE" },
{ title: "已完成", value: "COMPLETED" },
{ title: "已关闭", value: "CANCELLED" },
],
currentStatus: "",
};
},
methods: {
init() {
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
changeSort(e) {
this.searchForm.sort = e.prop;
this.searchForm.order = e.order === "ascending" ? "asc" : e.order === "descending" ? "desc" : "";
this.getDataList();
},
selectDateRange(v) {
if (v) {
this.searchForm.startDate = v[0];
this.searchForm.endDate = v[1];
}
},
getDataList() {
this.loading = true;
API_Order.getOrderList(this.searchForm).then((res) => {
this.loading = false;
import { Message, Modal } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
defineOptions({ name: "fictitiousOrderList" });
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "",
order: "",
startDate: "",
endDate: "",
orderType: "VIRTUAL",
orderSn: "",
buyerName: "",
orderStatus: "",
});
const selectDate = ref(null);
const data = ref([]);
const total = ref(0);
const orderStatus = ref([
{ title: "全部", value: "" },
{ title: "未付款", value: "UNPAID" },
{ title: "已付款", value: "PAID" },
{ title: "待核验", value: "TAKE" },
{ title: "已完成", value: "COMPLETED" },
{ title: "已关闭", value: "CANCELLED" },
]);
const currentStatus = ref("");
// searchFormRef/tableRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
const tableRef = ref(null);
onMounted(() => {
init();
});
function init() {
getDataList();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
}
function changeSort(e) {
searchForm.sort = e.prop;
searchForm.order = e.order === "ascending" ? "asc" : e.order === "descending" ? "desc" : "";
getDataList();
}
function selectDateRange(v) {
if (v) {
searchForm.startDate = v[0];
searchForm.endDate = v[1];
}
}
function getDataList() {
loading.value = true;
API_Order.getOrderList(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function confirmPrice(v) {
Modal.confirm({
title: "确认收款",
content: "您确定要收款吗?",
loading: true,
onOk: () => {
API_Order.orderPay(v.sn).then((res) => {
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
Message.success("收款成功");
getDataList();
}
Modal.remove();
});
},
confirmPrice(v) {
this.$Modal.confirm({
title: "确认收款",
content: "您确定要收款吗?",
loading: true,
onOk: () => {
API_Order.orderPay(v.sn).then((res) => {
if (res.success) {
this.$Message.success("收款成功");
this.getDataList();
}
this.$Modal.remove();
});
},
});
},
detail(v) {
let sn = v.sn;
this.$filters.customRouterPush({
name: "order-detail",
query: { sn: sn, orderType: v.orderType },
});
},
onStatusTabClick(tab) {
const item = tab.paneName;
this.currentStatus = item;
this.searchForm.orderStatus = item;
this.getDataList();
},
},
mounted() {
this.init();
},
};
});
}
function detail(v) {
let sn = v.sn;
customRouterPush({
name: "order-detail",
query: { sn: sn, orderType: v.orderType },
});
}
function onStatusTabClick(tab) {
const item = tab.paneName;
currentStatus.value = item;
searchForm.orderStatus = item;
getDataList();
}
</script>
<style lang="scss" scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="70px" class="search-form">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="70px" class="search-form">
<el-form-item label="订单编号" prop="orderSn">
<el-input v-model="searchForm.orderSn" clearable placeholder="请输入订单编号" style="width: 240px" />
</el-form-item>
@@ -26,7 +26,7 @@
<el-card>
<div class="receipt-tip">订单状态为已发货/已完成可开票</div>
<el-table ref="table" v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
<el-table ref="tableRef" v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
<el-table-column label="订单号" min-width="120">
<template #default="{ row }">
<a class="link-text" @click="$router.push({ name: 'order-detail', query: { sn: row.orderSn } })">
@@ -181,217 +181,219 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import * as API_Order from "@/api/order";
import { uploadFile } from "@/libs/axios";
import { getStore } from "@/libs/storage";
import { Message } from "@/utils/message";
export default {
name: "receipt",
data() {
return {
loading: true,
receiptModalVisible: false,
receiptDetailLoading: false,
invoiceSubmitting: false,
receiptModalMode: "detail",
uploadFileUrl: uploadFile,
accessToken: {},
receiptUploadData: { directoryPath: "default" }, // OSS 上传目录(与后端存储配置一致)
currentReceipt: {},
selectedReceiptRow: null,
searchForm: {
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
receiptStatus: "",
},
data: [],
total: 0,
};
},
methods: {
orderStatusText(status) {
const map = {
UNPAID: "未付款",
PAID: "已付款",
UNDELIVERED: "待发货",
DELIVERED: "已发货",
COMPLETED: "已完成",
TAKE: "待核验",
CANCELLED: "已取消",
};
return map[status] || status;
},
orderStatusTagType(status) {
const map = {
UNPAID: "danger",
PAID: "primary",
UNDELIVERED: "info",
DELIVERED: "info",
COMPLETED: "success",
TAKE: "warning",
CANCELLED: "danger",
};
return map[status] || "info";
},
canInvoicing(row) {
if (!row) return false;
return (
(row.orderStatus === "COMPLETED" || row.orderStatus === "DELIVERED") &&
Number(row.receiptStatus) === 0
);
},
initUploadAccessToken() {
this.accessToken = { accessToken: this.getStore("accessToken") };
},
hasValue(value) {
if (value === null || value === undefined) return false;
return String(value).trim() !== "";
},
hasPrice(value) {
return value !== null && value !== undefined && value !== "";
},
formatPrice(value) {
if (value === null || value === undefined || value === "") return "暂无";
return `¥${value}`;
},
isVatSpecialReceipt(receipt) {
if (!receipt) return false;
const receiptType = receipt.receiptType != null ? String(receipt.receiptType).trim() : "";
return receiptType === "2" || receiptType === "增值税专用发票" || receipt.invoiceKind === "VAT_SPECIAL";
},
formatReceiptType(receipt) {
if (!receipt || (!receipt.receiptType && !receipt.invoiceKind)) return "电子普通发票";
const receiptType = receipt.receiptType != null ? String(receipt.receiptType).trim() : "";
if (receiptType === "电子普通发票" || receiptType === "增值税专用发票") return receiptType;
return this.isVatSpecialReceipt(receipt) ? "增值税专用发票" : "电子普通发票";
},
getInvoiceAddress(receipt) {
if (!receipt) return "";
return receipt.invoiceAddress || receipt.invoiceFileUrl || "";
},
buildInvoicingPayload() {
const invoiceAddress = this.getInvoiceAddress(this.currentReceipt);
return invoiceAddress ? { invoiceAddress } : {};
},
beforeInvoiceUpload(file) {
const allowed = ["image/jpeg", "image/jpg", "image/png", "application/pdf"];
const okType = allowed.includes(file.type) || /\.(jpg|jpeg|png|pdf)$/i.test(file.name);
if (!okType) {
this.$Message.warning("请上传 jpg、jpeg、png 或 pdf 格式文件");
return false;
}
if (file.size / 1024 / 1024 > 10) {
this.$Message.warning("发票附件不能超过 10MB");
return false;
}
return true;
},
handleInvoiceUploadSuccess(res) {
if (res && res.success && res.result) {
this.currentReceipt.invoiceAddress = res.result;
if (this.selectedReceiptRow) {
this.selectedReceiptRow.invoiceAddress = res.result;
}
this.$Message.success("发票上传成功");
} else {
this.$Message.error((res && res.message) || "发票上传失败");
}
},
handleInvoiceUploadError() {
this.$Message.error("发票上传失败");
},
viewInvoiceFile(url) {
if (!url) return;
window.open(url, "_blank");
},
init() {
this.getData();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getData();
},
changePageSize(v) {
this.searchForm.pageSize = v;
this.getData();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getData();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
receiptStatus: "",
};
this.getData();
},
getData() {
this.loading = true;
API_Order.getReceiptPage(this.searchForm).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
async openReceiptModal(row, mode = "detail") {
if (!row) return;
this.receiptModalMode = mode;
this.selectedReceiptRow = row;
this.currentReceipt = { ...row };
this.receiptModalVisible = true;
this.receiptDetailLoading = true;
try {
const res = await API_Order.getReceiptDetail(row.id);
if (res.success && res.result) {
this.currentReceipt = { ...row, ...res.result };
} else {
this.$Message.warning("发票详情获取失败,已展示列表中的发票信息");
}
} catch (e) {
this.$Message.error("发票详情获取失败");
} finally {
this.receiptDetailLoading = false;
}
},
async submitInvoicing() {
if (!this.selectedReceiptRow) return;
if (!this.canInvoicing(this.selectedReceiptRow)) {
this.$Message.warning("当前订单状态不支持开票");
return;
}
const params = this.buildInvoicingPayload();
if (!params.invoiceAddress) {
this.$Message.warning("请先上传发票");
return;
}
this.invoiceSubmitting = true;
try {
const res = await API_Order.invoicing(this.selectedReceiptRow.id, params);
if (res.success) {
this.$Message.success("开票成功");
this.receiptModalVisible = false;
this.getData();
}
} catch (e) {
this.$Message.error("开票失败");
} finally {
this.invoiceSubmitting = false;
}
},
},
mounted() {
this.initUploadAccessToken();
this.init();
},
};
defineOptions({ name: "receipt" });
const loading = ref(true);
const receiptModalVisible = ref(false);
const receiptDetailLoading = ref(false);
const invoiceSubmitting = ref(false);
const receiptModalMode = ref("detail");
const uploadFileUrl = ref(uploadFile);
const accessToken = ref({});
const receiptUploadData = ref({ directoryPath: "default" }); // OSS 上传目录(与后端存储配置一致)
const currentReceipt = ref({});
const selectedReceiptRow = ref(null);
const searchForm = ref({
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
receiptStatus: "",
});
const data = ref([]);
const total = ref(0);
// searchFormRef / tableRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
const tableRef = ref(null);
function orderStatusText(status) {
const map = {
UNPAID: "未付款",
PAID: "已付款",
UNDELIVERED: "待发货",
DELIVERED: "已发货",
COMPLETED: "已完成",
TAKE: "待核验",
CANCELLED: "已取消",
};
return map[status] || status;
}
function orderStatusTagType(status) {
const map = {
UNPAID: "danger",
PAID: "primary",
UNDELIVERED: "info",
DELIVERED: "info",
COMPLETED: "success",
TAKE: "warning",
CANCELLED: "danger",
};
return map[status] || "info";
}
function canInvoicing(row) {
if (!row) return false;
return (
(row.orderStatus === "COMPLETED" || row.orderStatus === "DELIVERED") &&
Number(row.receiptStatus) === 0
);
}
function initUploadAccessToken() {
accessToken.value = { accessToken: getStore("accessToken") };
}
function hasValue(value) {
if (value === null || value === undefined) return false;
return String(value).trim() !== "";
}
function hasPrice(value) {
return value !== null && value !== undefined && value !== "";
}
function formatPrice(value) {
if (value === null || value === undefined || value === "") return "暂无";
return `¥${value}`;
}
function isVatSpecialReceipt(receipt) {
if (!receipt) return false;
const receiptType = receipt.receiptType != null ? String(receipt.receiptType).trim() : "";
return receiptType === "2" || receiptType === "增值税专用发票" || receipt.invoiceKind === "VAT_SPECIAL";
}
function formatReceiptType(receipt) {
if (!receipt || (!receipt.receiptType && !receipt.invoiceKind)) return "电子普通发票";
const receiptType = receipt.receiptType != null ? String(receipt.receiptType).trim() : "";
if (receiptType === "电子普通发票" || receiptType === "增值税专用发票") return receiptType;
return isVatSpecialReceipt(receipt) ? "增值税专用发票" : "电子普通发票";
}
function getInvoiceAddress(receipt) {
if (!receipt) return "";
return receipt.invoiceAddress || receipt.invoiceFileUrl || "";
}
function buildInvoicingPayload() {
const invoiceAddress = getInvoiceAddress(currentReceipt.value);
return invoiceAddress ? { invoiceAddress } : {};
}
function beforeInvoiceUpload(file) {
const allowed = ["image/jpeg", "image/jpg", "image/png", "application/pdf"];
const okType = allowed.includes(file.type) || /\.(jpg|jpeg|png|pdf)$/i.test(file.name);
if (!okType) {
Message.warning("请上传 jpg、jpeg、png 或 pdf 格式文件");
return false;
}
if (file.size / 1024 / 1024 > 10) {
Message.warning("发票附件不能超过 10MB");
return false;
}
return true;
}
function handleInvoiceUploadSuccess(res) {
if (res && res.success && res.result) {
currentReceipt.value.invoiceAddress = res.result;
if (selectedReceiptRow.value) {
selectedReceiptRow.value.invoiceAddress = res.result;
}
Message.success("发票上传成功");
} else {
Message.error((res && res.message) || "发票上传失败");
}
}
function handleInvoiceUploadError() {
Message.error("发票上传失败");
}
function viewInvoiceFile(url) {
if (!url) return;
window.open(url, "_blank");
}
function init() {
getData();
}
function changePage(v) {
searchForm.value.pageNumber = v;
getData();
}
function changePageSize(v) {
searchForm.value.pageSize = v;
getData();
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getData();
}
function handleReset() {
searchForm.value = {
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
receiptStatus: "",
};
getData();
}
function getData() {
loading.value = true;
API_Order.getReceiptPage(searchForm.value).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
async function openReceiptModal(row, mode = "detail") {
if (!row) return;
receiptModalMode.value = mode;
selectedReceiptRow.value = row;
currentReceipt.value = { ...row };
receiptModalVisible.value = true;
receiptDetailLoading.value = true;
try {
const res = await API_Order.getReceiptDetail(row.id);
if (res.success && res.result) {
currentReceipt.value = { ...row, ...res.result };
} else {
Message.warning("发票详情获取失败,已展示列表中的发票信息");
}
} catch (e) {
Message.error("发票详情获取失败");
} finally {
receiptDetailLoading.value = false;
}
}
async function submitInvoicing() {
if (!selectedReceiptRow.value) return;
if (!canInvoicing(selectedReceiptRow.value)) {
Message.warning("当前订单状态不支持开票");
return;
}
const params = buildInvoicingPayload();
if (!params.invoiceAddress) {
Message.warning("请先上传发票");
return;
}
invoiceSubmitting.value = true;
try {
const res = await API_Order.invoicing(selectedReceiptRow.value.id, params);
if (res.success) {
Message.success("开票成功");
receiptModalVisible.value = false;
getData();
}
} catch (e) {
Message.error("开票失败");
} finally {
invoiceSubmitting.value = false;
}
}
onMounted(() => {
initUploadAccessToken();
init();
});
</script>
<style lang="scss">
@import "@/styles/table-common.scss";

View File

@@ -1,7 +1,7 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="80px">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="80px">
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
@@ -86,7 +86,8 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { ElMessageBox } from "element-plus";
import {
getDamageReportPage,
@@ -103,193 +104,204 @@ import {
damageStatusTag,
formatMoney,
} from "../constants";
import { Message, Modal } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
export default {
name: "procurement-damage-report-list",
data() {
return {
loading: false,
data: [],
total: 0,
dateRange: [],
currentStatus: "ALL",
statusCountData: {},
searchForm: {
pageNumber: 1,
pageSize: 10,
status: "",
},
};
},
computed: {
statusTabs() {
const tabs = [{ title: "全部", value: "ALL" }];
Object.keys(DAMAGE_STATUS).forEach((key) => {
const count = this.statusCountData[key];
const label = DAMAGE_STATUS[key];
tabs.push({
title: count ? `${label}(${count})` : label,
value: key,
});
});
return tabs;
},
},
mounted() {
this.init();
},
methods: {
damageStatusText,
damageStatusTag,
formatMoney,
init() {
this.getStatusCount();
this.getDataList();
},
buildSearchParams(includeStatus = true) {
const params = { ...this.searchForm };
if (!includeStatus) {
delete params.status;
defineOptions({ name: "procurement-damage-report-list" });
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const dateRange = ref([]);
const currentStatus = ref("ALL");
const statusCountData = ref({});
const searchForm = ref({
pageNumber: 1,
pageSize: 10,
status: "",
});
const statusTabs = computed(() => {
const tabs = [{ title: "全部", value: "ALL" }];
Object.keys(DAMAGE_STATUS).forEach((key) => {
const count = statusCountData.value[key];
const label = DAMAGE_STATUS[key];
tabs.push({
title: count ? `${label}(${count})` : label,
value: key,
});
});
return tabs;
});
onMounted(() => {
init();
});
function init() {
getStatusCount();
getDataList();
}
function buildSearchParams(includeStatus = true) {
const params = { ...searchForm.value };
if (!includeStatus) {
delete params.status;
}
if (dateRange.value && dateRange.value.length === 2) {
params.startDate = dateRange.value[0];
params.endDate = dateRange.value[1];
}
return params;
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getStatusCount();
getDataList();
}
function resetSearch() {
searchForm.value = { pageNumber: 1, pageSize: 10, status: "" };
dateRange.value = [];
currentStatus.value = "ALL";
getStatusCount();
getDataList();
}
function onStatusTabClick(tab) {
statusClick(tab.paneName);
}
function statusClick(name) {
if (name === "ALL" || name === "" || name === undefined) {
searchForm.value.status = "";
currentStatus.value = "ALL";
} else {
searchForm.value.status = name;
currentStatus.value = name;
}
searchForm.value.pageNumber = 1;
getDataList();
}
function getStatusCount() {
getDamageReportStatusCount(buildSearchParams(false)).then((res) => {
if (res.success) {
statusCountData.value = res.result || {};
}
});
}
function getDataList() {
loading.value = true;
getDamageReportPage(buildSearchParams(true))
.then((res) => {
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
if (this.dateRange && this.dateRange.length === 2) {
params.startDate = this.dateRange[0];
params.endDate = this.dateRange[1];
}
return params;
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getStatusCount();
this.getDataList();
},
resetSearch() {
this.searchForm = { pageNumber: 1, pageSize: 10, status: "" };
this.dateRange = [];
this.currentStatus = "ALL";
this.getStatusCount();
this.getDataList();
},
onStatusTabClick(tab) {
this.statusClick(tab.paneName);
},
statusClick(name) {
if (name === "ALL" || name === "" || name === undefined) {
this.searchForm.status = "";
this.currentStatus = "ALL";
} else {
this.searchForm.status = name;
this.currentStatus = name;
}
this.searchForm.pageNumber = 1;
this.getDataList();
},
getStatusCount() {
getDamageReportStatusCount(this.buildSearchParams(false)).then((res) => {
})
.finally(() => {
loading.value = false;
});
}
function refreshList() {
getStatusCount();
getDataList();
}
function goCreate() {
customRouterPush({ name: "procurement-damage-report-operation" });
}
function goDetail(row) {
customRouterPush({
name: "procurement-damage-report-operation",
query: { id: row.id, readonly: 1 },
});
}
function handleSubmit(row) {
Modal.confirm({
title: "提交报损单",
content: "确认提交该报损单?",
onOk: () => {
submitDamageReport(row.id).then((res) => {
if (res.success) {
this.statusCountData = res.result || {};
Message.success("提交成功");
refreshList();
}
});
},
getDataList() {
this.loading = true;
getDamageReportPage(this.buildSearchParams(true))
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
refreshList() {
this.getStatusCount();
this.getDataList();
},
goCreate() {
this.$filters.customRouterPush({ name: "procurement-damage-report-operation" });
},
goDetail(row) {
this.$filters.customRouterPush({
name: "procurement-damage-report-operation",
query: { id: row.id, readonly: 1 },
});
}
function handleApprove(row) {
Modal.confirm({
title: "审核通过",
content: "确认通过该报损单?",
onOk: () => {
approveDamageReport(row.id).then((res) => {
if (res.success) {
Message.success("审核通过");
refreshList();
}
});
},
handleSubmit(row) {
this.$Modal.confirm({
title: "提交报损单",
content: "确认提交该报损单?",
onOk: () => {
submitDamageReport(row.id).then((res) => {
if (res.success) {
this.$Message.success("提交成功");
this.refreshList();
}
});
},
});
}
function handleReject(row) {
ElMessageBox.prompt("请输入驳回原因", "驳回报损单", {
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPlaceholder: "驳回原因",
})
.then(({ value }) => {
rejectDamageReport(row.id, value || "驳回").then((res) => {
if (res.success) {
Message.success("已驳回");
refreshList();
}
});
})
.catch(() => {});
}
function handleCancel(row) {
Modal.confirm({
title: "作废报损单",
content: "确认作废该报损单?",
onOk: () => {
cancelDamageReport(row.id).then((res) => {
if (res.success) {
Message.success("已作废");
refreshList();
}
});
},
handleApprove(row) {
this.$Modal.confirm({
title: "审核通过",
content: "确认通过该报损单?",
onOk: () => {
approveDamageReport(row.id).then((res) => {
if (res.success) {
this.$Message.success("审核通过");
this.refreshList();
}
});
},
});
}
function handleComplete(row) {
Modal.confirm({
title: "完成报损",
content: "完成后将扣减库存,是否继续?",
onOk: () => {
completeDamageReport(row.id).then((res) => {
if (res.success) {
Message.success("报损完成,库存已扣减");
refreshList();
}
});
},
handleReject(row) {
ElMessageBox.prompt("请输入驳回原因", "驳回报损单", {
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPlaceholder: "驳回原因",
})
.then(({ value }) => {
rejectDamageReport(row.id, value || "驳回").then((res) => {
if (res.success) {
this.$Message.success("已驳回");
this.refreshList();
}
});
})
.catch(() => {});
},
handleCancel(row) {
this.$Modal.confirm({
title: "作废报损单",
content: "确认作废该报损单?",
onOk: () => {
cancelDamageReport(row.id).then((res) => {
if (res.success) {
this.$Message.success("已作废");
this.refreshList();
}
});
},
});
},
handleComplete(row) {
this.$Modal.confirm({
title: "完成报损",
content: "完成后将扣减库存,是否继续?",
onOk: () => {
completeDamageReport(row.id).then((res) => {
if (res.success) {
this.$Message.success("报损完成,库存已扣减");
this.refreshList();
}
});
},
});
},
},
};
});
}
</script>
<style scoped>

View File

@@ -149,11 +149,13 @@
</template>
</el-card>
<liliDialog ref="liliDialog" @selectedGoodsData="onGoodsSelected" />
<liliDialog ref="liliDialogRef" @selectedGoodsData="onGoodsSelected" />
</div>
</template>
<script>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import liliDialog from "@/views/lili-dialog";
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
import {
@@ -164,179 +166,170 @@ import {
submitDamageReport,
} from "@/api/procurement";
import { damageStatusText, damageStatusTag, formatMoney } from "../constants";
import { Message } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
export default {
name: "procurement-damage-report-operation",
components: { liliDialog, uploadPicThumb },
data() {
return {
readonly: false,
submitLoading: false,
form: {
damageDate: "",
damageReasonId: "",
remark: "",
evidence: [],
},
items: [],
detail: {},
reasonOptions: [],
evidenceImageList: [],
};
},
computed: {
pageTitle() {
return this.readonly ? "报损单详情" : "新建报损单";
},
reasonText() {
const reason = this.reasonOptions.find((item) => item.id === this.detail.damageReasonId);
return reason ? reason.reason : "-";
},
totalQuantity() {
return this.items.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0);
},
totalAmount() {
return this.items.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
},
},
mounted() {
this.readonly = this.$route.query.readonly === "1" || !!this.$route.query.id;
this.loadReasons();
if (this.$route.query.id) {
this.loadDetail(this.$route.query.id);
defineOptions({ name: "procurement-damage-report-operation" });
const route = useRoute();
const readonly = ref(false);
const submitLoading = ref(false);
const form = reactive({
damageDate: "",
damageReasonId: "",
remark: "",
evidence: [],
});
const items = ref([]);
const detail = ref({});
const reasonOptions = ref([]);
const evidenceImageList = ref([]);
const liliDialogRef = ref(null);
const pageTitle = computed(() => (readonly.value ? "报损单详情" : "新建报损单"));
const reasonText = computed(() => {
const reason = reasonOptions.value.find((item) => item.id === detail.value.damageReasonId);
return reason ? reason.reason : "-";
});
const totalQuantity = computed(() =>
items.value.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0)
);
const totalAmount = computed(() =>
items.value.reduce((sum, item) => sum + (Number(item.amount) || 0), 0)
);
onMounted(() => {
readonly.value = route.query.readonly === "1" || !!route.query.id;
loadReasons();
if (route.query.id) {
loadDetail(route.query.id);
}
});
function loadReasons() {
getStockReasonList({ category: "DAMAGE", pageNumber: 1, pageSize: 100 }).then((res) => {
if (res.success) {
reasonOptions.value = res.result.records || [];
}
},
methods: {
damageStatusText,
damageStatusTag,
formatMoney,
loadReasons() {
getStockReasonList({ category: "DAMAGE", pageNumber: 1, pageSize: 100 }).then((res) => {
if (res.success) {
this.reasonOptions = res.result.records || [];
}
});
},
recalcRow(row) {
row.amount = (Number(row.unitPrice) || 0) * (Number(row.quantity) || 0);
},
onQuantityBlur(row) {
const val = parseInt(row.quantity, 10);
row.quantity = Number.isNaN(val) || val < 1 ? 1 : val;
this.recalcRow(row);
},
onUnitPriceBlur(row) {
const val = parseFloat(row.unitPrice);
row.unitPrice = Number.isNaN(val) || val < 0 ? 0 : Math.round(val * 100) / 100;
this.recalcRow(row);
},
loadDetail(id) {
getDamageReportDetail(id).then((res) => {
if (res.success && res.result) {
this.detail = res.result;
this.form.damageDate = res.result.damageDate;
this.form.damageReasonId = res.result.damageReasonId;
this.form.remark = res.result.remark;
this.form.evidence = res.result.evidence;
this.evidenceImageList = this.parseEvidenceImages(res.result.evidence);
}
});
getDamageReportItems(id).then((res) => {
if (res.success) {
this.items = res.result || [];
}
});
},
parseEvidenceImages(val) {
if (!val) return [];
if (Array.isArray(val)) return val.filter(Boolean);
return String(val)
.split(",")
.map((s) => s.trim())
.filter(Boolean);
},
openSkuPicker() {
this.$refs.liliDialog.goodsData = this.items.map((item) => ({
id: item.skuId,
goodsId: item.goodsId,
goodsName: item.goodsName,
price: item.unitPrice,
cost: item.unitPrice,
}));
this.$refs.liliDialog.open("goods");
},
onGoodsSelected(selected) {
const map = new Map(this.items.map((item) => [item.skuId, item]));
this.items = selected.map((sku) => {
const existed = map.get(sku.id);
if (existed) return existed;
const unitPrice = sku.cost || sku.price || 0;
return {
goodsId: sku.goodsId,
skuId: sku.id,
goodsName: sku.goodsName,
quantity: 1,
unitPrice,
amount: unitPrice,
};
});
},
removeItem(index) {
this.items.splice(index, 1);
},
validateForm() {
if (!this.form.damageReasonId) {
this.$Message.warning("请选择报损原因");
return false;
}
if (!this.items.length) {
this.$Message.warning("请添加报损商品");
return false;
}
return true;
},
handleSave(andSubmit) {
if (!this.validateForm()) return;
this.submitLoading = true;
const payload = {
damageDate: this.form.damageDate || undefined,
damageReasonId: this.form.damageReasonId,
remark: this.form.remark,
evidence: Array.isArray(this.form.evidence)
? this.form.evidence.join(",")
: this.form.evidence || "",
items: this.items.map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
quantity: item.quantity,
unitPrice: item.unitPrice,
})),
};
createDamageReport(payload)
.then((res) => {
if (!res.success) return;
const reportId = res.result.id;
if (andSubmit) {
return submitDamageReport(reportId).then((subRes) => {
if (subRes.success) {
this.$Message.success("保存并提交成功");
this.goBack();
}
});
});
}
function recalcRow(row) {
row.amount = (Number(row.unitPrice) || 0) * (Number(row.quantity) || 0);
}
function onQuantityBlur(row) {
const val = parseInt(row.quantity, 10);
row.quantity = Number.isNaN(val) || val < 1 ? 1 : val;
recalcRow(row);
}
function onUnitPriceBlur(row) {
const val = parseFloat(row.unitPrice);
row.unitPrice = Number.isNaN(val) || val < 0 ? 0 : Math.round(val * 100) / 100;
recalcRow(row);
}
function loadDetail(id) {
getDamageReportDetail(id).then((res) => {
if (res.success && res.result) {
detail.value = res.result;
form.damageDate = res.result.damageDate;
form.damageReasonId = res.result.damageReasonId;
form.remark = res.result.remark;
form.evidence = res.result.evidence;
evidenceImageList.value = parseEvidenceImages(res.result.evidence);
}
});
getDamageReportItems(id).then((res) => {
if (res.success) {
items.value = res.result || [];
}
});
}
function parseEvidenceImages(val) {
if (!val) return [];
if (Array.isArray(val)) return val.filter(Boolean);
return String(val)
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
function openSkuPicker() {
liliDialogRef.value.goodsData = items.value.map((item) => ({
id: item.skuId,
goodsId: item.goodsId,
goodsName: item.goodsName,
price: item.unitPrice,
cost: item.unitPrice,
}));
liliDialogRef.value.open("goods");
}
function onGoodsSelected(selected) {
const map = new Map(items.value.map((item) => [item.skuId, item]));
items.value = selected.map((sku) => {
const existed = map.get(sku.id);
if (existed) return existed;
const unitPrice = sku.cost || sku.price || 0;
return {
goodsId: sku.goodsId,
skuId: sku.id,
goodsName: sku.goodsName,
quantity: 1,
unitPrice,
amount: unitPrice,
};
});
}
function removeItem(index) {
items.value.splice(index, 1);
}
function validateForm() {
if (!form.damageReasonId) {
Message.warning("请选择报损原因");
return false;
}
if (!items.value.length) {
Message.warning("请添加报损商品");
return false;
}
return true;
}
function handleSave(andSubmit) {
if (!validateForm()) return;
submitLoading.value = true;
const payload = {
damageDate: form.damageDate || undefined,
damageReasonId: form.damageReasonId,
remark: form.remark,
evidence: Array.isArray(form.evidence) ? form.evidence.join(",") : form.evidence || "",
items: items.value.map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
quantity: item.quantity,
unitPrice: item.unitPrice,
})),
};
createDamageReport(payload)
.then((res) => {
if (!res.success) return;
const reportId = res.result.id;
if (andSubmit) {
return submitDamageReport(reportId).then((subRes) => {
if (subRes.success) {
Message.success("保存并提交成功");
goBack();
}
this.$Message.success("保存成功");
this.goBack();
})
.finally(() => {
this.submitLoading = false;
});
},
goBack() {
this.$filters.customRouterPush({ name: "procurementDamageReportList" });
},
},
};
}
Message.success("保存成功");
goBack();
})
.finally(() => {
submitLoading.value = false;
});
}
function goBack() {
customRouterPush({ name: "procurementDamageReportList" });
}
</script>
<style scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="80px">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="80px">
<el-form-item label="入库单号">
<el-input v-model="searchForm.inboundSn" placeholder="入库单号" clearable style="width: 200px" />
</el-form-item>
@@ -48,62 +48,65 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { getProcurementInboundPage } from "@/api/procurement";
import { formatMoney } from "../constants";
import { customRouterPush } from "@/utils/filters";
export default {
name: "procurement-inbound-list",
data() {
return {
loading: false,
data: [],
total: 0,
searchForm: {
pageNumber: 1,
pageSize: 10,
inboundSn: "",
sort: "createTime",
order: "desc",
},
};
},
mounted() {
this.getDataList();
},
methods: {
formatMoney,
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
resetSearch() {
this.searchForm.inboundSn = "";
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getProcurementInboundPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
goCreate() {
this.$filters.customRouterPush({ name: "procurement-inbound-operation" });
},
goDetail(row) {
this.$filters.customRouterPush({
name: "procurement-inbound-operation",
query: { id: row.id, readonly: 1 },
});
},
},
};
defineOptions({ name: "procurement-inbound-list" });
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const searchForm = reactive({
pageNumber: 1,
pageSize: 10,
inboundSn: "",
sort: "createTime",
order: "desc",
});
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
onMounted(() => {
getDataList();
});
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function resetSearch() {
searchForm.inboundSn = "";
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
getProcurementInboundPage(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
function goCreate() {
customRouterPush({ name: "procurement-inbound-operation" });
}
function goDetail(row) {
customRouterPush({
name: "procurement-inbound-operation",
query: { id: row.id, readonly: 1 },
});
}
</script>

View File

@@ -100,7 +100,9 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
import {
getProcurementOrderPage,
@@ -110,153 +112,146 @@ import {
getProcurementInboundItems,
} from "@/api/procurement";
import { procurementStatusText, formatMoney } from "../constants";
import { Message } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
export default {
name: "procurement-inbound-operation",
components: { uploadPicThumb },
data() {
return {
readonly: false,
submitLoading: false,
form: {
procurementOrderId: "",
certificateImages: [],
},
orderOptions: [],
items: [],
detail: {},
inboundItems: [],
certificateImageList: [],
};
},
computed: {
pageTitle() {
return this.readonly ? "入库单详情" : "新建入库单";
},
totalInboundQuantity() {
return (this.inboundItems || []).reduce(
(sum, item) => sum + (Number(item.inboundQuantity) || 0),
0
);
},
},
mounted() {
this.readonly = this.$route.query.readonly === "1";
if (this.$route.query.id) {
this.loadInboundDetail(this.$route.query.id);
} else {
this.loadPendingOrders();
if (this.$route.query.procurementOrderId) {
this.form.procurementOrderId = this.$route.query.procurementOrderId;
this.onOrderChange(this.$route.query.procurementOrderId);
}
defineOptions({ name: "procurement-inbound-operation" });
const route = useRoute();
const readonly = ref(false);
const submitLoading = ref(false);
const form = reactive({
procurementOrderId: "",
certificateImages: [],
});
const orderOptions = ref([]);
const items = ref([]);
const detail = ref({});
const inboundItems = ref([]);
const certificateImageList = ref([]);
const pageTitle = computed(() => (readonly.value ? "入库单详情" : "新建入库单"));
const totalInboundQuantity = computed(() =>
(inboundItems.value || []).reduce(
(sum, item) => sum + (Number(item.inboundQuantity) || 0),
0
)
);
onMounted(() => {
readonly.value = route.query.readonly === "1";
if (route.query.id) {
loadInboundDetail(route.query.id);
} else {
loadPendingOrders();
if (route.query.procurementOrderId) {
form.procurementOrderId = route.query.procurementOrderId;
onOrderChange(route.query.procurementOrderId);
}
},
methods: {
procurementStatusText,
formatMoney,
remainQty(row) {
return (row.quantity || 0) - (row.receivedQuantity || 0);
},
onInboundQtyBlur(row) {
const max = this.remainQty(row);
const val = parseInt(row.inboundQuantity, 10);
if (Number.isNaN(val) || val < 0) {
row.inboundQuantity = 0;
} else if (val > max) {
row.inboundQuantity = max;
} else {
row.inboundQuantity = val;
}
});
function remainQty(row) {
return (row.quantity || 0) - (row.receivedQuantity || 0);
}
function onInboundQtyBlur(row) {
const max = remainQty(row);
const val = parseInt(row.inboundQuantity, 10);
if (Number.isNaN(val) || val < 0) {
row.inboundQuantity = 0;
} else if (val > max) {
row.inboundQuantity = max;
} else {
row.inboundQuantity = val;
}
}
function loadPendingOrders() {
getProcurementOrderPage({
pageNumber: 1,
pageSize: 100,
status: "",
}).then((res) => {
if (res.success) {
orderOptions.value = (res.result.records || []).filter((o) =>
["PENDING_INBOUND", "PARTIAL_INBOUND"].includes(o.status)
);
}
});
}
function onOrderChange(orderId) {
if (!orderId) {
items.value = [];
return;
}
getProcurementOrderDetail(orderId).then((res) => {
if (res.success && res.result) {
items.value = (res.result.items || [])
.filter((item) => remainQty(item) > 0)
.map((item) => ({
...item,
procurementOrderItemId: item.id,
inboundQuantity: remainQty(item),
}));
}
});
}
function loadInboundDetail(id) {
getProcurementInboundDetail(id).then((res) => {
if (res.success) {
detail.value = res.result || {};
certificateImageList.value = parseCertificateImages(detail.value.certificateImages);
}
});
getProcurementInboundItems(id).then((res) => {
if (res.success) inboundItems.value = res.result || [];
});
}
function parseCertificateImages(val) {
if (!val) return [];
if (Array.isArray(val)) return val.filter(Boolean);
return String(val)
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
function handleSubmit() {
if (!form.procurementOrderId) {
Message.warning("请选择采购单");
return;
}
const validItems = items.value.filter((item) => item.inboundQuantity > 0);
if (!validItems.length) {
Message.warning("请填写本次入库数量");
return;
}
submitLoading.value = true;
createProcurementInbound({
procurementOrderId: form.procurementOrderId,
certificateImages: Array.isArray(form.certificateImages)
? form.certificateImages.join(",")
: form.certificateImages || "",
items: validItems.map((item) => ({
procurementOrderItemId: item.procurementOrderItemId || item.id,
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
inboundQuantity: item.inboundQuantity,
})),
})
.then((res) => {
if (res.success) {
Message.success("入库成功");
goBack();
}
},
loadPendingOrders() {
getProcurementOrderPage({
pageNumber: 1,
pageSize: 100,
status: "",
}).then((res) => {
if (res.success) {
this.orderOptions = (res.result.records || []).filter((o) =>
["PENDING_INBOUND", "PARTIAL_INBOUND"].includes(o.status)
);
}
});
},
onOrderChange(orderId) {
if (!orderId) {
this.items = [];
return;
}
getProcurementOrderDetail(orderId).then((res) => {
if (res.success && res.result) {
this.items = (res.result.items || [])
.filter((item) => this.remainQty(item) > 0)
.map((item) => ({
...item,
procurementOrderItemId: item.id,
inboundQuantity: this.remainQty(item),
}));
}
});
},
loadInboundDetail(id) {
getProcurementInboundDetail(id).then((res) => {
if (res.success) {
this.detail = res.result || {};
this.certificateImageList = this.parseCertificateImages(this.detail.certificateImages);
}
});
getProcurementInboundItems(id).then((res) => {
if (res.success) this.inboundItems = res.result || [];
});
},
parseCertificateImages(val) {
if (!val) return [];
if (Array.isArray(val)) return val.filter(Boolean);
return String(val)
.split(",")
.map((s) => s.trim())
.filter(Boolean);
},
handleSubmit() {
if (!this.form.procurementOrderId) {
this.$Message.warning("请选择采购单");
return;
}
const validItems = this.items.filter((item) => item.inboundQuantity > 0);
if (!validItems.length) {
this.$Message.warning("请填写本次入库数量");
return;
}
this.submitLoading = true;
createProcurementInbound({
procurementOrderId: this.form.procurementOrderId,
certificateImages: Array.isArray(this.form.certificateImages)
? this.form.certificateImages.join(",")
: this.form.certificateImages || "",
items: validItems.map((item) => ({
procurementOrderItemId: item.procurementOrderItemId || item.id,
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
inboundQuantity: item.inboundQuantity,
})),
})
.then((res) => {
if (res.success) {
this.$Message.success("入库成功");
this.goBack();
}
})
.finally(() => {
this.submitLoading = false;
});
},
goBack() {
this.$filters.customRouterPush({ name: "procurementInboundList" });
},
},
};
})
.finally(() => {
submitLoading.value = false;
});
}
function goBack() {
customRouterPush({ name: "procurementInboundList" });
}
</script>
<style scoped>

View File

@@ -68,7 +68,9 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import {
getInventoryCountDetail,
getInventoryCountItemsPage,
@@ -76,83 +78,80 @@ import {
} from "@/api/procurement";
import { MARKET_ENABLE } from "../constants";
import { downloadBlob } from "@/utils/downloadBlob";
import { customRouterPush } from "@/utils/filters";
export default {
name: "procurement-inventory-count-detail",
data() {
return {
MARKET_ENABLE,
loading: false,
exporting: false,
detail: {},
data: [],
total: 0,
sn: "",
searchForm: {
pageNumber: 1,
pageSize: 20,
goodsName: "",
},
};
},
mounted() {
this.sn = this.$route.query.sn || "";
const id = this.$route.query.id;
if (id) {
getInventoryCountDetail(id).then((res) => {
if (res.success) {
this.detail = res.result || {};
this.sn = this.detail.sn || this.sn;
}
});
this.getDataList();
}
},
methods: {
getDataList() {
const id = this.$route.query.id;
if (!id) return;
this.loading = true;
getInventoryCountItemsPage(id, this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 20,
goodsName: "",
};
this.getDataList();
},
handleExport() {
const id = this.$route.query.id;
if (!id) return;
this.exporting = true;
exportInventoryCountItems(id)
.then((blob) => {
downloadBlob(blob, `盘点单_${this.sn || id}.xlsx`);
})
.finally(() => {
this.exporting = false;
});
},
goBack() {
this.$filters.customRouterPush({ name: "procurementInventoryCountList" });
},
},
};
defineOptions({ name: "procurement-inventory-count-detail" });
const route = useRoute();
const loading = ref(false);
const exporting = ref(false);
const detail = ref({});
const data = ref([]);
const total = ref(0);
const sn = ref("");
const searchForm = ref({
pageNumber: 1,
pageSize: 20,
goodsName: "",
});
onMounted(() => {
sn.value = route.query.sn || "";
const id = route.query.id;
if (id) {
getInventoryCountDetail(id).then((res) => {
if (res.success) {
detail.value = res.result || {};
sn.value = detail.value.sn || sn.value;
}
});
getDataList();
}
});
function getDataList() {
const id = route.query.id;
if (!id) return;
loading.value = true;
getInventoryCountItemsPage(id, searchForm.value)
.then((res) => {
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getDataList();
}
function handleReset() {
searchForm.value = {
pageNumber: 1,
pageSize: 20,
goodsName: "",
};
getDataList();
}
function handleExport() {
const id = route.query.id;
if (!id) return;
exporting.value = true;
exportInventoryCountItems(id)
.then((blob) => {
downloadBlob(blob, `盘点单_${sn.value || id}.xlsx`);
})
.finally(() => {
exporting.value = false;
});
}
function goBack() {
customRouterPush({ name: "procurementInventoryCountList" });
}
</script>
<style scoped>

View File

@@ -33,67 +33,67 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { createInventoryCount, getInventoryCountPage } from "@/api/procurement";
import { Message, Modal } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
export default {
name: "procurement-inventory-count-list",
data() {
return {
loading: false,
creating: false,
data: [],
total: 0,
searchForm: {
pageNumber: 1,
pageSize: 10,
},
};
},
mounted() {
this.getDataList();
},
methods: {
getDataList() {
this.loading = true;
getInventoryCountPage(this.searchForm)
defineOptions({ name: "procurement-inventory-count-list" });
const loading = ref(false);
const creating = ref(false);
const data = ref([]);
const total = ref(0);
const searchForm = reactive({
pageNumber: 1,
pageSize: 10,
});
onMounted(() => {
getDataList();
});
function getDataList() {
loading.value = true;
getInventoryCountPage(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
function handleCreate() {
Modal.confirm({
title: "生成盘点单",
content: "将快照当前店铺全部 SKU 库存,是否继续?",
onOk: () => {
creating.value = true;
createInventoryCount()
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
Message.success("盘点单已生成");
getDataList();
}
})
.finally(() => {
this.loading = false;
creating.value = false;
});
},
handleCreate() {
this.$Modal.confirm({
title: "生成盘点单",
content: "将快照当前店铺全部 SKU 库存,是否继续?",
onOk: () => {
this.creating = true;
createInventoryCount()
.then((res) => {
if (res.success) {
this.$Message.success("盘点单已生成");
this.getDataList();
}
})
.finally(() => {
this.creating = false;
});
},
});
},
goDetail(row) {
this.$filters.customRouterPush({
name: "procurement-inventory-count-detail",
query: { id: row.id, sn: row.sn },
});
},
},
};
});
}
function goDetail(row) {
customRouterPush({
name: "procurement-inventory-count-detail",
query: { id: row.id, sn: row.sn },
});
}
</script>
<style scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="80px" class="search-form">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="80px" class="search-form">
<el-form-item label="单据编号">
<el-input v-model="searchForm.orderSn" placeholder="采购单号" clearable style="width: 200px" />
</el-form-item>
@@ -94,7 +94,9 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import {
getProcurementOrderPage,
getProcurementOrderStatusCount,
@@ -108,187 +110,199 @@ import {
procurementStatusTag,
formatMoney,
} from "../constants";
import { customRouterPush } from "@/utils/filters";
import { Message, Modal } from "@/utils/message";
export default {
name: "procurement-purchase-order-list",
data() {
return {
loading: false,
data: [],
total: 0,
createRange: [],
currentStatus: "ALL",
statusCountData: {},
searchForm: {
pageNumber: 1,
pageSize: 10,
orderSn: "",
goodsName: "",
status: "",
sort: "createTime",
order: "desc",
},
};
},
computed: {
statusTabs() {
const tabs = [{ title: "全部", value: "ALL" }];
Object.keys(PROCUREMENT_STATUS).forEach((key) => {
const count = this.statusCountData[key];
const label = PROCUREMENT_STATUS[key];
tabs.push({
title: count ? `${label}(${count})` : label,
value: key,
});
});
return tabs;
},
},
mounted() {
const status = this.$route.query.status;
if (status) {
this.currentStatus = status;
this.searchForm.status = status;
defineOptions({ name: "procurement-purchase-order-list" });
const route = useRoute();
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const createRange = ref([]);
const currentStatus = ref("ALL");
const statusCountData = ref({});
const searchForm = ref({
pageNumber: 1,
pageSize: 10,
orderSn: "",
goodsName: "",
status: "",
sort: "createTime",
order: "desc",
});
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
const statusTabs = computed(() => {
const tabs = [{ title: "全部", value: "ALL" }];
Object.keys(PROCUREMENT_STATUS).forEach((key) => {
const count = statusCountData.value[key];
const label = PROCUREMENT_STATUS[key];
tabs.push({
title: count ? `${label}(${count})` : label,
value: key,
});
});
return tabs;
});
onMounted(() => {
const status = route.query.status;
if (status) {
currentStatus.value = status;
searchForm.value.status = status;
}
init();
});
function init() {
getStatusCount();
getDataList();
}
function buildSearchParams(includeStatus = true) {
const params = { ...searchForm.value };
if (!includeStatus) {
delete params.status;
}
if (createRange.value && createRange.value.length === 2) {
params.startCreateTime = createRange.value[0];
params.endCreateTime = createRange.value[1];
}
return params;
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getStatusCount();
getDataList();
}
function resetSearch() {
searchForm.value = {
pageNumber: 1,
pageSize: 10,
orderSn: "",
goodsName: "",
status: "",
sort: "createTime",
order: "desc",
};
createRange.value = [];
currentStatus.value = "ALL";
getStatusCount();
getDataList();
}
function onStatusTabClick(tab) {
statusClick(tab.paneName);
}
function statusClick(name) {
if (name === "ALL" || name === "" || name === undefined) {
searchForm.value.status = "";
currentStatus.value = "ALL";
} else {
searchForm.value.status = name;
currentStatus.value = name;
}
searchForm.value.pageNumber = 1;
getDataList();
}
function getStatusCount() {
getProcurementOrderStatusCount(buildSearchParams(false)).then((res) => {
if (res.success) {
statusCountData.value = res.result || {};
}
this.init();
},
methods: {
procurementStatusText,
procurementStatusTag,
formatMoney,
init() {
this.getStatusCount();
this.getDataList();
},
buildSearchParams(includeStatus = true) {
const params = { ...this.searchForm };
if (!includeStatus) {
delete params.status;
});
}
function getDataList() {
loading.value = true;
getProcurementOrderPage(buildSearchParams(true))
.then((res) => {
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
if (this.createRange && this.createRange.length === 2) {
params.startCreateTime = this.createRange[0];
params.endCreateTime = this.createRange[1];
}
return params;
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getStatusCount();
this.getDataList();
},
resetSearch() {
this.searchForm = {
pageNumber: 1,
pageSize: 10,
orderSn: "",
goodsName: "",
status: "",
sort: "createTime",
order: "desc",
};
this.createRange = [];
this.currentStatus = "ALL";
this.getStatusCount();
this.getDataList();
},
onStatusTabClick(tab) {
this.statusClick(tab.paneName);
},
statusClick(name) {
if (name === "ALL" || name === "" || name === undefined) {
this.searchForm.status = "";
this.currentStatus = "ALL";
} else {
this.searchForm.status = name;
this.currentStatus = name;
}
this.searchForm.pageNumber = 1;
this.getDataList();
},
getStatusCount() {
getProcurementOrderStatusCount(this.buildSearchParams(false)).then((res) => {
})
.finally(() => {
loading.value = false;
});
}
function refreshList() {
getStatusCount();
getDataList();
}
function goCreate() {
customRouterPush({ name: "procurement-purchase-order-operation" });
}
function goDetail(row) {
customRouterPush({
name: "procurement-purchase-order-operation",
query: { id: row.id, readonly: 1 },
});
}
function goInbound(row) {
customRouterPush({
name: "procurement-inbound-operation",
query: { procurementOrderId: row.id },
});
}
function handleSubmit(row) {
Modal.confirm({
title: "确认提交",
content: "提交后采购单将进入审核流程,是否继续?",
onOk: () => {
submitProcurementOrder(row.id).then((res) => {
if (res.success) {
this.statusCountData = res.result || {};
Message.success("提交成功");
refreshList();
}
});
},
getDataList() {
this.loading = true;
getProcurementOrderPage(this.buildSearchParams(true))
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
refreshList() {
this.getStatusCount();
this.getDataList();
},
goCreate() {
this.$filters.customRouterPush({ name: "procurement-purchase-order-operation" });
},
goDetail(row) {
this.$filters.customRouterPush({
name: "procurement-purchase-order-operation",
query: { id: row.id, readonly: 1 },
});
}
function handleAudit(row, pass) {
const title = pass ? "审核通过" : "审核驳回";
Modal.confirm({
title,
content: pass ? "确认通过该采购单?" : "确认驳回该采购单?",
onOk: () => {
auditProcurementOrder(row.id, { pass, remark: "" }).then((res) => {
if (res.success) {
Message.success("操作成功");
refreshList();
}
});
},
goInbound(row) {
this.$filters.customRouterPush({
name: "procurement-inbound-operation",
query: { procurementOrderId: row.id },
});
}
function handleClose(row) {
Modal.confirm({
title: "关闭采购单",
content: "关闭后不可继续入库,是否继续?",
onOk: () => {
closeProcurementOrder(row.id).then((res) => {
if (res.success) {
Message.success("已关闭");
refreshList();
}
});
},
handleSubmit(row) {
this.$Modal.confirm({
title: "确认提交",
content: "提交后采购单将进入审核流程,是否继续?",
onOk: () => {
submitProcurementOrder(row.id).then((res) => {
if (res.success) {
this.$Message.success("提交成功");
this.refreshList();
}
});
},
});
},
handleAudit(row, pass) {
const title = pass ? "审核通过" : "审核驳回";
this.$Modal.confirm({
title,
content: pass ? "确认通过该采购单?" : "确认驳回该采购单?",
onOk: () => {
auditProcurementOrder(row.id, { pass, remark: "" }).then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.refreshList();
}
});
},
});
},
handleClose(row) {
this.$Modal.confirm({
title: "关闭采购单",
content: "关闭后不可继续入库,是否继续?",
onOk: () => {
closeProcurementOrder(row.id).then((res) => {
if (res.success) {
this.$Message.success("已关闭");
this.refreshList();
}
});
},
});
},
},
};
});
}
</script>
<style scoped>

View File

@@ -122,11 +122,13 @@
</div>
</el-card>
<liliDialog ref="liliDialog" @selectedGoodsData="onGoodsSelected" />
<liliDialog ref="liliDialogRef" @selectedGoodsData="onGoodsSelected" />
</div>
</template>
<script>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import liliDialog from "@/views/lili-dialog";
import {
createProcurementOrder,
@@ -140,182 +142,179 @@ import {
procurementStatusTag,
formatMoney,
} from "../constants";
import { Message } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
export default {
name: "procurement-purchase-order-operation",
components: { liliDialog },
data() {
return {
form: { remark: "", stockReasonId: "" },
items: [],
orderDetail: {},
reasonOptions: [],
submitLoading: false,
readonly: false,
};
},
computed: {
pageTitle() {
if (this.readonly) return "采购单详情";
return this.$route.query.id ? "编辑采购单" : "新建采购单";
},
totalQuantity() {
return this.items.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0);
},
totalAmount() {
return this.items.reduce((sum, item) => sum + this.calcSubtotal(item), 0);
},
},
mounted() {
this.loadReasons();
if (this.$route.query.id) {
this.readonly = true;
this.loadDetail(this.$route.query.id);
defineOptions({ name: "procurement-purchase-order-operation" });
const route = useRoute();
const form = reactive({ remark: "", stockReasonId: "" });
const items = ref([]);
const orderDetail = ref({});
const reasonOptions = ref([]);
const submitLoading = ref(false);
const readonly = ref(false);
// liliDialogRef:模板 ref 改名避免与 import 的组件同名互相覆盖
const liliDialogRef = ref(null);
const pageTitle = computed(() => {
if (readonly.value) return "采购单详情";
return route.query.id ? "编辑采购单" : "新建采购单";
});
const totalQuantity = computed(() =>
items.value.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0)
);
const totalAmount = computed(() =>
items.value.reduce((sum, item) => sum + calcSubtotal(item), 0)
);
onMounted(() => {
loadReasons();
if (route.query.id) {
readonly.value = true;
loadDetail(route.query.id);
}
});
function loadReasons() {
getStockReasonList({ category: "INBOUND", pageNumber: 1, pageSize: 100 }).then((res) => {
if (res.success) {
reasonOptions.value = res.result.records || [];
}
},
methods: {
procurementStatusText,
procurementStatusTag,
formatMoney,
loadReasons() {
getStockReasonList({ category: "INBOUND", pageNumber: 1, pageSize: 100 }).then((res) => {
if (res.success) {
this.reasonOptions = res.result.records || [];
}
});
},
calcSubtotal(row) {
return (Number(row.unitPriceWithTax) || 0) * (Number(row.quantity) || 0);
},
recalcRow() {},
onQuantityBlur(row) {
const val = parseInt(row.quantity, 10);
row.quantity = Number.isNaN(val) || val < 1 ? 1 : val;
this.recalcRow(row);
},
onTaxRateBlur(row) {
const val = parseInt(row.taxRate, 10);
if (Number.isNaN(val) || val < 0) {
row.taxRate = 0;
} else if (val > 100) {
row.taxRate = 100;
} else {
row.taxRate = val;
}
this.recalcRow(row);
},
onDecimalBlur(row, field) {
const val = parseFloat(row[field]);
row[field] = Number.isNaN(val) || val < 0 ? 0 : Math.round(val * 100) / 100;
this.recalcRow(row);
},
loadDetail(id) {
getProcurementOrderDetail(id).then((res) => {
if (res.success && res.result) {
this.orderDetail = res.result;
this.form.remark = res.result.remark || "";
this.form.stockReasonId = res.result.stockReasonId || "";
this.items = (res.result.items || []).map((item) => ({ ...item }));
}
});
},
openSkuPicker() {
this.$refs.liliDialog.goodsData = this.items.map((item) => ({
id: item.skuId,
goodsId: item.goodsId,
goodsName: item.goodsName,
price: item.retailPrice,
}));
this.$refs.liliDialog.open("goods");
},
onGoodsSelected(selected) {
const map = new Map(this.items.map((item) => [item.skuId, item]));
this.items = selected.map((sku) => {
const existed = map.get(sku.id);
if (existed) return existed;
return {
goodsId: sku.goodsId,
skuId: sku.id,
goodsName: sku.goodsName,
retailPrice: sku.price || 0,
quantity: 1,
taxRate: 0,
unitPriceWithTax: sku.cost || sku.price || 0,
};
});
},
removeItem(index) {
this.items.splice(index, 1);
},
validateItems() {
if (!this.form.stockReasonId) {
this.$Message.warning("请选择出入库原因");
return false;
}
if (!this.items.length) {
this.$Message.warning("请至少添加一个商品");
return false;
}
for (const item of this.items) {
if (!item.quantity || item.quantity < 1) {
this.$Message.warning("采购数量必须大于0");
return false;
}
if (item.unitPriceWithTax == null || item.unitPriceWithTax < 0) {
this.$Message.warning("请填写含税单价");
return false;
}
}
return true;
},
handleSave(andSubmit) {
if (!this.validateItems()) return;
this.submitLoading = true;
const payload = {
remark: this.form.remark,
stockReasonId: this.form.stockReasonId,
items: this.items.map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
retailPrice: item.retailPrice,
quantity: item.quantity,
taxRate: item.taxRate || 0,
unitPriceWithTax: item.unitPriceWithTax,
})),
};
createProcurementOrder(payload)
.then((res) => {
if (!res.success) return;
const orderId = res.result.id;
if (andSubmit) {
return submitProcurementOrder(orderId).then((subRes) => {
if (subRes.success) {
this.$Message.success("保存并提交成功");
this.goBack();
}
});
});
}
function calcSubtotal(row) {
return (Number(row.unitPriceWithTax) || 0) * (Number(row.quantity) || 0);
}
function recalcRow() {}
function onQuantityBlur(row) {
const val = parseInt(row.quantity, 10);
row.quantity = Number.isNaN(val) || val < 1 ? 1 : val;
recalcRow(row);
}
function onTaxRateBlur(row) {
const val = parseInt(row.taxRate, 10);
if (Number.isNaN(val) || val < 0) {
row.taxRate = 0;
} else if (val > 100) {
row.taxRate = 100;
} else {
row.taxRate = val;
}
recalcRow(row);
}
function onDecimalBlur(row, field) {
const val = parseFloat(row[field]);
row[field] = Number.isNaN(val) || val < 0 ? 0 : Math.round(val * 100) / 100;
recalcRow(row);
}
function loadDetail(id) {
getProcurementOrderDetail(id).then((res) => {
if (res.success && res.result) {
orderDetail.value = res.result;
form.remark = res.result.remark || "";
form.stockReasonId = res.result.stockReasonId || "";
items.value = (res.result.items || []).map((item) => ({ ...item }));
}
});
}
function openSkuPicker() {
liliDialogRef.value.goodsData = items.value.map((item) => ({
id: item.skuId,
goodsId: item.goodsId,
goodsName: item.goodsName,
price: item.retailPrice,
}));
liliDialogRef.value.open("goods");
}
function onGoodsSelected(selected) {
const map = new Map(items.value.map((item) => [item.skuId, item]));
items.value = selected.map((sku) => {
const existed = map.get(sku.id);
if (existed) return existed;
return {
goodsId: sku.goodsId,
skuId: sku.id,
goodsName: sku.goodsName,
retailPrice: sku.price || 0,
quantity: 1,
taxRate: 0,
unitPriceWithTax: sku.cost || sku.price || 0,
};
});
}
function removeItem(index) {
items.value.splice(index, 1);
}
function validateItems() {
if (!form.stockReasonId) {
Message.warning("请选择出入库原因");
return false;
}
if (!items.value.length) {
Message.warning("请至少添加一个商品");
return false;
}
for (const item of items.value) {
if (!item.quantity || item.quantity < 1) {
Message.warning("采购数量必须大于0");
return false;
}
if (item.unitPriceWithTax == null || item.unitPriceWithTax < 0) {
Message.warning("请填写含税单价");
return false;
}
}
return true;
}
function handleSave(andSubmit) {
if (!validateItems()) return;
submitLoading.value = true;
const payload = {
remark: form.remark,
stockReasonId: form.stockReasonId,
items: items.value.map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
retailPrice: item.retailPrice,
quantity: item.quantity,
taxRate: item.taxRate || 0,
unitPriceWithTax: item.unitPriceWithTax,
})),
};
createProcurementOrder(payload)
.then((res) => {
if (!res.success) return;
const orderId = res.result.id;
if (andSubmit) {
return submitProcurementOrder(orderId).then((subRes) => {
if (subRes.success) {
Message.success("保存并提交成功");
goBack();
}
this.$Message.success("保存成功");
this.goBack();
})
.finally(() => {
this.submitLoading = false;
});
},
handleAudit(pass) {
auditProcurementOrder(this.orderDetail.id, { pass, remark: "" }).then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.goBack();
}
});
},
goBack() {
this.$filters.customRouterPush({ name: "procurementPurchaseOrderList" });
},
},
};
}
Message.success("保存成功");
goBack();
})
.finally(() => {
submitLoading.value = false;
});
}
function handleAudit(pass) {
auditProcurementOrder(orderDetail.value.id, { pass, remark: "" }).then((res) => {
if (res.success) {
Message.success("操作成功");
goBack();
}
});
}
function goBack() {
customRouterPush({ name: "procurementPurchaseOrderList" });
}
</script>
<style scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<el-card>
<el-form ref="form" :model="form" label-width="120px" :rules="formRule">
<el-form ref="formRef" :model="form" label-width="120px" :rules="formRule">
<div class="base-info-item">
<h4>基本信息</h4>
<div class="form-item-view">
@@ -232,350 +232,358 @@
</div>
</el-form>
</el-card>
<sku-select ref="skuSelect" @selectedGoodsData="selectedGoodsData"></sku-select>
<sku-select ref="skuSelectRef" @selectedGoodsData="selectedGoodsData"></sku-select>
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { saveShopCoupon, getShopCoupon, editShopCoupon } from "@/api/promotion";
import { getGoodsCategoryAll } from "@/api/goods";
import { regular } from "@/utils";
import { Message, Modal } from "@/utils/message";
import { unixToDate } from "@/utils/filters";
import { useAppStore } from "@/stores/app";
import skuSelect from "@/views/lili-dialog";
export default {
name: "add-coupon",
components: {
skuSelect,
},
watch: {
"form.getType": {
handler(val) {
if (val == "FREE") {
this.rangeTimeType = 1;
} else {
this.rangeTimeType = 0;
}
if (this.rangeTimeType == 0) {
delete this.formRule.rangeTime;
}
},
deep: true,
},
$route(e) {
this.id = e.query.id;
if (this.id) {
this.getCoupon();
} else {
this.$refs.form.resetFields();
}
},
},
data() {
const checkPrice = (rule, value, callback) => {
if (!value && value !== 0) {
return callback(new Error("面额不能为空"));
} else if (!regular.money.test(value)) {
callback(new Error("请输入正整数或者两位小数"));
} else if (parseFloat(value) > 99999999) {
callback(new Error("面额设置超过上限值"));
} else {
callback();
}
};
const checkWeight = (rule, value, callback) => {
if (!value && typeof value !== "number") {
callback(new Error("消费门槛不能为空"));
} else if (!regular.money.test(value)) {
callback(new Error("请输入正整数或者两位小数"));
} else if (parseFloat(value) > 99999999) {
callback(new Error("消费门槛设置超过上限值"));
} else {
callback();
}
};
return {
disabled: this.$route.query.onlyView,
rangeTimeType: 1,
modalType: 0,
form: {
storeCommission: 0,
publishNum: 0,
scopeType: "ALL",
couponLimitNum: 1,
couponType: "PRICE",
couponName: "",
promotionName: "",
getType: "FREE",
promotionGoodsList: [],
scopeIdGoods: [],
rangeDayType: "",
effectiveDays: 1,
},
id: this.$route.query.id,
submitLoading: false,
selectedGoods: [],
goodsCategoryList: [],
formRule: {
promotionName: [{ required: true, message: "活动名称不能为空" }],
couponName: [{ required: true, message: "优惠券名称不能为空" }],
price: [{ required: true, message: "请输入面额" }, { validator: checkPrice }],
rangeTime: [{ required: true, message: "请选择优惠券有效期" }],
consumeThreshold: [
{ required: true, message: "请输入消费门槛" },
{ validator: checkWeight },
],
couponDiscount: [
{ required: true, message: "请输入折扣" },
{
pattern: regular.discount,
message: "请输入0-10的数字,可有一位小数",
},
],
storeCommission: [
{ required: true, message: "请输入店铺承担比例" },
{ pattern: regular.rate, message: "请输入0-100的正整数" },
],
publishNum: [
{ required: true, message: "请输入发放数量" },
{ pattern: regular.Integer, message: "请输入正整数" },
],
couponLimitNum: [
{ required: true, message: "领取限制不能为空" },
{ pattern: regular.Integer, message: "请输入正整数" },
],
description: [{ required: true, message: "请输入范围描述" }],
},
options: {
disabledDate(date) {
return date && date.valueOf() < Date.now() - 86400000;
},
},
};
},
async mounted() {
await this.getCagetoryList();
if (this.id) {
this.getCoupon();
this.modalType = 1;
}
},
methods: {
getCoupon() {
getShopCoupon(this.id).then((res) => {
let data = res.result;
if (!data.promotionGoodsList) data.promotionGoodsList = [];
this.rangeTimeType = data.rangeDayType === "DYNAMICTIME" ? 0 : 1;
if (data.scopeType == "PORTION_GOODS_CATEGORY") {
let prevCascader = data.scopeId.split(",");
function next(params, prev) {
for (let i = 0; i < params.length; i++) {
const item = params[i];
if (item.children) {
next(item.children, [...prev, item]);
} else {
if (prevCascader.includes(item.id)) {
prevCascader = prevCascader.map((key) => {
if (key === item.id) {
let result = prev.map((item) => item.id);
return [...result, item.id];
} else {
return key;
}
});
} else {
i === params.length - 1 && (prev = []);
}
}
}
}
defineOptions({ name: "add-coupon" });
next(this.goodsCategoryList, []);
data.scopeIdGoods = prevCascader;
}
data.rangeTime = [];
if (data.startTime && data.endTime) {
data.rangeTime.push(new Date(data.startTime), new Date(data.endTime));
}
this.form = data;
});
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
const params = JSON.parse(JSON.stringify(this.form));
params.getType != "ACTIVITY" ? delete params.effectiveDays : "";
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
if (this.rangeTimeType == 1) {
params.rangeDayType = "FIXEDTIME";
const rangeTime = this.form.rangeTime;
const start = rangeTime[0] instanceof Date ? rangeTime[0] : new Date(rangeTime[0]);
const end = rangeTime[1] instanceof Date ? rangeTime[1] : new Date(rangeTime[1]);
params.startTime = this.$filters.unixToDate(start.getTime() / 1000);
params.endTime = this.$filters.unixToDate(end.getTime() / 1000);
delete params.effectiveDays;
} else {
params.rangeDayType = "DYNAMICTIME";
delete params.rangeTime;
}
const formRef = ref(null);
const skuSelectRef = ref(null);
let scopeId = [];
const checkPrice = (rule, value, callback) => {
if (!value && value !== 0) {
return callback(new Error("面额不能为空"));
} else if (!regular.money.test(value)) {
callback(new Error("请输入正整数或者两位小数"));
} else if (parseFloat(value) > 99999999) {
callback(new Error("面额设置超过上限值"));
} else {
callback();
}
};
const checkWeight = (rule, value, callback) => {
if (!value && typeof value !== "number") {
callback(new Error("消费门槛不能为空"));
} else if (!regular.money.test(value)) {
callback(new Error("请输入正整数或者两位小数"));
} else if (parseFloat(value) > 99999999) {
callback(new Error("消费门槛设置超过上限值"));
} else {
callback();
}
};
if (
params.scopeType == "PORTION_GOODS" &&
(!params.promotionGoodsList || params.promotionGoodsList.length == 0)
) {
this.$Modal.warning({ title: "提示", content: "请选择指定商品" });
return;
}
if (
params.scopeType == "PORTION_GOODS_CATEGORY" &&
(!params.scopeIdGoods || params.scopeIdGoods.length == 0)
) {
this.$Modal.warning({ title: "提示", content: "请选择商品分类" });
return;
}
if (params.scopeType == "PORTION_GOODS") {
params.promotionGoodsList.forEach((item) => {
scopeId.push(item.skuId);
});
params.scopeId = scopeId.toString();
} else if (params.scopeType == "ALL") {
delete params.promotionGoodsList;
} else if (params.scopeType == "PORTION_GOODS_CATEGORY") {
scopeId = this.filterCategoryId(params.scopeIdGoods, []);
params.scopeId = scopeId.toString();
delete params.promotionGoodsList;
}
delete params.scopeIdGoods;
this.submitLoading = true;
if (this.modalType === 0) {
delete params.id;
saveShopCoupon(params).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("优惠券发送成功");
this.closeCurrentPage();
}
});
} else {
delete params.consumeLimit;
delete params.updateTime;
editShopCoupon(params).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("优惠券修改成功");
this.closeCurrentPage();
}
});
}
}
});
},
closeCurrentPage() {
this.$store.commit("removeTag", "add-coupon");
localStorage.pageOpenedList = JSON.stringify(this.$store.state.app.pageOpenedList);
this.$router.go(-1);
},
openSkuList() {
this.$refs.skuSelect.open("goods");
let data = JSON.parse(JSON.stringify(this.form.promotionGoodsList));
data.forEach((e) => {
e.id = e.skuId;
});
this.$refs.skuSelect.goodsData = data;
},
changeSelect(e) {
this.selectedGoods = e;
},
delSelectGoods() {
if (this.selectedGoods.length <= 0) {
this.$Message.warning("您还未选择要删除的数据");
return;
}
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除所选商品吗?",
onOk: () => {
let ids = [];
this.selectedGoods.forEach(function (e) {
ids.push(e.skuId);
});
this.form.promotionGoodsList = this.form.promotionGoodsList.filter((item) => {
return !ids.includes(item.skuId);
});
},
});
},
delGoods(index) {
this.form.promotionGoodsList.splice(index, 1);
},
selectedGoodsData(item) {
let list = [];
item.forEach((e) => {
list.push({
goodsName: e.goodsName,
price: e.price,
originalPrice: e.price,
quantity: e.quantity,
storeId: e.storeId,
storeName: e.storeName,
skuId: e.id,
categoryPath: e.categoryPath,
thumbnail: e.small,
goodsType: e.goodsType,
goodsId: e.goodsId,
originPrice: e.price,
});
});
this.form.promotionGoodsList = list;
},
async getCagetoryList() {
let data = await getGoodsCategoryAll();
this.goodsCategoryList = data.result;
this.goodsCategoryList = this.goodsCategoryList.map((item) => {
if (item.children) {
item.children = item.children.map((child) => {
if (child.children) {
child.children = child.children.map((son) => {
return {
value: son.id,
label: son.name,
};
});
return {
value: child.id,
label: child.name,
children: child.children,
};
} else {
return {
value: child.id,
label: child.name,
};
}
});
}
return { value: item.id, label: item.name, children: item.children };
});
},
filterCategoryId(list, idArr) {
list.forEach((e) => {
if (e instanceof Array) {
this.filterCategoryId(e, idArr);
} else {
if (!idArr.includes(e)) idArr.push(e);
}
});
return idArr;
const disabled = ref(route.query.onlyView);
const rangeTimeType = ref(1);
const modalType = ref(0);
const form = ref({
storeCommission: 0,
publishNum: 0,
scopeType: "ALL",
couponLimitNum: 1,
couponType: "PRICE",
couponName: "",
promotionName: "",
getType: "FREE",
promotionGoodsList: [],
scopeIdGoods: [],
rangeDayType: "",
effectiveDays: 1,
});
const id = ref(route.query.id);
const submitLoading = ref(false);
const selectedGoods = ref([]);
const goodsCategoryList = ref([]);
const formRule = reactive({
promotionName: [{ required: true, message: "活动名称不能为空" }],
couponName: [{ required: true, message: "优惠券名称不能为空" }],
price: [{ required: true, message: "请输入面额" }, { validator: checkPrice }],
rangeTime: [{ required: true, message: "请选择优惠券有效期" }],
consumeThreshold: [
{ required: true, message: "请输入消费门槛" },
{ validator: checkWeight },
],
couponDiscount: [
{ required: true, message: "请输入折扣" },
{
pattern: regular.discount,
message: "请输入0-10的数字,可有一位小数",
},
],
storeCommission: [
{ required: true, message: "请输入店铺承担比例" },
{ pattern: regular.rate, message: "请输入0-100的正整数" },
],
publishNum: [
{ required: true, message: "请输入发放数量" },
{ pattern: regular.Integer, message: "请输入正整数" },
],
couponLimitNum: [
{ required: true, message: "领取限制不能为空" },
{ pattern: regular.Integer, message: "请输入正整数" },
],
description: [{ required: true, message: "请输入范围描述" }],
});
const options = {
disabledDate(date) {
return date && date.valueOf() < Date.now() - 86400000;
},
};
watch(
() => form.value.getType,
(val) => {
if (val == "FREE") {
rangeTimeType.value = 1;
} else {
rangeTimeType.value = 0;
}
if (rangeTimeType.value == 0) {
delete formRule.rangeTime;
}
},
{ deep: true }
);
watch(
() => route.query,
(e) => {
id.value = e.id;
if (id.value) {
getCoupon();
} else {
formRef.value.resetFields();
}
}
);
onMounted(async () => {
await getCagetoryList();
if (id.value) {
getCoupon();
modalType.value = 1;
}
});
function getCoupon() {
getShopCoupon(id.value).then((res) => {
let data = res.result;
if (!data.promotionGoodsList) data.promotionGoodsList = [];
rangeTimeType.value = data.rangeDayType === "DYNAMICTIME" ? 0 : 1;
if (data.scopeType == "PORTION_GOODS_CATEGORY") {
let prevCascader = data.scopeId.split(",");
function next(params, prev) {
for (let i = 0; i < params.length; i++) {
const item = params[i];
if (item.children) {
next(item.children, [...prev, item]);
} else {
if (prevCascader.includes(item.id)) {
prevCascader = prevCascader.map((key) => {
if (key === item.id) {
let result = prev.map((item) => item.id);
return [...result, item.id];
} else {
return key;
}
});
} else {
i === params.length - 1 && (prev = []);
}
}
}
}
next(goodsCategoryList.value, []);
data.scopeIdGoods = prevCascader;
}
data.rangeTime = [];
if (data.startTime && data.endTime) {
data.rangeTime.push(new Date(data.startTime), new Date(data.endTime));
}
form.value = data;
});
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (valid) {
const params = JSON.parse(JSON.stringify(form.value));
params.getType != "ACTIVITY" ? delete params.effectiveDays : "";
if (rangeTimeType.value == 1) {
params.rangeDayType = "FIXEDTIME";
const rangeTime = form.value.rangeTime;
const start = rangeTime[0] instanceof Date ? rangeTime[0] : new Date(rangeTime[0]);
const end = rangeTime[1] instanceof Date ? rangeTime[1] : new Date(rangeTime[1]);
params.startTime = unixToDate(start.getTime() / 1000);
params.endTime = unixToDate(end.getTime() / 1000);
delete params.effectiveDays;
} else {
params.rangeDayType = "DYNAMICTIME";
delete params.rangeTime;
}
let scopeId = [];
if (
params.scopeType == "PORTION_GOODS" &&
(!params.promotionGoodsList || params.promotionGoodsList.length == 0)
) {
Modal.warning({ title: "提示", content: "请选择指定商品" });
return;
}
if (
params.scopeType == "PORTION_GOODS_CATEGORY" &&
(!params.scopeIdGoods || params.scopeIdGoods.length == 0)
) {
Modal.warning({ title: "提示", content: "请选择商品分类" });
return;
}
if (params.scopeType == "PORTION_GOODS") {
params.promotionGoodsList.forEach((item) => {
scopeId.push(item.skuId);
});
params.scopeId = scopeId.toString();
} else if (params.scopeType == "ALL") {
delete params.promotionGoodsList;
} else if (params.scopeType == "PORTION_GOODS_CATEGORY") {
scopeId = filterCategoryId(params.scopeIdGoods, []);
params.scopeId = scopeId.toString();
delete params.promotionGoodsList;
}
delete params.scopeIdGoods;
submitLoading.value = true;
if (modalType.value === 0) {
delete params.id;
saveShopCoupon(params).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("优惠券发送成功");
closeCurrentPage();
}
});
} else {
delete params.consumeLimit;
delete params.updateTime;
editShopCoupon(params).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("优惠券修改成功");
closeCurrentPage();
}
});
}
}
});
}
function closeCurrentPage() {
appStore.removeTag("add-coupon");
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
router.go(-1);
}
function openSkuList() {
skuSelectRef.value.open("goods");
let data = JSON.parse(JSON.stringify(form.value.promotionGoodsList));
data.forEach((e) => {
e.id = e.skuId;
});
skuSelectRef.value.goodsData = data;
}
function changeSelect(e) {
selectedGoods.value = e;
}
function delSelectGoods() {
if (selectedGoods.value.length <= 0) {
Message.warning("您还未选择要删除的数据");
return;
}
Modal.confirm({
title: "确认删除",
content: "您确认要删除所选商品吗?",
onOk: () => {
let ids = [];
selectedGoods.value.forEach(function (e) {
ids.push(e.skuId);
});
form.value.promotionGoodsList = form.value.promotionGoodsList.filter((item) => {
return !ids.includes(item.skuId);
});
},
});
}
function delGoods(index) {
form.value.promotionGoodsList.splice(index, 1);
}
function selectedGoodsData(item) {
let list = [];
item.forEach((e) => {
list.push({
goodsName: e.goodsName,
price: e.price,
originalPrice: e.price,
quantity: e.quantity,
storeId: e.storeId,
storeName: e.storeName,
skuId: e.id,
categoryPath: e.categoryPath,
thumbnail: e.small,
goodsType: e.goodsType,
goodsId: e.goodsId,
originPrice: e.price,
});
});
form.value.promotionGoodsList = list;
}
async function getCagetoryList() {
let data = await getGoodsCategoryAll();
goodsCategoryList.value = data.result;
goodsCategoryList.value = goodsCategoryList.value.map((item) => {
if (item.children) {
item.children = item.children.map((child) => {
if (child.children) {
child.children = child.children.map((son) => {
return {
value: son.id,
label: son.name,
};
});
return {
value: child.id,
label: child.name,
children: child.children,
};
} else {
return {
value: child.id,
label: child.name,
};
}
});
}
return { value: item.id, label: item.name, children: item.children };
});
}
function filterCategoryId(list, idArr) {
list.forEach((e) => {
if (e instanceof Array) {
filterCategoryId(e, idArr);
} else {
if (!idArr.includes(e)) idArr.push(e);
}
});
return idArr;
}
</script>
<style lang="scss" scoped>

View File

@@ -3,7 +3,7 @@
<el-button style="margin-bottom: 10px" @click="back()">返回</el-button>
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="90px" class="search-form mb_10">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="90px" class="search-form mb_10">
<el-form-item label="优惠券名称" prop="couponName">
<el-input
v-model="searchForm.couponName"
@@ -144,93 +144,111 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getCouponReceiveList } from "@/api/promotion";
export default {
name: "coupon-recevie",
props: {
promotionStatus: {
type: String,
default: "",
},
import { useAppStore } from "@/stores/app";
defineOptions({ name: "coupon-recevie" });
const props = defineProps({
promotionStatus: {
type: String,
default: "",
},
data() {
return {
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
getType: "",
couponId: this.$route.query.couponId,
},
selectList: [],
selectCount: 0,
data: [],
total: 0,
refreshTable: true,
selectDate: [],
};
},
watch: {
$route(e) {
this.searchForm.couponId = e.query.couponId;
if (this.couponId) {
this.getDataList();
} else {
this.$refs.searchForm?.resetFields();
}
},
},
methods: {
back() {
this.$store.commit("removeTag", "coupon-receive");
this.$router.go(-1);
},
check() {
this.$emit("selected", this.selectList);
},
init() {
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
changeSelect(e) {
this.selectList = e;
this.selectCount = e.length;
if (this.getType === "ACTIVITY") this.check();
},
getDataList() {
this.loading = true;
if (this.selectDate && this.selectDate[0] && this.selectDate[1]) {
this.searchForm.startTime = this.selectDate[0].getTime();
this.searchForm.endTime = this.selectDate[1].getTime();
} else {
this.searchForm.startTime = null;
this.searchForm.endTime = null;
}
getCouponReceiveList(this.searchForm).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
},
mounted() {
this.init();
},
};
});
const emit = defineEmits(["selected"]);
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
getType: "",
couponId: route.query.couponId,
});
const selectList = ref([]);
const selectCount = ref(0);
const data = ref([]);
const total = ref(0);
const refreshTable = ref(true);
const selectDate = ref([]);
const searchFormRef = ref(null);
watch(
() => route.query,
(query) => {
searchForm.couponId = query.couponId;
// 原代码此处判断的 couponId 在 data 中并不存在,恒为 undefined;保持原行为
if (props.couponId) {
getDataList();
} else {
searchFormRef.value?.resetFields();
}
}
);
function back() {
appStore.removeTag("coupon-receive");
router.go(-1);
}
function check() {
emit("selected", selectList.value);
}
function init() {
getDataList();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
}
function changeSelect(e) {
selectList.value = e;
selectCount.value = e.length;
// 原代码此处判断的 getType 组件并未声明该 prop,恒为 undefined;保持原行为
if (props.getType === "ACTIVITY") check();
}
function getDataList() {
loading.value = true;
if (selectDate.value && selectDate.value[0] && selectDate.value[1]) {
searchForm.startTime = selectDate.value[0].getTime();
searchForm.endTime = selectDate.value[1].getTime();
} else {
searchForm.startTime = null;
searchForm.endTime = null;
}
getCouponReceiveList(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
onMounted(() => {
init();
});
</script>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="90px"
@@ -181,315 +181,350 @@
</div>
</template>
<script>
<script setup>
import { nextTick, onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
getShopCouponList,
updateCouponStatus,
deleteShopCoupon,
} from "@/api/promotion";
import { formatPromotionCouponValidityHtml } from "@/utils/promotions";
import { Message, Modal } from "@/utils/message";
export default {
name: "coupon",
props: {
getType: {
type: String,
default: "",
},
promotionStatus: {
type: String,
default: "",
},
selectedList: {
type: Array,
default: () => [],
},
defineOptions({ name: "coupon" });
const props = defineProps({
getType: {
type: String,
default: "",
},
data() {
return {
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
getType: "",
},
selectList: [],
selectCount: 0,
data: [],
total: 0,
selectDate: [],
showActionColumn: true,
showStatusColumn: true,
isSyncingSelection: false,
};
promotionStatus: {
type: String,
default: "",
},
watch: {
$route(to) {
if (to.fullPath == "/promotions/manager-coupon") {
this.init();
selectedList: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["selected"]);
const route = useRoute();
const router = useRouter();
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
getType: "",
});
const selectList = ref([]);
const selectCount = ref(0);
const data = ref([]);
const total = ref(0);
const selectDate = ref([]);
const showActionColumn = ref(true);
const showStatusColumn = ref(true);
const isSyncingSelection = ref(false);
// searchFormRef/table:模板 ref 绑定(searchFormRef 原名 searchForm,改名避免与顶层变量互相覆盖)
const searchFormRef = ref(null);
const table = ref(null);
watch(
() => route.fullPath,
(fullPath) => {
if (fullPath == "/promotions/manager-coupon") {
init();
}
}
);
watch(
() => props.selectedList,
(val) => {
if (isSyncingSelection.value) return;
if (isSameSelection(val, selectList.value)) return;
nextTick(() => {
syncTableSelection(val);
});
},
{ deep: true }
);
function formatActivityTime(row) {
return formatPromotionCouponValidityHtml(row);
}
function promotionStatusText(status) {
const map = {
NEW: "未开始",
START: "已开始",
END: "已结束",
CLOSE: "已关闭",
};
return map[status] || "未知";
}
function promotionStatusTagType(status) {
const map = {
NEW: "info",
START: "success",
END: "danger",
CLOSE: "danger",
};
return map[status] || "danger";
}
function scopeTypeText(type) {
const map = {
ALL: "全品类",
PORTION_GOODS_CATEGORY: "商品分类",
PORTION_SHOP_CATEGORY: "店铺分类",
PORTION_GOODS: "指定商品",
};
return map[type] || "未知";
}
function scopeTypeTagType(type) {
const map = {
ALL: "info",
PORTION_GOODS_CATEGORY: "warning",
PORTION_SHOP_CATEGORY: "warning",
PORTION_GOODS: "primary",
};
return map[type] || "danger";
}
function getTypeText(type) {
const map = {
FREE: "免费获取",
ACTIVITY: "活动获取",
INSIDE: "内购",
IGAME: "游戏人生",
};
return map[type] || "未知";
}
function getTypeTagType(type) {
const map = {
FREE: "danger",
ACTIVITY: "warning",
INSIDE: "success",
IGAME: "success",
};
return map[type] || "info";
}
function couponTypeText(type) {
const map = {
DISCOUNT: "打折",
PRICE: "减免现金",
};
return map[type] || "未知";
}
function couponTypeTagType(type) {
const map = {
DISCOUNT: "primary",
PRICE: "info",
};
return map[type] || "info";
}
function isSameSelection(next, current) {
const a = next || [];
const b = current || [];
if (a.length !== b.length) return false;
const nextIds = a.map((item) => item.id).sort().join(",");
const currentIds = b.map((item) => item.id).sort().join(",");
return nextIds === currentIds;
}
function syncTableSelection(selected) {
const tableEl = table.value;
if (!tableEl) return;
const nextList = selected ? [...selected] : [];
isSyncingSelection.value = true;
tableEl.clearSelection();
if (nextList.length) {
data.value.forEach((row) => {
if (nextList.some((item) => item.id === row.id)) {
tableEl.toggleRowSelection(row, true);
}
},
selectedList: {
handler(val) {
if (this.isSyncingSelection) return;
if (this.isSameSelection(val, this.selectList)) return;
this.$nextTick(() => {
this.syncTableSelection(val);
});
},
deep: true,
},
},
methods: {
formatActivityTime(row) {
return formatPromotionCouponValidityHtml(row);
},
promotionStatusText(status) {
const map = {
NEW: "未开始",
START: "已开始",
END: "已结束",
CLOSE: "已关闭",
};
return map[status] || "未知";
},
promotionStatusTagType(status) {
const map = {
NEW: "info",
START: "success",
END: "danger",
CLOSE: "danger",
};
return map[status] || "danger";
},
scopeTypeText(type) {
const map = {
ALL: "全品类",
PORTION_GOODS_CATEGORY: "商品分类",
PORTION_SHOP_CATEGORY: "店铺分类",
PORTION_GOODS: "指定商品",
};
return map[type] || "未知";
},
scopeTypeTagType(type) {
const map = {
ALL: "info",
PORTION_GOODS_CATEGORY: "warning",
PORTION_SHOP_CATEGORY: "warning",
PORTION_GOODS: "primary",
};
return map[type] || "danger";
},
getTypeText(type) {
const map = {
FREE: "免费获取",
ACTIVITY: "活动获取",
INSIDE: "内购",
IGAME: "游戏人生",
};
return map[type] || "未知";
},
getTypeTagType(type) {
const map = {
FREE: "danger",
ACTIVITY: "warning",
INSIDE: "success",
IGAME: "success",
};
return map[type] || "info";
},
couponTypeText(type) {
const map = {
DISCOUNT: "打折",
PRICE: "减免现金",
};
return map[type] || "未知";
},
couponTypeTagType(type) {
const map = {
DISCOUNT: "primary",
PRICE: "info",
};
return map[type] || "info";
},
isSameSelection(next, current) {
const a = next || [];
const b = current || [];
if (a.length !== b.length) return false;
const nextIds = a.map((item) => item.id).sort().join(",");
const currentIds = b.map((item) => item.id).sort().join(",");
return nextIds === currentIds;
},
syncTableSelection(selected) {
const table = this.$refs.table;
if (!table) return;
const nextList = selected ? [...selected] : [];
this.isSyncingSelection = true;
table.clearSelection();
if (nextList.length) {
this.data.forEach((row) => {
if (nextList.some((item) => item.id === row.id)) {
table.toggleRowSelection(row, true);
});
}
selectList.value = nextList;
selectCount.value = nextList.length;
nextTick(() => {
isSyncingSelection.value = false;
});
}
function check() {
emit("selected", selectList.value);
}
function receivePage(id) {
if (id) {
router.push({ name: "coupon-receive", query: { couponId: id } });
} else {
router.push({ name: "coupon-receive" });
}
}
function init() {
getDataList();
}
function add() {
router.push({ name: "add-coupon" });
}
function changePage(v) {
searchForm.pageNumber = v;
getDataList();
}
function changePageSize(v) {
searchForm.pageNumber = 1;
searchForm.pageSize = v;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
}
function clearSelectAll() {
table.value?.clearSelection();
}
function changeSelect(e) {
if (isSyncingSelection.value) return;
selectList.value = e;
selectCount.value = e.length;
if (props.getType === "ACTIVITY") check();
}
function getDataList() {
loading.value = true;
if (selectDate.value && selectDate.value[0] && selectDate.value[1]) {
searchForm.startTime = new Date(selectDate.value[0]).getTime();
searchForm.endTime = new Date(selectDate.value[1]).getTime();
} else {
searchForm.startTime = null;
searchForm.endTime = null;
}
getShopCouponList(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
nextTick(() => {
if (props.selectedList.length) {
syncTableSelection(props.selectedList);
}
});
}
this.selectList = nextList;
this.selectCount = nextList.length;
this.$nextTick(() => {
this.isSyncingSelection = false;
});
},
check() {
this.$emit("selected", this.selectList);
},
receivePage(id) {
if (id) {
this.$router.push({ name: "coupon-receive", query: { couponId: id } });
} else {
this.$router.push({ name: "coupon-receive" });
}
},
init() {
this.getDataList();
},
add() {
this.$router.push({ name: "add-coupon" });
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
},
changePageSize(v) {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = v;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
clearSelectAll() {
this.$refs.table?.clearSelection();
},
changeSelect(e) {
if (this.isSyncingSelection) return;
this.selectList = e;
this.selectCount = e.length;
if (this.getType === "ACTIVITY") this.check();
},
getDataList() {
this.loading = true;
if (this.selectDate && this.selectDate[0] && this.selectDate[1]) {
this.searchForm.startTime = new Date(this.selectDate[0]).getTime();
this.searchForm.endTime = new Date(this.selectDate[1]).getTime();
} else {
this.searchForm.startTime = null;
this.searchForm.endTime = null;
}
getShopCouponList(this.searchForm)
})
.finally(() => {
loading.value = false;
});
}
function see(v, only) {
const data = only ? { onlyView: true, id: v.id } : { id: v.id };
router.push({ name: "add-coupon", query: data });
}
function close(v) {
Modal.confirm({
title: "确认关闭",
content: "确认要关闭此优惠券么?",
loading: true,
onOk: () => {
updateCouponStatus({
couponIds: v.id,
effectiveDays: 0,
})
.then((res) => {
Modal.remove();
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
this.$nextTick(() => {
if (this.selectedList.length) {
this.syncTableSelection(this.selectedList);
}
});
Message.success("优惠券已关闭");
getDataList();
}
})
.finally(() => {
this.loading = false;
.catch(() => {
Modal.remove();
});
},
see(v, only) {
const data = only ? { onlyView: true, id: v.id } : { id: v.id };
this.$router.push({ name: "add-coupon", query: data });
});
}
function remove(v) {
Modal.confirm({
title: "确认删除",
content: "确认要删除此优惠券么?",
loading: true,
onOk: () => {
deleteShopCoupon(v.id)
.then((res) => {
Modal.remove();
if (res.success) {
Message.success("优惠券已删除");
getDataList();
}
})
.catch(() => {
Modal.remove();
});
},
close(v) {
this.$Modal.confirm({
title: "确认关闭",
content: "确认要关闭此优惠券么?",
loading: true,
onOk: () => {
updateCouponStatus({
couponIds: v.id,
effectiveDays: 0,
})
.then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("优惠券已关闭");
this.getDataList();
}
})
.catch(() => {
this.$Modal.remove();
});
},
});
}
function delAll() {
if (selectCount.value <= 0) {
Message.warning("您还未选择要下架的优惠券");
return;
}
Modal.confirm({
title: "确认下架",
content: "您确认要下架所选的 " + selectCount.value + " 条数据?",
loading: true,
onOk: () => {
const ids = selectList.value.map((e) => e.id);
updateCouponStatus({
couponIds: ids.toString(),
promotionStatus: "CLOSE",
}).then((res) => {
Modal.remove();
if (res.success) {
Message.success("下架成功");
clearSelectAll();
getDataList();
}
});
},
remove(v) {
this.$Modal.confirm({
title: "确认删除",
content: "确认要删除此优惠券么?",
loading: true,
onOk: () => {
deleteShopCoupon(v.id)
.then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("优惠券已删除");
this.getDataList();
}
})
.catch(() => {
this.$Modal.remove();
});
},
});
},
delAll() {
if (this.selectCount <= 0) {
this.$Message.warning("您还未选择要下架的优惠券");
return;
}
this.$Modal.confirm({
title: "确认下架",
content: "您确认要下架所选的 " + this.selectCount + " 条数据?",
loading: true,
onOk: () => {
const ids = this.selectList.map((e) => e.id);
updateCouponStatus({
couponIds: ids.toString(),
promotionStatus: "CLOSE",
}).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("下架成功");
this.clearSelectAll();
this.getDataList();
}
});
},
});
},
},
mounted() {
if (this.getType) {
this.searchForm.getType = this.getType;
this.showActionColumn = false;
}
if (this.promotionStatus) {
this.searchForm.promotionStatus = this.promotionStatus;
this.showStatusColumn = false;
}
this.init();
},
};
});
}
onMounted(() => {
if (props.getType) {
searchForm.getType = props.getType;
showActionColumn.value = false;
}
if (props.promotionStatus) {
searchForm.promotionStatus = props.promotionStatus;
showStatusColumn.value = false;
}
init();
});
</script>
<style scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<el-card>
<el-form ref="form" :model="form" label-width="120px">
<el-form ref="formRef" :model="form" label-width="120px">
<el-form-item label="活动名称" required>
<el-input v-model="form.promotionName" style="width: 320px" />
</el-form-item>
@@ -49,82 +49,88 @@
</el-form-item>
</el-form>
</el-card>
<liliDialog ref="liliDialog" @selectedGoodsData="selectedGoods" />
<liliDialog ref="liliDialogRef" @selectedGoodsData="selectedGoods" />
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getFlashDiscountDetail, saveFlashDiscount, editFlashDiscount } from "@/api/promotion.js";
import { promotionPriceMin } from "@/constants/goodsType";
import { unixToDate } from "@/utils/filters";
import { Message } from "@/utils/message";
export default {
data() {
return {
form: {
promotionName: "",
rangeTime: [],
limitNum: 0,
description: "",
scopeType: "PORTION_GOODS",
promotionGoodsList: [],
},
};
},
mounted() {
if (this.$route.query.id) {
getFlashDiscountDetail(this.$route.query.id).then((res) => {
if (res.success) {
Object.assign(this.form, res.result);
this.form.rangeTime = [new Date(res.result.startTime), new Date(res.result.endTime)];
}
const route = useRoute();
const router = useRouter();
const form = reactive({
promotionName: "",
rangeTime: [],
limitNum: 0,
description: "",
scopeType: "PORTION_GOODS",
promotionGoodsList: [],
});
// formRef / liliDialogRef:模板 ref 改名避免与 form 变量、liliDialog 全局组件互相覆盖
const formRef = ref(null);
const liliDialogRef = ref(null);
onMounted(() => {
if (route.query.id) {
getFlashDiscountDetail(route.query.id).then((res) => {
if (res.success) {
Object.assign(form, res.result);
form.rangeTime = [new Date(res.result.startTime), new Date(res.result.endTime)];
}
});
}
});
function openGoods() {
liliDialogRef.value.open("goods");
}
function selectedGoods(list) {
list.forEach((g) => {
if (!form.promotionGoodsList.find((i) => i.skuId === g.id)) {
form.promotionGoodsList.push({
skuId: g.id,
goodsId: g.goodsId,
goodsName: g.goodsName,
originalPrice: g.price,
price: g.price,
quantity: g.quantity,
goodsType: g.goodsType,
});
}
},
methods: {
promotionPriceMin,
openGoods() {
this.$refs.liliDialog.open("goods");
},
selectedGoods(list) {
list.forEach((g) => {
if (!this.form.promotionGoodsList.find((i) => i.skuId === g.id)) {
this.form.promotionGoodsList.push({
skuId: g.id,
goodsId: g.goodsId,
goodsName: g.goodsName,
originalPrice: g.price,
price: g.price,
quantity: g.quantity,
goodsType: g.goodsType,
});
}
});
},
submit() {
const payload = { ...this.form };
if (payload.rangeTime && payload.rangeTime.length === 2) {
const start =
payload.rangeTime[0] instanceof Date
? payload.rangeTime[0]
: new Date(payload.rangeTime[0]);
const end =
payload.rangeTime[1] instanceof Date
? payload.rangeTime[1]
: new Date(payload.rangeTime[1]);
payload.startTime = this.$filters.unixToDate(start.getTime() / 1000);
payload.endTime = this.$filters.unixToDate(end.getTime() / 1000);
}
delete payload.rangeTime;
const req = payload.id ? editFlashDiscount(payload) : saveFlashDiscount(payload);
req.then((res) => {
if (res.success) {
this.$Message.success("保存成功");
this.$router.back();
}
});
},
},
};
});
}
function submit() {
const payload = { ...form };
if (payload.rangeTime && payload.rangeTime.length === 2) {
const start =
payload.rangeTime[0] instanceof Date
? payload.rangeTime[0]
: new Date(payload.rangeTime[0]);
const end =
payload.rangeTime[1] instanceof Date
? payload.rangeTime[1]
: new Date(payload.rangeTime[1]);
payload.startTime = unixToDate(start.getTime() / 1000);
payload.endTime = unixToDate(end.getTime() / 1000);
}
delete payload.rangeTime;
const req = payload.id ? editFlashDiscount(payload) : saveFlashDiscount(payload);
req.then((res) => {
if (res.success) {
Message.success("保存成功");
router.back();
}
});
}
</script>
<style scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="70px" class="search-form" @keyup.enter="handleSearch">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="70px" class="search-form" @keyup.enter="handleSearch">
<el-form-item label="活动名称" prop="promotionName">
<el-input v-model="searchForm.promotionName" placeholder="请输入活动名称" clearable style="width: 240px" />
</el-form-item>
@@ -50,58 +50,60 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { getFlashDiscountList, updateFlashDiscountStatus } from "@/api/promotion.js";
import { Message } from "@/utils/message";
export default {
data() {
return {
loading: false,
total: 0,
data: [],
searchForm: { pageNumber: 1, pageSize: 20, sort: "createTime", order: "desc" },
};
},
mounted() {
this.getDataList();
},
methods: {
statusText(s) {
return { NEW: "未开始", START: "进行中", END: "已结束", CLOSE: "已关闭" }[s] || s;
},
statusType(s) {
return { NEW: "info", START: "success", END: "danger", CLOSE: "danger" }[s] || "info";
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getFlashDiscountList(this.searchForm).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
goAdd() {
this.$router.push({ name: "flash-discount-add" });
},
goEdit(row) {
this.$router.push({ name: "flash-discount-add", query: { id: row.id } });
},
toggleStatus(row) {
updateFlashDiscountStatus(row.id).then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
},
};
const router = useRouter();
const loading = ref(false);
const total = ref(0);
const data = ref([]);
const searchForm = reactive({ pageNumber: 1, pageSize: 20, sort: "createTime", order: "desc" });
// searchFormRef:模板 ref 改名避免与顶层变量 searchForm 互相覆盖
const searchFormRef = ref(null);
onMounted(() => {
getDataList();
});
function statusText(s) {
return { NEW: "未开始", START: "进行中", END: "已结束", CLOSE: "已关闭" }[s] || s;
}
function statusType(s) {
return { NEW: "info", START: "success", END: "danger", CLOSE: "danger" }[s] || "info";
}
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
getFlashDiscountList(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function goAdd() {
router.push({ name: "flash-discount-add" });
}
function goEdit(row) {
router.push({ name: "flash-discount-add", query: { id: row.id } });
}
function toggleStatus(row) {
updateFlashDiscountStatus(row.id).then((res) => {
if (res.success) {
Message.success("操作成功");
getDataList();
}
});
}
</script>
<style scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<el-card>
<el-form ref="form" :model="form" label-width="120px" :rules="formRule">
<el-form ref="formRef" :model="form" label-width="120px" :rules="formRule">
<div class="base-info-item">
<h4>基本信息</h4>
<div class="form-item-view">
@@ -230,11 +230,13 @@
</div>
</el-form>
</el-card>
<liliDialog ref="liliDialog" @selectedGoodsData="selectedGoodsData" />
<liliDialog ref="liliDialogRef" @selectedGoodsData="selectedGoodsData" />
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
getShopCouponList,
getFullDiscountById,
@@ -243,260 +245,260 @@ import {
} from "@/api/promotion";
import { getGoodsSkuListDataSeller } from "@/api/goods";
import { regular } from "@/utils";
import vueQr from "vue-qr";
import { goodsTypeLabel } from "@/constants/goodsType";
import { Message, Modal } from "@/utils/message";
import { unixToDate } from "@/utils/filters";
export default {
name: "add-full-discount",
components: {
"vue-qr": vueQr,
},
data() {
const checkWeight = (rule, value, callback) => {
if (!value && typeof value !== "number") {
callback(new Error("优惠门槛不能为空"));
} else if (!regular.money.test(value)) {
callback(new Error("请输入正整数或者两位小数"));
} else if (parseFloat(value) > 99999999) {
callback(new Error("优惠门槛设置超过上限值"));
} else {
callback();
}
};
const checkDiscount = (rule, value, callback) => {
if (value === null || value === undefined || value === "") {
callback(new Error("请填写优惠折扣"));
} else if (value < 0.1 || value > 9.9) {
callback(new Error("请输入0-10的数字,可有一位小数"));
} else {
callback();
}
};
return {
form: {
discountType: "fullMinusFlag",
scopeType: "ALL",
promotionGoodsList: [],
promotionStatus: "NEW",
},
id: this.$route.query.id,
submitLoading: false,
selectedGoods: [],
couponList: [],
giftList: [],
giftLoading: false,
couponLoading: false,
formRule: {
promotionName: [{ required: true, message: "活动名称不能为空" }],
rangeTime: [{ required: true, message: "请选择活动时间" }],
description: [{ required: true, message: "请填写活动描述" }],
fullMoney: [{ required: true, validator: checkWeight }],
fullMinus: [
{ required: true, message: "请填写优惠金额" },
{ pattern: regular.money, message: "请输入正确金额" },
],
fullRate: [{ required: true, validator: checkDiscount }],
},
options: {
disabledDate(date) {
return date && date.valueOf() < Date.now() - 86400000;
},
},
};
},
computed: {
fieldDisabled() {
return !!this.id && this.form.promotionStatus !== "NEW";
},
},
mounted() {
if (this.id) {
this.getDetail();
}
this.getCouponList();
this.getGiftList();
},
methods: {
giftOptionLabel(item) {
if (!item) return "";
const typeLabel =
item.goodsType === "E_COUPON" ? ` [${goodsTypeLabel(item.goodsType)}]` : "";
const stock =
item.goodsType === "E_COUPON" && item.quantity != null
? ` · 库存${item.quantity}`
: "";
return `${item.goodsName || ""}${typeLabel}${stock}`;
},
closeCurrentPage() {
this.$router.back();
},
openSkuList() {
const data = JSON.parse(JSON.stringify(this.form.promotionGoodsList || []));
data.forEach((e) => {
e.id = e.skuId || e.id;
});
// 先写入已选商品,再打开选择器,保证回显时 selectedWay 已就绪
this.$refs.liliDialog.goodsData = data;
this.$refs.liliDialog.open("goods");
},
getDetail() {
getFullDiscountById(this.id).then((res) => {
const data = res.result;
if (data.scopeType === "ALL") {
data.promotionGoodsList = [];
}
if (data.fullMinusFlag) {
data.discountType = "fullMinusFlag";
delete data.fullMinusFlag;
} else {
data.discountType = "fullRateFlag";
delete data.fullRateFlag;
}
data.rangeTime = [new Date(data.startTime), new Date(data.endTime)];
if (data.fullRate !== null && data.fullRate !== undefined && data.fullRate !== "") {
data.fullRate = parseFloat(Number(data.fullRate).toFixed(1));
}
this.form = data;
});
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return;
const params = JSON.parse(JSON.stringify(this.form));
params.startTime = this.$filters.unixToDate(this.form.rangeTime[0] / 1000);
params.endTime = this.$filters.unixToDate(this.form.rangeTime[1] / 1000);
defineOptions({ name: "add-full-discount" });
if (params.couponFlag && !params.couponId) {
this.$Message.warning("请选择优惠券");
return;
}
if (params.giftFlag && !params.giftId) {
this.$Message.warning("请选择赠品");
return;
}
if (params.pointFlag && !params.point) {
this.$Message.warning("请填写积分");
return;
}
const route = useRoute();
const router = useRouter();
if (
params.scopeType === "PORTION_GOODS" &&
(!params.promotionGoodsList || params.promotionGoodsList.length === 0)
) {
this.$Message.warning("请选择指定商品");
return;
}
if (params.scopeType === "ALL") {
delete params.promotionGoodsList;
params.number = -1;
} else {
const scopeId = [];
params.number = 1;
params.promotionGoodsList.forEach((e) => {
e.startTime = params.startTime;
e.endTime = params.endTime;
scopeId.push(e.skuId);
});
params.scopeId = scopeId.toString();
}
if (params.discountType === "fullMinusFlag") {
params.fullMinusFlag = true;
} else {
params.fullRateFlag = true;
}
delete params.discountType;
delete params.rangeTime;
if (!this.id) {
delete params.id;
} else {
delete params.updateTime;
}
const checkWeight = (rule, value, callback) => {
if (!value && typeof value !== "number") {
callback(new Error("优惠门槛不能为空"));
} else if (!regular.money.test(value)) {
callback(new Error("请输入正整数或者两位小数"));
} else if (parseFloat(value) > 99999999) {
callback(new Error("优惠门槛设置超过上限值"));
} else {
callback();
}
};
const checkDiscount = (rule, value, callback) => {
if (value === null || value === undefined || value === "") {
callback(new Error("请填写优惠折扣"));
} else if (value < 0.1 || value > 9.9) {
callback(new Error("请输入0-10的数字,可有一位小数"));
} else {
callback();
}
};
this.submitLoading = true;
const request = this.id ? editFullDiscount(params) : newFullDiscount(params);
request.then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success(this.id ? "编辑活动成功" : "添加活动成功");
this.closeCurrentPage();
}
});
});
},
changeSelect(e) {
this.selectedGoods = e;
},
delSelectGoods() {
if (this.selectedGoods.length <= 0) {
this.$Message.warning("您还未选择要删除的数据");
return;
}
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除所选商品吗?",
onOk: () => {
const ids = this.selectedGoods.map((e) => e.skuId || e.id);
this.form.promotionGoodsList = this.form.promotionGoodsList.filter(
(item) => !ids.includes(item.skuId)
);
},
});
},
delGoods(index) {
this.form.promotionGoodsList.splice(index, 1);
},
selectedGoodsData(item) {
const list = [];
item.forEach((e) => {
list.push({
goodsName: e.goodsName,
price: e.price,
quantity: e.quantity,
storeId: e.storeId,
storeName: e.storeName,
thumbnail: e.thumbnail,
skuId: e.id,
goodsId: e.goodsId,
goodsType: e.goodsType,
});
});
this.form.promotionGoodsList = list;
},
getCouponList(query) {
const params = {
pageSize: 20,
pageNumber: 1,
getType: "ACTIVITY",
couponName: query,
promotionStatus: "START",
};
this.couponLoading = true;
getShopCouponList(params).then((res) => {
this.couponLoading = false;
if (res.success) {
this.couponList = res.result.records;
}
});
},
getGiftList(query) {
const params = {
pageSize: 20,
pageNumber: 1,
id: query === this.form.giftId ? this.form.giftId : null,
goodsName: query === this.form.giftId ? null : query,
marketEnable: "UPPER",
authFlag: "PASS",
};
this.giftLoading = true;
getGoodsSkuListDataSeller(params).then((res) => {
this.giftLoading = false;
if (res.success) {
this.giftList = res.result.records;
}
});
},
const form = ref({
discountType: "fullMinusFlag",
scopeType: "ALL",
promotionGoodsList: [],
promotionStatus: "NEW",
});
const id = ref(route.query.id);
const submitLoading = ref(false);
const selectedGoods = ref([]);
const couponList = ref([]);
const giftList = ref([]);
const giftLoading = ref(false);
const couponLoading = ref(false);
const formRule = {
promotionName: [{ required: true, message: "活动名称不能为空" }],
rangeTime: [{ required: true, message: "请选择活动时间" }],
description: [{ required: true, message: "请填写活动描述" }],
fullMoney: [{ required: true, validator: checkWeight }],
fullMinus: [
{ required: true, message: "请填写优惠金额" },
{ pattern: regular.money, message: "请输入正确金额" },
],
fullRate: [{ required: true, validator: checkDiscount }],
};
const options = {
disabledDate(date) {
return date && date.valueOf() < Date.now() - 86400000;
},
};
// formRef:模板 ref 改名避免与顶层变量互相覆盖
const formRef = ref(null);
const liliDialogRef = ref(null);
const fieldDisabled = computed(() => {
return !!id.value && form.value.promotionStatus !== "NEW";
});
onMounted(() => {
if (id.value) {
getDetail();
}
getCouponList();
getGiftList();
});
function giftOptionLabel(item) {
if (!item) return "";
const typeLabel =
item.goodsType === "E_COUPON" ? ` [${goodsTypeLabel(item.goodsType)}]` : "";
const stock =
item.goodsType === "E_COUPON" && item.quantity != null
? ` · 库存${item.quantity}`
: "";
return `${item.goodsName || ""}${typeLabel}${stock}`;
}
function closeCurrentPage() {
router.back();
}
function openSkuList() {
const data = JSON.parse(JSON.stringify(form.value.promotionGoodsList || []));
data.forEach((e) => {
e.id = e.skuId || e.id;
});
// 先写入已选商品,再打开选择器,保证回显时 selectedWay 已就绪
liliDialogRef.value.goodsData = data;
liliDialogRef.value.open("goods");
}
function getDetail() {
getFullDiscountById(id.value).then((res) => {
const data = res.result;
if (data.scopeType === "ALL") {
data.promotionGoodsList = [];
}
if (data.fullMinusFlag) {
data.discountType = "fullMinusFlag";
delete data.fullMinusFlag;
} else {
data.discountType = "fullRateFlag";
delete data.fullRateFlag;
}
data.rangeTime = [new Date(data.startTime), new Date(data.endTime)];
if (data.fullRate !== null && data.fullRate !== undefined && data.fullRate !== "") {
data.fullRate = parseFloat(Number(data.fullRate).toFixed(1));
}
form.value = data;
});
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (!valid) return;
const params = JSON.parse(JSON.stringify(form.value));
params.startTime = unixToDate(form.value.rangeTime[0] / 1000);
params.endTime = unixToDate(form.value.rangeTime[1] / 1000);
if (params.couponFlag && !params.couponId) {
Message.warning("请选择优惠券");
return;
}
if (params.giftFlag && !params.giftId) {
Message.warning("请选择赠品");
return;
}
if (params.pointFlag && !params.point) {
Message.warning("请填写积分");
return;
}
if (
params.scopeType === "PORTION_GOODS" &&
(!params.promotionGoodsList || params.promotionGoodsList.length === 0)
) {
Message.warning("请选择指定商品");
return;
}
if (params.scopeType === "ALL") {
delete params.promotionGoodsList;
params.number = -1;
} else {
const scopeId = [];
params.number = 1;
params.promotionGoodsList.forEach((e) => {
e.startTime = params.startTime;
e.endTime = params.endTime;
scopeId.push(e.skuId);
});
params.scopeId = scopeId.toString();
}
if (params.discountType === "fullMinusFlag") {
params.fullMinusFlag = true;
} else {
params.fullRateFlag = true;
}
delete params.discountType;
delete params.rangeTime;
if (!id.value) {
delete params.id;
} else {
delete params.updateTime;
}
submitLoading.value = true;
const request = id.value ? editFullDiscount(params) : newFullDiscount(params);
request.then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success(id.value ? "编辑活动成功" : "添加活动成功");
closeCurrentPage();
}
});
});
}
function changeSelect(e) {
selectedGoods.value = e;
}
function delSelectGoods() {
if (selectedGoods.value.length <= 0) {
Message.warning("您还未选择要删除的数据");
return;
}
Modal.confirm({
title: "确认删除",
content: "您确认要删除所选商品吗?",
onOk: () => {
const ids = selectedGoods.value.map((e) => e.skuId || e.id);
form.value.promotionGoodsList = form.value.promotionGoodsList.filter(
(item) => !ids.includes(item.skuId)
);
},
});
}
function delGoods(index) {
form.value.promotionGoodsList.splice(index, 1);
}
function selectedGoodsData(item) {
const list = [];
item.forEach((e) => {
list.push({
goodsName: e.goodsName,
price: e.price,
quantity: e.quantity,
storeId: e.storeId,
storeName: e.storeName,
thumbnail: e.thumbnail,
skuId: e.id,
goodsId: e.goodsId,
goodsType: e.goodsType,
});
});
form.value.promotionGoodsList = list;
}
function getCouponList(query) {
const params = {
pageSize: 20,
pageNumber: 1,
getType: "ACTIVITY",
couponName: query,
promotionStatus: "START",
};
couponLoading.value = true;
getShopCouponList(params).then((res) => {
couponLoading.value = false;
if (res.success) {
couponList.value = res.result.records;
}
});
}
function getGiftList(query) {
const params = {
pageSize: 20,
pageNumber: 1,
id: query === form.value.giftId ? form.value.giftId : null,
goodsName: query === form.value.giftId ? null : query,
marketEnable: "UPPER",
authFlag: "PASS",
};
giftLoading.value = true;
getGoodsSkuListDataSeller(params).then((res) => {
giftLoading.value = false;
if (res.success) {
giftList.value = res.result.records;
}
});
}
</script>
<style lang="scss" scoped>

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