merge: 合并远端 script setup + pinia 迁移,保留装修编辑器 CRMEB 化功能

This commit is contained in:
pikachu1995@126.com
2026-10-13 11:28:23 +08:00
676 changed files with 71575 additions and 70256 deletions

View File

@@ -25,6 +25,8 @@ module.exports = {
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
// 全端已完成 Options API → <script setup> 迁移,防止新代码回退
'vue/component-api-style': ['warn', ['script-setup']],
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,

View File

@@ -2,11 +2,33 @@
## 已完成(P1–P3 骨架)
- 依赖升级:`vue@3`、`vue-router@4`、`vuex@4`、`vue-i18n@9`、`element-plus`
- 入口:`src/main.js`(`createApp`)
- 依赖升级:`vue@3`、`vue-router@4`、`vue-i18n@9`、`element-plus`、`pinia@4`
- 入口:`src/main.js`(`createApp` + `createPinia`)
- 路由、动态路由、全局 `$Message` / `$Modal` / `$Notice`、`$filters`、`priceColorScheme`、`vue-qr@5`
- TinyMCE:移除已弃用 `template` 插件
## 组合式 API 迁移(2026-10 完成)
全部 280 个 `.vue` 组件已从 Options API 转换为 `<script setup>`,并完成 vuex → pinia:
- **状态管理**:`src/stores/` 下 4 个 store(app/user/setting/dict)。app store 合并了原 vuex 根 state(`openStyleStore/openStoreId/notices`)+ 原 app 模块;mutation 全部转为同名 action,两个例外:`pageOpenedList` mutation 改名 `updateOpenedList`(避免与 state 同名);`clearOtherTags` 改收路由 name 字符串(不再传组件实例 vm)。原 `src/store/` 已删除,package.json 不再依赖 vuex。
- **util.js 去 vm 化**:`src/libs/util.js` 的 `openNewPage(name, argu, query)` / `initRouter()` / `initMenuData(data)` 已去掉 vm 首参,内部直接用 `useAppStore()`。
- **defineExpose 清单**(被外部通过 ref 访问的组件,改其内部成员时注意保持暴露面):
- `components/lili-dialog/index.vue`:`open` / `singleGoods` / `clearGoodsSelected` / `goodsData` / `goodsFlag` / `flag` / `close`
- `components/lili-dialog/goods-dialog.vue`:`type`
- `components/verify/index.vue`:`show` / `init`
- `components/lili/config-drawer.vue`:`open` / `close`
- `components/lili/department-tree-choose.vue`:`setData` / `clearSelect`
- `components/map/multiple-map.vue`:`open`
- `components/editor/index.vue`(Tinymce):`getEditor` / `setContent` / `getContent`
- `sys/oss-manage/ossManage.vue`:`selectImage` / `fileType` / `changeFileType`
- `components/hotzone/index.vue`:`open` / `close`;`hotzone/components/Hotzone.vue`:`zones` / `addItem` / `eraseItem` / `isOverRange` / `overRange` / `changeItem`(被自定义指令经 `binding.instance` / `$parent` 访问);`hotzone/components/Zone.vue`:`showModalFn` / `delItem` / `handlehideZone` / `changeInfo`
- `page-decoration/modelForm.vue`:`navList` / `topAdvert`
- `promotions/coupon/coupon.vue`:`check` / `getSelection` / `clearSelectAll`
- `live/components/LivePurchaseRecords.vue`:`refresh`
- **i18n**:script 内 `this.$t` 统一改为 `i18n.global.t`(`@/locale`),模板 `$t` 不变。
- **修复的编译级问题**:`page-decoration/wap/index.vue` 的 `function package()` 改名 `packageModel`(保留字)。
## P5 批量修复(Table / 过滤器)
列表页 **60+** 已 `el-table`;过滤器已改为 `$filters.*`;全项目已无 `$set` / `$options.filters`。

View File

@@ -24,6 +24,7 @@
"hls.js": "^1.6.16",
"js-cookie": "^2.2.1",
"nprogress": "^0.2.0",
"pinia": "^4.0.3",
"sass": "^1.63.6",
"sass-loader": "^10.4.1",
"sockjs-client": "^1.4.0",
@@ -34,7 +35,6 @@
"vue-qr": "^5.0.3",
"vue-router": "^4.6.4",
"vuedraggable": "^4.1.0",
"vuex": "^4.1.0",
"xss": "^1.0.7"
},
"devDependencies": {

View File

@@ -4,32 +4,39 @@
</div>
</template>
<script>
<script setup>
import { onMounted, onUpdated } from "vue";
import { useRoute } from "vue-router";
import Cookies from "js-cookie";
import { getCategoryTree } from "@/api/goods.js";
import util from "@/libs/util";
import { getStore } from "@/libs/storage";
import { useAppStore } from "@/stores/app";
export default {
name: "App",
updated() {
if (!localStorage.getItem("category") && this.$route.path !== "/login") {
getCategoryTree(0).then((res) => {
if (res.success) {
localStorage.setItem("category", JSON.stringify(res.result));
}
});
}
},
mounted() {
const loggedIn = this.getStore("accessToken") || Cookies.get("userInfoManager");
if (loggedIn) {
util.bootstrapDynamicRoutesFromCache();
util.initRouter(this);
this.$store.commit("setOpenedList");
this.$store.commit("initCachePage");
}
},
};
defineOptions({ name: "App" });
const route = useRoute();
const appStore = useAppStore();
onUpdated(() => {
if (!localStorage.getItem("category") && route.path !== "/login") {
getCategoryTree(0).then((res) => {
if (res.success) {
localStorage.setItem("category", JSON.stringify(res.result));
}
});
}
});
onMounted(() => {
const loggedIn = getStore("accessToken") || Cookies.get("userInfoManager");
if (loggedIn) {
util.bootstrapDynamicRoutesFromCache();
util.initRouter();
appStore.setOpenedList();
appStore.initCachePage();
}
});
</script>
<style>

View File

@@ -46,120 +46,117 @@
</div>
</template>
<script>
<script setup>
import { onMounted, 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: [],
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 = ref({
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([]);
onMounted(() => {
getFiveYears();
getShopList();
});
function getShopList() {
getShopListData(params.value).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 list = [];
for (let year = lastFiveYear; year <= getYear; year++) {
for (let month = 1; month <= 12; month++) {
if (year === getYear && month > maxMonth) {
continue;
}
list.push({ year, month });
}
}
dates.value = list.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();
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);
}
</script>
<style lang="scss" scoped>

View File

@@ -63,120 +63,120 @@
</el-dialog>
</template>
<script>
<script setup>
import { ref, watch } from "vue";
import { getDistributionMemberPage } from "@/api/distribution";
import { Message } from "@/utils/message";
export default {
name: "distributionSelector",
props: {
modelValue: {
type: Boolean,
default: false,
},
excludeId: {
type: String,
default: "",
},
defineOptions({ name: "distributionSelector" });
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
emits: ["update:modelValue", "confirm"],
data() {
return {
loading: false,
searchForm: {
pageNumber: 1,
pageSize: 10,
mobile: "",
nickname: "",
},
data: [],
total: 0,
selectedId: "",
selectedRow: null,
};
excludeId: {
type: String,
default: "",
},
watch: {
modelValue(val) {
if (val) {
this.resetState();
this.getDataList();
});
const emit = defineEmits(["update:modelValue", "confirm"]);
const loading = ref(false);
// searchForm 在 resetState 中被整体替换,用 ref 而非 reactive
const searchForm = ref({
pageNumber: 1,
pageSize: 10,
mobile: "",
nickname: "",
});
const data = ref([]);
const total = ref(0);
const selectedId = ref("");
const selectedRow = ref(null);
watch(
() => props.modelValue,
(val) => {
if (val) {
resetState();
getDataList();
}
}
);
function resetState() {
searchForm.value = {
pageNumber: 1,
pageSize: 10,
mobile: "",
nickname: "",
};
selectedId.value = "";
selectedRow.value = null;
}
function buildParams() {
const params = {
pageNumber: searchForm.value.pageNumber,
pageSize: searchForm.value.pageSize,
distributionStatus: "PASS",
};
if (searchForm.value.mobile) params.mobile = searchForm.value.mobile;
if (searchForm.value.nickname) params.nickname = searchForm.value.nickname;
return params;
}
function getDataList() {
loading.value = true;
getDistributionMemberPage(buildParams())
.then((res) => {
loading.value = false;
if (!res.success) return;
const result = res.result || {};
let records = result.records || [];
const beforeCount = records.length;
if (props.excludeId) {
records = records.filter((item) => item.id !== props.excludeId);
}
},
},
methods: {
resetState() {
this.searchForm = {
pageNumber: 1,
pageSize: 10,
mobile: "",
nickname: "",
};
this.selectedId = "";
this.selectedRow = null;
},
buildParams() {
const params = {
pageNumber: this.searchForm.pageNumber,
pageSize: this.searchForm.pageSize,
distributionStatus: "PASS",
};
if (this.searchForm.mobile) params.mobile = this.searchForm.mobile;
if (this.searchForm.nickname) params.nickname = this.searchForm.nickname;
return params;
},
getDataList() {
this.loading = true;
getDistributionMemberPage(this.buildParams())
.then((res) => {
this.loading = false;
if (!res.success) return;
const result = res.result || {};
let records = result.records || [];
const beforeCount = records.length;
if (this.excludeId) {
records = records.filter((item) => item.id !== this.excludeId);
}
this.data = records;
const excludedCount = beforeCount - records.length;
this.total = Math.max(0, (result.total || 0) - excludedCount);
})
.catch(() => {
this.loading = false;
});
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleRowClick(row) {
if (!row) return;
this.selectedRow = row;
this.selectedId = row.id;
},
handleCurrentChange(row) {
if (!row) return;
this.selectedRow = row;
this.selectedId = row.id;
},
handleRadioChange(row) {
this.selectedRow = row;
},
handleClose() {
this.$emit("update:modelValue", false);
},
handleConfirm() {
if (!this.selectedRow) {
this.$Message.warning("请选择分销员");
return;
}
this.$emit("confirm", this.selectedRow);
this.handleClose();
},
},
};
data.value = records;
const excludedCount = beforeCount - records.length;
total.value = Math.max(0, (result.total || 0) - excludedCount);
})
.catch(() => {
loading.value = false;
});
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getDataList();
}
function changePageSize() {
searchForm.value.pageNumber = 1;
getDataList();
}
function handleRowClick(row) {
if (!row) return;
selectedRow.value = row;
selectedId.value = row.id;
}
function handleCurrentChange(row) {
if (!row) return;
selectedRow.value = row;
selectedId.value = row.id;
}
function handleRadioChange(row) {
selectedRow.value = row;
}
function handleClose() {
emit("update:modelValue", false);
}
function handleConfirm() {
if (!selectedRow.value) {
Message.warning("请选择分销员");
return;
}
emit("confirm", selectedRow.value);
handleClose();
}
</script>
<style scoped lang="scss">

View File

@@ -8,132 +8,142 @@
</div>
</div>
</template>
<script>
<script setup>
import {
computed,
nextTick,
onActivated,
onDeactivated,
onMounted,
onUnmounted,
ref,
watch,
} from "vue";
import { initEditor } from "@/components/editor/config";
import uploadImage from "@/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'
}
},
computed: {
editorContent() {
return this.modelValue ?? this.value ?? "";
},
},
data() {
return {
// 引入编辑器的配置
initEditor,
hasChange: false, // 标记内容是否有更改
hasInit: false, // 标记编辑器是否已初始化
tinymceId:
"tinymce-" + +new Date() + ((Math.random() * 1000).toFixed(0) + ""), // 生成唯一的编辑器 ID
fullscreen: false, // 标记编辑器是否处于全屏模式
toolbar: [], // 工具栏配置
content: "", // 编辑器内容
};
},
created() {
this.init();
},
watch: {
editorContent: {
handler(val) {
if (!this.hasChange && this.hasInit) {
// 当内容有更改且编辑器已初始化时,更新编辑器的内容
this.$nextTick(() => {
const editor = window.tinymce?.get(this.tinymceId);
if (editor) editor.setContent(val || "");
});
}
},
},
},
methods: {
emitContent(content) {
this.$emit("update:modelValue", content);
this.$emit("input", content);
},
// 数据返回并给富文本框插入图片
insertImage(arr){
arr.forEach(v => window.tinymce.get(this.tinymceId).insertContent(`<img src="${v}" >`))
},
init() {
// 初始化编辑器
this.initTinymce();
},
initTinymce() {
const _this = this;
window.tinymce.init({
selector: `#${this.tinymceId}`,
convert_urls: false,
init_instance_callback: (editor) => {
if (_this.editorContent) {
// 如果有初始值,则设置编辑器的内容为初始值
_this.$nextTick(() => editor.setContent(_this.editorContent));
}
_this.hasInit = true;
// 监听编辑器内容的变化
editor.on("NodeChange Change KeyUp SetContent", () => {
_this.hasChange = true;
// Vue3 v-model 使用 update:modelValue,同时保留 input 兼容旧用法
_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 tinymceId =
"tinymce-" + +new Date() + ((Math.random() * 1000).toFixed(0) + ""); // 生成唯一的编辑器 ID
const fullscreen = ref(false); // 标记编辑器是否处于全屏模式
const toolbar = ref([]); // 工具栏配置
const content = ref(""); // 编辑器内容
const editorContent = computed(() => {
return props.modelValue ?? props.value ?? "";
});
watch(editorContent, (val) => {
if (!hasChange.value && hasInit.value) {
// 当内容有更改且编辑器已初始化时,更新编辑器的内容
nextTick(() => {
const editor = window.tinymce?.get(tinymceId);
if (editor) editor.setContent(val || "");
});
}
});
function emitContent(content) {
emit("update:modelValue", content);
emit("input", content);
}
// 数据返回并给富文本框插入图片
function insertImage(arr) {
arr.forEach((v) =>
window.tinymce.get(tinymceId).insertContent(`<img src="${v}" >`)
);
}
function init() {
// 初始化编辑器
initTinymce();
}
function initTinymce() {
window.tinymce.init({
selector: `#${tinymceId}`,
convert_urls: false,
init_instance_callback: (editor) => {
if (editorContent.value) {
// 如果有初始值,则设置编辑器的内容为初始值
nextTick(() => editor.setContent(editorContent.value));
}
hasInit.value = true;
// 监听编辑器内容的变化
editor.on("NodeChange Change KeyUp SetContent", () => {
hasChange.value = true;
// Vue3 v-model 使用 update:modelValue,同时保留 input 兼容旧用法
emitContent(editor.getContent());
});
},
setContent(value) {
// 设置编辑器的内容
window.tinymce.get(this.tinymceId).setContent(value);
setup(editor) {
// 监听全屏状态变化
editor.on("FullscreenStateChanged", (e) => {
fullscreen.value = e.state;
});
},
getContent() {
// 获取编辑器的内容
return window.tinymce.get(this.tinymceId).getContent();
},
destroyTinymce() {
const tinymce = window.tinymce.get(this.tinymceId);
...initEditor,
if (tinymce) {
// 销毁编辑器实例
tinymce.destroy();
}
},
},
mounted() {
this.init();
},
activated() {
if (window.tinymce) {
this.initTinymce();
}
},
deactivated() {
this.destroyTinymce();
},
unmounted() {
this.destroyTinymce();
},
};
height: props.height,
});
}
function getEditor() {
// 获取 tinymce 实例(原组件无此方法,为对齐 seller 版暴露能力而新增)
return window.tinymce?.get(tinymceId);
}
function setContent(value) {
// 设置编辑器的内容
window.tinymce.get(tinymceId).setContent(value);
}
function getContent() {
// 获取编辑器的内容
return window.tinymce.get(tinymceId).getContent();
}
function destroyTinymce() {
const tinymce = window.tinymce.get(tinymceId);
if (tinymce) {
// 销毁编辑器实例
tinymce.destroy();
}
}
// created 钩子:顶层直接执行(保留原 created + mounted 双重 init 的历史行为)
init();
onMounted(() => {
init();
});
onActivated(() => {
if (window.tinymce) {
initTinymce();
}
});
onDeactivated(() => {
destroyTinymce();
});
onUnmounted(() => {
destroyTinymce();
});
// 父组件通过 ref 调用:articleList 使用 getContent;setContent/getEditor 对齐 seller 版暴露能力
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.vue";
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

@@ -10,7 +10,7 @@
:key="index"
:index="index"
:setting="zone"
:ref="`zone${index}`"
:ref="(el) => setZoneRef(el, index)"
@delItem="removeItem($event)"
@changeInfo="changeInfo($event)"
></zone>
@@ -62,160 +62,188 @@
</div>
</template>
<script>
<script setup>
import { ref, onMounted, getCurrentInstance } from "vue";
import Zone from "./Zone";
import addItem from "../directives/addItem";
import vAddItem from "../directives/addItem";
export default {
name: "HotZone",
data() {
return {
zones: [],
};
defineOptions({ name: "HotZone" });
const props = defineProps({
image: {
type: String,
required: true,
},
props: {
image: {
type: String,
required: true,
},
zonesInit: {
type: Array,
default: () => [],
},
max: {
type: Number,
},
zonesInit: {
type: Array,
default: () => [],
},
mounted() {
this.zones = this.zonesInit.concat();
max: {
type: Number,
},
methods: {
async addHotzone() {
let perInfo = {
topPer: 0.15,
leftPer: 0.3,
widthPer: 0.2,
heightPer: 0.2,
img: "",
link: "",
type: "",
title: "",
};
let images = await this.getImageSize(this.image);
if (images) {
if (images.height >= 1000) {
perInfo.heightPer = this.convertNumberToDecimal(images.height) / (images.height / 1000);
} else {
perInfo.heightPer = this.convertNumberToDecimal(images.height);
}
perInfo.widthPer = this.convertNumberToDecimal(images.width) / 2;
}
this.addItem(perInfo);
},
convertNumberToDecimal(num) {
if (num >= 10000) {
return num / 100000;
} else if (num >= 1000) {
return num / 10000;
} else if (num >= 100) {
return num / 1000;
} else if (num >= 10) {
return num / 100;
}
},
getImageSize(url) {
return new Promise(function (resolve, reject) {
let image = new Image();
image.onload = function () {
resolve({
width: image.width,
height: image.height,
});
};
image.onerror = function () {
reject(new Error("error"));
};
image.src = url;
});
const emit = defineEmits([
"choose",
"erase",
"overRange",
"remove",
"add",
"change",
]);
const zones = ref([]);
const zoneRefs = ref([]);
const instance = getCurrentInstance();
onMounted(() => {
zones.value = props.zonesInit.concat();
});
// v-for 内的动态 ref:按 index 收集 Zone 组件实例
function setZoneRef(el, index) {
if (el) zoneRefs.value[index] = el;
}
async function addHotzone() {
let perInfo = {
topPer: 0.15,
leftPer: 0.3,
widthPer: 0.2,
heightPer: 0.2,
img: "",
link: "",
type: "",
title: "",
};
let images = await getImageSize(props.image);
if (images) {
if (images.height >= 1000) {
perInfo.heightPer = convertNumberToDecimal(images.height) / (images.height / 1000);
} else {
perInfo.heightPer = convertNumberToDecimal(images.height);
}
perInfo.widthPer = convertNumberToDecimal(images.width) / 2;
}
addItem(perInfo);
}
function convertNumberToDecimal(num) {
if (num >= 10000) {
return num / 100000;
} else if (num >= 1000) {
return num / 10000;
} else if (num >= 100) {
return num / 1000;
} else if (num >= 10) {
return num / 100;
}
}
function getImageSize(url) {
return new Promise(function (resolve, reject) {
let image = new Image();
image.onload = function () {
resolve({
width: image.width,
height: image.height,
});
},
editZone(index) {
this.$refs[`zone${index}`][0].showModalFn(index);
},
delZone(index) {
this.$refs[`zone${index}`][0].delItem(index);
},
showZoneText(zone) {
switch (zone.type) {
case "goods":
return `商品:${zone.goodsName}`;
case "category":
return `分类:${zone.name}`;
case "shops":
return `店铺:${zone.storeName}`;
case "pages":
return `文章:${zone.title}`;
case "marketing":
return `促销活动商品:${zone.goodsName}`;
case "other":
return `${zone.title}`;
case "special":
return `专题:${zone.title}`;
default:
return "请选择跳转链接";
}
},
changeInfo(res) {
let { info, index, zoneInfo } = res;
info = { ...zoneInfo, ...info };
// 改变热区并发送change通知
Object.assign(this.zones[index], info);
this.hasChange("changeInfo");
this.$forceUpdate();
},
addItem(setting) {
this.zones.push(setting);
this.$emit("choose");
// this.hasChange() 不应该发送通知,mouseup判定成功才应该发
// this.$emit('add', setting)
},
eraseItem(index = this.zones.length - 1) {
this.zones.splice(index, 1);
this.$emit("erase", index);
},
isOverRange() {
let { max, zones } = this;
};
image.onerror = function () {
reject(new Error("error"));
};
image.src = url;
});
}
return max && zones.length > max;
},
overRange() {
const index = this.zones.length - 1;
function editZone(index) {
zoneRefs.value[index].showModalFn(index);
}
this.zones.splice(index, 1);
this.$emit("overRange", index);
},
removeItem(index = this.zones.length - 1) {
this.zones.splice(index, 1);
this.hasChange("removeItem");
this.$emit("remove", index);
},
changeItem(info, isAdd) {
const index = this.zones.length - 1;
// 改变热区并发送change通知
Object.assign(this.zones[index], info);
this.hasChange("changeItem");
isAdd && this.$emit("add", this.zones[index]);
},
hasChange(from) {
this.$emit("change", this.zones);
},
},
directives: {
addItem,
},
components: {
Zone,
},
};
function delZone(index) {
zoneRefs.value[index].delItem(index);
}
function showZoneText(zone) {
switch (zone.type) {
case "goods":
return `商品:${zone.goodsName}`;
case "category":
return `分类:${zone.name}`;
case "shops":
return `店铺:${zone.storeName}`;
case "pages":
return `文章:${zone.title}`;
case "marketing":
return `促销活动商品:${zone.goodsName}`;
case "other":
return `${zone.title}`;
case "special":
return `专题:${zone.title}`;
default:
return "请选择跳转链接";
}
}
function changeInfo(res) {
let { info, index, zoneInfo } = res;
info = { ...zoneInfo, ...info };
// 改变热区并发送change通知
Object.assign(zones.value[index], info);
hasChange("changeInfo");
instance.proxy.$forceUpdate();
}
function addItem(setting) {
zones.value.push(setting);
emit("choose");
// this.hasChange() 不应该发送通知,mouseup判定成功才应该发
// this.$emit('add', setting)
}
function eraseItem(index = zones.value.length - 1) {
zones.value.splice(index, 1);
emit("erase", index);
}
function isOverRange() {
return props.max && zones.value.length > props.max;
}
function overRange() {
const index = zones.value.length - 1;
zones.value.splice(index, 1);
emit("overRange", index);
}
function removeItem(index = zones.value.length - 1) {
zones.value.splice(index, 1);
hasChange("removeItem");
emit("remove", index);
}
function changeItem(info, isAdd) {
const index = zones.value.length - 1;
// 改变热区并发送change通知
Object.assign(zones.value[index], info);
hasChange("changeItem");
isAdd && emit("add", zones.value[index]);
}
function hasChange(from) {
emit("change", zones.value);
}
// v-add-item 指令通过 binding.instance 调用以下方法/读取 zones;
// Zone 的 v-drag-item / v-change-size 指令通过 ctx.$parent.zones 读取 zones
defineExpose({
zones,
addItem,
eraseItem,
isOverRange,
overRange,
changeItem,
});
</script>
<style scoped lang="scss">

View File

@@ -86,171 +86,177 @@
@callback="callbackSelected"
:isComponent="true"
:initialize="picModelFlag"
ref="ossManage"
ref="ossManageRef"
/>
<template #footer />
</el-dialog>
</li>
</template>
<script>
import changeSize from "../directives/changeSize";
import dragItem from "../directives/dragItem";
<script setup>
import { ref, reactive, watch, nextTick, onMounted } from "vue";
import vChangeSize from "../directives/changeSize";
import vDragItem from "../directives/dragItem";
import ossManage from "@/views/sys/oss-manage/ossManage";
import { formatLinkType } from "@/utils/filters";
export default {
name: "Zone",
components: {
ossManage,
},
defineOptions({ name: "Zone" });
data() {
return {
zoneTop: "",
zoneLeft: "",
zoneWidth: "",
zoneHeight: "",
hideZone: false,
tooSmall: false,
showModal: false,
picModelFlag: false,
currentIndex: 0,
currentShowIndex: -1,
zoneForm: {
img: "",
link: "",
type: "",
},
};
},
props: ["index", "setting"],
mounted() {
this.setZoneInfo(this.setting);
},
methods: {
setZoneInfo(val) {
this.zoneTop = this.getZoneStyle(val.topPer);
this.zoneLeft = this.getZoneStyle(val.leftPer);
this.zoneWidth = this.getZoneStyle(val.widthPer);
this.zoneHeight = this.getZoneStyle(val.heightPer);
this.tooSmall = val.widthPer < 0.01 && val.heightPer < 0.01;
this.zoneForm.link = val.link;
this.settingZone(val);
},
handlehideZone(isHide = true) {
if (this.hideZone === isHide) {
return;
}
const props = defineProps(["index", "setting"]);
const emit = defineEmits(["changeInfo", "delItem"]);
this.hideZone = isHide;
},
changeInfo(info = {}) {
const { index } = this;
const zoneTop = ref("");
const zoneLeft = ref("");
const zoneWidth = ref("");
const zoneHeight = ref("");
const hideZone = ref(false);
const tooSmall = ref(false);
const showModal = ref(false);
const picModelFlag = ref(false);
const currentIndex = ref(0);
const currentShowIndex = ref(-1);
const zoneForm = reactive({
img: "",
link: "",
type: "",
});
this.$emit("changeInfo", {
info,
index,
zoneInfo: this.zoneForm,
});
},
showModalFn(index) {
this.showModal = true;
this.currentIndex = index;
},
// 选择图片
handleSelectImg() {
this.picModelFlag = true;
this.$nextTick(() => {
if (this.$refs.ossManage) {
this.$refs.ossManage.selectImage = true;
}
});
},
// 选择图片回调
callbackSelected(item) {
this.picModelFlag = false;
this.zoneForm.img = item.url;
},
// 未在原 data 中声明、直接挂载在 this 上的遗留字段(仅赋值,未被读取)
let selectedNav;
// 调起选择链接弹窗
handleSelectLink(item, index) {
if (item) this.selectedNav = item;
this.$refs.liliDialog.open("link");
},
// 已选链接
selectedLink(val) {
this.zoneForm.link = this.$filters.formatLinkType(val);
this.settingZone(val);
this.changeInfo(this.zoneForm);
},
settingZone(val) {
this.zoneForm.type = val.___type || val.type;
this.zoneForm.title = val.title;
if(val.pageType == "special"){
this.zoneForm.id = val.id;
this.zoneForm.pageType = val.pageType
this.zoneForm.type = val.pageType
this.zoneForm.title = val.name
}
switch (val.___type) {
case "goods":
this.zoneForm.id = val.id;
this.zoneForm.goodsId = val.goodsId;
this.zoneForm.goodsName = val.goodsName;
break;
case "category":
this.zoneForm.id = val.allId;
this.zoneForm.name = val.name;
break;
case "shops":
this.zoneForm.id = val.id;
this.zoneForm.storeName = val.storeName;
break;
case "pages":
this.zoneForm.id = val.id;
this.zoneForm.___path = val.___path;
this.zoneForm.title = val.title;
break;
case "marketing":
this.zoneForm.id = val.id;
this.zoneForm.goodsId = val.goodsId;
this.zoneForm.goodsName = val.goodsName;
break;
default:
break;
}
},
saveZone() {
this.showModal = false;
this.changeInfo(this.zoneForm);
},
cancelZone() {
this.showModal = false;
},
delZone() {
this.delItem(this.currentIndex);
},
delItem(index) {
this.$emit("delItem", index);
},
getZoneStyle(val) {
return `${(val || 0) * 100}%`;
},
},
const liliDialog = ref(null);
// 模板 ref 与 import 组件同名会互相覆盖,故改名 ossManageRef
const ossManageRef = ref(null);
watch: {
setting: {
handler: function (val) {
this.setZoneInfo(val);
},
deep: true,
},
onMounted(() => {
setZoneInfo(props.setting);
});
function setZoneInfo(val) {
zoneTop.value = getZoneStyle(val.topPer);
zoneLeft.value = getZoneStyle(val.leftPer);
zoneWidth.value = getZoneStyle(val.widthPer);
zoneHeight.value = getZoneStyle(val.heightPer);
tooSmall.value = val.widthPer < 0.01 && val.heightPer < 0.01;
zoneForm.link = val.link;
settingZone(val);
}
function handlehideZone(isHide = true) {
if (hideZone.value === isHide) {
return;
}
hideZone.value = isHide;
}
function changeInfo(info = {}) {
const index = props.index;
emit("changeInfo", {
info,
index,
zoneInfo: zoneForm,
});
}
function showModalFn(index) {
showModal.value = true;
currentIndex.value = index;
}
// 选择图片
function handleSelectImg() {
picModelFlag.value = true;
nextTick(() => {
if (ossManageRef.value) {
ossManageRef.value.selectImage = true;
}
});
}
// 选择图片回调
function callbackSelected(item) {
picModelFlag.value = false;
zoneForm.img = item.url;
}
// 调起选择链接弹窗
function handleSelectLink(item, index) {
if (item) selectedNav = item;
liliDialog.value.open("link");
}
// 已选链接
function selectedLink(val) {
zoneForm.link = formatLinkType(val);
settingZone(val);
changeInfo(zoneForm);
}
function settingZone(val) {
zoneForm.type = val.___type || val.type;
zoneForm.title = val.title;
if(val.pageType == "special"){
zoneForm.id = val.id;
zoneForm.pageType = val.pageType
zoneForm.type = val.pageType
zoneForm.title = val.name
}
switch (val.___type) {
case "goods":
zoneForm.id = val.id;
zoneForm.goodsId = val.goodsId;
zoneForm.goodsName = val.goodsName;
break;
case "category":
zoneForm.id = val.allId;
zoneForm.name = val.name;
break;
case "shops":
zoneForm.id = val.id;
zoneForm.storeName = val.storeName;
break;
case "pages":
zoneForm.id = val.id;
zoneForm.___path = val.___path;
zoneForm.title = val.title;
break;
case "marketing":
zoneForm.id = val.id;
zoneForm.goodsId = val.goodsId;
zoneForm.goodsName = val.goodsName;
break;
default:
break;
}
}
function saveZone() {
showModal.value = false;
changeInfo(zoneForm);
}
function cancelZone() {
showModal.value = false;
}
function delZone() {
delItem(currentIndex.value);
}
function delItem(index) {
emit("delItem", index);
}
function getZoneStyle(val) {
return `${(val || 0) * 100}%`;
}
watch(
() => props.setting,
(val) => {
setZoneInfo(val);
},
directives: {
changeSize,
dragItem,
},
};
{ deep: true }
);
// 外部访问:父组件 Hotzone.vue 通过 $refs 调用 showModalFn/delItem;
// 自定义指令 changeSize/dragItem 通过 binding.instance 调用 handlehideZone/changeInfo
defineExpose({
showModalFn,
delItem,
handlehideZone,
changeInfo,
});
</script>
<style scoped lang="scss">

View File

@@ -10,7 +10,7 @@
>
<template v-if="flag && hotzoneRes">
<hotzone
ref="hotzone"
ref="hotzoneRef"
@change="changeHotzone"
:zonesInit="hotzoneRes.zoneInfo"
:image="hotzoneRes.img"
@@ -22,47 +22,50 @@
</template>
</el-dialog>
</template>
<script>
<script setup>
import { ref } from "vue";
import hotzone from "./components/Hotzone.vue";
export default {
components: {
hotzone,
},
data() {
return {
flag: false,
hotzoneRes: null,
};
},
methods: {
changeHotzone(info) {
if (this.hotzoneRes) {
this.hotzoneRes.zoneInfo = info;
}
this.$emit("changeZone", info);
},
clickClose() {
this.flag = false;
this.hotzoneRes = null;
this.$emit("closeFlag", false);
},
clickOK() {
this.clickClose();
},
open(val) {
this.hotzoneRes = val || null;
if (this.hotzoneRes && !this.hotzoneRes.zoneInfo) {
this.hotzoneRes.zoneInfo = [];
}
this.flag = true;
},
close() {
this.flag = false;
this.hotzoneRes = null;
},
},
};
const emit = defineEmits(["changeZone", "closeFlag"]);
const flag = ref(false);
const hotzoneRes = ref(null);
// hotzoneRef:模板 ref 与组件 import 同名,改名避免互相覆盖
const hotzoneRef = ref(null);
function changeHotzone(info) {
if (hotzoneRes.value) {
hotzoneRes.value.zoneInfo = info;
}
emit("changeZone", info);
}
function clickClose() {
flag.value = false;
hotzoneRes.value = null;
emit("closeFlag", false);
}
function clickOK() {
clickClose();
}
function open(val) {
hotzoneRes.value = val || null;
if (hotzoneRes.value && !hotzoneRes.value.zoneInfo) {
hotzoneRes.value.zoneInfo = [];
}
flag.value = true;
}
function close() {
flag.value = false;
hotzoneRes.value = null;
}
// decorate.vue 通过 hotzoneRef.value.open(val) 调用
defineExpose({ open, close });
</script>
<style scoped lang="scss">
:deep(.el-dialog) {

View File

@@ -84,165 +84,165 @@
</div>
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import * as API_Goods from "@/api/goods";
import { goodsTypeLabel, goodsTypeTagType } from "@/constants/goodsType";
export default {
props: {
selectedWay: {
type: Array,
default: () => [],
},
},
data() {
return {
type: "multiple",
selectedList: [],
skuList: [],
total: 0,
goodsParams: {
pageNumber: 1,
pageSize: 15,
order: "desc",
goodsName: "",
sn: "",
categoryPath: "",
marketEnable: "UPPER",
authFlag: "PASS",
sort: "createTime",
},
category: [],
goodsData: [],
empty: false,
loading: false,
};
},
watch: {
category(val) {
this.goodsParams.categoryPath = val && val.length ? val[2] : "";
},
selectedWay: {
handler(val) {
this.selectedList = Array.isArray(val) ? val.slice() : [];
},
deep: true,
immediate: true,
},
"goodsParams.categoryPath": {
handler() {
this.goodsData = [];
this.goodsParams.pageNumber = 1;
this.getQueryGoodsList();
},
deep: true,
},
},
mounted() {
this.init();
},
methods: {
goodsTypeLabel,
goodsTypeTagType,
onSearchGoods() {
this.goodsData = [];
this.goodsParams.pageNumber = 1;
this.getQueryGoodsList();
},
changePageSize(v) {
this.goodsParams.pageNumber = v;
this.getQueryGoodsList();
},
getQueryGoodsList() {
this.loading = true;
API_Goods.getGoodsSkuData(this.goodsParams)
.then((res) => {
this.initGoods(res);
})
.finally(() => {
this.loading = false;
});
},
initGoods(res) {
const records = res?.result?.records || [];
if (records.length) {
records.forEach((item) => {
item.selected = false;
item.___type = "goods";
this.selectedList.forEach((e) => {
if (e.id && e.id === item.id) {
item.selected = true;
}
});
});
this.total = res.result.total || 0;
this.goodsData = records;
this.empty = false;
} else {
this.goodsData = [];
this.empty = true;
}
},
emitSelected(list) {
this.selectedList = list;
this.$emit("selected", this.selectedList);
},
init() {
API_Goods.getGoodsSkuData(this.goodsParams).then((res) => {
this.initGoods(res);
});
API_Goods.getCategoryTree({ deleteFlag: false }).then((res) => {
if (res.success) {
this.deepGroup(res.result);
}
});
},
deepGroup(val) {
val.forEach((item) => {
let childWay = [];
if (item.children) {
item.children.forEach((child) => {
if (child.children) {
child.children.forEach((grandson, index, arr) => {
arr[index] = {
value: grandson.id,
label: grandson.name,
};
});
}
childWay.push({
value: child.id,
label: child.name,
children: child.children,
});
});
}
this.skuList.push({
value: item.id,
label: item.name,
children: childWay,
});
});
},
checkedGoods(val) {
if (this.type !== "multiple") {
this.goodsData.forEach((item) => {
item.selected = false;
});
val.selected = true;
this.emitSelected([val]);
return;
}
if (!val.selected) {
val.selected = true;
this.emitSelected([...this.selectedList, val]);
} else {
val.selected = false;
this.emitSelected(this.selectedList.filter((item) => item.id !== val.id));
}
},
const props = defineProps({
selectedWay: {
type: Array,
default: () => [],
},
};
});
const emit = defineEmits(["selected"]);
const type = ref("multiple");
const selectedList = ref([]);
const skuList = ref([]);
const total = ref(0);
const goodsParams = reactive({
pageNumber: 1,
pageSize: 15,
order: "desc",
goodsName: "",
sn: "",
categoryPath: "",
marketEnable: "UPPER",
authFlag: "PASS",
sort: "createTime",
});
const category = ref([]);
const goodsData = ref([]);
const empty = ref(false);
const loading = ref(false);
watch(category, (val) => {
goodsParams.categoryPath = val && val.length ? val[2] : "";
});
watch(
() => props.selectedWay,
(val) => {
selectedList.value = Array.isArray(val) ? val.slice() : [];
},
{ deep: true, immediate: true }
);
watch(
() => goodsParams.categoryPath,
() => {
goodsData.value = [];
goodsParams.pageNumber = 1;
getQueryGoodsList();
},
{ deep: true }
);
onMounted(() => {
init();
});
function onSearchGoods() {
goodsData.value = [];
goodsParams.pageNumber = 1;
getQueryGoodsList();
}
function changePageSize(v) {
goodsParams.pageNumber = v;
getQueryGoodsList();
}
function getQueryGoodsList() {
loading.value = true;
API_Goods.getGoodsSkuData(goodsParams)
.then((res) => {
initGoods(res);
})
.finally(() => {
loading.value = false;
});
}
function initGoods(res) {
const records = res?.result?.records || [];
if (records.length) {
records.forEach((item) => {
item.selected = false;
item.___type = "goods";
selectedList.value.forEach((e) => {
if (e.id && e.id === item.id) {
item.selected = true;
}
});
});
total.value = res.result.total || 0;
goodsData.value = records;
empty.value = false;
} else {
goodsData.value = [];
empty.value = true;
}
}
function emitSelected(list) {
selectedList.value = list;
emit("selected", selectedList.value);
}
function init() {
API_Goods.getGoodsSkuData(goodsParams).then((res) => {
initGoods(res);
});
API_Goods.getCategoryTree({ deleteFlag: false }).then((res) => {
if (res.success) {
deepGroup(res.result);
}
});
}
function deepGroup(val) {
val.forEach((item) => {
let childWay = [];
if (item.children) {
item.children.forEach((child) => {
if (child.children) {
child.children.forEach((grandson, index, arr) => {
arr[index] = {
value: grandson.id,
label: grandson.name,
};
});
}
childWay.push({
value: child.id,
label: child.name,
children: child.children,
});
});
}
skuList.value.push({
value: item.id,
label: item.name,
children: childWay,
});
});
}
function checkedGoods(val) {
if (type.value !== "multiple") {
goodsData.value.forEach((item) => {
item.selected = false;
});
val.selected = true;
emitSelected([val]);
return;
}
if (!val.selected) {
val.selected = true;
emitSelected([...selectedList.value, val]);
} else {
val.selected = false;
emitSelected(selectedList.value.filter((item) => item.id !== val.id));
}
}
// 父组件 lili-dialog/index.vue 通过 $refs.goodsDialog.type = "single" 切换单选;
// expose 的 ref 外部读写等价于原 data 字段
defineExpose({ type });
</script>
<style scoped lang="scss">
@import "./style.scss";

View File

@@ -17,7 +17,7 @@
}
"
v-if="goodsFlag"
ref="goodsDialog"
ref="goodsDialogRef"
:selectedWay="goodsData"
/>
<linkDialog
@@ -36,68 +36,82 @@
</template>
</el-dialog>
</template>
<script>
<script setup>
import { ref } from "vue";
import goodsDialog from "./goods-dialog";
import linkDialog from "./link-dialog";
export default {
components: {
goodsDialog,
linkDialog,
},
data() {
return {
goodsFlag: false, // 是否商品选择器
goodsData: [], //选择的商品
linkData: "", //选择的链接
flag: false, // modal显隐
};
},
methods: {
clearGoodsSelected(){
this.goodsData = []
},
// 关闭弹窗
clickClose() {
this.flag = false;
this.goodsFlag = false;
this.$emit("closeFlag", false);
},
// 单选商品
singleGoods() {
var timer = setInterval(() => {
if (this.$refs.goodsDialog) {
this.$refs.goodsDialog.type = "single";
clearInterval(timer);
}
}, 100);
},
// 点击确认
clickOK() {
if (this.goodsFlag) {
this.$emit("selectedGoodsData", this.goodsData);
} else {
this.$emit("selectedLink", this.linkData);
}
this.clickClose();
},
// 打开组件方法
open(type, mutiple) {
this.flag = true;
if (type == "goods") {
this.goodsFlag = true;
if (mutiple) {
this.singleGoods();
}
} else {
this.goodsFlag = false;
}
},
// 关闭组件
close() {
this.flag = false;
},
},
};
const emit = defineEmits(["closeFlag", "selectedGoodsData", "selectedLink"]);
const goodsFlag = ref(false); // 是否商品选择器
const goodsData = ref([]); //选择的商品
const linkData = ref(""); //选择的链接
const flag = ref(false); // modal显隐
// 模板 ref:原 ref="goodsDialog" 与 import 的组件同名,改名避免互相覆盖
const goodsDialogRef = ref(null);
function clearGoodsSelected() {
goodsData.value = [];
}
// 关闭弹窗
function clickClose() {
flag.value = false;
goodsFlag.value = false;
emit("closeFlag", false);
}
// 单选商品
function singleGoods() {
var timer = setInterval(() => {
if (goodsDialogRef.value) {
goodsDialogRef.value.type = "single";
clearInterval(timer);
}
}, 100);
}
// 点击确认
function clickOK() {
if (goodsFlag.value) {
emit("selectedGoodsData", goodsData.value);
} else {
emit("selectedLink", linkData.value);
}
clickClose();
}
// 打开组件方法
function open(type, mutiple) {
flag.value = true;
if (type == "goods") {
goodsFlag.value = true;
if (mutiple) {
singleGoods();
}
} else {
goodsFlag.value = false;
}
}
// 关闭组件
function close() {
flag.value = false;
}
// 外部通过 ref 访问的成员(grep manager/src 全量确认):
// - open:page-decoration 各 modelList、modelForm/modelFormItem、hotzone/Zone、wap/advertising、alertAdvertising 等打开弹窗(goods/link)
// - singleGoods:modelList/forYour.vue、mixModel.vue 直接调用
// - clearGoodsSelected:wap/decorate.vue、modelList/goodsAndType.vue 调用
// - goodsData:modelList/newGoodsSort.vue、notEnough.vue 回写已选商品
// - goodsFlag:wap/decorate.vue 直接写入
// - flag:wap/decorate.vue 直接写入 true 打开弹窗
// - close:未被 grep 到外部调用,原 Options API 实例天然暴露,保守保留
defineExpose({
open,
close,
singleGoods,
clearGoodsSelected,
goodsData,
goodsFlag,
flag,
});
</script>
<style scoped lang="scss">
:deep(.el-dialog__body) {

View File

@@ -8,7 +8,7 @@
:key="i"
>
<component
ref="lili-component"
ref="liliComponentRef"
:is="templateWay[item.name]"
@selected="
(val) => {
@@ -20,94 +20,85 @@
</el-tabs>
</div>
</template>
<script>
import wap from "./wap.js";
<script setup>
import { nextTick, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import wapList from "./wap.js";
import goodsDialog from "./goods-dialog";
import templateWay from "./template/index";
export default {
components: {
goodsDialog
const emit = defineEmits(["selectedLink"]);
const route = useRoute();
const changed = ref(""); // 变更模板
const selected = ref(0); // 已选数据
const selectedLink = ref(""); //选中的链接
// wap 为共享的模块级数组,保持原 data 引用同一数组的行为
const wap = ref(wapList); // tab标签
const activeTab = ref(wapList[0]?.title || "");
// v-for 内模板 ref 收集为数组;原名 lili-component 含连字符,改名并同步 script
const liliComponentRef = ref([]);
watch(
changed,
(val) => {
emit("selectedLink", val[0]); //因为是单选,所以直接返回第一个
},
setup() {
return { templateWay };
},
data() {
return {
changed: "", // 变更模板
selected: 0, // 已选数据
selectedLink: "", //选中的链接
wap, // tab标签
activeTab: wap[0]?.title || "",
};
},
watch: {
changed: {
handler(val) {
this.$emit("selectedLink", val[0]); //因为是单选,所以直接返回第一个
},
deep: true
{ deep: true }
);
onMounted(() => {
nextTick(() => {
if (liliComponentRef.value?.[0]) {
liliComponentRef.value[0].type = "single"; //商品页面设置成为单选
}
},
mounted() {
this.$nextTick(() => {
this.$refs["lili-component"][0].type = "single"; //商品页面设置成为单选
});
let url = window.location.href
if(url.indexOf('/floorList/renovation') != -1){
//此处去重防止移动楼层多次点击push 太多数据
let obj = {};
this.wap = this.wap.reduce((cur, next) => {
//对象去重
if (next.title != undefined) {
obj[next.title]
? ""
: (obj[next.title] = true && cur.push(next));
}
return cur;
}, []);
if (this.$route.path !== '/floorList/main') {
this.wap.forEach((items, indexs) => {
if (items.title == '活动') {
this.wap.splice(indexs, 1)
}
})
});
let url = window.location.href;
if (url.indexOf("/floorList/renovation") != -1) {
//此处去重防止移动楼层多次点击push 太多数据
let obj = {};
wap.value = wap.value.reduce((cur, next) => {
//对象去重
if (next.title != undefined) {
obj[next.title]
? ""
: (obj[next.title] = true && cur.push(next));
}
}else{
this.wap.push( {
title: "活动",
url: "3",
name: "marketing"
})
let obj = {};
this.wap = this.wap.reduce((cur, next) => {
//对象去重
if (next.title != undefined) {
obj[next.title]
? ""
: (obj[next.title] = true && cur.push(next));
}
return cur;
}, []);
return cur;
}, []);
if (route.path !== "/floorList/main") {
wap.value.forEach((items, indexs) => {
if (items.title == "活动") {
wap.value.splice(indexs, 1);
}
});
}
this.wap.forEach((item) => {
if (item) {
item.selected = false;
}
} else {
wap.value.push({
title: "活动",
url: "3",
name: "marketing",
});
},
methods: {
// isVisible(item) {
// const type = this.$route.query.pagetype;
// if (type == "INDEX" && [ "discover"].includes(item.name)) {
// return false;
// }else if(type == "DISCOVER" && item.name == 'special'){
// return false;
// }else{
// return true
// }
// },
let obj = {};
wap.value = wap.value.reduce((cur, next) => {
//对象去重
if (next.title != undefined) {
obj[next.title]
? ""
: (obj[next.title] = true && cur.push(next));
}
return cur;
}, []);
}
};
wap.value.forEach((item) => {
if (item) {
item.selected = false;
}
});
});
// 无 defineExpose:grep manager/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
</script>
<style scoped lang="scss">
@import "./style.scss";

View File

@@ -22,94 +22,94 @@
</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]);
// 点击第一级的时候默认显示第二级第一个
// this.handleClickSecondChild(item.children, 0);
},
// 点击二级
handleClickSecondChild(second, index) {
second.___type = "category";
second.allId = `${second.parentId},${second.id}`
// 无 defineExpose:grep manager/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关;
// decorate.vue 等仅通过 @selected → selectedLink 事件交互)
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]);
// this.handleClickthirdChild(second.children[0], 0);
},
// 点击三级
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]);
// 点击第一级的时候默认显示第二级第一个
// handleClickSecondChild(item.children, 0);
}
// 点击二级
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]);
// handleClickthirdChild(second.children[0], 0);
}
// 点击三级
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

@@ -68,99 +68,95 @@
</div>
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref, watch } from "vue";
import { getAllPromotion } from "@/api/promotion";
export default {
data() {
return {
totals: "",
loading: true,
promotionList: "",
selectedIndex: 0,
promotions: "",
index: 999,
params: {
pageNumber: 1,
pageSize: 20,
},
showPromotionList: [],
};
const emit = defineEmits(["selected"]);
const totals = ref("");
const loading = ref(true);
const promotionList = ref("");
const selectedIndex = ref(0);
const promotions = ref("");
const index = ref(999);
const params = ref({
pageNumber: 1,
pageSize: 20,
});
const showPromotionList = ref([]);
const isSeckillMode = computed(() => {
return promotions.value === "SECKILL";
});
watch(
params,
() => {
index.value = 999;
typeOption(promotions.value) && typeOption(promotions.value).methodsed();
},
computed: {
isSeckillMode() {
return this.promotions === "SECKILL";
},
},
mounted() {
this.init();
},
watch: {
params: {
handler() {
this.index = 999;
this.typeOption(this.promotions) && this.typeOption(this.promotions).methodsed();
},
deep: true,
},
},
methods: {
seckillStatusText(v) {
if (v === "APPLY") return "申请";
if (v === "PASS") return "通过";
return "拒绝";
},
sortGoods(type) {
this.loading = false;
this.showPromotionList = this.promotionList[type];
},
typeOption(type) {
switch (type) {
case "FULL_DISCOUNT":
return { title: "满减", methodsed: () => { this.showPromotionList = []; this.sortGoods("FULL_DISCOUNT"); } };
case "PINTUAN":
return { title: "拼团", methodsed: () => { this.showPromotionList = []; this.sortGoods("PINTUAN"); } };
case "KANJIA":
return { title: "砍价", methodsed: () => { this.showPromotionList = []; this.sortGoods("KANJIA"); } };
case "SECKILL":
return { title: "秒杀", methodsed: () => { this.showPromotionList = []; this.sortGoods("SECKILL"); } };
case "POINTS_GOODS":
return { title: "积分商品", methodsed: () => { this.showPromotionList = []; this.sortGoods("POINTS_GOODS"); } };
default:
return {};
}
},
selectedPromotion(val) {
val.row.___type = "marketing";
val.row.___promotion = this.promotions;
this.$emit("selected", [val.row]);
this.index = val.index;
},
async init() {
const res = await getAllPromotion();
if (res.success) {
this.loading = false;
this.getPromotion(res);
} else {
this.loading = false;
}
},
getPromotion(res) {
if (res.result) {
this.promotionList = res.result;
delete this.promotionList.COUPON;
Object.keys(res.result)[0] && this.typeOption(Object.keys(res.result)[0]).methodsed();
this.promotions = Object.keys(res.result)[0];
}
},
clickPromotion(val, i) {
this.promotions = val;
this.selectedIndex = i;
this.params.pageNumber = 1;
this.typeOption(val) && this.typeOption(val).methodsed(this.promotionList[val].id);
},
},
};
{ deep: true }
);
function seckillStatusText(v) {
if (v === "APPLY") return "申请";
if (v === "PASS") return "通过";
return "拒绝";
}
function sortGoods(type) {
loading.value = false;
showPromotionList.value = promotionList.value[type];
}
function typeOption(type) {
switch (type) {
case "FULL_DISCOUNT":
return { title: "满减", methodsed: () => { showPromotionList.value = []; sortGoods("FULL_DISCOUNT"); } };
case "PINTUAN":
return { title: "拼团", methodsed: () => { showPromotionList.value = []; sortGoods("PINTUAN"); } };
case "KANJIA":
return { title: "砍价", methodsed: () => { showPromotionList.value = []; sortGoods("KANJIA"); } };
case "SECKILL":
return { title: "秒杀", methodsed: () => { showPromotionList.value = []; sortGoods("SECKILL"); } };
case "POINTS_GOODS":
return { title: "积分商品", methodsed: () => { showPromotionList.value = []; sortGoods("POINTS_GOODS"); } };
default:
return {};
}
}
function selectedPromotion(val) {
val.row.___type = "marketing";
val.row.___promotion = promotions.value;
emit("selected", [val.row]);
index.value = val.index;
}
async function init() {
const res = await getAllPromotion();
if (res.success) {
loading.value = false;
getPromotion(res);
} else {
loading.value = false;
}
}
function getPromotion(res) {
if (res.result) {
promotionList.value = res.result;
delete promotionList.value.COUPON;
Object.keys(res.result)[0] && typeOption(Object.keys(res.result)[0]).methodsed();
promotions.value = Object.keys(res.result)[0];
}
}
function clickPromotion(val, i) {
promotions.value = val;
selectedIndex.value = i;
params.value.pageNumber = 1;
typeOption(val) && typeOption(val).methodsed(promotionList.value[val].id);
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
.link-text {

View File

@@ -33,8 +33,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,
@@ -50,61 +51,61 @@ 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(VideoCamera), ___type: "live" },
{ title: "砍价", icon: markRaw(Share), ___type: "kanjia" },
{ title: "积分商城", icon: markRaw(ShoppingBag), ___type: "point" },
],
linkItem: {
title: "外部链接",
icon: markRaw(Link),
___type: "link",
url: "",
},
linkVisible: true,
selectedIndex: 9999999,
};
},
created() {
const urls = window.location.href;
if (urls.indexOf("/floorList/renovation") != -1) {
this.linkList = this.linkList.filter(
(item) =>
item.title !== "砍价" &&
item.title !== "小程序直播" &&
item.title !== "积分商城"
);
this.linkVisible = true;
} else {
this.linkVisible = false;
}
},
methods: {
showLinkItem(item) {
return (
(item.title !== "拼团频道" && item.title !== "签到") ||
this.$route.name !== "renovation"
);
},
handleLink(val, index) {
val = { ...val, ___type: "other" };
this.selectedIndex = index;
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(VideoCamera), ___type: "live" },
{ 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(true);
const selectedIndex = ref(9999999);
// 原 created 钩子
const urls = window.location.href;
if (urls.indexOf("/floorList/renovation") != -1) {
linkList.value = linkList.value.filter(
(item) =>
item.title !== "砍价" &&
item.title !== "小程序直播" &&
item.title !== "积分商城"
);
linkVisible.value = true;
} else {
linkVisible.value = false;
}
function showLinkItem(item) {
return (
(item.title !== "拼团频道" && item.title !== "签到") ||
route.name !== "renovation"
);
}
function handleLink(val, index) {
val = { ...val, ___type: "other" };
selectedIndex.value = index;
emit("selected", [val]);
}
// 无 defineExpose:grep manager/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 manager/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,56 @@
</div>
</div>
</template>
<script>
<script setup>
import { ref, reactive } 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);
// 无 defineExpose:grep manager/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关)
// 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

@@ -34,56 +34,57 @@
</div>
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import { getHomeList } from "@/api/other.js";
export default {
data() {
return {
loading: true,
index: 999,
data: [],
params: {
sort: "createTime",
order: "desc",
pageClientType: "H5",
pageNumber: 1,
pageSize: 20,
pageType: "SPECIAL",
},
total: 0,
};
},
mounted() {
this.init();
},
methods: {
selectRow(row, idx) {
this.index = idx;
const payload = { ...row, pageType: "special", ___type: "special" };
this.$emit("selected", [payload]);
},
changePageNum(val) {
this.params.pageNumber = val;
this.init();
},
changePageSize(val) {
this.params.pageNumber = 1;
this.params.pageSize = val;
this.init();
},
async init() {
this.params.pageClientType = this.$route.name === "renovation" ? "PC" : "H5";
const res = await getHomeList(this.params);
if (res.success) {
this.loading = false;
this.data = res.result.records;
this.total = res.result.total;
} else {
this.loading = false;
}
},
},
};
const emit = defineEmits(["selected"]);
const route = useRoute();
const loading = ref(true);
const index = ref(999);
const data = ref([]);
const params = reactive({
sort: "createTime",
order: "desc",
pageClientType: "H5",
pageNumber: 1,
pageSize: 20,
pageType: "SPECIAL",
});
const total = ref(0);
function selectRow(row, idx) {
index.value = idx;
const payload = { ...row, pageType: "special", ___type: "special" };
emit("selected", [payload]);
}
function changePageNum(val) {
params.pageNumber = val;
init();
}
function changePageSize(val) {
params.pageNumber = 1;
params.pageSize = val;
init();
}
async function init() {
params.pageClientType = route.name === "renovation" ? "PC" : "H5";
const res = await getHomeList(params);
if (res.success) {
loading.value = false;
data.value = res.result.records;
total.value = res.result.total;
} else {
loading.value = false;
}
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
img {

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

@@ -6,23 +6,22 @@
</div>
</template>
<script>
export default {
name: "configDrawer",
data() {
return {
drawer: false,
};
},
methods: {
open() {
this.drawer = true;
},
close() {
this.drawer = false;
},
},
};
<script setup>
import { ref } from "vue";
defineOptions({ name: "configDrawer" });
const drawer = ref(false);
function open() {
drawer.value = true;
}
function close() {
drawer.value = false;
}
defineExpose({ open, close });
</script>
<style 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 = 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 = 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();
// userManage.vue 通过 depRef 引用本组件(当前未实际调用方法),作为公共组件 API 保留
defineExpose({ clearSelect, selectDep });
</script>

View File

@@ -35,101 +35,100 @@
</div>
</template>
<script>
<script setup>
import { ref } from "vue";
import { initDepartment } 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.dataDep = this.cloneDep.filter(
(item) => item.title.indexOf(this.searchKey) > -1
);
} 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) {
dataDep.value = cloneDep.value.filter(
(item) => item.title.indexOf(searchKey.value) > -1
);
} 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();
// 父组件(userManage)通过 ref 调用 setData / clearSelect
defineExpose({ setData, clearSelect });
</script>
<style lang="scss" scoped>

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

@@ -60,7 +60,7 @@
@closed="resetPicModal"
>
<ossManage
ref="ossManage"
ref="ossManageRef"
:is-component="true"
:initialize="picModalFlag"
:max-select="1"
@@ -70,110 +70,112 @@
</div>
</template>
<script>
<script setup>
import { nextTick, ref, useAttrs, watch } from "vue";
import { View } from "@element-plus/icons-vue";
import { uploadFile } from "@/api/index";
import { getStore } from "@/libs/storage";
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: "",
};
showInput: {
type: Boolean,
default: true,
},
methods: {
handleCLickImg(val, index) {
this.picModalFlag = true;
this.selectedFormBtnName = val;
this.picIndex = index;
this.$nextTick(() => {
if (this.$refs.ossManage) {
this.$refs.ossManage.selectImage = true;
}
});
},
handleOssCallback(item) {
this.picModalFlag = false;
if (!item?.url) return;
this.currentValue = item.url;
this.picIndex = "";
this.emitValue(this.currentValue);
},
resetPicModal() {
this.selectedFormBtnName = "";
this.picIndex = "";
},
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("");
// 模板 ref:ossManage 与 import 组件同名,改名避免互相覆盖
const ossManageRef = ref(null);
function handleCLickImg(val, index) {
picModalFlag.value = true;
selectedFormBtnName.value = val;
picIndex.value = index;
nextTick(() => {
if (ossManageRef.value) {
ossManageRef.value.selectImage = true;
}
});
}
function handleOssCallback(item) {
picModalFlag.value = false;
if (!item?.url) return;
currentValue.value = item.url;
picIndex.value = "";
emitValue(currentValue.value);
}
function resetPicModal() {
selectedFormBtnName.value = "";
picIndex.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

@@ -75,167 +75,167 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref, watch } from "vue";
import { Camera, Delete, View } from "@element-plus/icons-vue";
import { uploadFile } from "@/api/index";
import { getStore } from "@/libs/storage";
import { Message } from "@/utils/message";
import vuedraggable from "vuedraggable";
export default {
name: "uploadPicThumb",
components: {
vuedraggable,
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,
},
limit: {
type: Number,
default: 10,
},
multiple: {
type: Boolean,
default: true,
},
emits: ["update:modelValue", "input", "on-change", "uploadchange"],
data() {
return {
accessToken: {},
uploadFileUrl: uploadFile,
uploadList: [],
viewImage: false,
imgUrl: "",
};
limit: {
type: Number,
default: 10,
},
computed: {
bindValue() {
return this.modelValue !== undefined ? this.modelValue : this.value;
},
},
methods: {
draggableItemKey(item) {
return item.url || item.uid || item.name;
},
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();
},
handleSuccess(res, file) {
if (res.success) {
file.url = res.result;
if (!this.multiple && this.uploadList.length > 0) {
this.uploadList.splice(0, 1);
}
this.uploadList.push(file);
this.returnValue();
} else {
this.$Message.error(res.message);
}
},
handleError(error) {
this.$Message.error(error.toString());
},
handleBeforeUpload(file) {
const okType = ["image/jpeg", "image/png", "image/gif", "image/jpg"].includes(file.type);
if (!okType) {
this.$Message.warning("请选择 .jpg .jpeg .png .gif 格式图片");
return false;
}
if (file.size / 1024 > 1024) {
this.$Message.warning("所选文件大小过大,不能超过 1M");
return false;
}
if (this.multiple && this.uploadList.length >= this.limit) {
this.$Message.warning("最多只能上传" + this.limit + "张图片");
return false;
}
return true;
},
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) {
this.$Message.warning("多张上传仅支持数组数据类型");
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();
},
};
});
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("");
// el-upload 模板 ref
const upload = 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 draggableItemKey(item) {
return item.url || item.uid || item.name;
}
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 handleSuccess(res, file) {
if (res.success) {
file.url = res.result;
if (!props.multiple && uploadList.value.length > 0) {
uploadList.value.splice(0, 1);
}
uploadList.value.push(file);
returnValue();
} else {
Message.error(res.message);
}
}
function handleError(error) {
Message.error(error.toString());
}
function handleBeforeUpload(file) {
const okType = ["image/jpeg", "image/png", "image/gif", "image/jpg"].includes(file.type);
if (!okType) {
Message.warning("请选择 .jpg .jpeg .png .gif 格式图片");
return false;
}
if (file.size / 1024 > 1024) {
Message.warning("所选文件大小过大,不能超过 1M");
return false;
}
if (props.multiple && uploadList.value.length >= props.limit) {
Message.warning("最多只能上传" + props.limit + "张图片");
return false;
}
return true;
}
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) {
Message.warning("多张上传仅支持数组数据类型");
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),
});
});
if (init) {
emitValue(list);
} else {
emit("on-change", list);
}
}
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -19,132 +19,129 @@
</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";
import { Message } from "@/utils/message";
export default {
name: "map",
data() {
return {
config,
showMap: false, // 地图显隐
mapSearch: "", // 地图搜索
map: null, // 初始化地图
autoComplete: null, // 初始化搜索方法
geocoder: null, // 初始化地理、坐标转化
positionPicker: null, // 地图拖拽选点
tips: [], //搜索关键字列表
addrContent: {}, // 回显地址信息
loading: false, // 加载状态
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: function (val) {
this.searchOfMap(val);
},
},
methods: {
ok() {
getRegion(params).then((res) => {
if (res.success) {
addrContent.value.addr = res.result.name.replace(/,/g, " ");
addrContent.value.addrId = res.result.id;
loading.value = false;
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配置是否正确')
emit("getAddress", addrContent.value);
}
});
} else {
Message.error('未获取到坐标信息!请查看高德API配置是否正确')
}
}
function init() {
AMapLoader.load({
key: config.aMapKey, // 申请好的Web端开发者Key,首次调用 load 时必填
version: "", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Autocomplete",
"AMap.PlaceSearch",
"AMap.Geolocation",
"AMap.Geocoder",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
AMapUI: {
// 是否加载 AMapUI,缺省不加载
version: "1.1", // AMapUI 缺省 1.1
plugins: ["misc/PositionPicker"], // 需要加载的 AMapUI ui插件
},
init() {
AMapLoader.load({
key: this.config.aMapKey, // 申请好的Web端开发者Key,首次调用 load 时必填
version: "", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Autocomplete",
"AMap.PlaceSearch",
"AMap.Geolocation",
"AMap.Geocoder",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
AMapUI: {
// 是否加载 AMapUI,缺省不加载
version: "1.1", // AMapUI 缺省 1.1
plugins: ["misc/PositionPicker"], // 需要加载的 AMapUI ui插件
},
})
.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());
// 实例化Autocomplete
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();
/**
*
* 所有回显数据,都在positionResult里面
* 需要字段可以查找
*
*/
that.positionPicker.on("success", function (positionResult) {
that.addrContent = positionResult;
});
})
.catch((e) => { });
},
searchOfMap(val) {
// 地图搜索
let that = this;
this.autoComplete.search(val, function (status, result) {
// 搜索成功时,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());
// 实例化Autocomplete
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();
/**
*
* 所有回显数据,都在positionResult里面
* 需要字段可以查找
*
*/
positionPicker.value.on("success", function (positionResult) {
addrContent.value = positionResult;
});
})
.catch((e) => { });
}
function searchOfMap(val) {
// 地图搜索
autoComplete.value.search(val, function (status, result) {
// 搜索成功时,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 "@/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,
});
}
// 父组件(member/index、memberRecycle、memberDetail、orderDetail、shopOperation)通过 ref 调用 open() 打开弹窗
defineExpose({ open });
</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

@@ -10,140 +10,140 @@
/>
</div>
</template>
<script>
<script setup>
import { computed, ref, watch } from "vue";
import { getChildRegion } from "@/api/common.js";
export default {
props: ["addressId"],
data() {
return {
data: [],
addr: [],
};
},
computed: {
cascaderProps() {
return {
value: "value",
label: "label",
lazy: true,
lazyLoad: this.loadData,
};
},
},
methods: {
change(val) {
if (!val || !val.length) {
this.$emit("selected", [[], []]);
return;
}
const labels = this.resolveLabels(val);
this.$emit("selected", [val, labels]);
},
resolveLabels(values) {
const labels = [];
let options = this.data;
for (const v of values) {
const node = options.find((item) => item.value === v);
if (!node) break;
labels.push(node.label);
options = node.children || [];
}
return labels;
},
loadData(node, resolve) {
const parentId = node.level === 0 ? 0 : node.value;
getChildRegion(parentId).then((res) => {
if (!res.result || res.result.length <= 0) {
resolve([]);
return;
}
const nodes = res.result.map((child) => {
const isLeaf =
child.level === "street" ||
node.label === "香港特别行政区" ||
child.name === "台湾省";
return {
value: child.id,
label: child.name,
leaf: isLeaf,
};
});
resolve(nodes);
});
},
async init() {
const data = await getChildRegion(0);
this.data = data.result.map((item) => ({
value: item.id,
label: item.name,
leaf: item.name === "台湾省",
}));
},
async reviewData() {
const addr = JSON.parse(JSON.stringify(this.addressId.split(",")));
const length = addr.length;
const root = await getChildRegion(0);
const arr0 = root.result.map((item) => ({
value: item.id,
label: item.name,
leaf: item.name === "台湾省",
children: [],
}));
const props = defineProps(["addressId"]);
const emit = defineEmits(["selected"]);
if (length > 0) {
const children = await getChildRegion(addr[0]);
const arr1 = this.handleData(children.result);
arr0.forEach((e) => {
if (e.value === addr[0]) {
e.children = arr1;
}
});
}
if (length > 1) {
let arr1 = arr0.find((e) => e.value === addr[0])?.children || [];
const children = await getChildRegion(addr[1]);
const arr2 = this.handleData(children.result);
arr1.forEach((e) => {
if (e.value === addr[1]) {
e.children = arr2;
}
});
}
if (length > 2) {
const arr1 = arr0.find((e) => e.value === addr[0])?.children || [];
const arr2 = arr1.find((e) => e.value === addr[1])?.children || [];
const children = await getChildRegion(addr[2]);
const arr3 = this.handleData(children.result);
arr2.forEach((e) => {
if (e.value === addr[2]) {
e.children = arr3;
}
});
}
this.data = arr0;
this.addr = addr;
},
handleData(data) {
return data.map((child) => ({
const data = ref([]);
const addr = ref([]);
const cascaderProps = computed(() => {
return {
value: "value",
label: "label",
lazy: true,
lazyLoad: loadData,
};
});
function change(val) {
if (!val || !val.length) {
emit("selected", [[], []]);
return;
}
const labels = resolveLabels(val);
emit("selected", [val, labels]);
}
function resolveLabels(values) {
const labels = [];
let options = data.value;
for (const v of values) {
const node = options.find((item) => item.value === v);
if (!node) break;
labels.push(node.label);
options = node.children || [];
}
return labels;
}
function loadData(node, resolve) {
const parentId = node.level === 0 ? 0 : node.value;
getChildRegion(parentId).then((res) => {
if (!res.result || res.result.length <= 0) {
resolve([]);
return;
}
const nodes = res.result.map((child) => {
const isLeaf =
child.level === "street" ||
node.label === "香港特别行政区" ||
child.name === "台湾省";
return {
value: child.id,
label: child.name,
leaf: child.level === "street",
children: child.level === "street" ? undefined : [],
}));
},
leaf: isLeaf,
};
});
resolve(nodes);
});
}
async function init() {
const res = await getChildRegion(0);
data.value = res.result.map((item) => ({
value: item.id,
label: item.name,
leaf: item.name === "台湾省",
}));
}
async function reviewData() {
const addrArr = JSON.parse(JSON.stringify(props.addressId.split(",")));
const length = addrArr.length;
const root = await getChildRegion(0);
const arr0 = root.result.map((item) => ({
value: item.id,
label: item.name,
leaf: item.name === "台湾省",
children: [],
}));
if (length > 0) {
const children = await getChildRegion(addrArr[0]);
const arr1 = handleData(children.result);
arr0.forEach((e) => {
if (e.value === addrArr[0]) {
e.children = arr1;
}
});
}
if (length > 1) {
let arr1 = arr0.find((e) => e.value === addrArr[0])?.children || [];
const children = await getChildRegion(addrArr[1]);
const arr2 = handleData(children.result);
arr1.forEach((e) => {
if (e.value === addrArr[1]) {
e.children = arr2;
}
});
}
if (length > 2) {
const arr1 = arr0.find((e) => e.value === addrArr[0])?.children || [];
const arr2 = arr1.find((e) => e.value === addrArr[1])?.children || [];
const children = await getChildRegion(addrArr[2]);
const arr3 = handleData(children.result);
arr2.forEach((e) => {
if (e.value === addrArr[2]) {
e.children = arr3;
}
});
}
data.value = arr0;
addr.value = addrArr;
}
function handleData(list) {
return list.map((child) => ({
value: child.id,
label: child.name,
leaf: child.level === "street",
children: child.level === "street" ? undefined : [],
}));
}
watch(
() => props.addressId,
(v) => {
if (v) {
reviewData();
} else {
init();
}
},
watch: {
addressId: {
handler(v) {
if (v) {
this.reviewData();
} else {
this.init();
}
},
immediate: true,
},
},
};
{ immediate: true }
);
</script>

View File

@@ -37,107 +37,106 @@
</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;
} else {
this.distance = offset;
}
}
},
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) => {
{ 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) {
this.data = res.result;
this.show = true;
bgColor.value = "green";
verifyText.value = "解锁成功";
emit("change", { status: true, distance: distance.value });
} else {
this.$Message.warning("请求失败请重试!");
bgColor.value = "red";
verifyText.value = "解锁失败";
setTimeout(() => init(), 1000);
emit("change", { status: false, distance: distance.value });
}
});
},
},
watch: {
verifyType: {
immediate: true,
handler(v) {
this.type = v;
},
},
},
};
} else {
init();
}
})
.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("请求失败请重试!");
}
});
}
// login.vue 通过 verifyRef.value.show = false 关闭、verifyRef.value.init() 打开
defineExpose({ show, init });
</script>
<style lang="scss" scoped>

View File

@@ -1,5 +1,6 @@
import lazyLoading from "./lazyLoading.js";
import lazyLoading from "./lazyLoading.js";
import { getRouter } from "./router-holder";
import { useAppStore } from "@/stores/app";
import Cookies from "js-cookie";
let util = {};
@@ -176,22 +177,21 @@ util.oneOf = function(ele, targetArr) {
};
// 打开新的页面
util.openNewPage = function(vm, name, argu, query) {
if (!vm.$store) {
return;
}
let pageOpenedList = vm.$store.state.app.pageOpenedList;
const currentPath = vm.$route ? vm.$route.path : "";
util.openNewPage = function(name, argu, query) {
const appStore = useAppStore();
let pageOpenedList = appStore.pageOpenedList;
const router = getRouter();
const currentPath = router ? router.currentRoute.value.path : "";
const openedIndex = util.findOpenedPageIndex(pageOpenedList, name, currentPath);
if (openedIndex >= 0) {
vm.$store.commit("pageOpenedList", {
appStore.updateOpenedList({
index: openedIndex,
argu: argu,
query: query
});
return;
}
let tag = vm.$store.state.app.tagsList.filter(item => {
let tag = appStore.tagsList.filter(item => {
if (item.children) {
return name == item.children[0].name;
} else {
@@ -200,7 +200,7 @@ util.openNewPage = function(vm, name, argu, query) {
});
tag = tag[0];
if (!tag) {
tag = vm.$store.state.app.tagsList.find((item) => {
tag = appStore.tagsList.find((item) => {
const candidate = item.children ? item.children[0] : item;
return util.isSameRoutePage(candidate, { name, path: currentPath });
});
@@ -217,7 +217,7 @@ util.openNewPage = function(vm, name, argu, query) {
if (query) {
tagObj.query = query;
}
vm.$store.commit("increateTag", tagObj);
appStore.increateTag(tagObj);
}
};
@@ -245,8 +245,9 @@ util.toDefaultPage = function(routers, name, route, next) {
}
};
util.initRouter = function(vm) {
util.initRouter = function() {
// 初始化路由
const appStore = useAppStore();
const constRoutes = [];
const otherRoutes = [];
@@ -267,7 +268,7 @@ util.initRouter = function(vm) {
// 未登录
return;
}
if (!vm.$store.state.app.added) {
if (!appStore.added) {
// 第一次加载 读取数据
// 加载菜单
import("@/api/index").then(({ getCurrentPermissionList }) => {
@@ -293,26 +294,25 @@ 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);
appStore.setAdded(false);
return;
}
let menuData = JSON.parse(data);
util.registerDynamicRoutes(menuData);
util.initMenuData(vm, menuData);
util.initMenuData(menuData);
}
};
@@ -330,7 +330,8 @@ util.initAllMenuData = function(constRoutes, data) {
};
// 生成菜单格式数据
util.initMenuData = function(vm, data) {
util.initMenuData = function(data) {
const appStore = useAppStore();
const menuRoutes = [];
let menuData = data;
// 顶部菜单 —— 首页固定置顶
@@ -345,26 +346,26 @@ util.initMenuData = function(vm, data) {
if (navList.length < 1) {
return;
}
// 存入vuex
vm.$store.commit("setNavList", navList);
// 存入pinia
appStore.setNavList(navList);
let currNav = window.localStorage.getItem("currNav");
if (currNav) {
// 读取缓存title
for (var item of navList) {
if (item.name == currNav) {
vm.$store.commit("setCurrNavTitle", item.title);
appStore.setCurrNavTitle(item.title);
break;
}
}
} else {
// 默认首页
currNav = 'home_index';
vm.$store.commit("setCurrNavTitle", '首页');
appStore.setCurrNavTitle('首页');
}
vm.$store.commit("setCurrNav", currNav);
appStore.setCurrNav(currNav);
// 首页不需要二级菜单
if (currNav === 'home_index') {
vm.$store.commit("updateMenulist", []);
appStore.updateMenulist([]);
return;
}
for (let item of menuData) {
@@ -376,20 +377,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);
};
// 生成路由节点

View File

@@ -1,10 +1,10 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import "core-js/stable";
import "./styles/theme.less";
import "@/styles/common.scss";
import App from "./App.vue";
import { router } from "./router/index";
import store from "./store";
import i18n from "@/locale";
import { setupElementPlus } from "@/plugins/element";
import { setupLegacyMessage } from "@/utils/message";
@@ -46,7 +46,7 @@ setupLegacyMessage(app);
installVueQr(app);
app.use(router);
app.use(store);
app.use(createPinia());
app.use(i18n);
app.component("liliDialog", liliDialog);

View File

@@ -4,7 +4,6 @@ import "nprogress/nprogress.css";
import Util from "../libs/util";
import { setRouter } from "../libs/router-holder";
import Cookies from "js-cookie";
import store from "@/store";
import { routers } from "./router";
NProgress.configure({ showSpinner: false });
@@ -43,7 +42,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,23 +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: {
openStyleStore: "",
openStoreId: "",
notices: "",
},
mutations: {},
actions: {},
modules: {
app,
user,
setting,
dict,
},
});
export default store;

View File

@@ -1,140 +0,0 @@
import {
otherRouter
} from '@/router/router';
import {
router
} from '@/router/index';
import Util from '@/libs/util';
import i18n from '@/locale';
const app = {
state: {
styleStore: "", // 移动端楼层装修中选择风格存储
added: false, // 加载路由标识
navList: [], // 顶部菜单
currNav: "", // 当前顶部菜单name
currNavTitle: "", // 当前顶部菜单标题
cachePage: [],
lang: '',
pageOpenedList: [{ // 打开过的页面,顶部历史菜单项
title: '首页',
path: '',
name: 'home_index'
}],
hotWords:[], //今日热词数据
// 面包屑数组 左侧菜单
menuList: [],
tagsList: [...otherRouter.children], //这块是面包屑的内容不能删除
routers: [otherRouter],
messageCount: 0,
// 在这里定义你不想要缓存的页面的name属性值(参见路由配置router.js)
dontCache: ['test', 'test']
},
mutations: {
// 动态添加主界面路由,需要缓存
updateAppRouter(state, routes) {
state.routers.push(...routes);
// 实际 addRoute 在 util.registerDynamicRoutes 中完成
},
updateDefaultRouter(state, routes) {
// 兼容旧调用,404 由 registerDynamicRoutes 注册
},
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;
},
closePage(state, name) {
state.cachePage.forEach((item, index) => {
if (item == name) {
state.cachePage.splice(index, 1);
}
});
},
initCachePage(state) {
if (localStorage.cachePage) {
state.cachePage = JSON.parse(localStorage.cachePage);
}
},
removeTag(state, name) {
state.pageOpenedList.map((item, index) => {
if (item.name == name) {
state.pageOpenedList.splice(index, 1);
}
});
},
pageOpenedList(state, get) {
let openedPage = state.pageOpenedList[get.index];
if (get.argu) {
openedPage.argu = get.argu;
}
if (get.query) {
openedPage.query = get.query;
}
state.pageOpenedList.splice(get.index, 1, openedPage);
localStorage.pageOpenedList = JSON.stringify(state.pageOpenedList);
},
clearAllTags(state) {
state.pageOpenedList.splice(1);
state.cachePage.length = 0;
localStorage.pageOpenedList = JSON.stringify(state.pageOpenedList);
},
clearOtherTags(state, vm) {
let currentName = vm.$route.name;
let currentIndex = 0;
state.pageOpenedList.forEach((item, index) => {
if (item.name == currentName) {
currentIndex = index;
}
});
if (currentIndex == 0) {
state.pageOpenedList.splice(1);
} else {
state.pageOpenedList.splice(currentIndex + 1);
state.pageOpenedList.splice(1, currentIndex - 1);
}
let newCachepage = state.cachePage.filter(item => {
return item == currentName;
});
state.cachePage = newCachepage;
localStorage.pageOpenedList = JSON.stringify(state.pageOpenedList);
},
setOpenedList(state) {
state.pageOpenedList = localStorage.pageOpenedList ? JSON.parse(localStorage.pageOpenedList) : [otherRouter.children[0]];
},
setAvatarPath(state, path) {
localStorage.avatorImgPath = path;
},
switchLang(state, lang) {
state.lang = lang;
localStorage.lang = lang;
i18n.global.locale = lang;
},
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.pageOpenedList.push(tagObj);
localStorage.pageOpenedList = JSON.stringify(state.pageOpenedList);
}
}
};
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";
const { setting } = config;
const localSetting = window.localStorage.getItem('admin-setting')
const settingData = {
state: {
setting:localSetting ? JSON.parse(localSetting) : setting,
},
mutations: {
updateSetting(state, data) {
state.setting = data;
},
},
};
export default settingData;

View File

@@ -1,15 +0,0 @@
import Cookies from 'js-cookie';
import util from '@/libs/util';
const user = {
state: {},
mutations: {
logout () {
util.clearDynamicRoutes();
Cookies.remove('userInfoManager');
localStorage.clear();
}
}
};
export default user;

146
manager/src/stores/app.js Normal file
View File

@@ -0,0 +1,146 @@
import { defineStore } from "pinia";
import { otherRouter } from "@/router/router";
import i18n from "@/locale";
// 原 vuex app 模块 + 根 state(openStyleStore/openStoreId/notices)合并
// mutation 全部转为同名 action;两个例外:
// - pageOpenedList mutation 与 state 同名,改名 updateOpenedList
// - clearOtherTags 原接收组件实例 vm,改为直接接收当前路由名 currentName
export const useAppStore = defineStore("app", {
state: () => ({
// 原根 state
openStyleStore: "",
openStoreId: "",
notices: "",
// 原 app 模块
styleStore: "", // 移动端楼层装修中选择风格存储
added: false, // 加载路由标识
navList: [], // 顶部菜单
currNav: "", // 当前顶部菜单name
currNavTitle: "", // 当前顶部菜单标题
cachePage: [],
lang: "",
pageOpenedList: [
{
// 打开过的页面,顶部历史菜单项
title: "首页",
path: "",
name: "home_index",
},
],
hotWords: [], //今日热词数据
// 面包屑数组 左侧菜单
menuList: [],
tagsList: [...otherRouter.children], //这块是面包屑的内容不能删除
routers: [otherRouter],
messageCount: 0,
// 在这里定义你不想要缓存的页面的name属性值(参见路由配置router.js)
dontCache: ["test", "test"],
}),
actions: {
// 动态添加主界面路由,需要缓存
updateAppRouter(routes) {
this.routers.push(...routes);
// 实际 addRoute 在 util.registerDynamicRoutes 中完成
},
updateDefaultRouter(routes) {
// 兼容旧调用,404 由 registerDynamicRoutes 注册
},
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;
},
closePage(name) {
this.cachePage.forEach((item, index) => {
if (item == name) {
this.cachePage.splice(index, 1);
}
});
},
initCachePage() {
if (localStorage.cachePage) {
this.cachePage = JSON.parse(localStorage.cachePage);
}
},
removeTag(name) {
this.pageOpenedList.map((item, index) => {
if (item.name == name) {
this.pageOpenedList.splice(index, 1);
}
});
},
updateOpenedList(get) {
let openedPage = this.pageOpenedList[get.index];
if (get.argu) {
openedPage.argu = get.argu;
}
if (get.query) {
openedPage.query = get.query;
}
this.pageOpenedList.splice(get.index, 1, openedPage);
localStorage.pageOpenedList = JSON.stringify(this.pageOpenedList);
},
clearAllTags() {
this.pageOpenedList.splice(1);
this.cachePage.length = 0;
localStorage.pageOpenedList = JSON.stringify(this.pageOpenedList);
},
clearOtherTags(currentName) {
let currentIndex = 0;
this.pageOpenedList.forEach((item, index) => {
if (item.name == currentName) {
currentIndex = index;
}
});
if (currentIndex == 0) {
this.pageOpenedList.splice(1);
} else {
this.pageOpenedList.splice(currentIndex + 1);
this.pageOpenedList.splice(1, currentIndex - 1);
}
let newCachepage = this.cachePage.filter((item) => {
return item == currentName;
});
this.cachePage = newCachepage;
localStorage.pageOpenedList = JSON.stringify(this.pageOpenedList);
},
setOpenedList() {
this.pageOpenedList = localStorage.pageOpenedList
? JSON.parse(localStorage.pageOpenedList)
: [otherRouter.children[0]];
},
setAvatarPath(path) {
localStorage.avatorImgPath = path;
},
switchLang(lang) {
this.lang = lang;
localStorage.lang = lang;
i18n.global.locale = lang;
},
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.pageOpenedList.push(tagObj);
localStorage.pageOpenedList = JSON.stringify(this.pageOpenedList);
},
},
});

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";
const { setting } = config;
const localSetting = window.localStorage.getItem("admin-setting");
export const useSettingStore = defineStore("setting", {
state: () => ({
setting: localSetting ? JSON.parse(localSetting) : setting,
}),
actions: {
updateSetting(data) {
this.setting = data;
},
},
});

View File

@@ -0,0 +1,13 @@
import { defineStore } from "pinia";
import Cookies from "js-cookie";
import util from "@/libs/util";
export const useUserStore = defineStore("user", {
actions: {
logout() {
util.clearDynamicRoutes();
Cookies.remove("userInfoManager");
localStorage.clear();
},
},
});

View File

@@ -76,7 +76,9 @@
</div>
</template>
<script>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { UserFilled } from "@element-plus/icons-vue";
import shrinkableMenu from "./main-parts/shrinkable-menu/shrinkable-menu.vue";
import tagsPageOpened from "./main-parts/tags-page-opened.vue";
@@ -87,123 +89,108 @@ import util from "@/libs/util.js";
import { getNoticePage, logout } from "@/api/index";
import config from "@/config/index.js";
import defaultLogo from "@/assets/logo.png";
import { setStore } from "@/libs/storage";
import { useAppStore } from "@/stores/app";
import { useUserStore } from "@/stores/user";
import { useSettingStore } from "@/stores/setting";
export default {
components: {
UserFilled,
shrinkableMenu,
tagsPageOpened,
messageTip,
circleLoading,
},
data() {
return {
config,
sliceNum: 5,
userInfo: "",
tipsMessage: "",
defaultLogo,
domainLogo: "",
};
},
computed: {
setting() {
return this.$store.state.setting.setting;
},
loading() {
return this.$store.state.app.loading;
},
menuList() {
return this.$store.state.app.menuList || [];
},
pageTagsList() {
return this.$store.state.app.pageOpenedList;
},
avatarPath() {
return localStorage.avatorImgPath;
},
},
methods: {
init() {
const userInfo = JSON.parse(Cookies.get("userInfoManager"));
this.userInfo = userInfo;
this.checkTag(this.$route.name);
this.domainLogo = localStorage.getItem("icon") || this.defaultLogo;
const link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
link.type = "image/x-icon";
link.href = localStorage.getItem("domainIcon");
link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link);
window.document.title = localStorage.getItem("title") + " - 运营后台";
getNoticePage({}).then((res) => {
if (res.success) {
this.tipsMessage = res.result;
this.$store.state.notices = res.result;
}
});
},
handleClickUserDropdown(name) {
if (name === "personalCenter") {
util.openNewPage(this, "personal-center");
this.$router.push({ name: "personal-center" });
} else if (name === "changePass") {
util.openNewPage(this, "change-password");
this.$router.push({ name: "change_password" });
} else if (name === "loginOut") {
logout().then(() => {
this.$store.commit("logout", this);
this.$store.commit("setAdded", false);
this.setStore("accessToken", "");
this.setStore("refreshToken", "");
this.$router.push({ path: "/login" });
});
}
},
checkTag(name) {
const openpageHasTag = util.findOpenedPageIndex(
this.pageTagsList,
name,
this.$route.path
) >= 0;
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;
},
onLogoError(e) {
if (e?.target && e.target.src !== this.defaultLogo) {
e.target.src = this.defaultLogo;
}
},
},
watch: {
$route(to) {
this.checkTag(to.name);
localStorage.currentPageName = to.name;
},
},
mounted() {
this.init();
this.resize();
window.addEventListener("resize", this.resize);
this.$store.commit("setOpenedList");
},
beforeUnmount() {
window.removeEventListener("resize", this.resize);
},
created() {
this.$store.commit("setOpenedList");
},
};
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const userStore = useUserStore();
const settingStore = useSettingStore();
const sliceNum = ref(5);
const userInfo = ref("");
const tipsMessage = ref("");
const domainLogo = ref("");
const setting = computed(() => settingStore.setting);
const loading = computed(() => appStore.loading);
const menuList = computed(() => appStore.menuList || []);
const pageTagsList = computed(() => appStore.pageOpenedList);
const avatarPath = computed(() => localStorage.avatorImgPath);
function init() {
const userInfoData = JSON.parse(Cookies.get("userInfoManager"));
userInfo.value = userInfoData;
checkTag(route.name);
domainLogo.value = localStorage.getItem("icon") || defaultLogo;
const link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
link.type = "image/x-icon";
link.href = localStorage.getItem("domainIcon");
link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link);
window.document.title = localStorage.getItem("title") + " - 运营后台";
getNoticePage({}).then((res) => {
if (res.success) {
tipsMessage.value = res.result;
appStore.notices = res.result;
}
});
}
function handleClickUserDropdown(name) {
if (name === "personalCenter") {
util.openNewPage("personal-center");
router.push({ name: "personal-center" });
} else if (name === "changePass") {
util.openNewPage("change-password");
router.push({ name: "change_password" });
} else if (name === "loginOut") {
logout().then(() => {
userStore.logout();
appStore.setAdded(false);
setStore("accessToken", "");
setStore("refreshToken", "");
router.push({ path: "/login" });
});
}
}
function checkTag(name) {
const openpageHasTag = util.findOpenedPageIndex(
pageTagsList.value,
name,
route.path
) >= 0;
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;
}
function onLogoError(e) {
if (e?.target && e.target.src !== defaultLogo) {
e.target.src = defaultLogo;
}
}
watch(
() => route.name,
(name) => {
checkTag(name);
localStorage.currentPageName = name;
}
);
appStore.setOpenedList();
onMounted(() => {
init();
resize();
window.addEventListener("resize", resize);
appStore.setOpenedList();
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resize);
});
</script>

View File

@@ -19,52 +19,48 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { getAppointmentOrderSettings, saveAppointmentOrderSettings } from "@/api/appointment";
import { ElMessage } from "element-plus";
export default {
name: "appointmentOrderSettings",
data() {
return {
loading: false,
saving: false,
form: {
serviceStartRemindMemberHours: 2,
inStoreNoShowAutoCompleteHours: 24,
},
};
},
mounted() {
this.loadData();
},
methods: {
loadData() {
this.loading = true;
getAppointmentOrderSettings()
.then((res) => {
if (res.success && res.result) {
this.form = { ...this.form, ...res.result };
}
})
.finally(() => {
this.loading = false;
});
},
handleSave() {
this.saving = true;
saveAppointmentOrderSettings(this.form)
.then((res) => {
if (res.success) {
ElMessage.success("保存成功");
}
})
.finally(() => {
this.saving = false;
});
},
},
};
defineOptions({ name: "appointmentOrderSettings" });
const loading = ref(false);
const saving = ref(false);
const form = ref({
serviceStartRemindMemberHours: 2,
inStoreNoShowAutoCompleteHours: 24,
});
onMounted(() => {
loadData();
});
function loadData() {
loading.value = true;
getAppointmentOrderSettings()
.then((res) => {
if (res.success && res.result) {
form.value = { ...form.value, ...res.result };
}
})
.finally(() => {
loading.value = false;
});
}
function handleSave() {
saving.value = true;
saveAppointmentOrderSettings(form.value)
.then((res) => {
if (res.success) {
ElMessage.success("保存成功");
}
})
.finally(() => {
saving.value = false;
});
}
</script>
<style scoped>

View File

@@ -43,9 +43,15 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { getManagerWorkOrderPage } from "@/api/appointment";
defineOptions({ name: "managerAppointmentWorkOrders" });
const route = useRoute();
const STATUS_OPTIONS = [
{ label: "未完成", value: "OPEN" },
{ label: "待领取", value: "PENDING_CLAIM" },
@@ -57,52 +63,45 @@ const STATUS_OPTIONS = [
{ label: "逾期未到店", value: "NO_SHOW" },
];
export default {
name: "managerAppointmentWorkOrders",
data() {
return {
loading: false,
data: [],
total: 0,
statusOptions: STATUS_OPTIONS,
searchForm: {
pageNumber: 1,
pageSize: 10,
orderSn: "",
workOrderSn: "",
serviceStatus: "",
},
};
},
mounted() {
const status = this.$route.query.serviceStatus;
if (status) {
this.searchForm.serviceStatus = status;
}
this.getData();
},
methods: {
statusText(value) {
const found = STATUS_OPTIONS.find((item) => item.value === value);
return found ? found.label : value || "—";
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getData();
},
getData() {
this.loading = true;
getManagerWorkOrderPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result?.records || [];
this.total = res.result?.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
},
};
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const statusOptions = ref(STATUS_OPTIONS);
const searchForm = ref({
pageNumber: 1,
pageSize: 10,
orderSn: "",
workOrderSn: "",
serviceStatus: "",
});
onMounted(() => {
const status = route.query.serviceStatus;
if (status) {
searchForm.value.serviceStatus = status;
}
getData();
});
function statusText(value) {
const found = STATUS_OPTIONS.find((item) => item.value === value);
return found ? found.label : value || "—";
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getData();
}
function getData() {
loading.value = true;
getManagerWorkOrderPage(searchForm.value)
.then((res) => {
if (res.success) {
data.value = res.result?.records || [];
total.value = res.result?.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
</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"
@@ -39,80 +39,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-password",
components: {
SetPassword,
},
data() {
const valideRePassword = (rule, value, callback) => {
if (value !== this.editPasswordForm.newPassword) {
callback(new Error("两次输入密码不一致"));
} else {
callback();
}
};
return {
savePassLoading: false,
editPasswordForm: {
oldPassword: "",
newPassword: "",
rePassword: "",
},
strength: "",
passwordValidate: {
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
newPassword: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, message: "请至少输入6个字符", trigger: "blur" },
{ max: 32, message: "最多输入32个字符", trigger: "blur" },
],
rePassword: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{ validator: valideRePassword, trigger: "blur" },
],
},
};
},
methods: {
changeInputPass(v, grade, strength) {
this.strength = strength;
},
saveEditPass() {
const params = {
password: this.md5(this.editPasswordForm.oldPassword),
newPassword: this.md5(this.editPasswordForm.newPassword),
passStrength: this.md5(this.strength),
};
this.$refs.editPasswordForm.validate((valid) => {
if (!valid) return;
this.savePassLoading = true;
changePass(params).then((res) => {
this.savePassLoading = false;
if (res.success) {
ElMessageBox.alert("修改密码成功,需重新登录", "修改密码成功", {
confirmButtonText: "确定",
callback: () => {
this.$store.commit("logout", this);
this.$router.push({ name: "login" });
},
});
}
});
});
},
cancelEditPass() {
this.$store.commit("removeTag", "change_password");
localStorage.pageOpenedList = JSON.stringify(this.$store.state.app.pageOpenedList);
const list = this.$store.state.app.pageOpenedList;
const lastPageName = list.length > 1 ? list[list.length - 1].name : list[0].name;
this.$router.push({ name: lastPageName });
},
},
defineOptions({ name: "change-password" });
const router = useRouter();
const appStore = useAppStore();
const userStore = useUserStore();
const editPasswordFormRef = ref(null);
const savePassLoading = ref(false);
const editPasswordForm = reactive({
oldPassword: "",
newPassword: "",
rePassword: "",
});
const strength = ref("");
const valideRePassword = (rule, value, callback) => {
if (value !== editPasswordForm.newPassword) {
callback(new Error("两次输入密码不一致"));
} else {
callback();
}
};
const passwordValidate = {
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
newPassword: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, message: "请至少输入6个字符", trigger: "blur" },
{ max: 32, message: "最多输入32个字符", trigger: "blur" },
],
rePassword: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{ validator: valideRePassword, trigger: "blur" },
],
};
function changeInputPass(v, grade, strengthVal) {
strength.value = strengthVal;
}
function saveEditPass() {
const params = {
password: md5(editPasswordForm.oldPassword),
newPassword: md5(editPasswordForm.newPassword),
passStrength: md5(strength.value),
};
editPasswordFormRef.value.validate((valid) => {
if (!valid) return;
savePassLoading.value = true;
changePass(params).then((res) => {
savePassLoading.value = false;
if (res.success) {
ElMessageBox.alert("修改密码成功,需重新登录", "修改密码成功", {
confirmButtonText: "确定",
callback: () => {
userStore.logout();
router.push({ name: "login" });
},
});
}
});
});
}
function cancelEditPass() {
appStore.removeTag("change_password");
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
const list = appStore.pageOpenedList;
const lastPageName = list.length > 1 ? list[list.length - 1].name : list[0].name;
router.push({ name: lastPageName });
}
</script>

View File

@@ -51,7 +51,7 @@
:close-on-click-modal="false"
destroy-on-close
>
<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="自定义分词" prop="name">
<el-input v-model="form.name" clearable style="width: 100%" />
</el-form-item>
@@ -64,7 +64,8 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import {
getCustomWordsPage,
delCustom,
@@ -72,160 +73,161 @@ import {
updateCustomWords,
} from "@/api/index";
import { regular } from "@/utils";
import { Message, Modal } from "@/utils/message";
export default {
name: "custom-words",
data() {
return {
loading: true,
modalType: 0,
modalVisible: false,
modalTitle: "",
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
words: "",
},
form: {
name: "",
},
formValidate: {
name: [regular.REQUIRED, regular.VARCHAR20],
},
submitLoading: false,
selectList: [],
selectCount: 0,
data: [],
total: 0,
};
},
methods: {
init() {
this.getDataList();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
this.clearSelectAll();
},
changePageSize(v) {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = v;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
clearSelectAll() {
this.$refs.table?.clearSelection();
},
changeSelect(e) {
this.selectList = e;
this.selectCount = e.length;
},
getDataList() {
this.loading = true;
getCustomWordsPage(this.searchForm)
.then((res) => {
defineOptions({ name: "custom-words" });
const table = ref(null);
const formRef = ref(null);
const loading = ref(true);
const modalType = ref(0);
const modalVisible = ref(false);
const modalTitle = ref("");
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
words: "",
});
const form = ref({
name: "",
});
const formValidate = reactive({
name: [regular.REQUIRED, regular.VARCHAR20],
});
const submitLoading = ref(false);
const selectList = ref([]);
const selectCount = ref(0);
const data = ref([]);
const total = ref(0);
// 未在 data 声明、detail() 中直接挂到 this 上的字段
let id;
function init() {
getDataList();
}
function changePage(v) {
searchForm.pageNumber = v;
getDataList();
clearSelectAll();
}
function changePageSize(v) {
searchForm.pageNumber = 1;
searchForm.pageSize = v;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function clearSelectAll() {
table.value?.clearSelection();
}
function changeSelect(e) {
selectList.value = e;
selectCount.value = e.length;
}
function getDataList() {
loading.value = true;
getCustomWordsPage(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
})
.finally(() => {
loading.value = false;
});
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true;
if (modalType.value == 0) {
delete form.value.id;
insertCustomWords(form.value).then((res) => {
submitLoading.value = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
Message.success("操作成功");
getDataList();
modalVisible.value = false;
}
})
.finally(() => {
this.loading = false;
});
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
this.submitLoading = true;
if (this.modalType == 0) {
delete this.form.id;
insertCustomWords(this.form).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
this.modalVisible = false;
}
});
} else {
this.form.id = this.id;
updateCustomWords(this.form).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
this.modalVisible = false;
}
});
} else {
form.value.id = id;
updateCustomWords(form.value).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("操作成功");
getDataList();
modalVisible.value = false;
}
});
}
}
});
}
function add() {
modalType.value = 0;
modalTitle.value = "添加";
form.value = {};
formRef.value?.resetFields();
modalVisible.value = true;
}
function detail(v) {
modalType.value = 1;
id = v.id;
modalTitle.value = "修改";
modalVisible.value = true;
form.value.name = v.name;
}
function remove(v) {
Modal.confirm({
title: "确认删除",
content: "您确认要删除 " + v.name + " ?",
loading: true,
onOk: () => {
delCustom(v.id).then((res) => {
Modal.remove();
if (res.success) {
Message.success("操作成功");
getDataList();
}
});
},
add() {
this.modalType = 0;
this.modalTitle = "添加";
this.form = {};
this.$refs.form?.resetFields();
this.modalVisible = true;
},
detail(v) {
this.modalType = 1;
this.id = v.id;
this.modalTitle = "修改";
this.modalVisible = true;
this.form.name = v.name;
},
remove(v) {
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除 " + v.name + " ?",
loading: true,
onOk: () => {
delCustom(v.id).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
});
}
function delAll() {
if (selectCount.value <= 0) {
Message.warning("您还未选择要删除的数据");
return;
}
Modal.confirm({
title: "确认删除",
content: "您确认要删除所选的 " + selectCount.value + " 条数据?",
loading: true,
onOk: () => {
let ids = "";
selectList.value.forEach(function (e) {
ids += e.id + ",";
});
ids = ids.substring(0, ids.length - 1);
delCustom(ids).then((res) => {
Modal.remove();
if (res.success) {
Message.success("操作成功");
clearSelectAll();
getDataList();
}
});
},
delAll() {
if (this.selectCount <= 0) {
this.$Message.warning("您还未选择要删除的数据");
return;
}
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除所选的 " + this.selectCount + " 条数据?",
loading: true,
onOk: () => {
let ids = "";
this.selectList.forEach(function (e) {
ids += e.id + ",";
});
ids = ids.substring(0, ids.length - 1);
delCustom(ids).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.clearSelectAll();
this.getDataList();
}
});
},
});
},
},
mounted() {
this.init();
},
};
});
}
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="100px"
@@ -420,7 +420,9 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
auditDistribution,
batchUpdateDistributionGrade,
@@ -436,469 +438,478 @@ import {
updateDistributionMember,
} from "@/api/distribution";
import { downloadBlob } from "@/utils/downloadBlob";
import { Message, Modal } from "@/utils/message";
import { unixToDate } from "@/utils/filters";
defineOptions({ name: "distribution" });
const MOBILE_PATTERN = /^1[3-9]\d{9}$/;
export default {
name: "distribution",
data() {
return {
loading: true,
currentDistributionStatus: "ALL",
joinTimeRange: [],
gradeOptions: [],
searchForm: {
pageNumber: 1,
pageSize: 20,
id: "",
mobile: "",
inviterMobile: "",
gradeId: "",
nickname: "",
},
data: [],
total: 0,
detailVisible: false,
detailLoading: false,
detail: null,
editVisible: false,
editLoading: false,
editForm: {
id: "",
mobile: "",
nickname: "",
inviterMobile: "",
gradeId: "",
},
editRules: {
nickname: [{ required: true, message: "请输入昵称", trigger: "blur" }],
gradeId: [{ required: true, message: "请选择分销等级", trigger: "change" }],
},
createVisible: false,
createLoading: false,
createForm: {
nickname: "",
mobile: "",
inviterMobile: "",
gradeId: "",
},
createRules: {
nickname: [{ required: true, message: "请输入昵称", trigger: "blur" }],
mobile: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{ pattern: MOBILE_PATTERN, message: "手机号格式有误", trigger: "blur" },
],
gradeId: [{ required: true, message: "请选择分销等级", trigger: "change" }],
},
selectedRows: [],
batchGradeVisible: false,
batchGradeLoading: false,
exportLoading: false,
templateLoading: false,
importVisible: false,
importLoading: false,
importFile: null,
importFileName: "",
batchGradeForm: {
gradeId: "",
},
batchGradeRules: {
gradeId: [{ required: true, message: "请选择分销等级", trigger: "change" }],
},
};
},
mounted() {
this.loadGradeOptions();
this.applyRouteQuery();
this.getDataList();
},
watch: {
$route() {
this.applyRouteQuery();
this.getDataList();
const route = useRoute();
const router = useRouter();
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
const searchFormRef = ref(null);
const table = ref(null);
const editFormRef = ref(null);
const createFormRef = ref(null);
const batchGradeFormRef = ref(null);
const loading = ref(true);
const currentDistributionStatus = ref("ALL");
const joinTimeRange = ref([]);
const gradeOptions = ref([]);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
id: "",
mobile: "",
inviterMobile: "",
gradeId: "",
nickname: "",
});
const data = ref([]);
const total = ref(0);
const detailVisible = ref(false);
const detailLoading = ref(false);
const detail = ref(null);
const editVisible = ref(false);
const editLoading = ref(false);
const editForm = ref({
id: "",
mobile: "",
nickname: "",
inviterMobile: "",
gradeId: "",
});
const editRules = {
nickname: [{ required: true, message: "请输入昵称", trigger: "blur" }],
gradeId: [{ required: true, message: "请选择分销等级", trigger: "change" }],
};
const createVisible = ref(false);
const createLoading = ref(false);
const createForm = ref({
nickname: "",
mobile: "",
inviterMobile: "",
gradeId: "",
});
const createRules = {
nickname: [{ required: true, message: "请输入昵称", trigger: "blur" }],
mobile: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{ pattern: MOBILE_PATTERN, message: "手机号格式有误", trigger: "blur" },
],
gradeId: [{ required: true, message: "请选择分销等级", trigger: "change" }],
};
const selectedRows = ref([]);
const batchGradeVisible = ref(false);
const batchGradeLoading = ref(false);
const exportLoading = ref(false);
const templateLoading = ref(false);
const importVisible = ref(false);
const importLoading = ref(false);
const importFile = ref(null);
const importFileName = ref("");
const batchGradeForm = reactive({
gradeId: "",
});
const batchGradeRules = {
gradeId: [{ required: true, message: "请选择分销等级", trigger: "change" }],
};
watch(
() => route.query,
() => {
applyRouteQuery();
getDataList();
}
);
function applyRouteQuery() {
const query = route.query || {};
searchForm.inviterMobile = query.inviterMobile ? String(query.inviterMobile) : "";
}
function openCustomerRelationPage() {
if (!detail.value) return;
const resolved = router.resolve({
path: "/distributionCustomerRelation",
query: {
distributionId: detail.value.id,
distributionMobile: detail.value.mobile || "",
},
},
methods: {
applyRouteQuery() {
const query = this.$route.query || {};
this.searchForm.inviterMobile = query.inviterMobile ? String(query.inviterMobile) : "";
});
window.open(resolved.href, "_blank");
}
function openInviteeDistributionPage() {
if (!detail.value) return;
const resolved = router.resolve({
path: "/distribution",
query: {
inviterMobile: detail.value.mobile || "",
},
openCustomerRelationPage() {
if (!this.detail) return;
const route = this.$router.resolve({
path: "/distributionCustomerRelation",
query: {
distributionId: this.detail.id,
distributionMobile: this.detail.mobile || "",
},
});
window.open(route.href, "_blank");
},
openInviteeDistributionPage() {
if (!this.detail) return;
const route = this.$router.resolve({
path: "/distribution",
query: {
inviterMobile: this.detail.mobile || "",
},
});
window.open(route.href, "_blank");
},
loadGradeOptions() {
getDistributionGrades().then((res) => {
});
window.open(resolved.href, "_blank");
}
function loadGradeOptions() {
getDistributionGrades().then((res) => {
if (res.success) {
gradeOptions.value = (res.result || []).slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
}
});
}
function statusLabel(status) {
const map = {
PASS: "审核通过",
APPLY: "待审核",
RETREAT: "已清退",
REFUSE: "审核拒绝",
};
return map[status] || status || "-";
}
function formatMoney(value) {
const num = Number(value);
if (Number.isNaN(num)) return "0.00";
return num.toFixed(2);
}
function onStatusTabChange() {
searchForm.pageNumber = 1;
clearSelectAll();
getDataList();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
clearSelectAll();
getDataList();
}
function changeSelect(selection) {
selectedRows.value = selection || [];
}
function clearSelectAll() {
selectedRows.value = [];
table.value?.clearSelection();
}
function openBatchGrade() {
if (!selectedRows.value.length) {
Message.warning("请先选择分销员");
return;
}
batchGradeForm.gradeId = "";
batchGradeVisible.value = true;
}
function resetBatchGradeForm() {
batchGradeLoading.value = false;
batchGradeForm.gradeId = "";
batchGradeFormRef.value?.clearValidate();
}
function submitBatchGrade() {
batchGradeFormRef.value.validate((valid) => {
if (!valid) return;
batchGradeLoading.value = true;
batchUpdateDistributionGrade({
ids: selectedRows.value.map((item) => item.id),
gradeId: batchGradeForm.gradeId,
})
.then((res) => {
batchGradeLoading.value = false;
if (res.success) {
this.gradeOptions = (res.result || []).slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
Message.success("设置成功");
batchGradeVisible.value = false;
clearSelectAll();
getDataList();
}
})
.catch(() => {
batchGradeLoading.value = false;
});
});
}
function handleExport() {
exportLoading.value = true;
const params = buildParams();
delete params.pageNumber;
delete params.pageSize;
exportDistributionMembers(params)
.then((blob) => {
if (blob && blob.type && blob.type.includes("application/json")) {
const reader = new FileReader();
reader.onload = () => {
try {
const result = JSON.parse(reader.result);
Message.error(result.message || "导出失败");
} catch (e) {
Message.error("导出失败");
}
};
reader.readAsText(blob);
return;
}
downloadBlob(blob, `分销员列表_${Date.now()}.xlsx`);
Message.success("导出成功");
})
.catch(() => {
Message.error("导出失败");
})
.finally(() => {
exportLoading.value = false;
});
}
function downloadImportTemplate() {
templateLoading.value = true;
downloadDistributionImportTemplate()
.then((blob) => {
downloadBlob(blob, "分销员批量导入模板.xlsx");
Message.success("模板已下载");
})
.catch(() => {
Message.error("模板下载失败");
})
.finally(() => {
templateLoading.value = false;
});
}
function openImportDialog() {
importVisible.value = true;
}
function resetImportDialog() {
importLoading.value = false;
importFile.value = null;
importFileName.value = "";
}
function onImportBeforeUpload(file) {
importFile.value = file;
importFileName.value = file.name;
return false;
}
function submitImport() {
if (!importFile.value) {
Message.warning("请先选择文件");
return;
}
const fd = new FormData();
fd.append("file", importFile.value);
importLoading.value = true;
importDistributionMembers(fd)
.then((res) => {
importLoading.value = false;
if (res.success) {
const result = res.result || {};
Message.success(
`导入完成:成功 ${result.successCount || 0} 条,失败 ${result.failCount || 0} 条`
);
importVisible.value = false;
getDataList();
}
})
.catch(() => {
importLoading.value = false;
});
}
function goImportRecord() {
router.push({
path: "/distributionImportRecord",
});
}
function buildParams() {
const params = {
pageNumber: searchForm.pageNumber,
pageSize: searchForm.pageSize,
};
if (currentDistributionStatus.value && currentDistributionStatus.value !== "ALL") {
params.distributionStatus = currentDistributionStatus.value;
}
if (searchForm.id) params.id = searchForm.id.trim();
if (searchForm.mobile) params.mobile = searchForm.mobile;
if (searchForm.inviterMobile) params.inviterMobile = searchForm.inviterMobile;
if (searchForm.gradeId) params.gradeId = searchForm.gradeId;
if (searchForm.nickname) params.nickname = searchForm.nickname;
if (joinTimeRange.value && joinTimeRange.value[0] && joinTimeRange.value[1]) {
const startTime = new Date(joinTimeRange.value[0]).getTime();
const endTime = new Date(joinTimeRange.value[1]).getTime();
params.joinTimeStart = unixToDate(startTime / 1000);
params.joinTimeEnd = unixToDate(endTime / 1000);
}
return params;
}
function getDataList() {
loading.value = true;
getDistributionMemberPage(buildParams())
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
})
.catch(() => {
loading.value = false;
});
}
function openDetail(row) {
detailVisible.value = true;
detailLoading.value = true;
detail.value = null;
getDistributionMemberDetail(row.id)
.then((res) => {
detailLoading.value = false;
if (res.success) {
detail.value = res.result;
}
})
.catch(() => {
detailLoading.value = false;
});
}
function openCreate() {
createForm.value = {
nickname: "",
mobile: "",
inviterMobile: "",
gradeId: "",
};
createVisible.value = true;
}
function resetCreateForm() {
createLoading.value = false;
createForm.value = {
nickname: "",
mobile: "",
inviterMobile: "",
gradeId: "",
};
createFormRef.value?.clearValidate();
}
function submitCreate() {
createFormRef.value.validate((valid) => {
if (!valid) return;
createLoading.value = true;
createDistributionMember({
nickname: createForm.value.nickname,
mobile: createForm.value.mobile,
inviterMobile: createForm.value.inviterMobile || "",
gradeId: createForm.value.gradeId,
})
.then((res) => {
createLoading.value = false;
if (res.success) {
Message.success("新增成功");
createVisible.value = false;
getDataList();
}
})
.catch(() => {
createLoading.value = false;
});
});
}
function openEdit(row) {
editForm.value = {
id: row.id,
mobile: row.mobile || "",
nickname: row.nickname || "",
inviterMobile: row.inviterMobile || "",
gradeId: resolveGradeOptionId(row.gradeId),
};
editVisible.value = true;
}
function resolveGradeOptionId(gradeRef) {
if (!gradeRef || !gradeOptions.value.length) return gradeRef || "";
const direct = gradeOptions.value.find((item) => item.id === gradeRef);
if (direct) return direct.id;
const bySort = gradeOptions.value.find((item) => String(item.sortOrder) === String(gradeRef));
return bySort ? bySort.id : gradeRef;
}
function resetEditForm() {
editLoading.value = false;
editForm.value = {
id: "",
mobile: "",
nickname: "",
inviterMobile: "",
gradeId: "",
};
editFormRef.value?.clearValidate();
}
function submitEdit() {
editFormRef.value.validate((valid) => {
if (!valid) return;
editLoading.value = true;
updateDistributionMember(editForm.value.id, {
nickname: editForm.value.nickname,
inviterMobile: editForm.value.inviterMobile || "",
gradeId: editForm.value.gradeId,
})
.then((res) => {
editLoading.value = false;
if (res.success) {
Message.success("保存成功");
editVisible.value = false;
getDataList();
}
})
.catch(() => {
editLoading.value = false;
});
});
}
function audit(row, status) {
const text = status === "PASS" ? "通过" : "拒绝";
Modal.confirm({
title: "审核确认",
content: `确认${text}该分销员申请?`,
loading: true,
onOk: () => {
auditDistribution(row.id, { status }).then((res) => {
Modal.remove();
if (res.success) {
Message.success("操作成功");
getDataList();
}
});
},
statusLabel(status) {
const map = {
PASS: "审核通过",
APPLY: "待审核",
RETREAT: "已清退",
REFUSE: "审核拒绝",
};
return map[status] || status || "-";
},
formatMoney(value) {
const num = Number(value);
if (Number.isNaN(num)) return "0.00";
return num.toFixed(2);
},
onStatusTabChange() {
this.searchForm.pageNumber = 1;
this.clearSelectAll();
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.clearSelectAll();
this.getDataList();
},
changeSelect(selection) {
this.selectedRows = selection || [];
},
clearSelectAll() {
this.selectedRows = [];
this.$refs.table?.clearSelection();
},
openBatchGrade() {
if (!this.selectedRows.length) {
this.$Message.warning("请先选择分销员");
return;
}
this.batchGradeForm.gradeId = "";
this.batchGradeVisible = true;
},
resetBatchGradeForm() {
this.batchGradeLoading = false;
this.batchGradeForm.gradeId = "";
this.$refs.batchGradeFormRef?.clearValidate();
},
submitBatchGrade() {
this.$refs.batchGradeFormRef.validate((valid) => {
if (!valid) return;
this.batchGradeLoading = true;
batchUpdateDistributionGrade({
ids: this.selectedRows.map((item) => item.id),
gradeId: this.batchGradeForm.gradeId,
})
.then((res) => {
this.batchGradeLoading = false;
if (res.success) {
this.$Message.success("设置成功");
this.batchGradeVisible = false;
this.clearSelectAll();
this.getDataList();
}
})
.catch(() => {
this.batchGradeLoading = false;
});
});
}
function retreat(row) {
Modal.confirm({
title: "清退确认",
content: "确认清退该分销员?清退后将停止后续计佣。",
onOk: () => {
retreatDistribution(row.id).then((res) => {
if (res.success) {
Message.success("操作成功");
getDataList();
}
});
},
handleExport() {
this.exportLoading = true;
const params = this.buildParams();
delete params.pageNumber;
delete params.pageSize;
exportDistributionMembers(params)
.then((blob) => {
if (blob && blob.type && blob.type.includes("application/json")) {
const reader = new FileReader();
reader.onload = () => {
try {
const result = JSON.parse(reader.result);
this.$Message.error(result.message || "导出失败");
} catch (e) {
this.$Message.error("导出失败");
}
};
reader.readAsText(blob);
return;
}
downloadBlob(blob, `分销员列表_${Date.now()}.xlsx`);
this.$Message.success("导出成功");
})
.catch(() => {
this.$Message.error("导出失败");
})
.finally(() => {
this.exportLoading = false;
});
},
downloadImportTemplate() {
this.templateLoading = true;
downloadDistributionImportTemplate()
.then((blob) => {
downloadBlob(blob, "分销员批量导入模板.xlsx");
this.$Message.success("模板已下载");
})
.catch(() => {
this.$Message.error("模板下载失败");
})
.finally(() => {
this.templateLoading = false;
});
},
openImportDialog() {
this.importVisible = true;
},
resetImportDialog() {
this.importLoading = false;
this.importFile = null;
this.importFileName = "";
},
onImportBeforeUpload(file) {
this.importFile = file;
this.importFileName = file.name;
return false;
},
submitImport() {
if (!this.importFile) {
this.$Message.warning("请先选择文件");
return;
}
const fd = new FormData();
fd.append("file", this.importFile);
this.importLoading = true;
importDistributionMembers(fd)
.then((res) => {
this.importLoading = false;
if (res.success) {
const result = res.result || {};
this.$Message.success(
`导入完成:成功 ${result.successCount || 0} 条,失败 ${result.failCount || 0} 条`
);
this.importVisible = false;
this.getDataList();
}
})
.catch(() => {
this.importLoading = false;
});
},
goImportRecord() {
this.$router.push({
path: "/distributionImportRecord",
});
}
function resume(row) {
Modal.confirm({
title: "恢复确认",
content: "确认恢复该分销员?",
onOk: () => {
resumeDistribution(row.id).then((res) => {
if (res.success) {
Message.success("操作成功");
getDataList();
}
});
},
buildParams() {
const params = {
pageNumber: this.searchForm.pageNumber,
pageSize: this.searchForm.pageSize,
};
if (this.currentDistributionStatus && this.currentDistributionStatus !== "ALL") {
params.distributionStatus = this.currentDistributionStatus;
}
if (this.searchForm.id) params.id = this.searchForm.id.trim();
if (this.searchForm.mobile) params.mobile = this.searchForm.mobile;
if (this.searchForm.inviterMobile) params.inviterMobile = this.searchForm.inviterMobile;
if (this.searchForm.gradeId) params.gradeId = this.searchForm.gradeId;
if (this.searchForm.nickname) params.nickname = this.searchForm.nickname;
if (this.joinTimeRange && this.joinTimeRange[0] && this.joinTimeRange[1]) {
const startTime = new Date(this.joinTimeRange[0]).getTime();
const endTime = new Date(this.joinTimeRange[1]).getTime();
params.joinTimeStart = this.$filters.unixToDate(startTime / 1000);
params.joinTimeEnd = this.$filters.unixToDate(endTime / 1000);
}
return params;
},
getDataList() {
this.loading = true;
getDistributionMemberPage(this.buildParams())
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
})
.catch(() => {
this.loading = false;
});
},
openDetail(row) {
this.detailVisible = true;
this.detailLoading = true;
this.detail = null;
getDistributionMemberDetail(row.id)
.then((res) => {
this.detailLoading = false;
if (res.success) {
this.detail = res.result;
}
})
.catch(() => {
this.detailLoading = false;
});
},
openCreate() {
this.createForm = {
nickname: "",
mobile: "",
inviterMobile: "",
gradeId: "",
};
this.createVisible = true;
},
resetCreateForm() {
this.createLoading = false;
this.createForm = {
nickname: "",
mobile: "",
inviterMobile: "",
gradeId: "",
};
this.$refs.createFormRef?.clearValidate();
},
submitCreate() {
this.$refs.createFormRef.validate((valid) => {
if (!valid) return;
this.createLoading = true;
createDistributionMember({
nickname: this.createForm.nickname,
mobile: this.createForm.mobile,
inviterMobile: this.createForm.inviterMobile || "",
gradeId: this.createForm.gradeId,
})
.then((res) => {
this.createLoading = false;
if (res.success) {
this.$Message.success("新增成功");
this.createVisible = false;
this.getDataList();
}
})
.catch(() => {
this.createLoading = false;
});
});
},
openEdit(row) {
this.editForm = {
id: row.id,
mobile: row.mobile || "",
nickname: row.nickname || "",
inviterMobile: row.inviterMobile || "",
gradeId: this.resolveGradeOptionId(row.gradeId),
};
this.editVisible = true;
},
resolveGradeOptionId(gradeRef) {
if (!gradeRef || !this.gradeOptions.length) return gradeRef || "";
const direct = this.gradeOptions.find((item) => item.id === gradeRef);
if (direct) return direct.id;
const bySort = this.gradeOptions.find((item) => String(item.sortOrder) === String(gradeRef));
return bySort ? bySort.id : gradeRef;
},
resetEditForm() {
this.editLoading = false;
this.editForm = {
id: "",
mobile: "",
nickname: "",
inviterMobile: "",
gradeId: "",
};
this.$refs.editFormRef?.clearValidate();
},
submitEdit() {
this.$refs.editFormRef.validate((valid) => {
if (!valid) return;
this.editLoading = true;
updateDistributionMember(this.editForm.id, {
nickname: this.editForm.nickname,
inviterMobile: this.editForm.inviterMobile || "",
gradeId: this.editForm.gradeId,
})
.then((res) => {
this.editLoading = false;
if (res.success) {
this.$Message.success("保存成功");
this.editVisible = false;
this.getDataList();
}
})
.catch(() => {
this.editLoading = false;
});
});
},
audit(row, status) {
const text = status === "PASS" ? "通过" : "拒绝";
this.$Modal.confirm({
title: "审核确认",
content: `确认${text}该分销员申请?`,
loading: true,
onOk: () => {
auditDistribution(row.id, { status }).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
});
},
retreat(row) {
this.$Modal.confirm({
title: "清退确认",
content: "确认清退该分销员?清退后将停止后续计佣。",
onOk: () => {
retreatDistribution(row.id).then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
});
},
resume(row) {
this.$Modal.confirm({
title: "恢复确认",
content: "确认恢复该分销员?",
onOk: () => {
resumeDistribution(row.id).then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
});
},
},
};
});
}
onMounted(() => {
loadGradeOptions();
applyRouteQuery();
getDataList();
});
</script>
<style scoped lang="scss">

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
class="search-form"
@keyup.enter="handleSearch"
@@ -173,144 +173,147 @@
</div>
</template>
<script>
<script setup>
import { ref, computed, onMounted } from "vue";
import { useRoute } from "vue-router";
import { auditDistributionCash, getDistributionCash } from "@/api/distribution";
import { Message, Modal } from "@/utils/message";
export default {
name: "distributionCash",
data() {
return {
tabStatus: "",
applyCount: 0,
loading: true,
drawerVisible: false,
drawerTitle: "",
result: "VIA_AUDITING",
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
},
handleStatus: "edit",
form: {
sn: "",
distributionId: "",
distributionName: "",
price: "",
name: "",
settlementBankAccountName: "",
settlementBankAccountNum: "",
},
submitLoading: false,
data: [],
total: 0,
};
},
computed: {
applyCountLabel() {
return this.applyCount > 0 ? `待处理(${this.applyCount})` : "待处理";
},
},
methods: {
init() {
this.getDataList();
this.loadApplyCount();
},
loadApplyCount() {
getDistributionCash({
distributionCashStatus: "APPLY",
pageSize: 1,
pageNumber: 1,
}).then((res) => {
if (res.success) {
this.applyCount = res.result.total || 0;
}
});
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
getDataList() {
this.loading = true;
const params = { ...this.searchForm };
if (this.tabStatus) {
params.distributionCashStatus = this.tabStatus;
}
getDistributionCash(params).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
handleTabChange(name) {
this.tabStatus = name;
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSubmit() {
let result = this.result == "VIA_AUDITING" ? "通过" : "拒绝";
this.$Modal.confirm({
title: "确认审核",
content: "您确认要审核" + result + "么?",
loading: true,
onOk: () => {
auditDistributionCash(this.form.id, { result: this.result }).then((res) => {
if (res.success) {
this.$Modal.remove();
this.$Message.success("审核成功");
this.drawerVisible = false;
this.getDataList();
this.loadApplyCount();
} else {
this.$Modal.remove();
}
});
},
});
},
edit(v) {
this.drawerTitle = "审核";
this.handleStatus = "edit";
this.result = "VIA_AUDITING";
const data = {};
for (let attr in v) {
data[attr] = v[attr] === null ? "" : v[attr];
}
this.form = data;
this.drawerVisible = true;
},
view(v) {
this.drawerTitle = "查看";
this.handleStatus = "view";
const data = {};
for (let attr in v) {
data[attr] = v[attr] === null ? "" : v[attr];
}
this.form = data;
this.drawerVisible = true;
},
},
mounted() {
// 处理路由参数,自动跳转到指定标签页
const tabStatus = this.$route.query.tabStatus;
if (tabStatus) {
this.tabStatus = tabStatus;
defineOptions({ name: "distributionCash" });
const route = useRoute();
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
const searchFormRef = ref(null);
const table = ref(null);
const tabStatus = ref("");
const applyCount = ref(0);
const loading = ref(true);
const drawerVisible = ref(false);
const drawerTitle = ref("");
const result = ref("VIA_AUDITING");
const searchForm = ref({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
});
const handleStatus = ref("edit");
const form = ref({
sn: "",
distributionId: "",
distributionName: "",
price: "",
name: "",
settlementBankAccountName: "",
settlementBankAccountNum: "",
});
const submitLoading = ref(false);
const data = ref([]);
const total = ref(0);
const applyCountLabel = computed(() => {
return applyCount.value > 0 ? `待处理(${applyCount.value})` : "待处理";
});
function init() {
getDataList();
loadApplyCount();
}
function loadApplyCount() {
getDistributionCash({
distributionCashStatus: "APPLY",
pageSize: 1,
pageNumber: 1,
}).then((res) => {
if (res.success) {
applyCount.value = res.result.total || 0;
}
this.init();
},
};
});
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.value.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.value.pageNumber = 1;
searchForm.value.pageSize = 20;
getDataList();
}
function getDataList() {
loading.value = true;
const params = { ...searchForm.value };
if (tabStatus.value) {
params.distributionCashStatus = tabStatus.value;
}
getDistributionCash(params).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function handleTabChange(name) {
tabStatus.value = name;
searchForm.value.pageNumber = 1;
getDataList();
}
function handleSubmit() {
let resultText = result.value == "VIA_AUDITING" ? "通过" : "拒绝";
Modal.confirm({
title: "确认审核",
content: "您确认要审核" + resultText + "么?",
loading: true,
onOk: () => {
auditDistributionCash(form.value.id, { result: result.value }).then((res) => {
if (res.success) {
Modal.remove();
Message.success("审核成功");
drawerVisible.value = false;
getDataList();
loadApplyCount();
} else {
Modal.remove();
}
});
},
});
}
function edit(v) {
drawerTitle.value = "审核";
handleStatus.value = "edit";
result.value = "VIA_AUDITING";
const formData = {};
for (let attr in v) {
formData[attr] = v[attr] === null ? "" : v[attr];
}
form.value = formData;
drawerVisible.value = true;
}
function view(v) {
drawerTitle.value = "查看";
handleStatus.value = "view";
const formData = {};
for (let attr in v) {
formData[attr] = v[attr] === null ? "" : v[attr];
}
form.value = formData;
drawerVisible.value = true;
}
onMounted(() => {
// 处理路由参数,自动跳转到指定标签页
const queryTabStatus = route.query.tabStatus;
if (queryTabStatus) {
tabStatus.value = queryTabStatus;
}
init();
});
</script>
<style lang="scss" scoped>
.ops a {

View File

@@ -0,0 +1,359 @@
<template>
<div class="search">
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="客户绑定" name="customer">
<el-card>
<el-form ref="searchFormRef" :model="searchForm" inline class="search-form" @keyup.enter="handleSearch">
<el-form-item label="客户昵称">
<el-input v-model="searchForm.memberName" clearable placeholder="客户昵称" style="width: 180px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="searchForm.memberMobile" clearable placeholder="手机号" style="width: 180px" />
</el-form-item>
<el-form-item label="分销员">
<el-input v-model="searchForm.distributionName" clearable placeholder="分销员名称" style="width: 180px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" clearable style="width: 120px">
<el-option v-for="item in bindStatusList" :key="item.value" :value="item.value" :label="item.label" />
</el-select>
</el-form-item>
<el-form-item label="来源">
<el-select v-model="searchForm.bindSource" clearable style="width: 140px">
<el-option v-for="item in bindSourceList" :key="item.value" :value="item.value" :label="item.label" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-table v-loading="loading" border :data="data" class="mt_10">
<el-table-column prop="memberName" label="客户昵称" min-width="120" />
<el-table-column prop="memberMobile" label="手机号" min-width="120" />
<el-table-column prop="distributionName" label="归属分销员" min-width="120" />
<el-table-column label="绑定来源" min-width="100">
<template #default="{ row }">{{ bindSourceLabel(row.bindSource) }}</template>
</el-table-column>
<el-table-column label="绑定类型" min-width="90">
<template #default="{ row }">
{{ row.bindType === 'PERMANENT' ? '永久' : '短期' }}
</template>
</el-table-column>
<el-table-column prop="bindStartTime" label="绑定时间" min-width="160" />
<el-table-column prop="bindExpireTime" label="到期时间" min-width="160" />
<el-table-column label="保护期剩余" min-width="100">
<template #default="{ row }">
<span v-if="row.protectRemainDays != null">{{ row.protectRemainDays }} 天</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag v-if="row.status === 'ACTIVE'" type="success">有效</el-tag>
<el-tag v-else type="info">已过期</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }">
<a class="link-text" @click="openRebind(row)">换绑</a>
<span class="split">|</span>
<a class="link-text" @click="viewMemberLog(row)">日志</a>
</template>
</el-table-column>
</el-table>
<div class="mt_10 page padding-row" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="getDataList"
@size-change="handlePageSizeChange"
/>
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="绑定日志" name="log">
<el-card>
<el-form ref="logSearchFormRef" :model="logSearchForm" inline class="search-form" @keyup.enter="handleLogSearch">
<el-form-item label="客户昵称">
<el-input v-model="logSearchForm.memberName" clearable placeholder="客户昵称" style="width: 180px" />
</el-form-item>
<el-form-item label="变更类型">
<el-select v-model="logSearchForm.changeType" clearable style="width: 140px">
<el-option v-for="item in bindChangeTypeList" :key="item.value" :value="item.value" :label="item.label" />
</el-select>
</el-form-item>
<el-form-item label="时间">
<el-date-picker
v-model="logDateRange"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始"
end-placeholder="结束"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogSearch">搜索</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-table v-loading="logLoading" border :data="logData" class="mt_10">
<el-table-column prop="createTime" label="时间" min-width="160" />
<el-table-column prop="memberName" label="客户" min-width="100" />
<el-table-column prop="fromDistributionName" label="原分销员" min-width="110" />
<el-table-column prop="toDistributionName" label="新分销员" min-width="110" />
<el-table-column label="变更类型" min-width="100">
<template #default="{ row }">{{ bindChangeTypeLabel(row.changeType) }}</template>
</el-table-column>
<el-table-column prop="operatorType" label="操作方" width="90" />
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
</el-table>
<div class="mt_10 page padding-row" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="logSearchForm.pageNumber"
v-model:page-size="logSearchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="logTotal"
layout="total, sizes, prev, pager, next, jumper"
@current-change="getLogList"
@size-change="handleLogPageSizeChange"
/>
</div>
</el-card>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="rebindVisible" title="手动换绑" width="520px">
<el-form ref="rebindFormRef" :model="rebindForm" :rules="rebindRules" label-width="100px">
<el-form-item label="客户">
<el-input :model-value="rebindForm.memberLabel" disabled />
</el-form-item>
<el-form-item label="新分销员" prop="distributionId">
<el-select
v-model="rebindForm.distributionId"
filterable
remote
reserve-keyword
placeholder="输入分销员名称搜索"
:remote-method="searchDistributor"
:loading="distributorLoading"
style="width: 100%"
>
<el-option
v-for="item in distributorOptions"
:key="item.id"
:label="item.memberName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="rebindForm.remark" type="textarea" :rows="3" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rebindVisible = false">取消</el-button>
<el-button type="primary" :loading="rebindSubmitting" @click="submitRebind">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import {
getDistributionCustomerPage,
getDistributionBindLogPage,
getDistributionListData,
rebindDistributionCustomer,
} from "@/api/distribution";
import { bindStatusList, bindSourceList, bindChangeTypeList } from "./dataJson";
import { Message } from "@/utils/message";
defineOptions({ name: "distributionCustomer" });
// 模板 el-form 的 ref(原 ref="searchForm"/"logSearchForm",与 data 同名会互相覆盖,改名为 searchFormRef/logSearchFormRef)
const searchFormRef = ref(null);
const logSearchFormRef = ref(null);
const rebindFormRef = ref(null);
const activeTab = ref("customer");
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "bindStartTime",
order: "desc",
memberName: "",
memberMobile: "",
distributionName: "",
status: "",
bindSource: "",
});
const logLoading = ref(false);
const logData = ref([]);
const logTotal = ref(0);
const logDateRange = ref([]);
const logSearchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
memberName: "",
memberId: "",
changeType: "",
startDate: "",
endDate: "",
});
const rebindVisible = ref(false);
const rebindSubmitting = ref(false);
const distributorLoading = ref(false);
const distributorOptions = ref([]);
const rebindForm = ref({
memberId: "",
memberLabel: "",
distributionId: "",
remark: "",
});
const rebindRules = {
distributionId: [{ required: true, message: "请选择分销员", trigger: "change" }],
};
function bindSourceLabel(value) {
const item = bindSourceList.find((i) => i.value === value);
return item ? item.label : value;
}
function bindChangeTypeLabel(value) {
const item = bindChangeTypeList.find((i) => i.value === value);
return item ? item.label : value;
}
function handleTabChange(name) {
if (name === "log" && logData.value.length === 0) {
getLogList();
}
}
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function handlePageSizeChange() {
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
getDistributionCustomerPage(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function handleLogSearch() {
logSearchForm.pageNumber = 1;
if (logDateRange.value && logDateRange.value.length === 2) {
logSearchForm.startDate = logDateRange.value[0];
logSearchForm.endDate = logDateRange.value[1];
} else {
logSearchForm.startDate = "";
logSearchForm.endDate = "";
}
getLogList();
}
function handleLogPageSizeChange() {
logSearchForm.pageNumber = 1;
getLogList();
}
function getLogList() {
logLoading.value = true;
getDistributionBindLogPage(logSearchForm).then((res) => {
logLoading.value = false;
if (res.success) {
logData.value = res.result.records;
logTotal.value = res.result.total;
}
});
}
function openRebind(row) {
rebindForm.value = {
memberId: row.memberId,
memberLabel: `${row.memberName || ""}${row.memberMobile ? " / " + row.memberMobile : ""}`,
distributionId: "",
remark: "",
};
distributorOptions.value = [];
rebindVisible.value = true;
}
function searchDistributor(query) {
if (!query) {
distributorOptions.value = [];
return;
}
distributorLoading.value = true;
getDistributionListData({
memberName: query,
distributionStatus: "PASS",
pageNumber: 1,
pageSize: 20,
}).then((res) => {
distributorLoading.value = false;
if (res.success) {
distributorOptions.value = (res.result.records || []).filter((item) => item.id !== rebindForm.value.distributionId);
}
});
}
function submitRebind() {
rebindFormRef.value.validate((valid) => {
if (!valid) return;
rebindSubmitting.value = true;
rebindDistributionCustomer({
memberId: rebindForm.value.memberId,
distributionId: rebindForm.value.distributionId,
remark: rebindForm.value.remark,
}).then((res) => {
rebindSubmitting.value = false;
if (res.success) {
Message.success("换绑成功");
rebindVisible.value = false;
getDataList();
}
}).catch(() => {
rebindSubmitting.value = false;
});
});
}
function viewMemberLog(row) {
activeTab.value = "log";
logSearchForm.memberId = row.memberId;
logSearchForm.memberName = "";
logSearchForm.pageNumber = 1;
getLogList();
}
onMounted(() => {
getDataList();
});
</script>
<style lang="scss" scoped>
.link-text {
color: #2d8cf0;
cursor: pointer;
}
.split {
margin: 0 8px;
color: #dcdee2;
}
</style>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="110px"
@@ -250,7 +250,7 @@
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="rebindForm" :model="rebindForm" :rules="rebindRules" label-width="110px">
<el-form ref="rebindFormRef" :model="rebindForm" :rules="rebindRules" label-width="110px">
<el-form-item label="客户">
<span>{{ rebindForm.memberNickname || rebindForm.memberMobile || rebindForm.memberId }}</span>
</el-form-item>
@@ -288,7 +288,7 @@
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="unbindForm" :model="unbindForm" label-width="110px">
<el-form ref="unbindFormRef" :model="unbindForm" label-width="110px">
<el-form-item label="客户">
<span>{{ unbindForm.memberNickname || unbindForm.memberMobile || unbindForm.memberId }}</span>
</el-form-item>
@@ -304,7 +304,9 @@
</div>
</template>
<script>
<script setup>
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import {
getCustomerRelationDetail,
getCustomerRelationLogs,
@@ -315,259 +317,260 @@ import {
import { relationEffectiveList, hasDeadlineList } from "./dataJson.js";
import DistributionSelector from "@/components/distribution/distribution-selector.vue";
import { QuestionFilled } from "@element-plus/icons-vue";
import { Message } from "@/utils/message";
import { unixToDate } from "@/utils/filters";
export default {
name: "distributionCustomerRelation",
components: { QuestionFilled, DistributionSelector },
data() {
return {
relationEffectiveList,
hasDeadlineList,
bindTimeRange: [],
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
memberMobile: "",
distributionMobile: "",
distributionId: "",
relationEffective: "",
hasDeadline: "",
},
data: [],
total: 0,
detailVisible: false,
detailLoading: false,
detail: null,
logVisible: false,
logLoading: false,
logList: [],
logTarget: null,
rebindVisible: false,
distributorSelectorVisible: false,
unbindVisible: false,
rebindLoading: false,
unbindLoading: false,
rebindForm: {
memberId: "",
memberNickname: "",
memberMobile: "",
currentDistributionId: "",
distributionId: "",
distributionNickname: "",
distributionMobile: "",
reason: "",
},
unbindForm: {
memberId: "",
memberNickname: "",
memberMobile: "",
reason: "",
},
rebindRules: {
distributionId: [{
required: true,
message: "请选择分销员",
trigger: "change",
}],
},
};
},
computed: {
selectedDistributorLabel() {
if (!this.rebindForm.distributionId) return "";
const name = this.rebindForm.distributionNickname || "-";
const mobile = this.rebindForm.distributionMobile || "-";
return `${name}(${mobile})`;
},
},
mounted() {
this.applyRouteQuery();
this.getDataList();
},
watch: {
$route() {
this.applyRouteQuery();
this.getDataList();
},
},
methods: {
applyRouteQuery() {
const query = this.$route.query || {};
this.searchForm.distributionId = query.distributionId ? String(query.distributionId) : "";
this.searchForm.distributionMobile = query.distributionMobile ? String(query.distributionMobile) : "";
},
formatInvalidTime(row) {
return row?.invalidTime || "-";
},
formatDetailInvalidTime(detail) {
if (!detail || this.isRelationEffective(detail)) return "-";
return detail.invalidTime || "-";
},
formatDetailInvalidReason(detail) {
if (!detail || this.isRelationEffective(detail)) return "-";
return detail.invalidReason || "客户关系失效";
},
isRelationEffective(row) {
if (!row) return false;
if (row.relationEffective === true) return true;
if (row.relationEffective === false) return false;
return !["EXPIRED", "UNBOUND"].includes(row.relationStatus);
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
const params = { ...this.searchForm };
if (!params.memberMobile) delete params.memberMobile;
if (!params.distributionMobile) delete params.distributionMobile;
if (!params.distributionId) delete params.distributionId;
if (params.relationEffective === "" || params.relationEffective === null || params.relationEffective === undefined) {
delete params.relationEffective;
} else {
params.relationEffective = params.relationEffective === true || params.relationEffective === "true";
defineOptions({ name: "distributionCustomerRelation" });
const route = useRoute();
const searchFormRef = ref(null);
const rebindFormRef = ref(null);
const unbindFormRef = ref(null);
const bindTimeRange = ref([]);
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
memberMobile: "",
distributionMobile: "",
distributionId: "",
relationEffective: "",
hasDeadline: "",
});
const data = ref([]);
const total = ref(0);
const detailVisible = ref(false);
const detailLoading = ref(false);
const detail = ref(null);
const logVisible = ref(false);
const logLoading = ref(false);
const logList = ref([]);
const logTarget = ref(null);
const rebindVisible = ref(false);
const distributorSelectorVisible = ref(false);
const unbindVisible = ref(false);
const rebindLoading = ref(false);
const unbindLoading = ref(false);
const rebindForm = ref({
memberId: "",
memberNickname: "",
memberMobile: "",
currentDistributionId: "",
distributionId: "",
distributionNickname: "",
distributionMobile: "",
reason: "",
});
const unbindForm = ref({
memberId: "",
memberNickname: "",
memberMobile: "",
reason: "",
});
const rebindRules = reactive({
distributionId: [{
required: true,
message: "请选择分销员",
trigger: "change",
}],
});
const selectedDistributorLabel = computed(() => {
if (!rebindForm.value.distributionId) return "";
const name = rebindForm.value.distributionNickname || "-";
const mobile = rebindForm.value.distributionMobile || "-";
return `${name}(${mobile})`;
});
watch(
() => route.query,
() => {
applyRouteQuery();
getDataList();
}
);
onMounted(() => {
applyRouteQuery();
getDataList();
});
function applyRouteQuery() {
const query = route.query || {};
searchForm.distributionId = query.distributionId ? String(query.distributionId) : "";
searchForm.distributionMobile = query.distributionMobile ? String(query.distributionMobile) : "";
}
function formatInvalidTime(row) {
return row?.invalidTime || "-";
}
function formatDetailInvalidTime(detail) {
if (!detail || isRelationEffective(detail)) return "-";
return detail.invalidTime || "-";
}
function formatDetailInvalidReason(detail) {
if (!detail || isRelationEffective(detail)) return "-";
return detail.invalidReason || "客户关系失效";
}
function isRelationEffective(row) {
if (!row) return false;
if (row.relationEffective === true) return true;
if (row.relationEffective === false) return false;
return !["EXPIRED", "UNBOUND"].includes(row.relationStatus);
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
const params = { ...searchForm };
if (!params.memberMobile) delete params.memberMobile;
if (!params.distributionMobile) delete params.distributionMobile;
if (!params.distributionId) delete params.distributionId;
if (params.relationEffective === "" || params.relationEffective === null || params.relationEffective === undefined) {
delete params.relationEffective;
} else {
params.relationEffective = params.relationEffective === true || params.relationEffective === "true";
}
if (params.hasDeadline === "" || params.hasDeadline === null || params.hasDeadline === undefined) {
delete params.hasDeadline;
} else {
params.hasDeadline = params.hasDeadline === true || params.hasDeadline === "true";
}
if (bindTimeRange.value && bindTimeRange.value[0] && bindTimeRange.value[1]) {
const startTime = new Date(bindTimeRange.value[0]).getTime();
const endTime = new Date(bindTimeRange.value[1]).getTime();
params.bindTimeStart = unixToDate(startTime / 1000);
params.bindTimeEnd = unixToDate(endTime / 1000);
}
getCustomerRelations(params).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function openDetail(row) {
detailVisible.value = true;
detailLoading.value = true;
detail.value = null;
getCustomerRelationDetail(row.id).then((res) => {
detailLoading.value = false;
if (res.success) {
detail.value = res.result;
}
}).catch(() => {
detailLoading.value = false;
});
}
function openLogs(row) {
logVisible.value = true;
logTarget.value = row;
logLoading.value = true;
logList.value = [];
getCustomerRelationLogs(row.id).then((res) => {
logLoading.value = false;
if (res.success) {
logList.value = res.result || [];
}
}).catch(() => {
logLoading.value = false;
});
}
function formatDistributorCell(nickname, mobile, id) {
if ((!nickname || nickname === "-") && !id) return "-";
const name = nickname && nickname !== "-" ? nickname : id;
if (mobile) return `${name}\n${mobile}`;
return name;
}
function openRebind(row) {
rebindForm.value = {
memberId: row.memberId,
memberNickname: row.memberNickname,
memberMobile: row.memberMobile,
currentDistributionId: row.distributionId || "",
distributionId: "",
distributionNickname: "",
distributionMobile: "",
reason: "",
};
rebindVisible.value = true;
}
function onDistributorSelected(row) {
rebindForm.value.distributionId = row.id;
rebindForm.value.distributionNickname = row.nickname;
rebindForm.value.distributionMobile = row.mobile;
nextTick(() => {
rebindFormRef.value?.validateField("distributionId");
});
}
function clearSelectedDistributor() {
rebindForm.value.distributionId = "";
rebindForm.value.distributionNickname = "";
rebindForm.value.distributionMobile = "";
nextTick(() => {
rebindFormRef.value?.validateField("distributionId");
});
}
function openUnbind(row) {
unbindForm.value = {
memberId: row.memberId,
memberNickname: row.memberNickname,
memberMobile: row.memberMobile,
reason: "",
};
unbindVisible.value = true;
}
function submitRebind() {
rebindFormRef.value.validate((valid) => {
if (!valid) return;
if (rebindForm.value.distributionId === rebindForm.value.currentDistributionId) {
Message.warning("新分销员不能与当前分销员相同");
return;
}
rebindLoading.value = true;
rebindCustomerRelation({
memberId: rebindForm.value.memberId,
distributionId: rebindForm.value.distributionId,
reason: rebindForm.value.reason,
}).then((res) => {
rebindLoading.value = false;
if (res.success) {
Message.success("操作成功");
rebindVisible.value = false;
getDataList();
}
if (params.hasDeadline === "" || params.hasDeadline === null || params.hasDeadline === undefined) {
delete params.hasDeadline;
} else {
params.hasDeadline = params.hasDeadline === true || params.hasDeadline === "true";
}
if (this.bindTimeRange && this.bindTimeRange[0] && this.bindTimeRange[1]) {
const startTime = new Date(this.bindTimeRange[0]).getTime();
const endTime = new Date(this.bindTimeRange[1]).getTime();
params.bindTimeStart = this.$filters.unixToDate(startTime / 1000);
params.bindTimeEnd = this.$filters.unixToDate(endTime / 1000);
}
getCustomerRelations(params).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
openDetail(row) {
this.detailVisible = true;
this.detailLoading = true;
this.detail = null;
getCustomerRelationDetail(row.id).then((res) => {
this.detailLoading = false;
if (res.success) {
this.detail = res.result;
}
}).catch(() => {
this.detailLoading = false;
});
},
openLogs(row) {
this.logVisible = true;
this.logTarget = row;
this.logLoading = true;
this.logList = [];
getCustomerRelationLogs(row.id).then((res) => {
this.logLoading = false;
if (res.success) {
this.logList = res.result || [];
}
}).catch(() => {
this.logLoading = false;
});
},
formatDistributorCell(nickname, mobile, id) {
if ((!nickname || nickname === "-") && !id) return "-";
const name = nickname && nickname !== "-" ? nickname : id;
if (mobile) return `${name}\n${mobile}`;
return name;
},
openRebind(row) {
this.rebindForm = {
memberId: row.memberId,
memberNickname: row.memberNickname,
memberMobile: row.memberMobile,
currentDistributionId: row.distributionId || "",
distributionId: "",
distributionNickname: "",
distributionMobile: "",
reason: "",
};
this.rebindVisible = true;
},
onDistributorSelected(row) {
this.rebindForm.distributionId = row.id;
this.rebindForm.distributionNickname = row.nickname;
this.rebindForm.distributionMobile = row.mobile;
this.$nextTick(() => {
this.$refs.rebindForm?.validateField("distributionId");
});
},
clearSelectedDistributor() {
this.rebindForm.distributionId = "";
this.rebindForm.distributionNickname = "";
this.rebindForm.distributionMobile = "";
this.$nextTick(() => {
this.$refs.rebindForm?.validateField("distributionId");
});
},
openUnbind(row) {
this.unbindForm = {
memberId: row.memberId,
memberNickname: row.memberNickname,
memberMobile: row.memberMobile,
reason: "",
};
this.unbindVisible = true;
},
submitRebind() {
this.$refs.rebindForm.validate((valid) => {
if (!valid) return;
if (this.rebindForm.distributionId === this.rebindForm.currentDistributionId) {
this.$Message.warning("新分销员不能与当前分销员相同");
return;
}
this.rebindLoading = true;
rebindCustomerRelation({
memberId: this.rebindForm.memberId,
distributionId: this.rebindForm.distributionId,
reason: this.rebindForm.reason,
}).then((res) => {
this.rebindLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.rebindVisible = false;
this.getDataList();
}
}).catch(() => {
this.rebindLoading = false;
});
});
},
submitUnbind() {
this.unbindLoading = true;
unbindCustomerRelation({
memberId: this.unbindForm.memberId,
reason: this.unbindForm.reason,
}).then((res) => {
this.unbindLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.unbindVisible = false;
this.getDataList();
}
}).catch(() => {
this.unbindLoading = false;
});
},
},
};
}).catch(() => {
rebindLoading.value = false;
});
});
}
function submitUnbind() {
unbindLoading.value = true;
unbindCustomerRelation({
memberId: unbindForm.value.memberId,
reason: unbindForm.value.reason,
}).then((res) => {
unbindLoading.value = false;
if (res.success) {
Message.success("操作成功");
unbindVisible.value = false;
getDataList();
}
}).catch(() => {
unbindLoading.value = false;
});
}
</script>
<style scoped lang="scss">

View File

@@ -2,7 +2,7 @@
<div>
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -189,206 +189,206 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, computed, onMounted } from "vue";
import { auditDistributionGoods, delDistributionGoods, getDistributionGoods } from "@/api/distribution";
import { getBrandListData, getCategoryTree } from "@/api/goods";
import { Message, Modal } from "@/utils/message";
export default {
name: "distributionGoods",
data() {
return {
loading: true,
currentAuthStatus: "ALL",
auditModalVisible: false,
auditLoading: false,
auditTargetIds: [],
auditForm: {
authStatus: "PASS",
authRemark: "",
},
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
goodsName: "",
storeName: "",
categoryId: "",
brandId: "",
authStatus: "",
},
categoryList: [],
categoryProps: {
value: "id",
label: "name",
children: "children",
checkStrictly: true,
emitPath: false,
},
brandList: [],
selectList: [],
selectCount: 0,
data: [],
total: 0,
};
},
computed: {
hasPendingAuditSelection() {
return this.selectList.some((item) => item.authStatus === "TOBEAUDITED");
},
},
methods: {
authStatusText(value) {
if (value === "TOBEAUDITED") return "待审核";
if (value === "REFUSE") return "审核未通过";
return "已审核";
},
authStatusTagType(value) {
if (value === "TOBEAUDITED") return "warning";
if (value === "REFUSE") return "danger";
return "success";
},
onAuthStatusChange(name) {
this.searchForm.authStatus = name === "ALL" ? "" : name;
this.searchForm.pageNumber = 1;
this.getDataList();
this.clearSelectAll();
},
openAudit(row) {
this.auditTargetIds = [row.id];
this.auditForm = { authStatus: "PASS", authRemark: "" };
this.auditModalVisible = true;
},
openBatchAudit() {
const pendingRows = this.selectList.filter((item) => item.authStatus === "TOBEAUDITED");
if (!pendingRows.length) {
this.$Message.warning("请选择待审核的分销商品");
return;
defineOptions({ name: "distributionGoods" });
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
const searchFormRef = ref(null);
const table = ref(null);
const loading = ref(true);
const currentAuthStatus = ref("ALL");
const auditModalVisible = ref(false);
const auditLoading = ref(false);
const auditTargetIds = ref([]);
const auditForm = ref({
authStatus: "PASS",
authRemark: "",
});
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
goodsName: "",
storeName: "",
categoryId: "",
brandId: "",
authStatus: "",
});
const categoryList = ref([]);
const categoryProps = reactive({
value: "id",
label: "name",
children: "children",
checkStrictly: true,
emitPath: false,
});
const brandList = ref([]);
const selectList = ref([]);
const selectCount = ref(0);
const data = ref([]);
const total = ref(0);
const hasPendingAuditSelection = computed(() =>
selectList.value.some((item) => item.authStatus === "TOBEAUDITED")
);
function authStatusText(value) {
if (value === "TOBEAUDITED") return "待审核";
if (value === "REFUSE") return "审核未通过";
return "已审核";
}
function authStatusTagType(value) {
if (value === "TOBEAUDITED") return "warning";
if (value === "REFUSE") return "danger";
return "success";
}
function onAuthStatusChange(name) {
searchForm.authStatus = name === "ALL" ? "" : name;
searchForm.pageNumber = 1;
getDataList();
clearSelectAll();
}
function openAudit(row) {
auditTargetIds.value = [row.id];
auditForm.value = { authStatus: "PASS", authRemark: "" };
auditModalVisible.value = true;
}
function openBatchAudit() {
const pendingRows = selectList.value.filter((item) => item.authStatus === "TOBEAUDITED");
if (!pendingRows.length) {
Message.warning("请选择待审核的分销商品");
return;
}
auditTargetIds.value = pendingRows.map((item) => item.id);
auditForm.value = { authStatus: "PASS", authRemark: "" };
auditModalVisible.value = true;
}
function submitAudit() {
if (auditForm.value.authStatus === "REFUSE" && !auditForm.value.authRemark.trim()) {
Message.warning("请填写驳回原因");
return;
}
auditLoading.value = true;
auditDistributionGoods({
ids: auditTargetIds.value,
authStatus: auditForm.value.authStatus,
authRemark: auditForm.value.authRemark.trim(),
})
.then((res) => {
if (res.success) {
Message.success("审核成功");
auditModalVisible.value = false;
clearSelectAll();
getDataList();
}
this.auditTargetIds = pendingRows.map((item) => item.id);
this.auditForm = { authStatus: "PASS", authRemark: "" };
this.auditModalVisible = true;
},
submitAudit() {
if (this.auditForm.authStatus === "REFUSE" && !this.auditForm.authRemark.trim()) {
this.$Message.warning("请填写驳回原因");
return;
})
.finally(() => {
auditLoading.value = false;
});
}
function goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
}
function init() {
loadFilterOptions();
getDataList();
}
function loadFilterOptions() {
getCategoryTree().then((res) => {
if (res.success) {
categoryList.value = res.result || [];
}
});
getBrandListData().then((res) => {
brandList.value = Array.isArray(res) ? res : res.result || [];
});
}
function changePage(v) {
searchForm.pageNumber = v;
getDataList();
clearSelectAll();
}
function changePageSize(v) {
searchForm.pageSize = v;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
}
function clearSelectAll() {
table.value?.clearSelection();
}
function changeSelect(e) {
selectList.value = e;
selectCount.value = e.length;
}
function getDataList() {
loading.value = true;
getDistributionGoods(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
this.auditLoading = true;
auditDistributionGoods({
ids: this.auditTargetIds,
authStatus: this.auditForm.authStatus,
authRemark: this.auditForm.authRemark.trim(),
})
.then((res) => {
if (res.success) {
this.$Message.success("审核成功");
this.auditModalVisible = false;
this.clearSelectAll();
this.getDataList();
}
})
.finally(() => {
this.auditLoading = false;
});
},
goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
},
init() {
this.loadFilterOptions();
this.getDataList();
},
loadFilterOptions() {
getCategoryTree().then((res) => {
})
.finally(() => {
loading.value = false;
});
}
function remove(v) {
Modal.confirm({
title: "确认下架",
content: "您确认要下架么?",
loading: true,
onOk: () => {
delDistributionGoods(v.id).then((res) => {
Modal.remove();
if (res.success) {
this.categoryList = res.result || [];
Message.success("下架成功");
getDataList();
}
});
getBrandListData().then((res) => {
this.brandList = Array.isArray(res) ? res : res.result || [];
},
});
}
function delAll() {
if (selectCount.value <= 0) {
Message.warning("您还未选择要下架的数据");
return;
}
Modal.confirm({
title: "确认下架",
content: "您确认要下架所选的 " + selectCount.value + " 条数据?",
loading: true,
onOk: () => {
const ids = selectList.value.map((item) => item.id);
delDistributionGoods(ids.toString()).then((res) => {
Modal.remove();
if (res.success) {
Message.success("下架成功");
clearSelectAll();
getDataList();
}
});
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
this.clearSelectAll();
},
changePageSize(v) {
this.searchForm.pageSize = v;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
clearSelectAll() {
this.$refs.table?.clearSelection();
},
changeSelect(e) {
this.selectList = e;
this.selectCount = e.length;
},
getDataList() {
this.loading = true;
getDistributionGoods(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
})
.finally(() => {
this.loading = false;
});
},
remove(v) {
this.$Modal.confirm({
title: "确认下架",
content: "您确认要下架么?",
loading: true,
onOk: () => {
delDistributionGoods(v.id).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("下架成功");
this.getDataList();
}
});
},
});
},
delAll() {
if (this.selectCount <= 0) {
this.$Message.warning("您还未选择要下架的数据");
return;
}
this.$Modal.confirm({
title: "确认下架",
content: "您确认要下架所选的 " + this.selectCount + " 条数据?",
loading: true,
onOk: () => {
const ids = this.selectList.map((item) => item.id);
delDistributionGoods(ids.toString()).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("下架成功");
this.clearSelectAll();
this.getDataList();
}
});
},
});
},
},
mounted() {
this.init();
},
};
});
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -108,12 +108,16 @@
</div>
</template>
<script>
<script setup>
import { ref, onMounted } from "vue";
import {
getDistributionGrades,
saveAllDistributionGrades,
deleteDistributionGrade,
} from "@/api/distribution";
import { Message, Modal } from "@/utils/message";
defineOptions({ name: "distributionGrade" });
const MAX_GRADE_COUNT = 5;
const GRADE_VALUE_OPTIONS = [1, 2, 3, 4, 5];
@@ -154,252 +158,244 @@ const buildDefaultRow = (sortOrder = null) => {
};
};
export default {
name: "distributionGrade",
data() {
return {
loading: true,
saveLoading: false,
data: [],
};
},
mounted() {
this.getDataList();
},
methods: {
normalizeRow(row, index) {
const normalized = {
...buildDefaultRow(row.sortOrder),
...row,
enableSalesCondition: row.enableSalesCondition !== false,
enableCommissionCondition: row.enableCommissionCondition !== false,
enableInviteCondition: row.enableInviteCondition !== false,
goodsCommissionRate: toPercent(row.goodsCommissionRate),
inviteCommissionRate: toPercent(row.inviteCommissionRate),
_key: row._key || row.id || `grade_${index}_${Date.now()}`,
};
if (this.isBaseGrade(normalized)) {
normalized.enableSalesCondition = false;
normalized.enableCommissionCondition = false;
normalized.enableInviteCondition = false;
if (!normalized.gradeName || !normalized.gradeName.trim()) {
normalized.gradeName = BASE_GRADE_NAME;
}
const loading = ref(true);
const saveLoading = ref(false);
const data = ref([]);
function normalizeRow(row, index) {
const normalized = {
...buildDefaultRow(row.sortOrder),
...row,
enableSalesCondition: row.enableSalesCondition !== false,
enableCommissionCondition: row.enableCommissionCondition !== false,
enableInviteCondition: row.enableInviteCondition !== false,
goodsCommissionRate: toPercent(row.goodsCommissionRate),
inviteCommissionRate: toPercent(row.inviteCommissionRate),
_key: row._key || row.id || `grade_${index}_${Date.now()}`,
};
if (isBaseGrade(normalized)) {
normalized.enableSalesCondition = false;
normalized.enableCommissionCondition = false;
normalized.enableInviteCondition = false;
if (!normalized.gradeName || !normalized.gradeName.trim()) {
normalized.gradeName = BASE_GRADE_NAME;
}
}
return normalized;
}
function isBaseGrade(row) {
return row && row.sortOrder === BASE_GRADE_VALUE;
}
function getSortedRows() {
return data.value.slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
}
function getGradeLabel(row) {
return row.gradeName && row.gradeName.trim() ? row.gradeName.trim() : `等级值${row.sortOrder}`;
}
function getNextGradeValue() {
const used = new Set(data.value.map((item) => item.sortOrder));
return GRADE_VALUE_OPTIONS.find((value) => value !== BASE_GRADE_VALUE && !used.has(value)) || null;
}
function getDataList() {
loading.value = true;
getDistributionGrades().then((res) => {
loading.value = false;
if (res.success) {
data.value = (res.result || [])
.slice()
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
.map((item, index) => normalizeRow(item, index));
}
});
}
function addRow() {
if (data.value.length >= MAX_GRADE_COUNT) {
Message.warning("最多添加5个分销等级");
return;
}
const nextGradeValue = getNextGradeValue();
if (!nextGradeValue) {
Message.warning("等级值1-5已全部使用");
return;
}
data.value.push(buildDefaultRow(nextGradeValue));
}
function validateGradeProgression() {
const rows = getSortedRows();
for (let i = 1; i < rows.length; i++) {
const lower = rows[i - 1];
const higher = rows[i];
const lowerLabel = getGradeLabel(lower);
const higherLabel = getGradeLabel(higher);
if (lower.enableSalesCondition) {
if (!higher.enableSalesCondition) {
Message.warning(`${higherLabel}必须启用${lowerLabel}已启用的销售额升级条件`);
return false;
}
return normalized;
},
isBaseGrade(row) {
return row && row.sortOrder === BASE_GRADE_VALUE;
},
getSortedRows() {
return this.data.slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
},
getGradeLabel(row) {
return row.gradeName && row.gradeName.trim() ? row.gradeName.trim() : `等级值${row.sortOrder}`;
},
getNextGradeValue() {
const used = new Set(this.data.map((item) => item.sortOrder));
return GRADE_VALUE_OPTIONS.find((value) => value !== BASE_GRADE_VALUE && !used.has(value)) || null;
},
getDataList() {
this.loading = true;
getDistributionGrades().then((res) => {
this.loading = false;
if (Number(higher.salesThreshold || 0) < Number(lower.salesThreshold || 0)) {
Message.warning(`${higherLabel}销售额阈值不能小于${lowerLabel}`);
return false;
}
}
if (lower.enableCommissionCondition) {
if (!higher.enableCommissionCondition) {
Message.warning(`${higherLabel}必须启用${lowerLabel}已启用的收益额升级条件`);
return false;
}
if (Number(higher.commissionThreshold || 0) < Number(lower.commissionThreshold || 0)) {
Message.warning(`${higherLabel}收益额阈值不能小于${lowerLabel}`);
return false;
}
}
if (lower.enableInviteCondition) {
if (!higher.enableInviteCondition) {
Message.warning(`${higherLabel}必须启用${lowerLabel}已启用的邀请人数升级条件`);
return false;
}
if (Number(higher.inviteThreshold || 0) < Number(lower.inviteThreshold || 0)) {
Message.warning(`${higherLabel}邀请人数阈值不能小于${lowerLabel}`);
return false;
}
}
}
return true;
}
function validateRows() {
if (!data.value.length) {
Message.warning("请至少添加一个分销等级");
return false;
}
const usedValues = [];
for (let i = 0; i < data.value.length; i++) {
const row = data.value[i];
if (!row.sortOrder) {
Message.warning(`请填写第${i + 1}行等级值`);
return false;
}
if (usedValues.includes(row.sortOrder)) {
Message.warning("等级值不能重复");
return false;
}
usedValues.push(row.sortOrder);
if (!row.gradeName || !row.gradeName.trim()) {
Message.warning(`请填写第${i + 1}行等级名`);
return false;
}
if (isBaseGrade(row)) {
continue;
}
if (!row.enableSalesCondition && !row.enableCommissionCondition && !row.enableInviteCondition) {
Message.warning(`第${i + 1}行请至少启用一项升级规则`);
return false;
}
const rowLabel = getGradeLabel(row);
if (row.enableSalesCondition && Number(row.salesThreshold || 0) <= 0) {
Message.warning(`${rowLabel}销售额必须大于0`);
return false;
}
if (row.enableCommissionCondition && Number(row.commissionThreshold || 0) <= 0) {
Message.warning(`${rowLabel}收益额必须大于0`);
return false;
}
if (row.enableInviteCondition && Number(row.inviteThreshold || 0) <= 0) {
Message.warning(`${rowLabel}邀请人数必须大于0`);
return false;
}
}
return validateGradeProgression() && validateCrossRates();
}
function validateCrossRates() {
let maxGoods = 0;
let maxInvite = 0;
for (const row of data.value) {
maxGoods = Math.max(maxGoods, Number(row.goodsCommissionRate || 0));
maxInvite = Math.max(maxInvite, Number(row.inviteCommissionRate || 0));
}
if (maxGoods + maxInvite > 100) {
Message.warning(
`所有等级中最高佣金比例(${maxGoods}%)与最高邀请奖励(${maxInvite}%)之和不能超过100%`
);
return false;
}
for (const row of data.value) {
const goods = Number(row.goodsCommissionRate || 0);
const invite = Number(row.inviteCommissionRate || 0);
if (goods + invite > 100) {
Message.warning(`${getGradeLabel(row)}的佣金比例与邀请奖励之和不能超过100%`);
return false;
}
}
return true;
}
function buildPayload(row) {
const base = isBaseGrade(row);
return {
id: row.id || undefined,
gradeName: row.gradeName.trim(),
sortOrder: row.sortOrder,
salesThreshold: row.salesThreshold,
commissionThreshold: row.commissionThreshold,
inviteThreshold: row.inviteThreshold,
enableSalesCondition: base ? false : row.enableSalesCondition,
enableCommissionCondition: base ? false : row.enableCommissionCondition,
enableInviteCondition: base ? false : row.enableInviteCondition,
goodsCommissionRate: toFraction(row.goodsCommissionRate),
inviteCommissionRate: toFraction(row.inviteCommissionRate),
};
}
async function saveAll() {
if (!validateRows()) {
return;
}
saveLoading.value = true;
try {
const payload = getSortedRows().map((row) => buildPayload(row));
const res = await saveAllDistributionGrades(payload);
if (res.success) {
Message.success("保存成功");
getDataList();
}
} catch (e) {
// request layer handles error message
} finally {
saveLoading.value = false;
}
}
function remove(row, index) {
if (isBaseGrade(row)) {
Message.warning("默认分销等级不能删除");
return;
}
const name = row.gradeName || `等级值${row.sortOrder || ""}`;
if (!row.id) {
Modal.confirm({
title: "确认删除",
content: `您确认要删除 ${name} 吗?`,
onOk: () => {
data.value.splice(index, 1);
},
});
return;
}
Modal.confirm({
title: "确认删除",
content: `您确认要删除 ${name} 吗?`,
loading: true,
onOk: () => {
deleteDistributionGrade(row.id).then((res) => {
Modal.remove();
if (res.success) {
this.data = (res.result || [])
.slice()
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
.map((item, index) => this.normalizeRow(item, index));
Message.success("删除成功");
getDataList();
}
});
},
addRow() {
if (this.data.length >= MAX_GRADE_COUNT) {
this.$Message.warning("最多添加5个分销等级");
return;
}
const nextGradeValue = this.getNextGradeValue();
if (!nextGradeValue) {
this.$Message.warning("等级值1-5已全部使用");
return;
}
this.data.push(buildDefaultRow(nextGradeValue));
},
validateGradeProgression() {
const rows = this.getSortedRows();
for (let i = 1; i < rows.length; i++) {
const lower = rows[i - 1];
const higher = rows[i];
const lowerLabel = this.getGradeLabel(lower);
const higherLabel = this.getGradeLabel(higher);
if (lower.enableSalesCondition) {
if (!higher.enableSalesCondition) {
this.$Message.warning(`${higherLabel}必须启用${lowerLabel}已启用的销售额升级条件`);
return false;
}
if (Number(higher.salesThreshold || 0) < Number(lower.salesThreshold || 0)) {
this.$Message.warning(`${higherLabel}销售额阈值不能小于${lowerLabel}`);
return false;
}
}
if (lower.enableCommissionCondition) {
if (!higher.enableCommissionCondition) {
this.$Message.warning(`${higherLabel}必须启用${lowerLabel}已启用的收益额升级条件`);
return false;
}
if (Number(higher.commissionThreshold || 0) < Number(lower.commissionThreshold || 0)) {
this.$Message.warning(`${higherLabel}收益额阈值不能小于${lowerLabel}`);
return false;
}
}
if (lower.enableInviteCondition) {
if (!higher.enableInviteCondition) {
this.$Message.warning(`${higherLabel}必须启用${lowerLabel}已启用的邀请人数升级条件`);
return false;
}
if (Number(higher.inviteThreshold || 0) < Number(lower.inviteThreshold || 0)) {
this.$Message.warning(`${higherLabel}邀请人数阈值不能小于${lowerLabel}`);
return false;
}
}
}
return true;
},
validateRows() {
if (!this.data.length) {
this.$Message.warning("请至少添加一个分销等级");
return false;
}
const usedValues = [];
for (let i = 0; i < this.data.length; i++) {
const row = this.data[i];
if (!row.sortOrder) {
this.$Message.warning(`请填写第${i + 1}行等级值`);
return false;
}
if (usedValues.includes(row.sortOrder)) {
this.$Message.warning("等级值不能重复");
return false;
}
usedValues.push(row.sortOrder);
if (!row.gradeName || !row.gradeName.trim()) {
this.$Message.warning(`请填写第${i + 1}行等级名`);
return false;
}
if (this.isBaseGrade(row)) {
continue;
}
if (!row.enableSalesCondition && !row.enableCommissionCondition && !row.enableInviteCondition) {
this.$Message.warning(`第${i + 1}行请至少启用一项升级规则`);
return false;
}
const rowLabel = this.getGradeLabel(row);
if (row.enableSalesCondition && Number(row.salesThreshold || 0) <= 0) {
this.$Message.warning(`${rowLabel}销售额必须大于0`);
return false;
}
if (row.enableCommissionCondition && Number(row.commissionThreshold || 0) <= 0) {
this.$Message.warning(`${rowLabel}收益额必须大于0`);
return false;
}
if (row.enableInviteCondition && Number(row.inviteThreshold || 0) <= 0) {
this.$Message.warning(`${rowLabel}邀请人数必须大于0`);
return false;
}
}
return this.validateGradeProgression() && this.validateCrossRates();
},
validateCrossRates() {
let maxGoods = 0;
let maxInvite = 0;
for (const row of this.data) {
maxGoods = Math.max(maxGoods, Number(row.goodsCommissionRate || 0));
maxInvite = Math.max(maxInvite, Number(row.inviteCommissionRate || 0));
}
if (maxGoods + maxInvite > 100) {
this.$Message.warning(
`所有等级中最高佣金比例(${maxGoods}%)与最高邀请奖励(${maxInvite}%)之和不能超过100%`
);
return false;
}
for (const row of this.data) {
const goods = Number(row.goodsCommissionRate || 0);
const invite = Number(row.inviteCommissionRate || 0);
if (goods + invite > 100) {
this.$Message.warning(`${this.getGradeLabel(row)}的佣金比例与邀请奖励之和不能超过100%`);
return false;
}
}
return true;
},
buildPayload(row) {
const isBaseGrade = this.isBaseGrade(row);
return {
id: row.id || undefined,
gradeName: row.gradeName.trim(),
sortOrder: row.sortOrder,
salesThreshold: row.salesThreshold,
commissionThreshold: row.commissionThreshold,
inviteThreshold: row.inviteThreshold,
enableSalesCondition: isBaseGrade ? false : row.enableSalesCondition,
enableCommissionCondition: isBaseGrade ? false : row.enableCommissionCondition,
enableInviteCondition: isBaseGrade ? false : row.enableInviteCondition,
goodsCommissionRate: toFraction(row.goodsCommissionRate),
inviteCommissionRate: toFraction(row.inviteCommissionRate),
};
},
async saveAll() {
if (!this.validateRows()) {
return;
}
this.saveLoading = true;
try {
const payload = this.getSortedRows().map((row) => this.buildPayload(row));
const res = await saveAllDistributionGrades(payload);
if (res.success) {
this.$Message.success("保存成功");
this.getDataList();
}
} catch (e) {
// request layer handles error message
} finally {
this.saveLoading = false;
}
},
remove(row, index) {
if (this.isBaseGrade(row)) {
this.$Message.warning("默认分销等级不能删除");
return;
}
const name = row.gradeName || `等级值${row.sortOrder || ""}`;
if (!row.id) {
this.$Modal.confirm({
title: "确认删除",
content: `您确认要删除 ${name} 吗?`,
onOk: () => {
this.data.splice(index, 1);
},
});
return;
}
this.$Modal.confirm({
title: "确认删除",
content: `您确认要删除 ${name} 吗?`,
loading: true,
onOk: () => {
deleteDistributionGrade(row.id).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("删除成功");
this.getDataList();
}
});
},
});
},
},
};
</script>
});
}
onMounted(() => {
getDataList();
});</script>
<style scoped lang="scss">
.grade-footer {

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="110px"
@@ -101,115 +101,114 @@
</div>
</template>
<script>
<script setup>
import { ref, onMounted } from "vue";
import { getDistributionGradeHistoryPage, getDistributionGrades } from "@/api/distribution";
export default {
name: "distributionGradeHistory",
data() {
return {
loading: false,
searchForm: {
pageNumber: 1,
pageSize: 20,
memberName: "",
distributionMobile: "",
gradeId: "",
upgradeTimeStart: "",
upgradeTimeEnd: "",
},
upgradeTimeRange: [],
gradeOptions: [],
data: [],
total: 0,
};
},
mounted() {
this.loadGradeOptions();
this.getDataList();
},
methods: {
loadGradeOptions() {
getDistributionGrades()
.then((res) => {
if (!res.success) {
return;
}
this.gradeOptions = (res.result || [])
.slice()
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
.map((item) => ({
value: String(item.sortOrder),
label: item.gradeName || `等级值${item.sortOrder}`,
}));
});
},
formatGrade(name, gradeId) {
if (name) {
return name;
defineOptions({ name: "distributionGradeHistory" });
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
const searchFormRef = ref(null);
const loading = ref(false);
const searchForm = ref({
pageNumber: 1,
pageSize: 20,
memberName: "",
distributionMobile: "",
gradeId: "",
upgradeTimeStart: "",
upgradeTimeEnd: "",
});
const upgradeTimeRange = ref([]);
const gradeOptions = ref([]);
const data = ref([]);
const total = ref(0);
onMounted(() => {
loadGradeOptions();
getDataList();
});
function loadGradeOptions() {
getDistributionGrades()
.then((res) => {
if (!res.success) {
return;
}
if (gradeId) {
return `等级值${gradeId}`;
gradeOptions.value = (res.result || [])
.slice()
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
.map((item) => ({
value: String(item.sortOrder),
label: item.gradeName || `等级值${item.sortOrder}`,
}));
});
}
function formatGrade(name, gradeId) {
if (name) {
return name;
}
if (gradeId) {
return `等级值${gradeId}`;
}
return "-";
}
function formatRate(value) {
const num = Number(value);
if (!Number.isFinite(num)) {
return "-";
}
return `${(num * 100).toFixed(2).replace(/\.?0+$/, "") || "0"}%`;
}
function applyUpgradeTimeRange() {
if (upgradeTimeRange.value && upgradeTimeRange.value.length === 2) {
searchForm.value.upgradeTimeStart = `${upgradeTimeRange.value[0]} 00:00:00`;
searchForm.value.upgradeTimeEnd = `${upgradeTimeRange.value[1]} 23:59:59`;
} else {
searchForm.value.upgradeTimeStart = "";
searchForm.value.upgradeTimeEnd = "";
}
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getDataList();
}
function handleReset() {
searchForm.value = {
pageNumber: 1,
pageSize: 20,
memberName: "",
distributionMobile: "",
gradeId: "",
upgradeTimeStart: "",
upgradeTimeEnd: "",
};
upgradeTimeRange.value = [];
getDataList();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.value.pageNumber = 1;
getDataList();
}
function getDataList() {
applyUpgradeTimeRange();
loading.value = true;
getDistributionGradeHistoryPage(searchForm.value)
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
return "-";
},
formatRate(value) {
const num = Number(value);
if (!Number.isFinite(num)) {
return "-";
}
return `${(num * 100).toFixed(2).replace(/\.?0+$/, "") || "0"}%`;
},
applyUpgradeTimeRange() {
if (this.upgradeTimeRange && this.upgradeTimeRange.length === 2) {
this.searchForm.upgradeTimeStart = `${this.upgradeTimeRange[0]} 00:00:00`;
this.searchForm.upgradeTimeEnd = `${this.upgradeTimeRange[1]} 23:59:59`;
} else {
this.searchForm.upgradeTimeStart = "";
this.searchForm.upgradeTimeEnd = "";
}
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 20,
memberName: "",
distributionMobile: "",
gradeId: "",
upgradeTimeStart: "",
upgradeTimeEnd: "",
};
this.upgradeTimeRange = [];
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.applyUpgradeTimeRange();
this.loading = true;
getDistributionGradeHistoryPage(this.searchForm)
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.catch(() => {
this.loading = false;
});
},
},
};
})
.catch(() => {
loading.value = false;
});
}
</script>
<style scoped>

View File

@@ -43,85 +43,82 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import {
downloadDistributionImportFailItems,
getDistributionImportBatches,
} from "@/api/distribution";
import { downloadBlob } from "@/utils/downloadBlob";
import { Message } from "@/utils/message";
export default {
name: "distributionImportRecord",
data() {
return {
loading: false,
searchForm: {
pageNumber: 1,
pageSize: 20,
},
data: [],
total: 0,
};
},
mounted() {
this.getDataList();
},
methods: {
statusLabel(status) {
const map = {
PROCESSING: "处理中",
DONE: "已完成",
FAILED: "失败",
};
return map[status] || status || "-";
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getDistributionImportBatches(this.searchForm)
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
defineOptions({ name: "distributionImportRecord" });
const loading = ref(false);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
});
const data = ref([]);
const total = ref(0);
onMounted(() => {
getDataList();
});
function statusLabel(status) {
const map = {
PROCESSING: "处理中",
DONE: "已完成",
FAILED: "失败",
};
return map[status] || status || "-";
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
getDistributionImportBatches(searchForm)
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
})
.catch(() => {
loading.value = false;
});
}
function downloadFailList(row) {
if (!row || !row.failCount) return;
downloadDistributionImportFailItems(row.id)
.then((blob) => {
if (blob && blob.type && blob.type.includes("application/json")) {
const reader = new FileReader();
reader.onload = () => {
try {
const result = JSON.parse(reader.result);
Message.error(result.message || "下载失败");
} catch (e) {
Message.error("下载失败");
}
})
.catch(() => {
this.loading = false;
});
},
downloadFailList(row) {
if (!row || !row.failCount) return;
downloadDistributionImportFailItems(row.id)
.then((blob) => {
if (blob && blob.type && blob.type.includes("application/json")) {
const reader = new FileReader();
reader.onload = () => {
try {
const result = JSON.parse(reader.result);
this.$Message.error(result.message || "下载失败");
} catch (e) {
this.$Message.error("下载失败");
}
};
reader.readAsText(blob);
return;
}
downloadBlob(blob, `分销员导入失败列表_${row.id}.xlsx`);
this.$Message.success("下载成功");
})
.catch(() => {
this.$Message.error("下载失败");
});
},
},
};
};
reader.readAsText(blob);
return;
}
downloadBlob(blob, `分销员导入失败列表_${row.id}.xlsx`);
Message.success("下载成功");
})
.catch(() => {
Message.error("下载失败");
});
}
</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"
@@ -159,135 +159,140 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, watch, onMounted } from "vue";
import { useRoute } from "vue-router";
import { getDistributionOrder } from "@/api/distribution";
import { orderStatusList } from "./dataJson";
import { getShopListData } from "@/api/shops";
import { unixToDate, unitPrice } from "@/utils/filters";
export default {
name: "distributionOrder",
data() {
return {
timeRange: [],
orderStatusList,
shopList: [],
distributionId: this.$route.query.id,
currentStatus: "ALL",
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
distributionOrderStatus: "",
},
data: [],
total: 0,
};
},
watch: {
$route(e) {
this.distributionId = e.query.id ? e.query.id : undefined;
this.getDataList();
},
},
methods: {
init() {
this.getDataList();
this.getShopList();
},
onStatusTabChange(name) {
this.currentStatus = name;
this.searchForm.distributionOrderStatus = name === "ALL" ? "" : name;
this.searchForm.pageNumber = 1;
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
getDataList() {
if (this.distributionId) {
this.searchForm.distributionId = this.distributionId;
} else {
delete this.searchForm.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);
} else {
this.searchForm.startTime = null;
this.searchForm.endTime = null;
}
getDistributionOrder(this.searchForm).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
getShopList(val) {
const params = {
pageNumber: 1,
pageSize: 20,
storeName: val || "",
};
getShopListData(params).then((res) => {
this.shopList = res.result.records;
});
},
searchChange(val) {
this.getShopList(val);
},
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";
},
formatCommission(amount) {
if (amount === null || amount === undefined || amount === "") {
return "-";
}
return this.$filters.unitPrice(amount, "¥");
},
},
mounted() {
this.init();
},
};
defineOptions({ name: "distributionOrder" });
const route = useRoute();
// 模板 el-form / el-table 的 ref(原 ref="searchForm" 与 data 同名会互相覆盖,改名为 searchFormRef)
const searchFormRef = ref(null);
const table = ref(null);
const timeRange = ref([]);
const shopList = ref([]);
const distributionId = ref(route.query.id);
const currentStatus = ref("ALL");
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
distributionOrderStatus: "",
});
const data = ref([]);
const total = ref(0);
watch(
() => route.query,
(query) => {
distributionId.value = query.id ? query.id : undefined;
getDataList();
}
);
function init() {
getDataList();
getShopList();
}
function onStatusTabChange(name) {
currentStatus.value = name;
searchForm.distributionOrderStatus = name === "ALL" ? "" : name;
searchForm.pageNumber = 1;
getDataList();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.pageNumber = 1;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
}
function getDataList() {
if (distributionId.value) {
searchForm.distributionId = distributionId.value;
} else {
delete searchForm.distributionId;
}
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 = unixToDate(startTime / 1000);
searchForm.endTime = unixToDate(endTime / 1000);
} else {
searchForm.startTime = null;
searchForm.endTime = null;
}
getDistributionOrder(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function getShopList(val) {
const params = {
pageNumber: 1,
pageSize: 20,
storeName: val || "",
};
getShopListData(params).then((res) => {
shopList.value = res.result.records;
});
}
function searchChange(val) {
getShopList(val);
}
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";
}
function formatCommission(amount) {
if (amount === null || amount === undefined || amount === "") {
return "-";
}
return unitPrice(amount, "¥");
}
onMounted(() => {
init();
});
</script>
<style scoped lang="scss">

View File

@@ -177,19 +177,23 @@
<el-button type="primary" :loading="planSaveLoading" @click="savePlan">保存</el-button>
</el-form-item>
</el-form>
<sku-select ref="skuSelect" @selectedGoodsData="selectedGoodsData" />
<sku-select ref="skuSelectRef" @selectedGoodsData="selectedGoodsData" />
</div>
</template>
<script>
<script setup>
import { onMounted, ref, watch } from "vue";
import {
getCurrentRecruitPlan,
saveRecruitPlan,
} from "@/api/distribution";
import { Message } from "@/utils/message";
import uploadPicInput from "@/components/lili/upload-pic-input";
import tinymec from "@/components/editor/index.vue";
import editor from "@/components/editor/index.vue";
import skuSelect from "@/components/lili-dialog";
defineOptions({ name: "distributionRecruit" });
const MAX_RECRUIT_FIELDS = 5;
const FIELD_TYPE_OPTIONS = [
@@ -231,228 +235,239 @@ const buildDefaultField = () => ({
placeholder: "",
});
export default {
name: "distributionRecruit",
components: { uploadPicInput, editor: tinymec, skuSelect },
data() {
return {
planSaveLoading: false,
editorReady: true,
planForm: buildDefaultPlan(),
recruitFields: [],
recruitGoodsList: [],
fieldTypeOptions: FIELD_TYPE_OPTIONS,
};
},
mounted() {
this.loadPlan();
},
watch: {
"planForm.applyMode"(value) {
if (value !== "MANUAL") {
this.planForm.requireApplyInfo = false;
this.recruitFields = [];
}
},
"planForm.requireApplyInfo"(value) {
if (!value) {
this.recruitFields = [];
}
},
"planForm.joinConditionType"(value) {
if (value !== "CONDITIONAL") {
this.planForm.requirePurchaseGoods = false;
this.planForm.requireSelfPurchaseAmount = false;
this.planForm.requireConsumeCount = false;
this.recruitGoodsList = [];
}
},
"planForm.requirePurchaseGoods"(value) {
if (!value) {
this.recruitGoodsList = [];
}
},
"planForm.requireSelfPurchaseAmount"(value) {
if (value && (!this.planForm.minSelfPurchaseAmount || this.planForm.minSelfPurchaseAmount < 0.01)) {
this.planForm.minSelfPurchaseAmount = 0.01;
}
},
"planForm.requireConsumeCount"(value) {
if (value && (!this.planForm.minConsumeCount || this.planForm.minConsumeCount < 1)) {
this.planForm.minConsumeCount = 1;
}
},
},
methods: {
normalizeFields(fields) {
if (!fields || !fields.length) {
return [];
}
return fields.map((item, index) => ({
...buildDefaultField(),
...item,
_key: item._key || item.id || `field_${index}_${Date.now()}`,
required: item.required !== false,
}));
},
addRecruitField() {
if (this.recruitFields.length >= MAX_RECRUIT_FIELDS) {
this.$Message.warning("最多添加5个字段");
return;
}
this.recruitFields.push(buildDefaultField());
},
removeRecruitField(index) {
this.recruitFields.splice(index, 1);
},
validateRecruitFields() {
if (!this.planForm.requireApplyInfo) {
return true;
}
if (!this.recruitFields.length) {
this.$Message.warning("请至少添加一个申请字段");
return false;
}
for (let i = 0; i < this.recruitFields.length; i++) {
const field = this.recruitFields[i];
if (!field.fieldName || !field.fieldName.trim()) {
this.$Message.warning(`请填写字段${i + 1}名称`);
return false;
const skuSelectRef = ref(null);
const planSaveLoading = ref(false);
const editorReady = ref(true);
const planForm = ref(buildDefaultPlan());
const recruitFields = ref([]);
const recruitGoodsList = ref([]);
const fieldTypeOptions = ref(FIELD_TYPE_OPTIONS);
onMounted(() => {
loadPlan();
});
watch(
() => planForm.value.applyMode,
(value) => {
if (value !== "MANUAL") {
planForm.value.requireApplyInfo = false;
recruitFields.value = [];
}
}
);
watch(
() => planForm.value.requireApplyInfo,
(value) => {
if (!value) {
recruitFields.value = [];
}
}
);
watch(
() => planForm.value.joinConditionType,
(value) => {
if (value !== "CONDITIONAL") {
planForm.value.requirePurchaseGoods = false;
planForm.value.requireSelfPurchaseAmount = false;
planForm.value.requireConsumeCount = false;
recruitGoodsList.value = [];
}
}
);
watch(
() => planForm.value.requirePurchaseGoods,
(value) => {
if (!value) {
recruitGoodsList.value = [];
}
}
);
watch(
() => planForm.value.requireSelfPurchaseAmount,
(value) => {
if (value && (!planForm.value.minSelfPurchaseAmount || planForm.value.minSelfPurchaseAmount < 0.01)) {
planForm.value.minSelfPurchaseAmount = 0.01;
}
}
);
watch(
() => planForm.value.requireConsumeCount,
(value) => {
if (value && (!planForm.value.minConsumeCount || planForm.value.minConsumeCount < 1)) {
planForm.value.minConsumeCount = 1;
}
}
);
function normalizeFields(fields) {
if (!fields || !fields.length) {
return [];
}
return fields.map((item, index) => ({
...buildDefaultField(),
...item,
_key: item._key || item.id || `field_${index}_${Date.now()}`,
required: item.required !== false,
}));
}
function addRecruitField() {
if (recruitFields.value.length >= MAX_RECRUIT_FIELDS) {
Message.warning("最多添加5个字段");
return;
}
recruitFields.value.push(buildDefaultField());
}
function removeRecruitField(index) {
recruitFields.value.splice(index, 1);
}
function validateRecruitFields() {
if (!planForm.value.requireApplyInfo) {
return true;
}
if (!recruitFields.value.length) {
Message.warning("请至少添加一个申请字段");
return false;
}
for (let i = 0; i < recruitFields.value.length; i++) {
const field = recruitFields.value[i];
if (!field.fieldName || !field.fieldName.trim()) {
Message.warning(`请填写字段${i + 1}名称`);
return false;
}
}
return true;
}
function validateJoinConditions() {
if (planForm.value.joinConditionType !== "CONDITIONAL") {
return true;
}
const {
requirePurchaseGoods,
requireSelfPurchaseAmount,
requireConsumeCount,
} = planForm.value;
if (!requirePurchaseGoods && !requireSelfPurchaseAmount && !requireConsumeCount) {
Message.warning("请至少选择一个加入条件");
return false;
}
if (requirePurchaseGoods && !recruitGoodsList.value.length) {
Message.warning("请选择指定商品");
return false;
}
if (requireSelfPurchaseAmount && planForm.value.minSelfPurchaseAmount < 0.01) {
Message.warning("自购金额最低设置为0.01元");
return false;
}
if (requireConsumeCount && planForm.value.minConsumeCount < 1) {
Message.warning("消费笔数最低设置为1笔");
return false;
}
return true;
}
function openGoodsSelect() {
skuSelectRef.value.open("goods");
const data = recruitGoodsList.value.map((item) => ({
id: item.skuId,
goodsId: item.goodsId,
goodsName: item.goodsName,
sn: item.sn,
simpleSpecs: item.simpleSpecs,
price: item.price,
quantity: item.quantity,
small: item.thumbnail,
}));
skuSelectRef.value.goodsData = data;
}
function selectedGoodsData(items) {
recruitGoodsList.value = (items || []).map((item) => ({
skuId: item.id,
goodsId: item.goodsId,
goodsName: item.goodsName,
sn: item.sn || "",
simpleSpecs: item.simpleSpecs || "",
price: item.price,
quantity: item.quantity,
thumbnail: item.small || item.thumbnail || "",
}));
}
function removeRecruitGoods(index) {
recruitGoodsList.value.splice(index, 1);
}
function buildSavePayload() {
const fields = planForm.value.requireApplyInfo
? recruitFields.value.map((field, index) => ({
id: field.id,
fieldName: field.fieldName.trim(),
fieldType: field.fieldType,
required: field.required !== false,
placeholder: field.placeholder || "",
sortOrder: index,
}))
: [];
return {
...planForm.value,
id: planForm.value.id || undefined,
publishStatus: true,
posterEnabled: !!(planForm.value.posterBackground && planForm.value.posterBackground.trim()),
recruitGoods: planForm.value.requirePurchaseGoods
? recruitGoodsList.value.map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
sn: item.sn,
simpleSpecs: item.simpleSpecs,
thumbnail: item.thumbnail,
price: item.price,
}))
: [],
goodsIds: planForm.value.requirePurchaseGoods
? recruitGoodsList.value.map((item) => item.goodsId)
: [],
fields,
};
}
function applyPlanResult(result) {
planForm.value = { ...buildDefaultPlan(), ...result };
recruitFields.value = normalizeFields(result.fields);
recruitGoodsList.value = (result.recruitGoods || []).map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
sn: item.sn || "",
simpleSpecs: item.simpleSpecs || "",
thumbnail: item.thumbnail,
price: item.price,
}));
}
function loadPlan() {
getCurrentRecruitPlan().then((res) => {
if (res.success && res.result) {
applyPlanResult(res.result);
}
});
}
function savePlan() {
if (!validateRecruitFields() || !validateJoinConditions()) {
return;
}
planSaveLoading.value = true;
saveRecruitPlan(buildSavePayload())
.then((res) => {
if (res.success) {
Message.success("保存成功");
if (res.result) {
applyPlanResult(res.result);
} else {
loadPlan();
}
}
return true;
},
validateJoinConditions() {
if (this.planForm.joinConditionType !== "CONDITIONAL") {
return true;
}
const {
requirePurchaseGoods,
requireSelfPurchaseAmount,
requireConsumeCount,
} = this.planForm;
if (!requirePurchaseGoods && !requireSelfPurchaseAmount && !requireConsumeCount) {
this.$Message.warning("请至少选择一个加入条件");
return false;
}
if (requirePurchaseGoods && !this.recruitGoodsList.length) {
this.$Message.warning("请选择指定商品");
return false;
}
if (requireSelfPurchaseAmount && this.planForm.minSelfPurchaseAmount < 0.01) {
this.$Message.warning("自购金额最低设置为0.01元");
return false;
}
if (requireConsumeCount && this.planForm.minConsumeCount < 1) {
this.$Message.warning("消费笔数最低设置为1笔");
return false;
}
return true;
},
openGoodsSelect() {
this.$refs.skuSelect.open("goods");
const data = this.recruitGoodsList.map((item) => ({
id: item.skuId,
goodsId: item.goodsId,
goodsName: item.goodsName,
sn: item.sn,
simpleSpecs: item.simpleSpecs,
price: item.price,
quantity: item.quantity,
small: item.thumbnail,
}));
this.$refs.skuSelect.goodsData = data;
},
selectedGoodsData(items) {
this.recruitGoodsList = (items || []).map((item) => ({
skuId: item.id,
goodsId: item.goodsId,
goodsName: item.goodsName,
sn: item.sn || "",
simpleSpecs: item.simpleSpecs || "",
price: item.price,
quantity: item.quantity,
thumbnail: item.small || item.thumbnail || "",
}));
},
removeRecruitGoods(index) {
this.recruitGoodsList.splice(index, 1);
},
buildSavePayload() {
const fields = this.planForm.requireApplyInfo
? this.recruitFields.map((field, index) => ({
id: field.id,
fieldName: field.fieldName.trim(),
fieldType: field.fieldType,
required: field.required !== false,
placeholder: field.placeholder || "",
sortOrder: index,
}))
: [];
return {
...this.planForm,
id: this.planForm.id || undefined,
publishStatus: true,
posterEnabled: !!(this.planForm.posterBackground && this.planForm.posterBackground.trim()),
recruitGoods: this.planForm.requirePurchaseGoods
? this.recruitGoodsList.map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
sn: item.sn,
simpleSpecs: item.simpleSpecs,
thumbnail: item.thumbnail,
price: item.price,
}))
: [],
goodsIds: this.planForm.requirePurchaseGoods
? this.recruitGoodsList.map((item) => item.goodsId)
: [],
fields,
};
},
applyPlanResult(result) {
this.planForm = { ...buildDefaultPlan(), ...result };
this.recruitFields = this.normalizeFields(result.fields);
this.recruitGoodsList = (result.recruitGoods || []).map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
sn: item.sn || "",
simpleSpecs: item.simpleSpecs || "",
thumbnail: item.thumbnail,
price: item.price,
}));
},
loadPlan() {
getCurrentRecruitPlan().then((res) => {
if (res.success && res.result) {
this.applyPlanResult(res.result);
}
});
},
savePlan() {
if (!this.validateRecruitFields() || !this.validateJoinConditions()) {
return;
}
this.planSaveLoading = true;
saveRecruitPlan(this.buildSavePayload())
.then((res) => {
if (res.success) {
this.$Message.success("保存成功");
if (res.result) {
this.applyPlanResult(res.result);
} else {
this.loadPlan();
}
}
})
.finally(() => {
this.planSaveLoading = false;
});
},
},
};
})
.finally(() => {
planSaveLoading.value = false;
});
}
</script>
<style scoped lang="scss">

View File

@@ -0,0 +1,91 @@
<template>
<div style="background-color: #fff">
<el-form ref="formRef" :model="form" label-width="160px" style="padding: 10px">
<el-divider content-position="left">招募计划</el-divider>
<el-form-item label="开启招募">
<el-switch v-model="form.recruitOpen" />
</el-form-item>
<el-form-item label="申请方式">
<el-radio-group v-model="form.applyMode">
<el-radio value="MANUAL_APPLY">手动申请</el-radio>
<el-radio value="AUTO_APPLY_WHEN_QUALIFIED">满足条件自动申请</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核方式">
<el-radio-group v-model="form.auditMode">
<el-radio value="MANUAL_AUDIT">人工审核</el-radio>
<el-radio value="AUTO_AUDIT">自动审核</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="开启加入门槛">
<el-switch v-model="form.thresholdEnabled" />
</el-form-item>
<template v-if="form.thresholdEnabled">
<el-form-item label="门槛组合">
<el-radio-group v-model="form.thresholdMatch">
<el-radio value="ALL">需同时满足</el-radio>
<el-radio value="ANY">满足其一</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="最低消费金额">
<el-input-number v-model="form.consumeAmountMin" :min="0" :precision="2" />
</el-form-item>
<el-form-item label="最低消费笔数">
<el-input-number v-model="form.consumeCountMin" :min="0" />
</el-form-item>
<el-form-item label="需购买指定商品">
<el-switch v-model="form.requireBuyGoods" />
</el-form-item>
</template>
<el-form-item label="招募页标题">
<el-input v-model="form.recruitPageTitle" />
</el-form-item>
<el-form-item label="招募页说明">
<el-input v-model="form.recruitPageDesc" type="textarea" />
</el-form-item>
<el-form-item label="展示招募入口">
<el-switch v-model="form.showRecruitEntry" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { setSetting, getSetting } from "@/api/index";
import { Message } from "@/utils/message";
defineOptions({ name: "distributionRecruitSetting" });
const formRef = ref(null);
const form = ref({
recruitOpen: true,
applyMode: "MANUAL_APPLY",
auditMode: "MANUAL_AUDIT",
thresholdEnabled: false,
thresholdMatch: "ALL",
consumeAmountMin: 0,
consumeCountMin: 0,
requireBuyGoods: false,
recruitPageTitle: "成为分销员",
recruitPageDesc: "分享商品赚取佣金",
showRecruitEntry: true,
});
onMounted(() => {
getSetting("DISTRIBUTION_RECRUIT_SETTING").then((res) => {
if (res.success && res.result) {
form.value = { ...form.value, ...res.result };
}
});
});
function submit() {
setSetting("DISTRIBUTION_RECRUIT_SETTING", form.value).then((res) => {
if (res.success) Message.success("保存成功");
});
}
</script>

View File

@@ -4,7 +4,7 @@
<el-tabs v-model="activeTab" class="setting-tabs">
<el-tab-pane label="基础设置" name="base">
<el-form
ref="form"
ref="formRef"
:model="form"
:rules="formRule"
label-width="180px"
@@ -69,7 +69,7 @@
<el-tab-pane label="客户关系" name="customer">
<el-form
ref="customerForm"
ref="customerFormRef"
:model="customerForm"
label-width="180px"
label-position="right"
@@ -116,7 +116,7 @@
<el-tab-pane label="结算方案" name="settlement">
<el-form
ref="settlementForm"
ref="settlementFormRef"
:model="settlementForm"
label-width="180px"
label-position="right"
@@ -234,7 +234,7 @@
<el-tab-pane label="商品分销设置" name="featured">
<el-form
ref="featuredForm"
ref="featuredFormRef"
:model="featuredForm"
label-width="220px"
label-position="right"
@@ -428,7 +428,8 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, computed, watch, onMounted, nextTick } from "vue";
import { setSetting, getSetting } from "@/api/index";
import {
getDistributionCustomerSetting,
@@ -445,6 +446,7 @@ import {
} from "@/api/distribution";
import uploadPicInput from "@/components/lili/upload-pic-input";
import { regular } from "@/utils";
import { Message } from "@/utils/message";
const DEFAULT_FORM = {
isOpen: true,
@@ -485,430 +487,426 @@ const DEFAULT_SORT_OPTIONS = [
{ value: "LATEST_LISTING", label: "商品上架越晚越靠前" },
];
export default {
name: "distributionSetting",
components: {
uploadPicInput,
},
data() {
return {
activeTab: "base",
saving: {
base: false,
customer: false,
settlement: false,
featured: false,
poster: false,
inviteCard: false,
},
form: { ...DEFAULT_FORM },
formRule: {
isOpen: [regular.REQUIRED],
distributorName: [regular.REQUIRED, regular.VARCHAR20],
},
customerForm: {
bindMode: "SHORT_TERM",
permanentBindMode: "PAID_WITHIN_DAYS",
permanentPaidDays: 1,
shortTermBindDays: 7,
},
settlementForm: {
selfPurchaseCommissionMode: "SELF",
},
posterForm: { ...DEFAULT_POSTER_FORM },
inviteCardForm: { ...DEFAULT_INVITE_CARD_FORM },
featuredForm: { ...DEFAULT_FEATURED_FORM },
defaultSortOptions: DEFAULT_SORT_OPTIONS,
featuredCustomGoods: [],
featuredPickerVisible: false,
featuredPickerLoading: false,
featuredPickerList: [],
featuredPickerTotal: 0,
featuredPickerDraft: [],
featuredPickerSelection: [],
syncingFeaturedPickerSelection: false,
featuredPickerSearch: {
goodsName: "",
authStatus: "PASS",
pageNumber: 1,
pageSize: 20,
},
defineOptions({ name: "distributionSetting" });
const formRef = ref(null);
const featuredPickerTable = ref(null);
const activeTab = ref("base");
const saving = reactive({
base: false,
customer: false,
settlement: false,
featured: false,
poster: false,
inviteCard: false,
});
const form = ref({ ...DEFAULT_FORM });
const formRule = reactive({
isOpen: [regular.REQUIRED],
distributorName: [regular.REQUIRED, regular.VARCHAR20],
});
const customerForm = ref({
bindMode: "SHORT_TERM",
permanentBindMode: "PAID_WITHIN_DAYS",
permanentPaidDays: 1,
shortTermBindDays: 7,
});
const settlementForm = ref({
selfPurchaseCommissionMode: "SELF",
});
const posterForm = ref({ ...DEFAULT_POSTER_FORM });
const inviteCardForm = ref({ ...DEFAULT_INVITE_CARD_FORM });
const featuredForm = ref({ ...DEFAULT_FEATURED_FORM });
const defaultSortOptions = ref(DEFAULT_SORT_OPTIONS);
const featuredCustomGoods = ref([]);
const featuredPickerVisible = ref(false);
const featuredPickerLoading = ref(false);
const featuredPickerList = ref([]);
const featuredPickerTotal = ref(0);
const featuredPickerDraft = ref([]);
const featuredPickerSelection = ref([]);
const syncingFeaturedPickerSelection = ref(false);
const featuredPickerSearch = reactive({
goodsName: "",
authStatus: "PASS",
pageNumber: 1,
pageSize: 20,
});
const featuredGoodsLimit = computed(() => {
return featuredForm.value.displayCount === 10 ? 10 : 5;
});
const featuredGoodsOverLimit = computed(() => {
return featuredForm.value.sourceMode === "CUSTOM" && featuredCustomGoods.value.length > featuredGoodsLimit.value;
});
const featuredDisplayCountDesc = computed(() => {
if (featuredForm.value.sourceMode === "CUSTOM") {
return `最多选择 ${featuredGoodsLimit.value} 个,分销员端按顺序展示。`;
}
return `分销员端展示前 ${featuredGoodsLimit.value} 个。`;
});
watch(
() => customerForm.value.permanentBindMode,
(value) => {
if (value === "PAID_WITHIN_DAYS") {
normalizePermanentPaidDays();
}
}
);
onMounted(() => {
init();
});
function init() {
loadBaseSetting();
loadFeaturedSetting();
loadPosterSetting();
loadInviteCardSetting();
loadCustomerSetting();
loadSettlementSetting();
}
function loadBaseSetting() {
getSetting("DISTRIBUTION_SETTING").then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
form.value = {
...DEFAULT_FORM,
...result,
distributorName: result.distributorName || "分销员",
parentAttributionMode: result.parentAttributionMode || DEFAULT_FORM.parentAttributionMode,
};
},
computed: {
featuredGoodsLimit() {
return this.featuredForm.displayCount === 10 ? 10 : 5;
},
featuredGoodsOverLimit() {
return this.featuredForm.sourceMode === "CUSTOM" && this.featuredCustomGoods.length > this.featuredGoodsLimit;
},
featuredDisplayCountDesc() {
if (this.featuredForm.sourceMode === "CUSTOM") {
return `最多选择 ${this.featuredGoodsLimit} 个,分销员端按顺序展示。`;
});
}
function loadPosterSetting() {
getDistributionPromotionPosterSetting().then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
posterForm.value = {
...DEFAULT_POSTER_FORM,
memberInfoVisible: result.memberInfoVisible !== false,
backgroundImage: result.backgroundImage || "",
slogan: result.slogan || DEFAULT_POSTER_FORM.slogan,
textColor: result.textColor || DEFAULT_POSTER_FORM.textColor,
};
});
}
function submitPoster() {
const slogan = (posterForm.value.slogan || "").trim();
if (!slogan) {
Message.warning("请填写推荐语");
return;
}
saving.poster = true;
putDistributionPromotionPosterSetting({
memberInfoVisible: posterForm.value.memberInfoVisible,
backgroundImage: (posterForm.value.backgroundImage || "").trim(),
slogan,
textColor: posterForm.value.textColor || DEFAULT_POSTER_FORM.textColor,
})
.then((res) => {
if (res.success) {
Message.success("操作成功");
loadPosterSetting();
}
return `分销员端展示前 ${this.featuredGoodsLimit} 个。`;
},
},
mounted() {
this.init();
},
watch: {
"customerForm.permanentBindMode"(value) {
if (value === "PAID_WITHIN_DAYS") {
this.normalizePermanentPaidDays();
})
.finally(() => {
saving.poster = false;
});
}
function loadInviteCardSetting() {
getDistributionInviteCardSetting().then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
inviteCardForm.value = {
...DEFAULT_INVITE_CARD_FORM,
memberInfoVisible: result.memberInfoVisible !== false,
backgroundImage: result.backgroundImage || "",
slogan: result.slogan || DEFAULT_INVITE_CARD_FORM.slogan,
textColor: result.textColor || DEFAULT_INVITE_CARD_FORM.textColor,
};
});
}
function submitInviteCard() {
const slogan = (inviteCardForm.value.slogan || "").trim();
if (!slogan) {
Message.warning("请填写推荐语");
return;
}
saving.inviteCard = true;
putDistributionInviteCardSetting({
memberInfoVisible: inviteCardForm.value.memberInfoVisible,
backgroundImage: (inviteCardForm.value.backgroundImage || "").trim(),
slogan,
textColor: inviteCardForm.value.textColor || DEFAULT_INVITE_CARD_FORM.textColor,
})
.then((res) => {
if (res.success) {
Message.success("操作成功");
loadInviteCardSetting();
}
},
},
methods: {
init() {
this.loadBaseSetting();
this.loadFeaturedSetting();
this.loadPosterSetting();
this.loadInviteCardSetting();
this.loadCustomerSetting();
this.loadSettlementSetting();
},
loadBaseSetting() {
getSetting("DISTRIBUTION_SETTING").then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
this.form = {
...DEFAULT_FORM,
...result,
distributorName: result.distributorName || "分销员",
parentAttributionMode: result.parentAttributionMode || DEFAULT_FORM.parentAttributionMode,
};
})
.finally(() => {
saving.inviteCard = false;
});
}
function loadFeaturedSetting() {
getDistributionFeaturedGoodsSetting().then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
featuredForm.value = {
...DEFAULT_FEATURED_FORM,
defaultSort: result.defaultSort || DEFAULT_FEATURED_FORM.defaultSort,
defaultShareText: result.defaultShareText || "",
centerVisible: !!result.centerVisible,
showSales: !!result.showSales,
sourceMode: result.sourceMode || DEFAULT_FEATURED_FORM.sourceMode,
matchRule: result.matchRule || DEFAULT_FEATURED_FORM.matchRule,
displayCount: result.displayCount === 10 ? 10 : 5,
};
featuredCustomGoods.value = Array.isArray(result.customGoods) ? [...result.customGoods] : [];
});
}
function loadCustomerSetting() {
getDistributionCustomerSetting().then((res) => {
if (res.success && res.result) {
customerForm.value = { ...customerForm.value, ...res.result };
normalizePermanentPaidDays();
}
});
}
function normalizePermanentPaidDays() {
const days = Number(customerForm.value.permanentPaidDays);
if (!Number.isFinite(days) || days < 1) {
customerForm.value.permanentPaidDays = 1;
} else {
customerForm.value.permanentPaidDays = Math.floor(days);
}
}
function onPermanentPaidDaysChange(value) {
if (value == null || value < 1) {
customerForm.value.permanentPaidDays = 1;
}
}
function loadSettlementSetting() {
getDistributionSettlementSetting().then((res) => {
if (res.success && res.result) {
settlementForm.value = { ...settlementForm.value, ...res.result };
}
});
}
function openFeaturedGoodsPicker() {
featuredPickerDraft.value = featuredCustomGoods.value.map((item) => ({ ...item }));
featuredPickerVisible.value = true;
}
function loadFeaturedPickerGoods() {
featuredPickerLoading.value = true;
syncingFeaturedPickerSelection.value = true;
getDistributionGoods({ ...featuredPickerSearch })
.then((res) => {
if (res.success) {
featuredPickerList.value = res.result?.records || [];
featuredPickerTotal.value = res.result?.total || 0;
}
})
.finally(() => {
featuredPickerLoading.value = false;
nextTick(() => {
syncFeaturedPickerSelection();
nextTick(() => {
syncingFeaturedPickerSelection.value = false;
});
});
},
loadPosterSetting() {
getDistributionPromotionPosterSetting().then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
this.posterForm = {
...DEFAULT_POSTER_FORM,
memberInfoVisible: result.memberInfoVisible !== false,
backgroundImage: result.backgroundImage || "",
slogan: result.slogan || DEFAULT_POSTER_FORM.slogan,
textColor: result.textColor || DEFAULT_POSTER_FORM.textColor,
};
});
},
submitPoster() {
const slogan = (this.posterForm.slogan || "").trim();
if (!slogan) {
this.$Message.warning("请填写推荐语");
return;
});
}
function syncFeaturedPickerSelection() {
const table = featuredPickerTable.value;
if (!table) {
return;
}
const selectedSkuIds = new Set(
featuredPickerDraft.value.map((item) => item.skuId).filter(Boolean)
);
table.clearSelection();
featuredPickerList.value.forEach((row) => {
if (row.skuId && selectedSkuIds.has(row.skuId)) {
table.toggleRowSelection(row, true);
}
});
}
function searchFeaturedPickerGoods() {
featuredPickerSearch.pageNumber = 1;
loadFeaturedPickerGoods();
}
function onFeaturedPickerPageSizeChange() {
featuredPickerSearch.pageNumber = 1;
loadFeaturedPickerGoods();
}
function onFeaturedPickerSelectionChange(selection) {
if (syncingFeaturedPickerSelection.value) {
return;
}
const selectedSkuIds = new Set((selection || []).map((item) => item.skuId).filter(Boolean));
const currentPageSkuIds = new Set(featuredPickerList.value.map((item) => item.skuId).filter(Boolean));
const remaining = featuredPickerDraft.value.filter((item) => {
if (!currentPageSkuIds.has(item.skuId)) {
return true;
}
return selectedSkuIds.has(item.skuId);
});
const remainingSkuIds = new Set(remaining.map((item) => item.skuId));
const added = [];
(selection || []).forEach((item) => {
if (!item.skuId || remainingSkuIds.has(item.skuId)) {
return;
}
remainingSkuIds.add(item.skuId);
added.push(toFeaturedGoodsItem(item));
});
featuredPickerDraft.value = [...remaining, ...added];
featuredPickerSelection.value = selection || [];
}
function toFeaturedGoodsItem(item) {
return {
id: item.id,
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
specs: item.specs,
thumbnail: item.thumbnail,
price: item.price,
commission: item.commission,
storeName: item.storeName,
};
}
function featuredGoodsLimitMessage() {
const limit = featuredGoodsLimit.value;
return `主推商品数量为${limit}时,最多选择${limit}个自定义商品`;
}
function confirmFeaturedPicker() {
const next = featuredPickerDraft.value.filter((item) => item.skuId);
if (!next.length) {
Message.warning("请至少选择一个分销商品");
return;
}
const limit = featuredGoodsLimit.value;
if (next.length > limit) {
Message.warning(featuredGoodsLimitMessage());
return;
}
featuredCustomGoods.value = next;
featuredPickerVisible.value = false;
featuredPickerSelection.value = [];
}
function removeFeaturedGoods(index) {
featuredCustomGoods.value.splice(index, 1);
}
function buildFeaturedPayload() {
return {
defaultSort: featuredForm.value.defaultSort,
defaultShareText: (featuredForm.value.defaultShareText || "").trim(),
centerVisible: featuredForm.value.centerVisible,
showSales: featuredForm.value.showSales,
sourceMode: featuredForm.value.sourceMode,
matchRule: featuredForm.value.matchRule,
displayCount: featuredForm.value.displayCount === 10 ? 10 : 5,
skuIds:
featuredForm.value.sourceMode === "CUSTOM"
? featuredCustomGoods.value.map((item) => item.skuId).filter(Boolean)
: undefined,
};
}
function submitFeatured() {
if (featuredForm.value.sourceMode === "CUSTOM" && !featuredCustomGoods.value.length) {
Message.warning("请至少选择一个自定义主推商品");
return;
}
if (featuredGoodsOverLimit.value) {
Message.warning(featuredGoodsLimitMessage());
return;
}
saving.featured = true;
putDistributionFeaturedGoodsSetting(buildFeaturedPayload())
.then((res) => {
if (res.success) {
Message.success("操作成功");
loadFeaturedSetting();
}
this.saving.poster = true;
putDistributionPromotionPosterSetting({
memberInfoVisible: this.posterForm.memberInfoVisible,
backgroundImage: (this.posterForm.backgroundImage || "").trim(),
slogan,
textColor: this.posterForm.textColor || DEFAULT_POSTER_FORM.textColor,
})
.finally(() => {
saving.featured = false;
});
}
function buildBasePayload() {
return {
isOpen: form.value.isOpen,
distributorName: (form.value.distributorName || "分销员").trim(),
upgradeConditionVisible: form.value.upgradeConditionVisible,
parentDistributorVisible: form.value.parentDistributorVisible,
parentAttributionMode: form.value.parentAttributionMode,
};
}
function submitBase() {
formRef.value.validate((valid) => {
if (!valid) return;
saving.base = true;
setSetting("DISTRIBUTION_SETTING", buildBasePayload())
.then((res) => {
if (res.success) {
Message.success("操作成功");
loadBaseSetting();
}
})
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadPosterSetting();
}
})
.finally(() => {
this.saving.poster = false;
});
},
loadInviteCardSetting() {
getDistributionInviteCardSetting().then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
this.inviteCardForm = {
...DEFAULT_INVITE_CARD_FORM,
memberInfoVisible: result.memberInfoVisible !== false,
backgroundImage: result.backgroundImage || "",
slogan: result.slogan || DEFAULT_INVITE_CARD_FORM.slogan,
textColor: result.textColor || DEFAULT_INVITE_CARD_FORM.textColor,
};
.finally(() => {
saving.base = false;
});
},
submitInviteCard() {
const slogan = (this.inviteCardForm.slogan || "").trim();
if (!slogan) {
this.$Message.warning("请填写推荐语");
return;
});
}
function submitCustomer() {
normalizePermanentPaidDays();
if (
customerForm.value.bindMode === "PERMANENT" &&
customerForm.value.permanentBindMode === "PAID_WITHIN_DAYS" &&
customerForm.value.permanentPaidDays < 1
) {
Message.warning("永久锁客支付天数不能低于1天");
return;
}
saving.customer = true;
putDistributionCustomerSetting(customerForm.value)
.then((res) => {
if (res.success) {
Message.success("操作成功");
loadCustomerSetting();
}
this.saving.inviteCard = true;
putDistributionInviteCardSetting({
memberInfoVisible: this.inviteCardForm.memberInfoVisible,
backgroundImage: (this.inviteCardForm.backgroundImage || "").trim(),
slogan,
textColor: this.inviteCardForm.textColor || DEFAULT_INVITE_CARD_FORM.textColor,
})
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadInviteCardSetting();
}
})
.finally(() => {
this.saving.inviteCard = false;
});
},
loadFeaturedSetting() {
getDistributionFeaturedGoodsSetting().then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
this.featuredForm = {
...DEFAULT_FEATURED_FORM,
defaultSort: result.defaultSort || DEFAULT_FEATURED_FORM.defaultSort,
defaultShareText: result.defaultShareText || "",
centerVisible: !!result.centerVisible,
showSales: !!result.showSales,
sourceMode: result.sourceMode || DEFAULT_FEATURED_FORM.sourceMode,
matchRule: result.matchRule || DEFAULT_FEATURED_FORM.matchRule,
displayCount: result.displayCount === 10 ? 10 : 5,
};
this.featuredCustomGoods = Array.isArray(result.customGoods) ? [...result.customGoods] : [];
});
},
loadCustomerSetting() {
getDistributionCustomerSetting().then((res) => {
if (res.success && res.result) {
this.customerForm = { ...this.customerForm, ...res.result };
this.normalizePermanentPaidDays();
}
});
},
normalizePermanentPaidDays() {
const days = Number(this.customerForm.permanentPaidDays);
if (!Number.isFinite(days) || days < 1) {
this.customerForm.permanentPaidDays = 1;
} else {
this.customerForm.permanentPaidDays = Math.floor(days);
})
.finally(() => {
saving.customer = false;
});
}
function submitSettlement() {
saving.settlement = true;
putDistributionSettlementSetting(settlementForm.value)
.then((res) => {
if (res.success) {
Message.success("操作成功");
loadSettlementSetting();
}
},
onPermanentPaidDaysChange(value) {
if (value == null || value < 1) {
this.customerForm.permanentPaidDays = 1;
}
},
loadSettlementSetting() {
getDistributionSettlementSetting().then((res) => {
if (res.success && res.result) {
this.settlementForm = { ...this.settlementForm, ...res.result };
}
});
},
openFeaturedGoodsPicker() {
this.featuredPickerDraft = this.featuredCustomGoods.map((item) => ({ ...item }));
this.featuredPickerVisible = true;
},
loadFeaturedPickerGoods() {
this.featuredPickerLoading = true;
this.syncingFeaturedPickerSelection = true;
getDistributionGoods({ ...this.featuredPickerSearch })
.then((res) => {
if (res.success) {
this.featuredPickerList = res.result?.records || [];
this.featuredPickerTotal = res.result?.total || 0;
}
})
.finally(() => {
this.featuredPickerLoading = false;
this.$nextTick(() => {
this.syncFeaturedPickerSelection();
this.$nextTick(() => {
this.syncingFeaturedPickerSelection = false;
});
});
});
},
syncFeaturedPickerSelection() {
const table = this.$refs.featuredPickerTable;
if (!table) {
return;
}
const selectedSkuIds = new Set(
this.featuredPickerDraft.map((item) => item.skuId).filter(Boolean)
);
table.clearSelection();
this.featuredPickerList.forEach((row) => {
if (row.skuId && selectedSkuIds.has(row.skuId)) {
table.toggleRowSelection(row, true);
}
});
},
searchFeaturedPickerGoods() {
this.featuredPickerSearch.pageNumber = 1;
this.loadFeaturedPickerGoods();
},
onFeaturedPickerPageSizeChange() {
this.featuredPickerSearch.pageNumber = 1;
this.loadFeaturedPickerGoods();
},
onFeaturedPickerSelectionChange(selection) {
if (this.syncingFeaturedPickerSelection) {
return;
}
const selectedSkuIds = new Set((selection || []).map((item) => item.skuId).filter(Boolean));
const currentPageSkuIds = new Set(this.featuredPickerList.map((item) => item.skuId).filter(Boolean));
const remaining = this.featuredPickerDraft.filter((item) => {
if (!currentPageSkuIds.has(item.skuId)) {
return true;
}
return selectedSkuIds.has(item.skuId);
});
const remainingSkuIds = new Set(remaining.map((item) => item.skuId));
const added = [];
(selection || []).forEach((item) => {
if (!item.skuId || remainingSkuIds.has(item.skuId)) {
return;
}
remainingSkuIds.add(item.skuId);
added.push(this.toFeaturedGoodsItem(item));
});
this.featuredPickerDraft = [...remaining, ...added];
this.featuredPickerSelection = selection || [];
},
toFeaturedGoodsItem(item) {
return {
id: item.id,
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
specs: item.specs,
thumbnail: item.thumbnail,
price: item.price,
commission: item.commission,
storeName: item.storeName,
};
},
featuredGoodsLimitMessage() {
const limit = this.featuredGoodsLimit;
return `主推商品数量为${limit}时,最多选择${limit}个自定义商品`;
},
confirmFeaturedPicker() {
const next = this.featuredPickerDraft.filter((item) => item.skuId);
if (!next.length) {
this.$Message.warning("请至少选择一个分销商品");
return;
}
const limit = this.featuredGoodsLimit;
if (next.length > limit) {
this.$Message.warning(this.featuredGoodsLimitMessage());
return;
}
this.featuredCustomGoods = next;
this.featuredPickerVisible = false;
this.featuredPickerSelection = [];
},
removeFeaturedGoods(index) {
this.featuredCustomGoods.splice(index, 1);
},
buildFeaturedPayload() {
return {
defaultSort: this.featuredForm.defaultSort,
defaultShareText: (this.featuredForm.defaultShareText || "").trim(),
centerVisible: this.featuredForm.centerVisible,
showSales: this.featuredForm.showSales,
sourceMode: this.featuredForm.sourceMode,
matchRule: this.featuredForm.matchRule,
displayCount: this.featuredForm.displayCount === 10 ? 10 : 5,
skuIds:
this.featuredForm.sourceMode === "CUSTOM"
? this.featuredCustomGoods.map((item) => item.skuId).filter(Boolean)
: undefined,
};
},
submitFeatured() {
if (this.featuredForm.sourceMode === "CUSTOM" && !this.featuredCustomGoods.length) {
this.$Message.warning("请至少选择一个自定义主推商品");
return;
}
if (this.featuredGoodsOverLimit) {
this.$Message.warning(this.featuredGoodsLimitMessage());
return;
}
this.saving.featured = true;
putDistributionFeaturedGoodsSetting(this.buildFeaturedPayload())
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadFeaturedSetting();
}
})
.finally(() => {
this.saving.featured = false;
});
},
buildBasePayload() {
return {
isOpen: this.form.isOpen,
distributorName: (this.form.distributorName || "分销员").trim(),
upgradeConditionVisible: this.form.upgradeConditionVisible,
parentDistributorVisible: this.form.parentDistributorVisible,
parentAttributionMode: this.form.parentAttributionMode,
};
},
submitBase() {
this.$refs.form.validate((valid) => {
if (!valid) return;
this.saving.base = true;
setSetting("DISTRIBUTION_SETTING", this.buildBasePayload())
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadBaseSetting();
}
})
.finally(() => {
this.saving.base = false;
});
});
},
submitCustomer() {
this.normalizePermanentPaidDays();
if (
this.customerForm.bindMode === "PERMANENT" &&
this.customerForm.permanentBindMode === "PAID_WITHIN_DAYS" &&
this.customerForm.permanentPaidDays < 1
) {
this.$Message.warning("永久锁客支付天数不能低于1天");
return;
}
this.saving.customer = true;
putDistributionCustomerSetting(this.customerForm)
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadCustomerSetting();
}
})
.finally(() => {
this.saving.customer = false;
});
},
submitSettlement() {
this.saving.settlement = true;
putDistributionSettlementSetting(this.settlementForm)
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadSettlementSetting();
}
})
.finally(() => {
this.saving.settlement = false;
});
},
},
};
})
.finally(() => {
saving.settlement = false;
});
}
</script>
<style scoped 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

@@ -4,22 +4,23 @@
</div>
</template>
<script>
export default {
name: "ExternalLink",
data() {
return {
link: this.$router.currentRoute.name,
};
},
mounted() {
let iframe = document.getElementById("ifram");
const deviceWidth = document.body.clientWidth;
const deviceHeight = document.body.clientHeight;
iframe.style.width = deviceWidth + "px";
iframe.style.height = deviceHeight + "px";
}
};
<script setup>
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
defineOptions({ name: "ExternalLink" });
const router = useRouter();
const link = ref(router.currentRoute.name);
onMounted(() => {
let iframe = document.getElementById("ifram");
const deviceWidth = document.body.clientWidth;
const deviceHeight = document.body.clientHeight;
iframe.style.width = deviceWidth + "px";
iframe.style.height = deviceHeight + "px";
});
</script>
<style lang="scss" scoped>

View File

@@ -24,39 +24,39 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import * as API_Finance from "@/api/finance";
import { downloadBlob } from "@/utils/downloadBlob";
export default {
name: "finance-payment-method",
data() {
return { loading: false, dateRange: [], data: [] };
},
mounted() {
this.loadData();
},
methods: {
buildParams() {
const p = {};
if (this.dateRange?.length === 2) {
p.startDate = this.dateRange[0];
p.endDate = this.dateRange[1];
}
return p;
},
loadData() {
this.loading = true;
API_Finance.getPaymentMethodReport(this.buildParams()).then((res) => {
this.loading = false;
if (res.success) this.data = res.result || [];
});
},
handleExport() {
API_Finance.exportPaymentMethodReport(this.buildParams()).then((blob) => {
downloadBlob(blob, "支付方式汇总.xlsx");
});
},
},
};
defineOptions({ name: "finance-payment-method" });
const loading = ref(false);
const dateRange = ref([]);
const data = ref([]);
onMounted(() => {
loadData();
});
function buildParams() {
const p = {};
if (dateRange.value?.length === 2) {
p.startDate = dateRange.value[0];
p.endDate = dateRange.value[1];
}
return p;
}
function loadData() {
loading.value = true;
API_Finance.getPaymentMethodReport(buildParams()).then((res) => {
loading.value = false;
if (res.success) data.value = res.result || [];
});
}
function handleExport() {
API_Finance.exportPaymentMethodReport(buildParams()).then((blob) => {
downloadBlob(blob, "支付方式汇总.xlsx");
});
}
</script>

View File

@@ -39,44 +39,40 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import * as API_Finance from "@/api/finance";
import { downloadBlob } from "@/utils/downloadBlob";
export default {
name: "finance-platform-report",
data() {
return {
loading: false,
dateRange: [],
params: { granularity: "DAY" },
data: [],
};
},
mounted() {
this.loadData();
},
methods: {
buildParams() {
const p = { ...this.params };
if (this.dateRange && this.dateRange.length === 2) {
p.startDate = this.dateRange[0];
p.endDate = this.dateRange[1];
}
return p;
},
loadData() {
this.loading = true;
API_Finance.getPlatformReport(this.buildParams()).then((res) => {
this.loading = false;
if (res.success) this.data = res.result || [];
});
},
handleExport() {
API_Finance.exportPlatformReport(this.buildParams()).then((blob) => {
downloadBlob(blob, "平台经营报表.xlsx");
});
},
},
};
defineOptions({ name: "finance-platform-report" });
const loading = ref(false);
const dateRange = ref([]);
const params = ref({ granularity: "DAY" });
const data = ref([]);
onMounted(() => {
loadData();
});
function buildParams() {
const p = { ...params.value };
if (dateRange.value && dateRange.value.length === 2) {
p.startDate = dateRange.value[0];
p.endDate = dateRange.value[1];
}
return p;
}
function loadData() {
loading.value = true;
API_Finance.getPlatformReport(buildParams()).then((res) => {
loading.value = false;
if (res.success) data.value = res.result || [];
});
}
function handleExport() {
API_Finance.exportPlatformReport(buildParams()).then((blob) => {
downloadBlob(blob, "平台经营报表.xlsx");
});
}
</script>

View File

@@ -34,44 +34,40 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import * as API_Finance from "@/api/finance";
import { downloadBlob } from "@/utils/downloadBlob";
export default {
name: "finance-refund-log",
data() {
return {
loading: false,
searchForm: { pageNumber: 1, pageSize: 20, orderSn: "" },
data: [],
total: 0,
};
},
mounted() {
this.getDataList();
},
methods: {
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
API_Finance.refundLogPage(this.searchForm).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
handleExport() {
const { pageNumber, pageSize, ...rest } = this.searchForm;
API_Finance.exportRefundFlow(rest).then((blob) => {
downloadBlob(blob, "退款流水.xlsx");
});
},
},
};
defineOptions({ name: "finance-refund-log" });
const loading = ref(false);
const searchForm = reactive({ pageNumber: 1, pageSize: 20, orderSn: "" });
const data = ref([]);
const total = ref(0);
onMounted(() => {
getDataList();
});
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
API_Finance.refundLogPage(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function handleExport() {
const { pageNumber, pageSize, ...rest } = searchForm;
API_Finance.exportRefundFlow(rest).then((blob) => {
downloadBlob(blob, "退款流水.xlsx");
});
}
</script>

View File

@@ -16,31 +16,30 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import * as API_Finance from "@/api/finance";
import { downloadBlob } from "@/utils/downloadBlob";
export default {
name: "finance-settlement-ledger",
data() {
return { loading: false, data: [] };
},
mounted() {
this.loadData();
},
methods: {
loadData() {
this.loading = true;
API_Finance.getSettlementLedger({}).then((res) => {
this.loading = false;
if (res.success) this.data = res.result || [];
});
},
handleExport() {
API_Finance.exportSettlementLedger({}).then((blob) => {
downloadBlob(blob, "结算台账.xlsx");
});
},
},
};
defineOptions({ name: "finance-settlement-ledger" });
const loading = ref(false);
const data = ref([]);
onMounted(() => {
loadData();
});
function loadData() {
loading.value = true;
API_Finance.getSettlementLedger({}).then((res) => {
loading.value = false;
if (res.success) data.value = res.result || [];
});
}
function handleExport() {
API_Finance.exportSettlementLedger({}).then((blob) => {
downloadBlob(blob, "结算台账.xlsx");
});
}
</script>

View File

@@ -26,39 +26,39 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import * as API_Finance from "@/api/finance";
import { downloadBlob } from "@/utils/downloadBlob";
export default {
name: "finance-store-settlement",
data() {
return { loading: false, dateRange: [], data: [] };
},
mounted() {
this.loadData();
},
methods: {
buildParams() {
const p = {};
if (this.dateRange?.length === 2) {
p.startDate = this.dateRange[0];
p.endDate = this.dateRange[1];
}
return p;
},
loadData() {
this.loading = true;
API_Finance.getStoreSettlementReport(this.buildParams()).then((res) => {
this.loading = false;
if (res.success) this.data = res.result || [];
});
},
handleExport() {
API_Finance.exportStoreSettlementReport(this.buildParams()).then((blob) => {
downloadBlob(blob, "店铺结算汇总.xlsx");
});
},
},
};
defineOptions({ name: "finance-store-settlement" });
const loading = ref(false);
const dateRange = ref([]);
const data = ref([]);
onMounted(() => {
loadData();
});
function buildParams() {
const p = {};
if (dateRange.value?.length === 2) {
p.startDate = dateRange.value[0];
p.endDate = dateRange.value[1];
}
return p;
}
function loadData() {
loading.value = true;
API_Finance.getStoreSettlementReport(buildParams()).then((res) => {
loading.value = false;
if (res.success) data.value = res.result || [];
});
}
function handleExport() {
API_Finance.exportStoreSettlementReport(buildParams()).then((blob) => {
downloadBlob(blob, "店铺结算汇总.xlsx");
});
}
</script>

View File

@@ -36,59 +36,55 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import * as API_Finance from "@/api/finance";
import { getRequest } from "@/libs/axios";
import { downloadBlob } from "@/utils/downloadBlob";
export default {
name: "finance-wallet-log",
data() {
return {
loading: false,
searchForm: { memberName: "" },
dateRange: [],
pageNumber: 1,
pageSize: 20,
data: [],
total: 0,
};
},
mounted() {
this.getDataList();
},
methods: {
handleSearch() {
this.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
const params = {
pageNumber: this.pageNumber,
pageSize: this.pageSize,
memberName: this.searchForm.memberName,
};
getRequest("/wallet/log", params).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
}).catch(() => {
this.loading = false;
});
},
handleExport() {
const params = { memberName: this.searchForm.memberName };
if (this.dateRange?.length === 2) {
params.startDate = this.dateRange[0];
params.endDate = this.dateRange[1];
}
API_Finance.exportWalletLog(params).then((blob) => {
downloadBlob(blob, "钱包流水.xlsx");
});
},
},
};
defineOptions({ name: "finance-wallet-log" });
const loading = ref(false);
const searchForm = reactive({ memberName: "" });
const dateRange = ref([]);
const pageNumber = ref(1);
const pageSize = ref(20);
const data = ref([]);
const total = ref(0);
onMounted(() => {
getDataList();
});
function handleSearch() {
pageNumber.value = 1;
getDataList();
}
function getDataList() {
loading.value = true;
const params = {
pageNumber: pageNumber.value,
pageSize: pageSize.value,
memberName: searchForm.memberName,
};
getRequest("/wallet/log", params).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
}).catch(() => {
loading.value = false;
});
}
function handleExport() {
const params = { memberName: searchForm.memberName };
if (dateRange.value?.length === 2) {
params.startDate = dateRange.value[0];
params.endDate = dateRange.value[1];
}
API_Finance.exportWalletLog(params).then((blob) => {
downloadBlob(blob, "钱包流水.xlsx");
});
}
</script>

View File

@@ -52,7 +52,7 @@
:close-on-click-modal="false"
destroy-on-close
>
<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="计量单位" prop="name">
<el-input v-model="form.name" clearable style="width: 100%" />
</el-form-item>
@@ -65,7 +65,8 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, onMounted } from "vue";
import {
addGoodsUnit,
getGoodsUnitPage,
@@ -73,161 +74,163 @@ import {
delGoodsUnit,
} from "@/api/index";
import { regular } from "@/utils";
import { Message, Modal } from "@/utils/message";
export default {
name: "goods-unit",
data() {
return {
loading: true,
modalVisible: false,
modalTitle: "",
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
name: "",
},
form: {
name: "",
},
formValidate: {
name: [regular.REQUIRED, regular.VARCHAR5],
},
submitLoading: false,
selectList: [],
selectCount: 0,
data: [],
total: 0,
};
},
methods: {
init() {
this.getDataList();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
this.clearSelectAll();
},
changePageSize(v) {
this.searchForm.pageSize = v;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
clearSelectAll() {
this.$refs.table?.clearSelection();
},
changeSelect(e) {
this.selectList = e;
this.selectCount = e.length;
},
getDataList() {
this.loading = true;
getGoodsUnitPage(this.searchForm)
.then((res) => {
defineOptions({ name: "goods-unit" });
const loading = ref(true);
const modalVisible = ref(false);
const modalTitle = ref("");
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
name: "",
});
const form = ref({
name: "",
});
const formValidate = reactive({
name: [regular.REQUIRED, regular.VARCHAR5],
});
const submitLoading = ref(false);
const selectList = ref([]);
const selectCount = ref(0);
const data = ref([]);
const total = ref(0);
const table = ref(null);
const formRef = ref(null);
// 未在 data 中声明、原代码直接挂载到 this 上的字段
let id = null;
function init() {
getDataList();
}
function changePage(v) {
searchForm.pageNumber = v;
getDataList();
clearSelectAll();
}
function changePageSize(v) {
searchForm.pageSize = v;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
}
function clearSelectAll() {
table.value?.clearSelection();
}
function changeSelect(e) {
selectList.value = e;
selectCount.value = e.length;
}
function getDataList() {
loading.value = true;
getGoodsUnitPage(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
})
.finally(() => {
loading.value = false;
});
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true;
if (modalTitle.value == "添加") {
if (data.value.find((item) => item.name == form.value.name)) {
Message.error("请勿添加重复计量单位!");
submitLoading.value = false;
return;
}
delete form.value.id;
addGoodsUnit(form.value).then((res) => {
submitLoading.value = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
Message.success("操作成功");
getDataList();
modalVisible.value = false;
}
})
.finally(() => {
this.loading = false;
});
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
this.submitLoading = true;
if (this.modalTitle == "添加") {
if (this.data.find((item) => item.name == this.form.name)) {
this.$Message.error("请勿添加重复计量单位!");
this.submitLoading = false;
return;
}
delete this.form.id;
addGoodsUnit(this.form).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
this.modalVisible = false;
}
});
} else {
updateGoodsUnit(this.id, this.form).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
this.modalVisible = false;
}
});
} else {
updateGoodsUnit(id, form.value).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("操作成功");
getDataList();
modalVisible.value = false;
}
});
}
}
});
}
function add() {
modalTitle.value = "添加";
form.value = {};
formRef.value?.resetFields();
modalVisible.value = true;
}
function edit(v) {
id = v.id;
modalTitle.value = "修改";
modalVisible.value = true;
form.value.name = v.name;
}
function remove(v) {
Modal.confirm({
title: "确认删除",
content: "您确认要删除 " + v.name + " ?",
loading: true,
onOk: () => {
delGoodsUnit(v.id).then((res) => {
Modal.remove();
if (res.success) {
Message.success("操作成功");
getDataList();
}
});
},
add() {
this.modalTitle = "添加";
this.form = {};
this.$refs.form?.resetFields();
this.modalVisible = true;
},
edit(v) {
this.id = v.id;
this.modalTitle = "修改";
this.modalVisible = true;
this.form.name = v.name;
},
remove(v) {
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除 " + v.name + " ?",
loading: true,
onOk: () => {
delGoodsUnit(v.id).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
});
}
function delAll() {
if (selectCount.value <= 0) {
Message.warning("您还未选择要删除的数据");
return;
}
Modal.confirm({
title: "确认删除",
content: "您确认要删除所选的 " + selectCount.value + " 条数据?",
loading: true,
onOk: () => {
let ids = "";
selectList.value.forEach(function (e) {
ids += e.id + ",";
});
ids = ids.substring(0, ids.length - 1);
delGoodsUnit(ids).then((res) => {
Modal.remove();
if (res.success) {
Message.success("操作成功");
clearSelectAll();
getDataList();
}
});
},
delAll() {
if (this.selectCount <= 0) {
this.$Message.warning("您还未选择要删除的数据");
return;
}
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除所选的 " + this.selectCount + " 条数据?",
loading: true,
onOk: () => {
let ids = "";
this.selectList.forEach(function (e) {
ids += e.id + ",";
});
ids = ids.substring(0, ids.length - 1);
delGoodsUnit(ids).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.clearSelectAll();
this.getDataList();
}
});
},
});
},
},
mounted() {
this.init();
},
};
});
}
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="70px"
@@ -216,7 +216,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>
@@ -255,7 +255,7 @@
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="batchAuditForm" :model="batchAuditForm" label-width="100px">
<el-form ref="batchAuditFormRef" :model="batchAuditForm" label-width="100px">
<el-form-item label="审核结果" prop="auth_flag">
<el-radio-group v-model="batchAuditForm.auth_flag">
<el-radio :value="1">审核通过</el-radio>
@@ -278,7 +278,7 @@
</el-dialog>
<el-dialog v-model="goodsGroupFlag" title="批量设置商品分组" width="420px">
<el-form ref="goodsGroupForm" :model="goodsGroupForm" :rules="goodsGroupRule" label-width="90px">
<el-form ref="goodsGroupFormRef" :model="goodsGroupForm" :rules="goodsGroupRule" label-width="90px">
<el-form-item label="商品分组" prop="groupId">
<el-select v-model="goodsGroupForm.groupId" clearable filterable style="width: 240px">
<el-option
@@ -298,7 +298,8 @@
</div>
</template>
<script>
<script setup>
import { computed, nextTick, onMounted, reactive, ref } from "vue";
import {
getGoodsListData,
getGoodsNumerData,
@@ -310,421 +311,423 @@ import {
} from "@/api/goods";
import { ElMessage, ElMessageBox } from "element-plus";
import { unitPrice, customRouterPush } from "@/utils/filters";
export default {
name: "goods",
data() {
return {
id: "", //要操作的id
loading: true, // 表单加载状态
modalVisible: false, // 添加或编辑显示
searchForm: {
// 搜索框初始化对象
pageNumber: 1, // 当前页数
pageSize: 20, // 页面大小
sort: "create_time", // 默认排序字段
order: "desc", // 默认排序方式
groupId: "",
},
underForm: {
// 下架原因
reason: "",
},
goodsAuditForm: {
// 商品审核表单
auth_flag: 1,
},
auditModalVisible: false, // 审核弹框显示状态
auditSubmitLoading: false,
currentAuditGoods: null, // 当前审核的商品
submitLoading: false, // 添加或编辑提交状态
data: [], // 表单数据
total: 0, // 表单数据总数
currentStatus: "ALL",
goodsNumerData: {},
selectedRows: [], // 选中的行数据
selectAll: false, // 全选状态
batchAuditModalVisible: false, // 批量审核弹框显示状态
batchAuditSubmitLoading: false,
batchAuditForm: {
auth_flag: 1,
reason: ''
},
goodsGroupFlag: false,
goodsGroupLoading: false,
goodsGroupList: [],
goodsGroupForm: {
groupId: ""
},
goodsGroupRule: {
groupId: [{ required: true, message: "请选择商品分组", trigger: "change" }]
}
defineOptions({ name: "goods" });
// 模板 ref(searchFormRef/underFormRef/batchAuditFormRef/goodsGroupFormRef 因与 data 同名改为 xxxRef)
const searchFormRef = ref(null);
const table = ref(null);
const underFormRef = ref(null);
const auditForm = ref(null);
const batchAuditFormRef = ref(null);
const goodsGroupFormRef = ref(null);
const id = ref(""); //要操作的id
const loading = ref(true); // 表单加载状态
const modalVisible = ref(false); // 添加或编辑显示
const searchForm = reactive({
// 搜索框初始化对象
pageNumber: 1, // 当前页数
pageSize: 20, // 页面大小
sort: "create_time", // 默认排序字段
order: "desc", // 默认排序方式
groupId: "",
});
const underForm = reactive({
// 下架原因
reason: "",
});
const goodsAuditForm = reactive({
// 商品审核表单
auth_flag: 1,
});
const auditModalVisible = ref(false); // 审核弹框显示状态
const auditSubmitLoading = ref(false);
const currentAuditGoods = ref(null); // 当前审核的商品
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 batchAuditModalVisible = ref(false); // 批量审核弹框显示状态
const batchAuditSubmitLoading = ref(false);
const batchAuditForm = ref({
auth_flag: 1,
reason: ''
});
const goodsGroupFlag = ref(false);
const goodsGroupLoading = ref(false);
const goodsGroupList = ref([]);
const goodsGroupForm = ref({
groupId: ""
});
const goodsGroupRule = reactive({
groupId: [{ required: true, message: "请选择商品分组", trigger: "change" }]
});
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() {
table.value?.clearSelection?.();
}
function onStatusTabClick(tab) {
const status = tab.paneName ?? tab.props?.name;
goodsStatusClick(status);
}
function salesModelText(v) {
if (v === "RETAIL") return "零售";
if (v === "WHOLESALE") return "批发";
return "其他类型";
}
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 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() {
getDataList();
getNumberData();
loadGoodsGroupList();
}
// 分页 改变页码
function changePage(v) {
searchForm.pageNumber = v;
getDataList();
}
// 分页 改变页数
function changePageSize(v) {
searchForm.pageSize = v;
getDataList();
}
// 搜索
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
getNumberData();
}
// 获取数据
function getDataList() {
loading.value = true;
getGoodsListData(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function getNumberData() {
// 创建一个不包含goodsStatus字段的搜索参数
const { goodsStatus, ...searchParams } = searchForm;
getGoodsNumerData(searchParams).then((res) => {
if (res.success) {
goodsNumerData.value = res.result;
}
})
}
// 编辑
function edit(v) {
id.value = v.id;
if (v.underMessage) {
underForm.reason = v.underMessage;
} else {
underForm.reason = "";
}
modalVisible.value = true;
}
// 下架
function lower() {
const reason = underForm.reason ? underForm.reason.trim() : "";
if (!reason) {
ElMessage.error("下架原因不能为空");
return;
}
let params = {
goodsId: id.value,
reason
};
lowGoods(params).then((res) => {
if (res.success) {
ElMessage.success("操作成功");
modalVisible.value = false;
getDataList();
getNumberData(); // 添加这行
}
});
}
// 上架
function upper(v) {
ElMessageBox.confirm("您确认要上架 " + v.goodsName + " ?", "确认上架", { type: "warning" }).then(() => {
let params = {
goodsId: v.id
};
},
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'}
];
return upGoods(params).then((res) => {
if (res.success) {
ElMessage.success("上架成功");
getDataList();
getNumberData(); // 添加这行
}
});
}).catch(() => {});
}
//查看商品详情
function showDetail(v) {
customRouterPush({
name: "goods-detail",
query: { id: v.id },
});
}
// 商品状态筛选
function goodsStatusClick(name) {
if (name === "ALL" || name === "" || name === undefined || name === null) {
delete searchForm.goodsStatus;
currentStatus.value = "ALL";
} else {
searchForm.goodsStatus = name;
currentStatus.value = name;
}
selectedRows.value = [];
clearTableSelection();
getDataList();
}
function examine(v, authFlag) {
// 审核商品
let examine = "通过";
goodsAuditForm.authFlag = "PASS";
if (authFlag != 1) {
examine = "拒绝";
goodsAuditForm.authFlag = "REFUSE";
}
ElMessageBox.confirm("您确认要审核" + examine + " " + v.goodsName + " ?", "确认审核", { type: "warning" }).then(() => {
goodsAuditForm.goodsIds=v.id;
let formData = new FormData();
formData.append('goodsIds', v.id);
formData.append('authFlag', goodsAuditForm.authFlag);
return authGoods(formData).then((res) => {
if (res.success) {
ElMessage.success("审核成功");
getDataList();
getNumberData();
}
});
}).catch(() => {});
}
// 打开审核弹框
function openAuditModal(goods) {
currentAuditGoods.value = goods;
goodsAuditForm.auth_flag = 1;
goodsAuditForm.reason = '';
auditSubmitLoading.value = false;
auditModalVisible.value = true;
}
// 提交审核
function submitAudit() {
if (!currentAuditGoods.value) {
return;
}
auditSubmitLoading.value = true;
const formData = new FormData();
formData.append('goodsIds', currentAuditGoods.value.id);
formData.append('authFlag', goodsAuditForm.auth_flag === 1 ? 'PASS' : 'REFUSE');
authGoods(formData).then((res) => {
if (res.success) {
ElMessage.success('审核成功');
auditModalVisible.value = false;
getDataList();
getNumberData();
}
},
methods: {
unitPrice,
clearTableSelection() {
this.$refs.table?.clearSelection?.();
},
onStatusTabClick(tab) {
const status = tab.paneName ?? tab.props?.name;
this.goodsStatusClick(status);
},
salesModelText(v) {
if (v === "RETAIL") return "零售";
if (v === "WHOLESALE") return "批发";
return "其他类型";
},
goodsTypeText(v) {
if (v === "PHYSICAL_GOODS") return "实物商品";
if (v === "VIRTUAL_GOODS") return "虚拟商品";
if (v === "E_COUPON") return "电子卡券"; // 卡密商品
if (v === "APPOINTMENT_GOODS") return "预约商品";
return v || "—";
},
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.getDataList();
this.getNumberData();
this.loadGoodsGroupList();
},
// 分页 改变页码
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.searchForm).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
getNumberData() {
// 创建一个不包含goodsStatus字段的搜索参数
const { goodsStatus, ...searchParams } = this.searchForm;
getGoodsNumerData(searchParams).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 = "";
}).finally(() => {
auditSubmitLoading.value = false;
});
}
function onSelectionChange(selection) {
selectedRows.value = selection;
}
// 批量上架
function batchUpper() {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要上架的商品');
return;
}
const goodsNames = selectedRows.value.map(item => item.goodsName).join('、');
ElMessageBox.confirm(`您确认要上架以下商品吗?\n${goodsNames}`, "确认批量上架", { type: "warning" }).then(() => {
const goodsIds = selectedRows.value.map(item => item.id);
const params = {
goodsId: goodsIds
};
return upGoods(params).then((res) => {
if (res.success) {
ElMessage.success('批量上架成功');
selectedRows.value = [];
selectAll.value = false;
getDataList();
getNumberData();
}
this.modalVisible = true;
},
// 下架
lower() {
const reason = this.underForm.reason ? this.underForm.reason.trim() : "";
if (!reason) {
ElMessage.error("下架原因不能为空");
return;
});
}).catch(() => {});
}
// 批量下架
function batchLower() {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要下架的商品');
return;
}
const goodsNames = selectedRows.value.map(item => item.goodsName).join('、');
ElMessageBox.confirm(`您确认要下架以下商品吗?\n${goodsNames}`, "确认批量下架", { type: "warning" }).then(() => {
const goodsIds = selectedRows.value.map(item => item.id);
const params = {
goodsId: goodsIds,
reason: '批量下架操作'
};
return lowGoods(params).then((res) => {
if (res.success) {
ElMessage.success('批量下架成功');
selectedRows.value = [];
selectAll.value = false;
getDataList();
getNumberData();
}
let params = {
goodsId: this.id,
reason
};
lowGoods(params).then((res) => {
if (res.success) {
ElMessage.success("操作成功");
this.modalVisible = false;
this.getDataList();
this.getNumberData(); // 添加这行
}
});
},
// 上架
upper(v) {
ElMessageBox.confirm("您确认要上架 " + v.goodsName + " ?", "确认上架", { type: "warning" }).then(() => {
let params = {
goodsId: v.id
};
return upGoods(params).then((res) => {
if (res.success) {
ElMessage.success("上架成功");
this.getDataList();
this.getNumberData(); // 添加这行
}
});
}).catch(() => {});
},
});
}).catch(() => {});
}
//查看商品详情
showDetail(v) {
customRouterPush({
name: "goods-detail",
query: { id: v.id },
});
},
// 批量审核
function batchAudit() {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要审核的商品');
return;
}
// 商品状态筛选
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();
},
examine(v, authFlag) {
// 审核商品
let examine = "通过";
this.goodsAuditForm.authFlag = "PASS";
if (authFlag != 1) {
examine = "拒绝";
this.goodsAuditForm.authFlag = "REFUSE";
}
// 重置批量审核表单
batchAuditForm.value = {
auth_flag: 1,
reason: ''
};
batchAuditModalVisible.value = true;
}
ElMessageBox.confirm("您确认要审核" + examine + " " + v.goodsName + " ?", "确认审核", { type: "warning" }).then(() => {
this.goodsAuditForm.goodsIds=v.id;
let formData = new FormData();
formData.append('goodsIds', v.id);
formData.append('authFlag', this.goodsAuditForm.authFlag);
// 提交批量审核
function submitBatchAudit() {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要审核的商品');
return;
}
return authGoods(formData).then((res) => {
if (res.success) {
ElMessage.success("审核成功");
this.getDataList();
this.getNumberData();
}
});
}).catch(() => {});
},
// 如果是拒绝审核,必须填写原因
if (batchAuditForm.value.auth_flag === 2 && !batchAuditForm.value.reason.trim()) {
ElMessage.warning('审核拒绝时必须填写拒绝原因');
return;
}
// 打开审核弹框
openAuditModal(goods) {
this.currentAuditGoods = goods;
this.goodsAuditForm.auth_flag = 1;
this.goodsAuditForm.reason = '';
this.auditSubmitLoading = false;
this.auditModalVisible = true;
},
const actionText = batchAuditForm.value.auth_flag === 1 ? '通过' : '拒绝';
const goodsIds = selectedRows.value.map(item => item.id);
const formData = new FormData();
formData.append('goodsIds', goodsIds.join(','));
formData.append('authFlag', batchAuditForm.value.auth_flag === 1 ? 'PASS' : 'REFUSE');
if (batchAuditForm.value.reason) {
formData.append('reason', batchAuditForm.value.reason);
}
// 提交审核
submitAudit() {
if (!this.currentAuditGoods) {
return;
}
this.auditSubmitLoading = true;
const formData = new FormData();
formData.append('goodsIds', this.currentAuditGoods.id);
formData.append('authFlag', this.goodsAuditForm.auth_flag === 1 ? 'PASS' : 'REFUSE');
authGoods(formData).then((res) => {
if (res.success) {
ElMessage.success('审核成功');
this.auditModalVisible = false;
this.getDataList();
this.getNumberData();
}
}).finally(() => {
this.auditSubmitLoading = false;
});
},
onSelectionChange(selection) {
this.selectedRows = selection;
},
// 批量上架
batchUpper() {
if (this.selectedRows.length === 0) {
ElMessage.warning('请先选择要上架的商品');
return;
}
const goodsNames = this.selectedRows.map(item => item.goodsName).join('、');
ElMessageBox.confirm(`您确认要上架以下商品吗?\n${goodsNames}`, "确认批量上架", { type: "warning" }).then(() => {
const goodsIds = this.selectedRows.map(item => item.id);
const params = {
goodsId: goodsIds
};
return upGoods(params).then((res) => {
if (res.success) {
ElMessage.success('批量上架成功');
this.selectedRows = [];
this.selectAll = false;
this.getDataList();
this.getNumberData();
}
});
}).catch(() => {});
},
// 批量下架
batchLower() {
if (this.selectedRows.length === 0) {
ElMessage.warning('请先选择要下架的商品');
return;
}
const goodsNames = this.selectedRows.map(item => item.goodsName).join('、');
ElMessageBox.confirm(`您确认要下架以下商品吗?\n${goodsNames}`, "确认批量下架", { type: "warning" }).then(() => {
const goodsIds = this.selectedRows.map(item => item.id);
const params = {
goodsId: goodsIds,
reason: '批量下架操作'
};
return lowGoods(params).then((res) => {
if (res.success) {
ElMessage.success('批量下架成功');
this.selectedRows = [];
this.selectAll = false;
this.getDataList();
this.getNumberData();
}
});
}).catch(() => {});
},
// 批量审核
batchAudit() {
if (this.selectedRows.length === 0) {
ElMessage.warning('请先选择要审核的商品');
return;
}
// 重置批量审核表单
this.batchAuditForm = {
auth_flag: 1,
reason: ''
};
this.batchAuditModalVisible = true;
},
// 提交批量审核
submitBatchAudit() {
if (this.selectedRows.length === 0) {
ElMessage.warning('请先选择要审核的商品');
return;
}
// 如果是拒绝审核,必须填写原因
if (this.batchAuditForm.auth_flag === 2 && !this.batchAuditForm.reason.trim()) {
ElMessage.warning('审核拒绝时必须填写拒绝原因');
return;
}
const actionText = this.batchAuditForm.auth_flag === 1 ? '通过' : '拒绝';
const goodsIds = this.selectedRows.map(item => item.id);
const formData = new FormData();
formData.append('goodsIds', goodsIds.join(','));
formData.append('authFlag', this.batchAuditForm.auth_flag === 1 ? 'PASS' : 'REFUSE');
if (this.batchAuditForm.reason) {
formData.append('reason', this.batchAuditForm.reason);
}
this.batchAuditSubmitLoading = true;
authGoods(formData).then((res) => {
if (res.success) {
ElMessage.success(`批量审核${actionText}成功`);
this.selectedRows = [];
this.selectAll = false;
this.batchAuditModalVisible = false;
this.clearTableSelection();
this.getDataList();
this.getNumberData();
}
}).finally(() => {
this.batchAuditSubmitLoading = false;
});
},
loadGoodsGroupList() {
getGoodsGroupByPage({ pageNumber: 1, pageSize: 1000 }).then((res) => {
if (res && res.success && res.result) {
this.goodsGroupList = res.result.records || [];
}
});
},
openSetGoodsGroup() {
if (this.selectedRows.length === 0) {
ElMessage.warning("请先选择商品");
return;
}
this.goodsGroupFlag = true;
this.goodsGroupLoading = false;
this.goodsGroupForm = { groupId: "" };
this.$nextTick(() => {
if (this.$refs.goodsGroupForm) this.$refs.goodsGroupForm.resetFields();
});
this.loadGoodsGroupList();
},
submitSetGoodsGroup() {
this.$refs.goodsGroupForm.validate((valid) => {
if (!valid) return;
const goodsIds = this.selectedRows.map((item) => item.id);
this.goodsGroupLoading = true;
addGoodsGroupItems(this.goodsGroupForm.groupId, goodsIds).then((res) => {
this.goodsGroupLoading = false;
if (res && res.success) {
ElMessage.success("设置成功");
this.goodsGroupFlag = false;
this.selectedRows = [];
this.selectAll = false;
this.clearTableSelection();
this.getDataList();
} else if (res && res.message) {
ElMessage.error(res.message);
}
}).catch(() => {
this.goodsGroupLoading = false;
ElMessage.error("设置失败,请检查权限或后端接口");
});
});
batchAuditSubmitLoading.value = true;
authGoods(formData).then((res) => {
if (res.success) {
ElMessage.success(`批量审核${actionText}成功`);
selectedRows.value = [];
selectAll.value = false;
batchAuditModalVisible.value = false;
clearTableSelection();
getDataList();
getNumberData();
}
},
mounted() {
this.init();
},
};
}).finally(() => {
batchAuditSubmitLoading.value = false;
});
}
function loadGoodsGroupList() {
getGoodsGroupByPage({ pageNumber: 1, pageSize: 1000 }).then((res) => {
if (res && res.success && res.result) {
goodsGroupList.value = res.result.records || [];
}
});
}
function openSetGoodsGroup() {
if (selectedRows.value.length === 0) {
ElMessage.warning("请先选择商品");
return;
}
goodsGroupFlag.value = true;
goodsGroupLoading.value = false;
goodsGroupForm.value = { groupId: "" };
nextTick(() => {
if (goodsGroupFormRef.value) goodsGroupFormRef.value.resetFields();
});
loadGoodsGroupList();
}
function submitSetGoodsGroup() {
goodsGroupFormRef.value.validate((valid) => {
if (!valid) return;
const goodsIds = selectedRows.value.map((item) => item.id);
goodsGroupLoading.value = true;
addGoodsGroupItems(goodsGroupForm.value.groupId, goodsIds).then((res) => {
goodsGroupLoading.value = false;
if (res && res.success) {
ElMessage.success("设置成功");
goodsGroupFlag.value = false;
selectedRows.value = [];
selectAll.value = false;
clearTableSelection();
getDataList();
} else if (res && res.message) {
ElMessage.error(res.message);
}
}).catch(() => {
goodsGroupLoading.value = false;
ElMessage.error("设置失败,请检查权限或后端接口");
});
});
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
// Tab组件样式

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -87,104 +87,103 @@
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from "vue";
import { authGoods, getAuthGoodsListData } from "@/api/goods";
import { ElMessage, ElMessageBox } from "element-plus";
import { unitPrice, customRouterPush } from "@/utils/filters";
export default {
name: "goodsApply",
data() {
return {
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
},
goodsAuditForm: {
auth_flag: 1,
},
data: [],
total: 0,
};
},
methods: {
unitPrice,
authFlagText(v) {
const map = {
TOBEAUDITED: "待审核",
PASS: "审核通过",
REFUSE: "审核拒绝",
};
return map[v] || v || "-";
},
authFlagTagType(v) {
const map = {
TOBEAUDITED: "warning",
PASS: "success",
REFUSE: "danger",
};
return map[v] || "info";
},
init() {
this.getDataList();
},
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();
},
getDataList() {
this.loading = true;
this.searchForm.authFlag = 0;
getAuthGoodsListData(this.searchForm)
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
})
.catch(() => {
this.loading = false;
});
},
examine(v, authFlag) {
const examine = authFlag === 1 ? "通过" : "拒绝";
this.goodsAuditForm.authFlag = authFlag === 1 ? "PASS" : "REFUSE";
ElMessageBox.confirm(`您确认要审核${examine} ${v.goodsName} ?`, "确认审核", { type: "warning" }).then(() => {
const formData = new FormData();
formData.append("goodsIds", v.id);
formData.append("authFlag", this.goodsAuditForm.authFlag);
return authGoods(formData).then((res) => {
if (res.success) {
ElMessage.success("审核成功");
this.getDataList();
}
});
}).catch(() => {});
},
showDetail(v) {
customRouterPush({
name: "goods-detail",
query: { id: v.id },
});
},
},
mounted() {
this.init();
},
};
defineOptions({ name: "goodsApply" });
// 模板中 el-form 的 ref 与数据 searchForm 同名,避免相互覆盖改名
const searchFormRef = ref(null);
const table = ref(null);
const loading = ref(true);
const searchForm = ref({
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
});
const goodsAuditForm = ref({
auth_flag: 1,
});
const data = ref([]);
const total = ref(0);
function authFlagText(v) {
const map = {
TOBEAUDITED: "待审核",
PASS: "审核通过",
REFUSE: "审核拒绝",
};
return map[v] || v || "-";
}
function authFlagTagType(v) {
const map = {
TOBEAUDITED: "warning",
PASS: "success",
REFUSE: "danger",
};
return map[v] || "info";
}
function init() {
getDataList();
}
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();
}
function getDataList() {
loading.value = true;
searchForm.value.authFlag = 0;
getAuthGoodsListData(searchForm.value)
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
})
.catch(() => {
loading.value = false;
});
}
function examine(v, authFlag) {
const examine = authFlag === 1 ? "通过" : "拒绝";
goodsAuditForm.value.authFlag = authFlag === 1 ? "PASS" : "REFUSE";
ElMessageBox.confirm(`您确认要审核${examine} ${v.goodsName} ?`, "确认审核", { type: "warning" }).then(() => {
const formData = new FormData();
formData.append("goodsIds", v.id);
formData.append("authFlag", goodsAuditForm.value.authFlag);
return authGoods(formData).then((res) => {
if (res.success) {
ElMessage.success("审核成功");
getDataList();
}
});
}).catch(() => {});
}
function showDetail(v) {
customRouterPush({
name: "goods-detail",
query: { id: v.id },
});
}
onMounted(() => {
init();
});
</script>
<style scoped>

View File

@@ -376,307 +376,304 @@
</div>
</template>
<script>
<script setup>
import { computed, nextTick, onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import { View } from "@element-plus/icons-vue";
import { authGoods, getBrandListData, getGoodsDetail } from "@/api/goods";
import { ElMessage } from "element-plus";
export default {
name: "goodsDetail",
components: { View },
data() {
return {
loading: false,
goods: {},
brandList: [],
previewGoodsPicture: "",
goodsPictureVisible: false,
wholesaleData: [],
skuData: [],
activeTab: "basic",
slotCapacityVisible: false,
currentSlotCapacities: [],
goodsAuditForm: {
auth_flag: 1,
},
auditModalVisible: false,
auditSubmitLoading: false,
};
},
computed: {
isAppointmentGoods() {
return this.goods.goodsType === "APPOINTMENT_GOODS";
},
needsLogistics() {
return this.goods.goodsType === "PHYSICAL_GOODS";
},
salesModelDisplayText() {
const type = this.goods.goodsType;
const model = this.goods.salesModel;
if (type === "APPOINTMENT_GOODS") return "预约型";
if (type === "E_COUPON") return "电子卡券";
if (type === "VIRTUAL_GOODS") return "虚拟型";
if (model === "WHOLESALE") return "批发型";
if (model === "RETAIL") return "零售型";
return model || "—";
},
sectionTabs() {
const tabs = [
{ name: "basic", label: "基础信息" },
{ name: "media", label: "图文信息" },
];
if (this.isAppointmentGoods) {
tabs.push({ name: "appointment", label: "预约规则" });
}
tabs.push(
{ name: "price", label: "价格库存" },
{ name: "other", label: "其他信息" },
);
return tabs;
},
categoryText() {
const names = this.goods.categoryName;
if (!names || !names.length) return "—";
return names.join(" > ");
},
/** 兼容扁平列表与历史嵌套分组两种 params 结构 */
displayGoodsParams() {
let list = this.goods.goodsParamsDTOList;
if ((!Array.isArray(list) || !list.length) && this.goods.params) {
list = this.parseParamsJson(this.goods.params);
}
if (!Array.isArray(list) || !list.length) return [];
const flat = [];
list.forEach((item) => {
if (Array.isArray(item?.goodsParamsItemDTOList) && item.goodsParamsItemDTOList.length) {
item.goodsParamsItemDTOList.forEach((child) => {
if (child?.paramName) flat.push(child);
});
} else if (item?.paramName) {
flat.push(item);
}
defineOptions({ name: "goodsDetail" });
const route = useRoute();
const loading = ref(false);
const goods = ref({});
const brandList = ref([]);
const previewGoodsPicture = ref("");
const goodsPictureVisible = ref(false);
const wholesaleData = ref([]);
const skuData = ref([]);
const activeTab = ref("basic");
const slotCapacityVisible = ref(false);
const currentSlotCapacities = ref([]);
const goodsAuditForm = reactive({
auth_flag: 1,
});
const auditModalVisible = ref(false);
const auditSubmitLoading = ref(false);
const isAppointmentGoods = computed(() => {
return goods.value.goodsType === "APPOINTMENT_GOODS";
});
const needsLogistics = computed(() => {
return goods.value.goodsType === "PHYSICAL_GOODS";
});
const salesModelDisplayText = computed(() => {
const type = goods.value.goodsType;
const model = goods.value.salesModel;
if (type === "APPOINTMENT_GOODS") return "预约型";
if (type === "E_COUPON") return "电子卡券";
if (type === "VIRTUAL_GOODS") return "虚拟型";
if (model === "WHOLESALE") return "批发型";
if (model === "RETAIL") return "零售型";
return model || "—";
});
const sectionTabs = computed(() => {
const tabs = [
{ name: "basic", label: "基础信息" },
{ name: "media", label: "图文信息" },
];
if (isAppointmentGoods.value) {
tabs.push({ name: "appointment", label: "预约规则" });
}
tabs.push(
{ name: "price", label: "价格库存" },
{ name: "other", label: "其他信息" },
);
return tabs;
});
const categoryText = computed(() => {
const names = goods.value.categoryName;
if (!names || !names.length) return "—";
return names.join(" > ");
});
/** 兼容扁平列表与历史嵌套分组两种 params 结构 */
const displayGoodsParams = computed(() => {
let list = goods.value.goodsParamsDTOList;
if ((!Array.isArray(list) || !list.length) && goods.value.params) {
list = parseParamsJson(goods.value.params);
}
if (!Array.isArray(list) || !list.length) return [];
const flat = [];
list.forEach((item) => {
if (Array.isArray(item?.goodsParamsItemDTOList) && item.goodsParamsItemDTOList.length) {
item.goodsParamsItemDTOList.forEach((child) => {
if (child?.paramName) flat.push(child);
});
return flat;
},
appointmentSettings() {
return this.goods.appointmentSettings || null;
},
appointmentSlotTemplate() {
return this.goods.appointmentSlotTemplate || null;
},
appointmentSlots() {
return this.appointmentSlotTemplate?.slots || [];
},
bookableDateText() {
const s = this.appointmentSettings;
if (!s) return "—";
if (s.bookableDateMode === "CUSTOM") {
const parts = [];
if (s.bookableCustomStartDate && s.bookableCustomEndDate) {
parts.push(`${s.bookableCustomStartDate} ~ ${s.bookableCustomEndDate}`);
}
const weekdays = this.formatWeekdays(s.bookableWeekdays);
if (weekdays) parts.push(weekdays);
return parts.length ? parts.join(";") : "自定义";
} else if (item?.paramName) {
flat.push(item);
}
});
return flat;
});
const appointmentSettings = computed(() => {
return goods.value.appointmentSettings || null;
});
const appointmentSlotTemplate = computed(() => {
return goods.value.appointmentSlotTemplate || null;
});
const appointmentSlots = computed(() => {
return appointmentSlotTemplate.value?.slots || [];
});
const bookableDateText = computed(() => {
const s = appointmentSettings.value;
if (!s) return "—";
if (s.bookableDateMode === "CUSTOM") {
const parts = [];
if (s.bookableCustomStartDate && s.bookableCustomEndDate) {
parts.push(`${s.bookableCustomStartDate} ~ ${s.bookableCustomEndDate}`);
}
const weekdays = formatWeekdays(s.bookableWeekdays);
if (weekdays) parts.push(weekdays);
return parts.length ? parts.join(";") : "自定义";
}
if (s.bookableDateMode === "EVERY_DAY") return "每天";
return "—";
});
const bookableDaysText = computed(() => {
const s = appointmentSettings.value;
if (!s || s.bookableDays == null) return "—";
return `对用户展示 ${s.bookableDays} 天内的可预约日期`;
});
const advanceBookingText = computed(() => {
const s = appointmentSettings.value;
if (!s) return "—";
if (s.advanceMode === "NONE") return "无需提前预约";
if (s.advanceMode === "REQUIRE_HOURS" && s.advanceHours != null) {
return `需提前 ${s.advanceHours} 小时进行预约`;
}
return "—";
});
const cancelRuleText = computed(() => {
const s = appointmentSettings.value;
if (!s) return "—";
if (s.cancelMode === "NOT_ALLOWED") return "不允许取消";
if (s.cancelMode === "ALLOW_BEFORE_HOURS" && s.cancelAheadHours != null) {
return `服务开始 ${s.cancelAheadHours} 小时之前,允许取消并自动退款`;
}
return "—";
});
const formSubmitText = computed(() => {
const s = appointmentSettings.value;
if (!s) return "—";
if (s.formSubmitMode === "PER_APPOINTMENT") return "每个预约提交一次";
if (s.formSubmitMode === "PER_ORDER") return "每单提交一次";
return "—";
});
onMounted(() => {
loadBrandList();
initGoods(route.query.id);
});
function sectionNo(name) {
const index = sectionTabs.value.findIndex((tab) => tab.name === name);
return index >= 0 ? index + 1 : "";
}
function parseParamsJson(paramsJson) {
if (!paramsJson) return [];
try {
const array = JSON.parse(paramsJson);
if (!Array.isArray(array)) return [];
const items = [];
array.forEach((element) => {
if (Array.isArray(element?.goodsParamsItemDTOList)) {
items.push(...element.goodsParamsItemDTOList);
} else if (element?.paramName) {
items.push(element);
}
if (s.bookableDateMode === "EVERY_DAY") return "每天";
return "—";
},
bookableDaysText() {
const s = this.appointmentSettings;
if (!s || s.bookableDays == null) return "—";
return `对用户展示 ${s.bookableDays} 天内的可预约日期`;
},
advanceBookingText() {
const s = this.appointmentSettings;
if (!s) return "—";
if (s.advanceMode === "NONE") return "无需提前预约";
if (s.advanceMode === "REQUIRE_HOURS" && s.advanceHours != null) {
return `需提前 ${s.advanceHours} 小时进行预约`;
}
return "—";
},
cancelRuleText() {
const s = this.appointmentSettings;
if (!s) return "—";
if (s.cancelMode === "NOT_ALLOWED") return "不允许取消";
if (s.cancelMode === "ALLOW_BEFORE_HOURS" && s.cancelAheadHours != null) {
return `服务开始 ${s.cancelAheadHours} 小时之前,允许取消并自动退款`;
}
return "—";
},
formSubmitText() {
const s = this.appointmentSettings;
if (!s) return "—";
if (s.formSubmitMode === "PER_APPOINTMENT") return "每个预约提交一次";
if (s.formSubmitMode === "PER_ORDER") return "每单提交一次";
return "—";
},
},
mounted() {
this.loadBrandList();
this.initGoods(this.$route.query.id);
},
methods: {
sectionNo(name) {
const index = this.sectionTabs.findIndex((tab) => tab.name === name);
return index >= 0 ? index + 1 : "";
},
parseParamsJson(paramsJson) {
if (!paramsJson) return [];
try {
const array = JSON.parse(paramsJson);
if (!Array.isArray(array)) return [];
const items = [];
array.forEach((element) => {
if (Array.isArray(element?.goodsParamsItemDTOList)) {
items.push(...element.goodsParamsItemDTOList);
} else if (element?.paramName) {
items.push(element);
}
});
return items;
} catch {
return [];
}
},
jumpToTab(name) {
this.activeTab = name;
this.$nextTick(() => {
const el = document.getElementById(`goods-section-${name}`);
el?.scrollIntoView({ behavior: "smooth", block: "start" });
});
},
loadBrandList() {
getBrandListData().then((res) => {
this.brandList = Array.isArray(res) ? res : res?.result || [];
});
},
initGoods(id) {
if (!id) return;
this.loading = true;
getGoodsDetail(id)
.then((res) => {
if (!res?.success) return;
this.goods = res.result || {};
this.skuData = (res.result?.skuList || []).map((sku) => ({
id: sku.id,
specs: sku.simpleSpecs || sku.goodsName,
sn: sku.sn,
weight: sku.weight,
price: sku.price,
image: sku.goodsGalleryList,
quantity: sku.quantity,
slotCapacities: sku.slotCapacities,
}));
this.wholesaleData = res.result?.wholesaleList || [];
})
.finally(() => {
this.loading = false;
});
},
brandText(brandId) {
if (!brandId || brandId === "0") return "—";
const brand = this.brandList.find((item) => String(item.id) === String(brandId));
return brand?.name || brandId;
},
goodsTypeText(v) {
if (v === "PHYSICAL_GOODS") return "实物商品";
if (v === "VIRTUAL_GOODS") return "虚拟商品";
if (v === "E_COUPON") return "电子卡券";
if (v === "APPOINTMENT_GOODS") return "预约商品";
return v || "—";
},
normalizeSlotCapacities(caps) {
if (!Array.isArray(caps) || !caps.length) return [];
return caps.map((item) => ({
startTime: item.startTime || "",
endTime: item.endTime || "",
capacity: item.slotCapacity ?? item.capacity ?? 0,
});
return items;
} catch {
return [];
}
}
function jumpToTab(name) {
activeTab.value = name;
nextTick(() => {
const el = document.getElementById(`goods-section-${name}`);
el?.scrollIntoView({ behavior: "smooth", block: "start" });
});
}
function loadBrandList() {
getBrandListData().then((res) => {
brandList.value = Array.isArray(res) ? res : res?.result || [];
});
}
function initGoods(id) {
if (!id) return;
loading.value = true;
getGoodsDetail(id)
.then((res) => {
if (!res?.success) return;
goods.value = res.result || {};
skuData.value = (res.result?.skuList || []).map((sku) => ({
id: sku.id,
specs: sku.simpleSpecs || sku.goodsName,
sn: sku.sn,
weight: sku.weight,
price: sku.price,
image: sku.goodsGalleryList,
quantity: sku.quantity,
slotCapacities: sku.slotCapacities,
}));
},
hasSlotCapacities(row) {
return this.slotCapacityCount(row) > 0;
},
slotCapacityCount(row) {
return this.normalizeSlotCapacities(row?.slotCapacities).length;
},
openSlotCapacityDialog(row) {
this.currentSlotCapacities = this.normalizeSlotCapacities(row?.slotCapacities);
this.slotCapacityVisible = true;
},
formatSlotCapacity(capacity) {
const value = Number(capacity);
if (!value) return "不限制";
return String(value);
},
marketEnableText(v) {
if (v === "UPPER") return "上架";
if (v === "DOWN") return "下架";
return "—";
},
authFlagText(v) {
if (v === "TOBEAUDITED") return "待审核";
if (v === "PASS") return "通过";
if (v === "REFUSE") return "拒绝";
return v || "—";
},
authFlagTagType(v) {
if (v === "PASS") return "success";
if (v === "TOBEAUDITED") return "warning";
if (v === "REFUSE") return "danger";
return "info";
},
formatServiceModes(modes) {
if (!modes || !modes.length) return "—";
const map = {
IN_STORE: "到店服务",
ON_SITE: "上门服务",
};
return modes.map((m) => map[m] || m).join("、");
},
formatWeekdays(weekdays) {
if (!Array.isArray(weekdays) || !weekdays.length) return "";
const map = {
1: "周一",
2: "周二",
3: "周三",
4: "周四",
5: "周五",
6: "周六",
7: "周日",
};
return weekdays.map((d) => map[d] || d).join("、");
},
handleViewGoodsPicture(url) {
this.previewGoodsPicture = url;
this.goodsPictureVisible = true;
},
openAuditModal() {
this.goodsAuditForm.auth_flag = 1;
this.auditSubmitLoading = false;
this.auditModalVisible = true;
},
submitAudit() {
if (!this.goods.id) return;
this.auditSubmitLoading = true;
const formData = new FormData();
formData.append("goodsIds", this.goods.id);
formData.append("authFlag", this.goodsAuditForm.auth_flag === 1 ? "PASS" : "REFUSE");
authGoods(formData)
.then((res) => {
if (res.success) {
ElMessage.success("审核成功");
this.auditModalVisible = false;
this.initGoods(this.goods.id);
}
})
.finally(() => {
this.auditSubmitLoading = false;
});
},
},
};
wholesaleData.value = res.result?.wholesaleList || [];
})
.finally(() => {
loading.value = false;
});
}
function brandText(brandId) {
if (!brandId || brandId === "0") return "—";
const brand = brandList.value.find((item) => String(item.id) === String(brandId));
return brand?.name || brandId;
}
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 normalizeSlotCapacities(caps) {
if (!Array.isArray(caps) || !caps.length) return [];
return caps.map((item) => ({
startTime: item.startTime || "",
endTime: item.endTime || "",
capacity: item.slotCapacity ?? item.capacity ?? 0,
}));
}
function hasSlotCapacities(row) {
return slotCapacityCount(row) > 0;
}
function slotCapacityCount(row) {
return normalizeSlotCapacities(row?.slotCapacities).length;
}
function openSlotCapacityDialog(row) {
currentSlotCapacities.value = normalizeSlotCapacities(row?.slotCapacities);
slotCapacityVisible.value = true;
}
function formatSlotCapacity(capacity) {
const value = Number(capacity);
if (!value) return "不限制";
return String(value);
}
function marketEnableText(v) {
if (v === "UPPER") return "上架";
if (v === "DOWN") return "下架";
return "—";
}
function authFlagText(v) {
if (v === "TOBEAUDITED") return "待审核";
if (v === "PASS") return "通过";
if (v === "REFUSE") return "拒绝";
return v || "—";
}
function authFlagTagType(v) {
if (v === "PASS") return "success";
if (v === "TOBEAUDITED") return "warning";
if (v === "REFUSE") return "danger";
return "info";
}
function formatServiceModes(modes) {
if (!modes || !modes.length) return "—";
const map = {
IN_STORE: "到店服务",
ON_SITE: "上门服务",
};
return modes.map((m) => map[m] || m).join("、");
}
function formatWeekdays(weekdays) {
if (!Array.isArray(weekdays) || !weekdays.length) return "";
const map = {
1: "周一",
2: "周二",
3: "周三",
4: "周四",
5: "周五",
6: "周六",
7: "周日",
};
return weekdays.map((d) => map[d] || d).join("、");
}
function handleViewGoodsPicture(url) {
previewGoodsPicture.value = url;
goodsPictureVisible.value = true;
}
function openAuditModal() {
goodsAuditForm.auth_flag = 1;
auditSubmitLoading.value = false;
auditModalVisible.value = true;
}
function submitAudit() {
if (!goods.value.id) return;
auditSubmitLoading.value = true;
const formData = new FormData();
formData.append("goodsIds", goods.value.id);
formData.append("authFlag", goodsAuditForm.auth_flag === 1 ? "PASS" : "REFUSE");
authGoods(formData)
.then((res) => {
if (res.success) {
ElMessage.success("审核成功");
auditModalVisible.value = false;
initGoods(goods.value.id);
}
})
.finally(() => {
auditSubmitLoading.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"
@@ -87,7 +87,7 @@
:close-on-click-modal="false"
destroy-on-close
>
<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="品牌名称" prop="name">
<el-input v-model="form.name" clearable style="width: 100%" />
</el-form-item>
@@ -110,7 +110,7 @@
<el-dialog v-model="picModelFlag" width="1200px" :show-close="true">
<ossManage
ref="ossManage"
ref="ossManageRef"
:is-component="true"
:initialize="picModelFlag"
@callback="callbackSelected"
@@ -146,13 +146,14 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, nextTick, onMounted, getCurrentInstance } from "vue";
import {
getManagerBrandPage,
addBrand,
updateBrand,
disableBrand,
delBrand,
delBrand as delBrandApi,
getCategoryTree,
getBrandCategoryListData,
saveBrandCategory,
@@ -162,222 +163,234 @@ import { regular } from "@/utils";
import { ElMessage, ElMessageBox } from "element-plus";
import defaultPic from "@/assets/default.png";
export default {
name: "brand",
components: {
ossManage,
},
data() {
return {
defaultPic,
loading: true,
modalType: 0,
modalVisible: false,
modalTitle: "",
picModelFlag: false,
categoryModalVisible: false,
categoryModalTitle: "关联分类",
categoryTreeLoading: false,
categoryTreeData: [],
categoryTreeKey: 0,
categorySubmitLoading: false,
currentBrandId: "",
selectedCategoryIds: [],
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
},
form: {
name: "",
logo: "",
deleteFlag: "",
},
formValidate: {
name: [regular.REQUIRED, regular.VARCHAR20],
logo: [regular.REQUIRED, regular.URL200],
},
submitLoading: false,
data: [],
total: 0,
};
},
methods: {
openLogoPicker() {
this.picModelFlag = true;
this.$nextTick(() => {
if (this.$refs.ossManage) {
this.$refs.ossManage.selectImage = true;
}
});
},
callbackSelected(val) {
this.picModelFlag = false;
this.form.logo = val.url;
},
buildCategoryTreeNodes(list) {
if (!Array.isArray(list) || list.length === 0) return [];
return list.map((item) => ({
id: item.id,
title: item.name,
children: this.buildCategoryTreeNodes(item.children || []),
}));
},
async openCategoryModal(row) {
this.currentBrandId = row.id;
this.categoryModalTitle = "关联分类 - " + (row.name || "");
this.categoryModalVisible = true;
this.categoryTreeLoading = true;
this.categoryTreeKey += 1;
this.categoryTreeData = [];
this.selectedCategoryIds = [];
try {
const [treeRes, bindRes] = await Promise.all([
getCategoryTree(),
getBrandCategoryListData(row.id),
]);
const selectedIds = Array.isArray(bindRes?.result)
? bindRes.result
.map((x) => (typeof x === "string" ? x : x && x.id))
.filter(Boolean)
: [];
this.selectedCategoryIds = selectedIds;
this.categoryTreeData =
treeRes && treeRes.success
? this.buildCategoryTreeNodes(treeRes.result || [])
: [];
} finally {
this.categoryTreeLoading = false;
defineOptions({ name: "brand" });
const { proxy } = getCurrentInstance();
const loading = ref(true);
const modalType = ref(0);
const modalVisible = ref(false);
const modalTitle = ref("");
const picModelFlag = ref(false);
const categoryModalVisible = ref(false);
const categoryModalTitle = ref("关联分类");
const categoryTreeLoading = ref(false);
const categoryTreeData = ref([]);
const categoryTreeKey = ref(0);
const categorySubmitLoading = ref(false);
const currentBrandId = ref("");
const selectedCategoryIds = ref([]);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
});
const form = ref({
name: "",
logo: "",
deleteFlag: "",
});
const formValidate = reactive({
name: [regular.REQUIRED, regular.VARCHAR20],
logo: [regular.REQUIRED, regular.URL200],
});
const submitLoading = ref(false);
const data = ref([]);
const total = ref(0);
const searchFormRef = ref(null);
const formRef = ref(null);
const ossManageRef = ref(null);
function openLogoPicker() {
picModelFlag.value = true;
nextTick(() => {
if (ossManageRef.value) {
ossManageRef.value.selectImage = true;
}
});
}
function callbackSelected(val) {
picModelFlag.value = false;
form.value.logo = val.url;
}
function buildCategoryTreeNodes(list) {
if (!Array.isArray(list) || list.length === 0) return [];
return list.map((item) => ({
id: item.id,
title: item.name,
children: buildCategoryTreeNodes(item.children || []),
}));
}
async function openCategoryModal(row) {
currentBrandId.value = row.id;
categoryModalTitle.value = "关联分类 - " + (row.name || "");
categoryModalVisible.value = true;
categoryTreeLoading.value = true;
categoryTreeKey.value += 1;
categoryTreeData.value = [];
selectedCategoryIds.value = [];
try {
const [treeRes, bindRes] = await Promise.all([
getCategoryTree(),
getBrandCategoryListData(row.id),
]);
const selectedIds = Array.isArray(bindRes?.result)
? bindRes.result
.map((x) => (typeof x === "string" ? x : x && x.id))
.filter(Boolean)
: [];
selectedCategoryIds.value = selectedIds;
categoryTreeData.value =
treeRes && treeRes.success
? buildCategoryTreeNodes(treeRes.result || [])
: [];
} finally {
categoryTreeLoading.value = false;
}
}
function onCategoryTreeCheckChange(_data, checkedInfo) {
selectedCategoryIds.value = checkedInfo?.checkedKeys || [];
}
function submitBrandCategory() {
if (!currentBrandId.value) return;
categorySubmitLoading.value = true;
saveBrandCategory(
currentBrandId.value,
(selectedCategoryIds.value || []).map((id) => String(id))
)
.then((res) => {
categorySubmitLoading.value = false;
if (res && res.success) {
ElMessage.success("操作成功");
categoryModalVisible.value = false;
}
},
onCategoryTreeCheckChange(_data, checkedInfo) {
this.selectedCategoryIds = checkedInfo?.checkedKeys || [];
},
submitBrandCategory() {
if (!this.currentBrandId) return;
this.categorySubmitLoading = true;
saveBrandCategory(
this.currentBrandId,
(this.selectedCategoryIds || []).map((id) => String(id))
)
.then((res) => {
this.categorySubmitLoading = false;
if (res && res.success) {
ElMessage.success("操作成功");
this.categoryModalVisible = false;
}
})
.catch(() => {
this.categorySubmitLoading = false;
});
},
handleStatusSwitchChange(row, checked) {
const disable = !checked;
ElMessageBox.confirm(
"您确认要" + (disable ? "禁用" : "启用") + "品牌 " + row.name + " ?",
disable ? "确认禁用" : "确认启用",
{ type: "warning" }
).then(() => {
return disableBrand(row.id, { disable }).then((res) => {
})
.catch(() => {
categorySubmitLoading.value = false;
});
}
function handleStatusSwitchChange(row, checked) {
const disable = !checked;
ElMessageBox.confirm(
"您确认要" + (disable ? "禁用" : "启用") + "品牌 " + row.name + " ?",
disable ? "确认禁用" : "确认启用",
{ type: "warning" }
).then(() => {
return disableBrand(row.id, { disable }).then((res) => {
if (res.success) {
ElMessage.success("操作成功");
}
getDataList();
});
}).catch(() => {
nextTick(() => proxy.$forceUpdate());
});
}
function delBrand(row) {
ElMessageBox.confirm("您确认要删除 " + row.name + " ?", "确认删除", { type: "warning" }).then(() => {
return delBrandApi(row.id).then((res) => {
if (res.success) {
ElMessage.success("品牌删除成功!");
getDataList();
}
});
}).catch(() => {});
}
function init() {
getDataList();
}
function changePage(v) {
searchForm.pageNumber = v;
getDataList();
}
function changePageSize(v) {
searchForm.pageSize = v;
getDataList();
}
function handleSearch() {
searchForm.pageNumber = 1;
searchForm.pageSize = 20;
getDataList();
}
function getDataList() {
loading.value = true;
getManagerBrandPage(searchForm).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) {
submitLoading.value = true;
if (modalType.value === 0) {
delete form.value.id;
addBrand(form.value).then((res) => {
submitLoading.value = false;
if (res.success) {
ElMessage.success("操作成功");
getDataList();
modalVisible.value = false;
}
this.getDataList();
});
}).catch(() => {
this.$nextTick(() => this.$forceUpdate());
});
},
delBrand(row) {
ElMessageBox.confirm("您确认要删除 " + row.name + " ?", "确认删除", { type: "warning" }).then(() => {
return delBrand(row.id).then((res) => {
} else {
updateBrand(form.value).then((res) => {
submitLoading.value = false;
if (res.success) {
ElMessage.success("品牌删除成功!");
this.getDataList();
ElMessage.success("操作成功");
getDataList();
modalVisible.value = false;
}
});
}).catch(() => {});
},
init() {
this.getDataList();
},
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();
},
getDataList() {
this.loading = true;
getManagerBrandPage(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) {
this.submitLoading = true;
if (this.modalType === 0) {
delete this.form.id;
addBrand(this.form).then((res) => {
this.submitLoading = false;
if (res.success) {
ElMessage.success("操作成功");
this.getDataList();
this.modalVisible = false;
}
});
} else {
updateBrand(this.form).then((res) => {
this.submitLoading = false;
if (res.success) {
ElMessage.success("操作成功");
this.getDataList();
this.modalVisible = false;
}
});
}
}
});
},
add() {
this.modalType = 0;
this.modalTitle = "添加";
this.$refs.form?.resetFields();
delete this.form.id;
this.modalVisible = true;
},
edit(v) {
this.modalType = 1;
this.modalTitle = "编辑";
this.$refs.form?.resetFields();
for (const attr in v) {
if (v[attr] === null) {
v[attr] = "";
}
}
const str = JSON.stringify(v);
const data = JSON.parse(str);
this.form = data;
this.modalVisible = true;
},
},
mounted() {
this.init();
},
};
}
});
}
function add() {
modalType.value = 0;
modalTitle.value = "添加";
formRef.value?.resetFields();
delete form.value.id;
modalVisible.value = true;
}
function edit(v) {
modalType.value = 1;
modalTitle.value = "编辑";
formRef.value?.resetFields();
for (const attr in v) {
if (v[attr] === null) {
v[attr] = "";
}
}
const str = JSON.stringify(v);
const data = JSON.parse(str);
form.value = data;
modalVisible.value = true;
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -62,7 +62,7 @@
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="form" :model="formAdd" label-width="110px" :rules="formValidate">
<el-form ref="formRef" :model="formAdd" label-width="110px" :rules="formValidate">
<el-form-item v-if="showParent" label="上级分类" prop="parentId">
{{ parentTitle }}
<el-input v-model="formAdd.parentId" style="display: none" />
@@ -106,7 +106,7 @@
width="500px"
:close-on-click-modal="false"
>
<el-form ref="brandForm" :model="brandForm" label-width="100px">
<el-form ref="brandFormRef" :model="brandForm" label-width="100px">
<el-select v-model="brandForm.categoryBrands" filterable multiple style="width: 100%">
<el-option v-for="item in brandWay" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
@@ -120,7 +120,8 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, onMounted } from "vue";
import {
delCategory,
disableCategory,
@@ -128,241 +129,249 @@ import {
getCategoryBrandListData,
getCategoryTree,
insertCategory,
saveCategoryBrand,
saveCategoryBrand as apiSaveCategoryBrand,
updateCategory,
} from "@/api/goods";
import uploadPicInput from "@/components/lili/upload-pic-input";
import { regular } from "@/utils";
import { ElMessage, ElMessageBox } from "element-plus";
export default {
name: "goods-category",
components: {
uploadPicInput,
},
data() {
return {
submitLoading: false,
categoryList: [],
loading: false,
brands: [],
categoryId: "",
modalType: 0,
modalVisible: false,
modalBrandVisible: false,
modalTitle: "",
showParent: false,
parentTitle: "",
modalBrandTitle: "",
formAdd: {
parentId: "",
name: "",
image: "",
sortOrder: 0,
deleteFlag: false,
commissionRate: 0,
level: 0,
},
brandForm: {
categoryBrands: [],
},
brandWay: [],
formValidate: {
commissionRate: [regular.REQUIRED, regular.INTEGER],
name: [regular.REQUIRED, regular.VARCHAR20],
sortOrder: [regular.REQUIRED, regular.INTEGER],
},
tableData: [],
};
},
methods: {
normalizeCategoryTree(list) {
if (!Array.isArray(list) || list.length === 0) return;
list.forEach((item) => {
if (!item || typeof item !== "object") return;
if (item.deleteFlag === 0) item.deleteFlag = false;
else if (item.deleteFlag === 1) item.deleteFlag = true;
else item.deleteFlag = !!item.deleteFlag;
if (Array.isArray(item.children) && item.children.length) {
this.normalizeCategoryTree(item.children);
}
});
},
onStatusSwitchChange(row, nextDeleteFlag) {
const previousDeleteFlag = !nextDeleteFlag;
const isClosing = nextDeleteFlag === true;
ElMessageBox.confirm(
`您是否要${isClosing ? "关闭" : "开启"}当前分类 ${row.name} 及其子分类?`,
isClosing ? "确认关闭" : "确认开启",
{ type: "warning", confirmButtonText: "是", cancelButtonText: "否" }
).then(() => {
row._statusLoading = true;
return disableCategory(row.id, { enableOperations: isClosing ? true : 0 }).then((res) => {
row._statusLoading = false;
if (res && res.success) {
ElMessage.success("操作成功");
this.getAllList();
return;
}
row.deleteFlag = previousDeleteFlag;
});
}).catch(() => {
row.deleteFlag = previousDeleteFlag;
});
},
init() {
this.getAllList();
this.getBrandList();
},
getBrandList() {
getBrandListData().then((res) => {
this.brandWay = res;
});
},
brandOperation(v) {
getCategoryBrandListData(v.id).then((res) => {
this.categoryId = v.id;
this.modalBrandTitle = "品牌关联";
this.brandForm.categoryBrands = res.result.map((item) => item.id);
this.modalBrandVisible = true;
});
},
saveCategoryBrand() {
saveCategoryBrand(this.categoryId, this.brandForm).then((res) => {
this.submitLoading = false;
defineOptions({ name: "goods-category" });
const submitLoading = ref(false);
const categoryList = ref([]);
const loading = ref(false);
const brands = ref([]);
const categoryId = ref("");
const modalType = ref(0);
const modalVisible = ref(false);
const modalBrandVisible = ref(false);
const modalTitle = ref("");
const showParent = ref(false);
const parentTitle = ref("");
const modalBrandTitle = ref("");
const formAdd = reactive({
parentId: "",
name: "",
image: "",
sortOrder: 0,
deleteFlag: false,
commissionRate: 0,
level: 0,
});
const brandForm = reactive({
categoryBrands: [],
});
const brandWay = ref([]);
const formValidate = reactive({
commissionRate: [regular.REQUIRED, regular.INTEGER],
name: [regular.REQUIRED, regular.VARCHAR20],
sortOrder: [regular.REQUIRED, regular.INTEGER],
});
const tableData = ref([]);
const formRef = ref(null);
const brandFormRef = ref(null);
function normalizeCategoryTree(list) {
if (!Array.isArray(list) || list.length === 0) return;
list.forEach((item) => {
if (!item || typeof item !== "object") return;
if (item.deleteFlag === 0) item.deleteFlag = false;
else if (item.deleteFlag === 1) item.deleteFlag = true;
else item.deleteFlag = !!item.deleteFlag;
if (Array.isArray(item.children) && item.children.length) {
normalizeCategoryTree(item.children);
}
});
}
function onStatusSwitchChange(row, nextDeleteFlag) {
const previousDeleteFlag = !nextDeleteFlag;
const isClosing = nextDeleteFlag === true;
ElMessageBox.confirm(
`您是否要${isClosing ? "关闭" : "开启"}当前分类 ${row.name} 及其子分类?`,
isClosing ? "确认关闭" : "确认开启",
{ type: "warning", confirmButtonText: "是", cancelButtonText: "否" }
).then(() => {
row._statusLoading = true;
return disableCategory(row.id, { enableOperations: isClosing ? true : 0 }).then((res) => {
row._statusLoading = false;
if (res && res.success) {
ElMessage.success("操作成功");
getAllList();
return;
}
row.deleteFlag = previousDeleteFlag;
});
}).catch(() => {
row.deleteFlag = previousDeleteFlag;
});
}
function init() {
getAllList();
getBrandList();
}
function getBrandList() {
getBrandListData().then((res) => {
brandWay.value = res;
});
}
function brandOperation(v) {
getCategoryBrandListData(v.id).then((res) => {
categoryId.value = v.id;
modalBrandTitle.value = "品牌关联";
brandForm.categoryBrands = res.result.map((item) => item.id);
modalBrandVisible.value = true;
});
}
function saveCategoryBrand() {
apiSaveCategoryBrand(categoryId.value, brandForm).then((res) => {
submitLoading.value = false;
if (res.success) {
ElMessage.success("操作成功");
modalBrandVisible.value = false;
}
});
}
function addChildren(v) {
modalType.value = 0;
modalTitle.value = "添加子分类";
parentTitle.value = v.name;
formAdd.level = Number(v.level) + 1;
formAdd.commissionRate = v.commissionRate;
showParent.value = true;
delete formAdd.id;
formAdd.parentId = v.id;
modalVisible.value = true;
}
function edit(v) {
modalType.value = 1;
modalTitle.value = "编辑";
formAdd.id = v.id;
formAdd.name = v.name;
formAdd.level = v.level;
formAdd.parentId = v.parentId;
formAdd.sortOrder = v.sortOrder;
formAdd.commissionRate = v.commissionRate;
formAdd.deleteFlag = v.deleteFlag;
formAdd.image = v.image;
showParent.value = false;
modalVisible.value = true;
}
function addParent() {
modalType.value = 0;
modalTitle.value = "添加一级分类";
parentTitle.value = "顶级分类";
showParent.value = true;
formRef.value?.resetFields();
delete formAdd.id;
formAdd.parentId = 0;
formAdd.level = 0;
modalVisible.value = true;
}
function Submit() {
formRef.value.validate((valid) => {
if (!valid) return;
submitLoading.value = true;
if (modalType.value === 0) {
delete formAdd.id;
insertCategory(formAdd).then((res) => {
submitLoading.value = false;
if (res.success) {
ElMessage.success("操作成功");
this.modalBrandVisible = false;
ElMessage.success("添加成功");
getAllList();
modalVisible.value = false;
formRef.value.resetFields();
}
});
},
addChildren(v) {
this.modalType = 0;
this.modalTitle = "添加子分类";
this.parentTitle = v.name;
this.formAdd.level = Number(v.level) + 1;
this.formAdd.commissionRate = v.commissionRate;
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;
this.formAdd.sortOrder = v.sortOrder;
this.formAdd.commissionRate = v.commissionRate;
this.formAdd.deleteFlag = v.deleteFlag;
this.formAdd.image = v.image;
this.showParent = false;
this.modalVisible = true;
},
addParent() {
this.modalType = 0;
this.modalTitle = "添加一级分类";
this.parentTitle = "顶级分类";
this.showParent = true;
this.$refs.form?.resetFields();
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;
if (this.modalType === 0) {
delete this.formAdd.id;
insertCategory(this.formAdd).then((res) => {
this.submitLoading = false;
if (res.success) {
ElMessage.success("添加成功");
this.getAllList();
this.modalVisible = false;
this.$refs.form.resetFields();
}
});
} else {
updateCategory(this.formAdd).then((res) => {
this.submitLoading = false;
if (res.success) {
ElMessage.success("修改成功");
this.getAllList();
this.modalVisible = false;
this.$refs.form.resetFields();
}
});
} else {
updateCategory(formAdd).then((res) => {
submitLoading.value = false;
if (res.success) {
ElMessage.success("修改成功");
getAllList();
modalVisible.value = false;
formRef.value.resetFields();
}
});
},
remove(v) {
ElMessageBox.confirm("您确认要删除 " + v.name + " ?", "确认删除", { type: "warning" }).then(() => {
return delCategory(v.id).then((res) => {
if (res.success) {
ElMessage.success("操作成功");
this.getAllList();
}
});
}).catch(() => {});
},
getAllList() {
this.loading = true;
getCategoryTree()
.then((res) => {
this.loading = false;
if (res.success) {
localStorage.setItem("category", JSON.stringify(res.result));
this.normalizeCategoryTree(res.result);
this.categoryList = JSON.parse(JSON.stringify(res.result));
this.tableData = JSON.parse(JSON.stringify(res.result));
}
})
.catch(() => {
this.loading = false;
});
},
enable(v) {
ElMessageBox.confirm(
"您是否要启用当前分类 " + v.name + " 及其子分类?",
"确认启用",
{ type: "warning", confirmButtonText: "是", cancelButtonText: "否" }
).then(() => {
return disableCategory(v.id, { enableOperations: 0 }).then((res) => {
if (res.success) {
ElMessage.success("操作成功");
this.getAllList();
}
});
}).catch(() => {
this.getAllList();
});
},
disable(v) {
ElMessageBox.confirm(
"您是否要禁用当前分类 " + v.name + " 及其子分类?",
"确认禁用",
{ type: "warning", confirmButtonText: "是", cancelButtonText: "否" }
).then(() => {
return disableCategory(v.id, { enableOperations: true }).then((res) => {
if (res.success) {
ElMessage.success("操作成功");
this.getAllList();
}
});
}).catch(() => {
this.getAllList();
});
},
},
mounted() {
this.init();
},
};
}
});
}
function remove(v) {
ElMessageBox.confirm("您确认要删除 " + v.name + " ?", "确认删除", { type: "warning" }).then(() => {
return delCategory(v.id).then((res) => {
if (res.success) {
ElMessage.success("操作成功");
getAllList();
}
});
}).catch(() => {});
}
function getAllList() {
loading.value = true;
getCategoryTree()
.then((res) => {
loading.value = false;
if (res.success) {
localStorage.setItem("category", JSON.stringify(res.result));
normalizeCategoryTree(res.result);
categoryList.value = JSON.parse(JSON.stringify(res.result));
tableData.value = JSON.parse(JSON.stringify(res.result));
}
})
.catch(() => {
loading.value = false;
});
}
function enable(v) {
ElMessageBox.confirm(
"您是否要启用当前分类 " + v.name + " 及其子分类?",
"确认启用",
{ type: "warning", confirmButtonText: "是", cancelButtonText: "否" }
).then(() => {
return disableCategory(v.id, { enableOperations: 0 }).then((res) => {
if (res.success) {
ElMessage.success("操作成功");
getAllList();
}
});
}).catch(() => {
getAllList();
});
}
function disable(v) {
ElMessageBox.confirm(
"您是否要禁用当前分类 " + v.name + " 及其子分类?",
"确认禁用",
{ type: "warning", confirmButtonText: "是", cancelButtonText: "否" }
).then(() => {
return disableCategory(v.id, { enableOperations: true }).then((res) => {
if (res.success) {
ElMessage.success("操作成功");
getAllList();
}
});
}).catch(() => {
getAllList();
});
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div class="search">
<el-card>
<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="参数名称" prop="paramName">
<el-input v-model="form.paramName" clearable style="width: 520px" />
</el-form-item>
@@ -92,7 +92,9 @@
</div>
</template>
<script>
<script setup>
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
getCategoryTree,
getGoodsParamsDetail,
@@ -102,6 +104,11 @@ import {
import { regular } from "@/utils";
import { ElMessage } from "element-plus";
defineOptions({ name: "parameterEdit" });
const route = useRoute();
const router = useRouter();
const getOptionText = (value) => {
if (value && typeof value === "object") return value.value;
return value;
@@ -213,228 +220,228 @@ const buildFormState = (source, parameterId) => {
return formState;
};
export default {
name: "parameterEdit",
data() {
return {
submitLoading: false,
modalType: 0,
categoryModalVisible: false,
categoryModalTitle: "关联分类",
categoryTreeLoading: false,
categoryTreeData: [],
categoryTreeSource: [],
categoryIdNameMap: {},
categoryTreeKey: 0,
selectedCategoryIds: [],
form: {
paramName: "",
options: [{ value: "" }],
required: 0,
isIndex: 0,
sort: 0,
categoryIds: [],
},
formValidate: {
paramName: [regular.REQUIRED, regular.VARCHAR5],
options: [{ required: true, validator: validateOptions, trigger: "change" }],
required: [{ required: true, validator: validateRadioRequired("请选择是否必填"), trigger: "change" }],
isIndex: [{ required: true, validator: validateRadioRequired("请选择是否索引"), trigger: "change" }],
sort: [regular.REQUIRED, regular.INTEGER],
categoryIds: [
{
type: "array",
required: true,
min: 1,
message: "请选择关联分类",
trigger: "change",
},
],
},
};
},
computed: {
id() {
return this.$route.query && this.$route.query.id ? String(this.$route.query.id) : "";
const formRef = ref(null);
const categoryTree = ref(null);
const submitLoading = ref(false);
const modalType = ref(0);
const categoryModalVisible = ref(false);
const categoryModalTitle = ref("关联分类");
const categoryTreeLoading = ref(false);
const categoryTreeData = ref([]);
const categoryTreeSource = ref([]);
const categoryIdNameMap = ref({});
const categoryTreeKey = ref(0);
const selectedCategoryIds = ref([]);
const form = ref({
paramName: "",
options: [{ value: "" }],
required: 0,
isIndex: 0,
sort: 0,
categoryIds: [],
});
const formValidate = reactive({
paramName: [regular.REQUIRED, regular.VARCHAR5],
options: [{ required: true, validator: validateOptions, trigger: "change" }],
required: [{ required: true, validator: validateRadioRequired("请选择是否必填"), trigger: "change" }],
isIndex: [{ required: true, validator: validateRadioRequired("请选择是否索引"), trigger: "change" }],
sort: [regular.REQUIRED, regular.INTEGER],
categoryIds: [
{
type: "array",
required: true,
min: 1,
message: "请选择关联分类",
trigger: "change",
},
selectedCategoryNamesText() {
if (!Array.isArray(this.categoryTreeSource) || this.categoryTreeSource.length === 0) return "";
const selectedSet = new Set(toStringArray(this.selectedCategoryIds));
const isSelected = (node) => {
if (!node) return false;
const nodeId = node.id !== undefined && node.id !== null ? String(node.id) : "";
if (nodeId && selectedSet.has(nodeId)) return true;
const children = Array.isArray(node.children) ? node.children : [];
if (children.length === 0) return false;
return children.every(isSelected);
};
const collect = (node, out) => {
if (!node) return;
if (isSelected(node)) {
if (node.name) out.push(node.name);
return;
}
const children = Array.isArray(node.children) ? node.children : [];
children.forEach((c) => collect(c, out));
};
const names = [];
this.categoryTreeSource.forEach((n) => collect(n, names));
return names.join(",");
},
},
watch: {
"form.required"(val) {
const n = normalize01(val, 0);
if (val !== n) this.form.required = n;
},
"form.isIndex"(val) {
const n = normalize01(val, 0);
if (val !== n) this.form.isIndex = n;
},
},
methods: {
back() {
this.$router.push({ name: "goods-parameter" });
},
syncCategoryIds(ids) {
const categoryIds = toStringArray(ids);
this.selectedCategoryIds = categoryIds;
if (!this.form) this.form = {};
this.form.categoryIds = categoryIds;
},
touchOptionsValidate() {
this.$nextTick(() => {
const form = this.$refs.form;
if (form && typeof form.validateField === "function") {
// validateField rejects on failure; catch to avoid dev-server [object Object] overlay
form.validateField("options", () => {});
}
});
},
addOptionRow() {
if (!Array.isArray(this.form.options)) this.form.options = [];
this.form.options.push({ value: "" });
},
removeOptionRow(index) {
if (!Array.isArray(this.form.options)) this.form.options = [];
this.form.options.splice(index, 1);
this.touchOptionsValidate();
},
buildCategoryTreeNodes(list) {
if (!Array.isArray(list) || list.length === 0) return [];
return list.map((item) => ({
id: item.id,
title: item.name,
children: this.buildCategoryTreeNodes(item.children || []),
}));
},
async loadDetail(parameterId) {
if (!parameterId) return;
const res = await getGoodsParamsDetail(parameterId).catch(() => null);
if (!(res && res.success && res.result)) return;
const dto = res.result;
this.form = buildFormState(dto, parameterId);
this.selectedCategoryIds = [...this.form.categoryIds];
},
async openCategoryModal() {
this.categoryModalVisible = true;
this.categoryTreeLoading = true;
this.categoryTreeKey += 1;
],
});
const id = computed(() =>
route.query && route.query.id ? String(route.query.id) : ""
);
const selectedCategoryNamesText = computed(() => {
if (!Array.isArray(categoryTreeSource.value) || categoryTreeSource.value.length === 0) return "";
const selectedSet = new Set(toStringArray(selectedCategoryIds.value));
const isSelected = (node) => {
if (!node) return false;
const nodeId = node.id !== undefined && node.id !== null ? String(node.id) : "";
if (nodeId && selectedSet.has(nodeId)) return true;
const children = Array.isArray(node.children) ? node.children : [];
if (children.length === 0) return false;
return children.every(isSelected);
};
const collect = (node, out) => {
if (!node) return;
if (isSelected(node)) {
if (node.name) out.push(node.name);
return;
}
const children = Array.isArray(node.children) ? node.children : [];
children.forEach((c) => collect(c, out));
};
const names = [];
categoryTreeSource.value.forEach((n) => collect(n, names));
return names.join(",");
});
watch(
() => form.value.required,
(val) => {
const n = normalize01(val, 0);
if (val !== n) form.value.required = n;
}
);
watch(
() => form.value.isIndex,
(val) => {
const n = normalize01(val, 0);
if (val !== n) form.value.isIndex = n;
}
);
function back() {
router.push({ name: "goods-parameter" });
}
function syncCategoryIds(ids) {
const categoryIds = toStringArray(ids);
selectedCategoryIds.value = categoryIds;
if (!form.value) form.value = {};
form.value.categoryIds = categoryIds;
}
function touchOptionsValidate() {
nextTick(() => {
const formEl = formRef.value;
if (formEl && typeof formEl.validateField === "function") {
// validateField rejects on failure; catch to avoid dev-server [object Object] overlay
formEl.validateField("options", () => {});
}
});
}
function addOptionRow() {
if (!Array.isArray(form.value.options)) form.value.options = [];
form.value.options.push({ value: "" });
}
function removeOptionRow(index) {
if (!Array.isArray(form.value.options)) form.value.options = [];
form.value.options.splice(index, 1);
touchOptionsValidate();
}
function buildCategoryTreeNodes(list) {
if (!Array.isArray(list) || list.length === 0) return [];
return list.map((item) => ({
id: item.id,
title: item.name,
children: buildCategoryTreeNodes(item.children || []),
}));
}
async function loadDetail(parameterId) {
if (!parameterId) return;
const res = await getGoodsParamsDetail(parameterId).catch(() => null);
if (!(res && res.success && res.result)) return;
const dto = res.result;
form.value = buildFormState(dto, parameterId);
selectedCategoryIds.value = [...form.value.categoryIds];
}
async function openCategoryModal() {
categoryModalVisible.value = true;
categoryTreeLoading.value = true;
categoryTreeKey.value += 1;
try {
if (!Array.isArray(categoryTreeSource.value) || categoryTreeSource.value.length === 0) {
const treeRes = await getCategoryTree();
categoryTreeSource.value = treeRes && treeRes.success ? treeRes.result || [] : [];
const map = {};
buildCategoryIdNameMap(categoryTreeSource.value, map);
categoryIdNameMap.value = map;
}
categoryTreeData.value = buildCategoryTreeNodes(categoryTreeSource.value || []);
} finally {
categoryTreeLoading.value = false;
}
}
function onCategoryTreeCheckChange(_data, checkedInfo) {
syncCategoryIds(checkedInfo?.checkedKeys || []);
nextTick(() => {
const formEl = formRef.value;
if (formEl && typeof formEl.validateField === "function") {
formEl.validateField("categoryIds", () => {});
}
});
}
function initForm() {
if (id.value) {
modalType.value = 1;
const cached = window.sessionStorage.getItem(cacheKey(id.value));
if (cached) {
try {
if (!Array.isArray(this.categoryTreeSource) || this.categoryTreeSource.length === 0) {
const treeRes = await getCategoryTree();
this.categoryTreeSource = treeRes && treeRes.success ? treeRes.result || [] : [];
const map = {};
buildCategoryIdNameMap(this.categoryTreeSource, map);
this.categoryIdNameMap = map;
}
this.categoryTreeData = this.buildCategoryTreeNodes(this.categoryTreeSource || []);
} finally {
this.categoryTreeLoading = false;
const row = JSON.parse(cached);
form.value = buildFormState(row, row.id);
selectedCategoryIds.value = [...form.value.categoryIds];
} catch (e) {
modalType.value = 0;
}
},
onCategoryTreeCheckChange(_data, checkedInfo) {
this.syncCategoryIds(checkedInfo?.checkedKeys || []);
this.$nextTick(() => {
const form = this.$refs.form;
if (form && typeof form.validateField === "function") {
form.validateField("categoryIds", () => {});
}
});
},
initForm() {
if (this.id) {
this.modalType = 1;
const cached = window.sessionStorage.getItem(cacheKey(this.id));
if (cached) {
try {
const row = JSON.parse(cached);
this.form = buildFormState(row, row.id);
this.selectedCategoryIds = [...this.form.categoryIds];
} catch (e) {
this.modalType = 0;
}
}
this.loadDetail(String(this.id));
} else {
this.modalType = 0;
if (!Array.isArray(this.form.options) || this.form.options.length === 0) {
this.form.options = [{ value: "" }];
}
this.syncCategoryIds([]);
}
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return;
this.submitLoading = true;
const options = normalizeOptions(this.form.options);
const categoryIds = toStringArray(this.form.categoryIds);
const payload = {
...this.form,
options: options.join(","),
required: Number(this.form.required),
isIndex: Number(this.form.isIndex),
sort: Number(this.form.sort || 0),
categoryParameterList: categoryIds.map((categoryId) => ({ categoryId })),
};
if (this.modalType === 0) {
delete payload.id;
insertGoodsParams(buildSpringFormPayload(payload))
.then((res) => {
if (!(res && res.success)) return;
ElMessage.success("操作成功");
this.back();
})
.finally(() => {
this.submitLoading = false;
});
} else {
updateGoodsParams(buildSpringFormPayload(payload))
.then((res) => {
if (!(res && res.success)) return;
ElMessage.success("操作成功");
this.back();
})
.finally(() => {
this.submitLoading = false;
});
}
});
},
},
mounted() {
getCategoryTree().then((res) => {
if (res && res.success) {
this.categoryTreeSource = res.result || [];
const map = {};
buildCategoryIdNameMap(this.categoryTreeSource, map);
this.categoryIdNameMap = map;
}
});
this.initForm();
},
};
}
loadDetail(String(id.value));
} else {
modalType.value = 0;
if (!Array.isArray(form.value.options) || form.value.options.length === 0) {
form.value.options = [{ value: "" }];
}
syncCategoryIds([]);
}
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (!valid) return;
submitLoading.value = true;
const options = normalizeOptions(form.value.options);
const categoryIds = toStringArray(form.value.categoryIds);
const payload = {
...form.value,
options: options.join(","),
required: Number(form.value.required),
isIndex: Number(form.value.isIndex),
sort: Number(form.value.sort || 0),
categoryParameterList: categoryIds.map((categoryId) => ({ categoryId })),
};
if (modalType.value === 0) {
delete payload.id;
insertGoodsParams(buildSpringFormPayload(payload))
.then((res) => {
if (!(res && res.success)) return;
ElMessage.success("操作成功");
back();
})
.finally(() => {
submitLoading.value = false;
});
} else {
updateGoodsParams(buildSpringFormPayload(payload))
.then((res) => {
if (!(res && res.success)) return;
ElMessage.success("操作成功");
back();
})
.finally(() => {
submitLoading.value = false;
});
}
});
}
onMounted(() => {
getCategoryTree().then((res) => {
if (res && res.success) {
categoryTreeSource.value = res.result || [];
const map = {};
buildCategoryIdNameMap(categoryTreeSource.value, map);
categoryIdNameMap.value = map;
}
});
initForm();
});
</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"
@@ -27,7 +27,7 @@
<div class="operation padding-row">
<el-button type="primary" @click="goAdd">添加</el-button>
</div>
<el-table v-loading="loading" border :data="data" ref="table" style="width: 100%">
<el-table v-loading="loading" border :data="data" ref="tableRef" style="width: 100%">
<el-table-column prop="paramName" label="参数名称" width="300" />
<el-table-column prop="options" label="参数值" min-width="260" show-overflow-tooltip />
<el-table-column label="必填" width="300" align="center">
@@ -65,83 +65,93 @@
</el-card>
</div>
</template>
<script>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { useRouter } from "vue-router";
import { deleteParams, getGoodsParamsPage } from "@/api/goods";
import { ElMessage, ElMessageBox } from "element-plus";
export default {
name: "categoryParams",
data() {
return {
submitLoading: false,
loading: true,
total: 0,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
paramName: "",
},
data: [],
};
},
methods: {
isTruthyFlag(val) {
return val === 1 || val === "1" || val === true;
},
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;
getGoodsParamsPage(this.searchForm).then((res) => {
this.loading = false;
if (res && res.success) {
this.data = (res.result && res.result.records) || [];
this.total = (res.result && res.result.total) || 0;
}
});
},
goAdd() {
this.$router.push({ name: "goods-parameter-edit" });
},
goEdit(row) {
if (!row || !row.id) return;
try {
window.sessionStorage.setItem(`goods-parameter-edit:${row.id}`, JSON.stringify(row));
} catch (e) {}
this.$router.push({ name: "goods-parameter-edit", query: { id: row.id } });
},
remove(row) {
ElMessageBox.confirm("您确认要删除 " + (row.paramName || "") + " ?", "确认删除", { type: "warning" }).then(() => {
return deleteParams(row.id).then((res) => {
if (res && res.success) {
ElMessage.success("删除成功");
this.getDataList();
}
});
}).catch(() => {});
},
},
mounted() {
this.init();
},
};
defineOptions({ name: "categoryParams" });
const router = useRouter();
const submitLoading = ref(false);
const loading = ref(true);
const total = ref(0);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
paramName: "",
});
const data = ref([]);
const searchFormRef = ref(null);
const tableRef = ref(null);
function isTruthyFlag(val) {
return val === 1 || val === "1" || val === true;
}
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;
getGoodsParamsPage(searchForm).then((res) => {
loading.value = false;
if (res && res.success) {
data.value = (res.result && res.result.records) || [];
total.value = (res.result && res.result.total) || 0;
}
});
}
function goAdd() {
router.push({ name: "goods-parameter-edit" });
}
function goEdit(row) {
if (!row || !row.id) return;
try {
window.sessionStorage.setItem(`goods-parameter-edit:${row.id}`, JSON.stringify(row));
} catch (e) {}
router.push({ name: "goods-parameter-edit", query: { id: row.id } });
}
function remove(row) {
ElMessageBox.confirm("您确认要删除 " + (row.paramName || "") + " ?", "确认删除", { type: "warning" }).then(() => {
return deleteParams(row.id).then((res) => {
if (res && res.success) {
ElMessage.success("删除成功");
getDataList();
}
});
}).catch(() => {});
}
onMounted(() => {
init();
});
</script>
<style lang="scss">
.link-text {

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -170,145 +170,144 @@
</div>
</template>
<script>
<script setup>
import { nextTick, onMounted, ref } from "vue";
import * as API_Member from "@/api/member";
import { ElMessage, ElMessageBox } from "element-plus";
export default {
name: "goods-review",
data() {
return {
infoData: {},
infoFlag: false,
infoTitle: "",
currentReviewId: "",
detailStatusReady: false,
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
goodsName: "",
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
},
data: [],
total: 0,
};
},
methods: {
gradeTagType(grade) {
if (grade === "GOOD") return "success";
if (grade === "MODERATE") return "warning";
return "danger";
},
gradeText(grade) {
if (grade === "GOOD") return "好评";
if (grade === "MODERATE") return "中评";
return "差评";
},
changeSwitchView(status) {
if (!this.detailStatusReady || !this.currentReviewId) {
return;
defineOptions({ name: "goods-review" });
// 模板中的 el-form ref(原 ref="searchForm",为避免与数据 searchForm 同名互相覆盖而改名),script 内未使用
const searchFormRef = ref(null);
const infoData = ref({});
const infoFlag = ref(false);
const infoTitle = ref("");
const currentReviewId = ref("");
const detailStatusReady = ref(false);
const loading = ref(true);
const searchForm = ref({
pageNumber: 1,
pageSize: 20,
goodsName: "",
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
});
const data = ref([]);
const total = ref(0);
function gradeTagType(grade) {
if (grade === "GOOD") return "success";
if (grade === "MODERATE") return "warning";
return "danger";
}
function gradeText(grade) {
if (grade === "GOOD") return "好评";
if (grade === "MODERATE") return "中评";
return "差评";
}
function changeSwitchView(status) {
if (!detailStatusReady.value || !currentReviewId.value) {
return;
}
API_Member.updateMemberReview(currentReviewId.value, { status }).then((res) => {
if (res.success) {
ElMessage.success("修改成功!");
getDataList();
}
});
}
function init() {
getDataList();
}
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();
}
function changeSwitch(row) {
if (!row || !row.id) {
return;
}
API_Member.updateMemberReview(row.id, { status: row.status }).then((res) => {
if (res.success) {
getDataList();
}
});
}
function updateTop(v) {
const top = !v.top;
API_Member.updateMemberReviewTop(v.id, { top }).then((res) => {
if (res.success) {
data.value = data.value.map((item) => {
if (item.goodsId !== v.goodsId) {
return item;
}
return {
...item,
top: item.id === v.id ? top : false,
};
});
ElMessage.success(top ? "置顶成功!" : "取消置顶成功!");
init();
}
});
}
function getDataList() {
loading.value = true;
API_Member.getMemberReview(searchForm.value)
.then((res) => {
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
API_Member.updateMemberReview(this.currentReviewId, { status }).then((res) => {
if (res.success) {
ElMessage.success("修改成功!");
this.getDataList();
}
})
.finally(() => {
loading.value = false;
});
}
function info(v) {
currentReviewId.value = v.id;
detailStatusReady.value = false;
infoData.value = {};
infoFlag.value = true;
infoTitle.value = `用户${v.memberName}的评价详情`;
API_Member.getMemberInfoReview(v.id).then((res) => {
if (res.result) {
infoData.value = res.result;
nextTick(() => {
detailStatusReady.value = true;
});
},
init() {
this.getDataList();
},
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();
},
changeSwitch(row) {
if (!row || !row.id) {
return;
}
API_Member.updateMemberReview(row.id, { status: row.status }).then((res) => {
if (res.success) {
this.getDataList();
}
});
},
updateTop(v) {
const top = !v.top;
API_Member.updateMemberReviewTop(v.id, { top }).then((res) => {
if (res.success) {
this.data = this.data.map((item) => {
if (item.goodsId !== v.goodsId) {
return item;
}
return {
...item,
top: item.id === v.id ? top : false,
};
});
ElMessage.success(top ? "置顶成功!" : "取消置顶成功!");
this.init();
}
});
},
getDataList() {
this.loading = true;
API_Member.getMemberReview(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
})
.finally(() => {
this.loading = false;
});
},
info(v) {
this.currentReviewId = v.id;
this.detailStatusReady = false;
this.infoData = {};
this.infoFlag = true;
this.infoTitle = `用户${v.memberName}的评价详情`;
API_Member.getMemberInfoReview(v.id).then((res) => {
if (res.result) {
this.infoData = res.result;
this.$nextTick(() => {
this.detailStatusReady = true;
});
}
});
},
onInfoDialogClosed() {
this.detailStatusReady = false;
this.currentReviewId = "";
this.infoData = {};
},
remove(v) {
ElMessageBox.confirm("您确认要删除客户" + v.memberName + "的评论?", "确认删除", { type: "warning" }).then(() => {
return API_Member.delMemberReview(v.id).then(() => {
ElMessage.success("修改成功");
this.init();
});
}).catch(() => {});
},
},
mounted() {
this.init();
},
};
}
});
}
function onInfoDialogClosed() {
detailStatusReady.value = false;
currentReviewId.value = "";
infoData.value = {};
}
function remove(v) {
ElMessageBox.confirm("您确认要删除客户" + v.memberName + "的评论?", "确认删除", { type: "warning" }).then(() => {
return API_Member.delMemberReview(v.id).then(() => {
ElMessage.success("修改成功");
init();
});
}).catch(() => {});
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -36,7 +36,7 @@
</el-card>
<el-dialog v-model="addFlag" title="添加商品分组" width="500px" :close-on-click-modal="false" destroy-on-close>
<el-form ref="addForm" :model="formAdd" :rules="rulesAdd" label-width="90px">
<el-form ref="addFormRef" :model="formAdd" :rules="rulesAdd" label-width="90px">
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
<el-input v-model="formAdd.groupName" maxlength="30" placeholder="请输入分组名称" />
</el-form-item>
@@ -51,7 +51,7 @@
</el-dialog>
<el-dialog v-model="editFlag" title="编辑商品分组" width="500px" :close-on-click-modal="false" destroy-on-close>
<el-form ref="editForm" :model="formEdit" :rules="rulesEdit" label-width="90px">
<el-form ref="editFormRef" :model="formEdit" :rules="rulesEdit" label-width="90px">
<el-input v-model="formEdit.id" style="display: none" />
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
<el-input v-model="formEdit.groupName" maxlength="30" placeholder="请输入分组名称" />
@@ -68,166 +68,173 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, nextTick, onMounted } from "vue";
import * as API_Goods from "@/api/goods.js";
import { ElMessage, ElMessageBox } from "element-plus";
export default {
name: "goodsGroup",
data() {
return {
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
},
data: [],
total: 0,
addFlag: false,
editFlag: false,
submitAddLoading: false,
submitEditLoading: false,
formAdd: {
groupName: "",
description: "",
},
formEdit: {
id: "",
groupName: "",
description: "",
},
rulesAdd: {
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
},
rulesEdit: {
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
},
};
},
methods: {
init() {
this.getData();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getData();
},
changePageSize(v) {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = v;
this.getData();
},
getData() {
this.loading = true;
API_Goods.getGoodsGroupByPage(this.searchForm)
.then((res) => {
this.loading = false;
if (res && res.success && res.result) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
} else if (res && res.message) {
ElMessage.error(res.message);
}
})
.catch(() => {
this.loading = false;
ElMessage.error("加载商品分组失败");
});
},
openAdd() {
this.addFlag = true;
this.$nextTick(() => {
if (this.$refs.addForm) this.$refs.addForm.resetFields();
this.formAdd = { groupName: "", description: "" };
defineOptions({ name: "goodsGroup" });
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
});
const data = ref([]);
const total = ref(0);
const addFlag = ref(false);
const editFlag = ref(false);
const submitAddLoading = ref(false);
const submitEditLoading = ref(false);
const formAdd = ref({
groupName: "",
description: "",
});
const formEdit = ref({
id: "",
groupName: "",
description: "",
});
const rulesAdd = reactive({
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
});
const rulesEdit = reactive({
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
});
const addFormRef = ref(null);
const editFormRef = ref(null);
function init() {
getData();
}
function changePage(v) {
searchForm.pageNumber = v;
getData();
}
function changePageSize(v) {
searchForm.pageNumber = 1;
searchForm.pageSize = v;
getData();
}
function getData() {
loading.value = true;
API_Goods.getGoodsGroupByPage(searchForm)
.then((res) => {
loading.value = false;
if (res && res.success && res.result) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
} else if (res && res.message) {
ElMessage.error(res.message);
}
})
.catch(() => {
loading.value = false;
ElMessage.error("加载商品分组失败");
});
}
function openAdd() {
addFlag.value = true;
nextTick(() => {
if (addFormRef.value) addFormRef.value.resetFields();
formAdd.value = { groupName: "", description: "" };
});
}
function submitAdd() {
addFormRef.value.validate((valid) => {
if (!valid) return;
submitAddLoading.value = true;
API_Goods.addGoodsGroup(formAdd.value)
.then((res) => {
submitAddLoading.value = false;
if (res && res.success) {
ElMessage.success("添加成功");
addFlag.value = false;
getData();
} else if (res && res.message) {
ElMessage.error(res.message);
}
})
.catch(() => {
submitAddLoading.value = false;
ElMessage.error("添加失败,请检查权限或后端接口");
});
},
submitAdd() {
this.$refs.addForm.validate((valid) => {
if (!valid) return;
this.submitAddLoading = true;
API_Goods.addGoodsGroup(this.formAdd)
.then((res) => {
this.submitAddLoading = false;
if (res && res.success) {
ElMessage.success("添加成功");
this.addFlag = false;
this.getData();
} else if (res && res.message) {
ElMessage.error(res.message);
}
})
.catch(() => {
this.submitAddLoading = false;
ElMessage.error("添加失败,请检查权限或后端接口");
});
});
}
function openEdit(row) {
editFlag.value = true;
submitEditLoading.value = false;
API_Goods.getGoodsGroup(row.id)
.then((res) => {
if (res && res.success && res.result) {
formEdit.value = {
id: res.result.id,
groupName: res.result.groupName || "",
description: res.result.description || "",
};
} else {
formEdit.value = {
id: row.id,
groupName: row.groupName || "",
description: row.description || "",
};
}
})
.catch(() => {
ElMessage.error("获取分组详情失败");
});
}
function submitEdit() {
editFormRef.value.validate((valid) => {
if (!valid) return;
submitEditLoading.value = true;
const { id, groupName, description } = formEdit.value;
API_Goods.updateGoodsGroup(id, { id, groupName, description })
.then((res) => {
submitEditLoading.value = false;
if (res && res.success) {
ElMessage.success("修改成功");
editFlag.value = false;
getData();
} else if (res && res.message) {
ElMessage.error(res.message);
}
})
.catch(() => {
submitEditLoading.value = false;
ElMessage.error("修改失败,请检查权限或后端接口");
});
},
openEdit(row) {
this.editFlag = true;
this.submitEditLoading = false;
API_Goods.getGoodsGroup(row.id)
.then((res) => {
if (res && res.success && res.result) {
this.formEdit = {
id: res.result.id,
groupName: res.result.groupName || "",
description: res.result.description || "",
};
} else {
this.formEdit = {
id: row.id,
groupName: row.groupName || "",
description: row.description || "",
};
}
})
.catch(() => {
ElMessage.error("获取分组详情失败");
});
},
submitEdit() {
this.$refs.editForm.validate((valid) => {
if (!valid) return;
this.submitEditLoading = true;
const { id, groupName, description } = this.formEdit;
API_Goods.updateGoodsGroup(id, { id, groupName, description })
.then((res) => {
this.submitEditLoading = false;
if (res && res.success) {
ElMessage.success("修改成功");
this.editFlag = false;
this.getData();
} else if (res && res.message) {
ElMessage.error(res.message);
}
})
.catch(() => {
this.submitEditLoading = false;
ElMessage.error("修改失败,请检查权限或后端接口");
});
});
}
function remove(row) {
ElMessageBox.confirm("确定删除该分组?", "提示", { type: "warning" }).then(() => {
return API_Goods.deleteGoodsGroup(row.id)
.then((res) => {
if (res && res.success) {
ElMessage.success("删除成功");
getData();
} else if (res && res.message) {
ElMessage.error(res.message);
}
})
.catch(() => {
ElMessage.error("删除失败,请检查权限或后端接口");
});
},
remove(row) {
ElMessageBox.confirm("确定删除该分组?", "提示", { type: "warning" }).then(() => {
return API_Goods.deleteGoodsGroup(row.id)
.then((res) => {
if (res && res.success) {
ElMessage.success("删除成功");
this.getData();
} else if (res && res.message) {
ElMessage.error(res.message);
}
})
.catch(() => {
ElMessage.error("删除失败,请检查权限或后端接口");
});
}).catch(() => {});
},
},
mounted() {
this.init();
},
};
}).catch(() => {});
}
onMounted(() => {
init();
});
</script>
<style scoped>

View File

@@ -2,7 +2,7 @@
<div class="search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="70px"
@@ -68,7 +68,7 @@
</div>
<el-table
ref="table"
ref="tableRef"
v-loading="loading"
:data="data"
class="mt_10"
@@ -208,7 +208,8 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import {
batchUpdateGoodsSkuVirtualSales,
getGoodsSkuData,
@@ -225,223 +226,222 @@ const VIRTUAL_SALES_FIELDS = [
"salesVolume",
];
export default {
name: "goodsVirtualSales",
data() {
return {
loading: true,
editModalVisible: false,
modalSubmitting: false,
modalMode: "single",
currentSku: null,
currentIndex: -1,
selectedRows: [],
editForm: {
virtualSales: 0,
},
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
goodsName: "",
goodsId: "",
sn: "",
storeName: "",
},
data: [],
total: 0,
};
},
methods: {
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.getDataList();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
goodsName: "",
goodsId: "",
sn: "",
storeName: "",
};
this.getDataList();
},
changeSort({ prop, order }) {
if (!order) {
this.searchForm.sort = "create_time";
this.searchForm.order = "desc";
} else {
this.searchForm.sort = prop;
this.searchForm.order = order === "ascending" ? "asc" : "desc";
}
this.getDataList();
},
handleSelectionChange(selection) {
this.selectedRows = selection;
},
getDataList() {
this.loading = true;
getGoodsSkuData(this.searchForm)
.then((res) => {
if (res && res.success && res.result) {
this.data = (res.result.records || []).map((item) => ({
...item,
virtualSalesInput: this.getVirtualSalesValue(item),
saving: false,
}));
this.total = res.result.total || 0;
this.selectedRows = [];
} else {
this.data = [];
this.total = 0;
this.selectedRows = [];
ElMessage.error((res && res.message) || "加载规格列表失败");
}
})
.catch(() => {
this.data = [];
this.total = 0;
this.selectedRows = [];
ElMessage.error("加载规格列表失败");
})
.finally(() => {
this.loading = false;
});
},
getVirtualSalesValue(row) {
const field = VIRTUAL_SALES_FIELDS.find(
(item) => row[item] !== undefined && row[item] !== null && row[item] !== ""
);
return field ? Number(row[field]) || 0 : 0;
},
openVirtualSalesModal(row, index) {
this.modalMode = "single";
this.currentSku = { ...row };
this.currentIndex = index;
this.editForm.virtualSales = this.getVirtualSalesValue(row);
this.editModalVisible = true;
},
openBatchVirtualSalesModal() {
if (!this.selectedRows.length) {
ElMessage.warning("请先选择要设置的商品规格");
return;
}
this.modalMode = "batch";
this.currentSku = null;
this.currentIndex = -1;
this.editForm.virtualSales = 0;
this.editModalVisible = true;
},
handleCancelVirtualSales() {
if (this.modalSubmitting) return;
this.resetVirtualSalesModal();
},
resetVirtualSalesModal() {
this.editModalVisible = false;
this.modalMode = "single";
this.currentSku = null;
this.currentIndex = -1;
this.editForm.virtualSales = 0;
},
handleSubmitVirtualSales() {
const virtualSales = Number(this.editForm.virtualSales);
if (!Number.isInteger(virtualSales) || virtualSales < 0) {
ElMessage.warning("请输入大于等于 0 的整数虚拟销量");
return;
}
if (this.modalMode === "batch") {
this.handleBatchSubmitVirtualSales(virtualSales);
return;
}
if (!this.currentSku || this.currentIndex < 0) return;
defineOptions({ name: "goodsVirtualSales" });
const currentIndex = this.currentIndex;
const currentSkuId = this.currentSku.id;
this.modalSubmitting = true;
if (this.data[currentIndex]) {
this.data[currentIndex].saving = true;
// 模板 ref(避免与同名数据字段冲突而改名)
const searchFormRef = ref(null);
const tableRef = ref(null);
const loading = ref(true);
const editModalVisible = ref(false);
const modalSubmitting = ref(false);
const modalMode = ref("single");
const currentSku = ref(null);
const currentIndex = ref(-1);
const selectedRows = ref([]);
const editForm = reactive({
virtualSales: 0,
});
const searchForm = ref({
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
goodsName: "",
goodsId: "",
sn: "",
storeName: "",
});
const data = ref([]);
const total = ref(0);
function init() {
getDataList();
}
function changePage(v) {
searchForm.value.pageNumber = v;
getDataList();
}
function changePageSize(v) {
searchForm.value.pageNumber = 1;
searchForm.value.pageSize = v;
getDataList();
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getDataList();
}
function handleReset() {
searchForm.value = {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
goodsName: "",
goodsId: "",
sn: "",
storeName: "",
};
getDataList();
}
function changeSort({ prop, order }) {
if (!order) {
searchForm.value.sort = "create_time";
searchForm.value.order = "desc";
} else {
searchForm.value.sort = prop;
searchForm.value.order = order === "ascending" ? "asc" : "desc";
}
getDataList();
}
function handleSelectionChange(selection) {
selectedRows.value = selection;
}
function getDataList() {
loading.value = true;
getGoodsSkuData(searchForm.value)
.then((res) => {
if (res && res.success && res.result) {
data.value = (res.result.records || []).map((item) => ({
...item,
virtualSalesInput: getVirtualSalesValue(item),
saving: false,
}));
total.value = res.result.total || 0;
selectedRows.value = [];
} else {
data.value = [];
total.value = 0;
selectedRows.value = [];
ElMessage.error((res && res.message) || "加载规格列表失败");
}
updateGoodsSkuVirtualSales(currentSkuId, { virtualSales })
.then((res) => {
if (res && res.success) {
if (this.data[currentIndex]) {
this.data[currentIndex].virtualSales = virtualSales;
this.data[currentIndex].virtualSalesInput = virtualSales;
}
ElMessage.success("虚拟销量设置成功");
this.resetVirtualSalesModal();
} else {
ElMessage.error((res && res.message) || "规格虚拟销量设置失败");
}
})
.catch(() => {
ElMessage.error("规格虚拟销量设置失败");
})
.finally(() => {
if (this.data[currentIndex]) {
this.data[currentIndex].saving = false;
}
this.modalSubmitting = false;
});
},
handleBatchSubmitVirtualSales(virtualSales) {
const skuIds = this.selectedRows.map((item) => item.id).filter(Boolean);
if (!skuIds.length) {
ElMessage.warning("请先选择要设置的商品规格");
return;
})
.catch(() => {
data.value = [];
total.value = 0;
selectedRows.value = [];
ElMessage.error("加载规格列表失败");
})
.finally(() => {
loading.value = false;
});
}
function getVirtualSalesValue(row) {
const field = VIRTUAL_SALES_FIELDS.find(
(item) => row[item] !== undefined && row[item] !== null && row[item] !== ""
);
return field ? Number(row[field]) || 0 : 0;
}
function openVirtualSalesModal(row, index) {
modalMode.value = "single";
currentSku.value = { ...row };
currentIndex.value = index;
editForm.virtualSales = getVirtualSalesValue(row);
editModalVisible.value = true;
}
function openBatchVirtualSalesModal() {
if (!selectedRows.value.length) {
ElMessage.warning("请先选择要设置的商品规格");
return;
}
modalMode.value = "batch";
currentSku.value = null;
currentIndex.value = -1;
editForm.virtualSales = 0;
editModalVisible.value = true;
}
function handleCancelVirtualSales() {
if (modalSubmitting.value) return;
resetVirtualSalesModal();
}
function resetVirtualSalesModal() {
editModalVisible.value = false;
modalMode.value = "single";
currentSku.value = null;
currentIndex.value = -1;
editForm.virtualSales = 0;
}
function handleSubmitVirtualSales() {
const virtualSales = Number(editForm.virtualSales);
if (!Number.isInteger(virtualSales) || virtualSales < 0) {
ElMessage.warning("请输入大于等于 0 的整数虚拟销量");
return;
}
if (modalMode.value === "batch") {
handleBatchSubmitVirtualSales(virtualSales);
return;
}
if (!currentSku.value || currentIndex.value < 0) return;
const curIndex = currentIndex.value;
const currentSkuId = currentSku.value.id;
modalSubmitting.value = true;
if (data.value[curIndex]) {
data.value[curIndex].saving = true;
}
updateGoodsSkuVirtualSales(currentSkuId, { virtualSales })
.then((res) => {
if (res && res.success) {
if (data.value[curIndex]) {
data.value[curIndex].virtualSales = virtualSales;
data.value[curIndex].virtualSalesInput = virtualSales;
}
ElMessage.success("虚拟销量设置成功");
resetVirtualSalesModal();
} else {
ElMessage.error((res && res.message) || "规格虚拟销量设置失败");
}
this.modalSubmitting = true;
batchUpdateGoodsSkuVirtualSales({ skuIds, virtualSales })
.then((res) => {
if (res && res.success) {
this.data = this.data.map((item) => {
if (!skuIds.includes(item.id)) return item;
return {
...item,
virtualSales,
virtualSalesInput: virtualSales,
};
});
this.selectedRows = [];
this.$refs.table?.clearSelection();
ElMessage.success("虚拟销量设置成功");
this.resetVirtualSalesModal();
} else {
ElMessage.error((res && res.message) || "虚拟销量设置失败");
}
})
.catch(() => {
ElMessage.error("虚拟销量设置失败");
})
.finally(() => {
this.modalSubmitting = false;
})
.catch(() => {
ElMessage.error("规格虚拟销量设置失败");
})
.finally(() => {
if (data.value[curIndex]) {
data.value[curIndex].saving = false;
}
modalSubmitting.value = false;
});
}
function handleBatchSubmitVirtualSales(virtualSales) {
const skuIds = selectedRows.value.map((item) => item.id).filter(Boolean);
if (!skuIds.length) {
ElMessage.warning("请先选择要设置的商品规格");
return;
}
modalSubmitting.value = true;
batchUpdateGoodsSkuVirtualSales({ skuIds, virtualSales })
.then((res) => {
if (res && res.success) {
data.value = data.value.map((item) => {
if (!skuIds.includes(item.id)) return item;
return {
...item,
virtualSales,
virtualSalesInput: virtualSales,
};
});
},
getTotalSales(row) {
return (Number(row.buyCount) || 0) + (Number(row.virtualSalesInput) || 0);
},
},
mounted() {
this.init();
},
};
selectedRows.value = [];
tableRef.value?.clearSelection();
ElMessage.success("虚拟销量设置成功");
resetVirtualSalesModal();
} else {
ElMessage.error((res && res.message) || "虚拟销量设置失败");
}
})
.catch(() => {
ElMessage.error("虚拟销量设置失败");
})
.finally(() => {
modalSubmitting.value = false;
});
}
function getTotalSales(row) {
return (Number(row.buyCount) || 0) + (Number(row.virtualSalesInput) || 0);
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>

View File

@@ -48,37 +48,37 @@
<h4>今日待办</h4>
<div class="todo-list flex">
<div class="todo-item" @click="navigateTo('applyGoods')">
<div class="counts">{{ $store.state.notices.goods || 0 }}</div>
<div class="counts">{{ notices.goods || 0 }}</div>
<div>待审核商品</div>
</div>
<div class="todo-item" @click="navigateTo('shopAuth')">
<div class="counts">{{ $store.state.notices.store || 0 }}</div>
<div class="counts">{{ notices.store || 0 }}</div>
<div>待审核店铺</div>
</div>
<div class="todo-item" @click="navigateTo('orderComplaint')">
<div class="counts">{{ $store.state.notices.complain || 0 }}</div>
<div class="counts">{{ notices.complain || 0 }}</div>
<div>待审核投诉</div>
</div>
<div class="todo-item" @click="navigateTo('afterSaleOrder')">
<div class="counts">{{ $store.state.notices.refund || 0 }}</div>
<div class="counts">{{ notices.refund || 0 }}</div>
<div>待审核售后</div>
</div>
<div class="todo-item" @click="goDistributionCash">
<div class="counts">
{{ $store.state.notices.distributionCash || 0 }}
{{ notices.distributionCash || 0 }}
</div>
<div>待审核分销提现</div>
</div>
<div class="todo-item" @click="navigateTo('accountStatementBill')">
<div class="counts">{{ $store.state.notices.waitPayBill || 0 }}</div>
<div class="counts">{{ notices.waitPayBill || 0 }}</div>
<div>待审核分账</div>
</div>
<div class="todo-item" @click="goAppointmentWorkOrders">
<div class="counts">{{ $store.state.notices.appointmentWorkOrder || 0 }}</div>
<div class="counts">{{ notices.appointmentWorkOrder || 0 }}</div>
<div>未完成预约工单</div>
</div>
<div class="todo-item" @click="goProcurementAudit">
<div class="counts">{{ $store.state.notices.procurementAudit || 0 }}</div>
<div class="counts">{{ notices.procurementAudit || 0 }}</div>
<div>待审核采购单</div>
</div>
</div>
@@ -237,353 +237,372 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { Picture, User, List, DataLine } from "@element-plus/icons-vue";
import { homeStatistics, hotGoods, hotShops, getNoticePage } from "@/api/index";
import * as API_Goods from "@/api/goods";
import { Chart } from "@antv/g2";
import * as API_Member from "@/api/member";
export default {
name: "home",
components: { Picture, User, List, DataLine },
import { useAppStore } from "@/stores/app";
data() {
return {
topHotGoodsData: [], //热卖商品集合
topHotShopsData: [], //热卖店铺集合
awaitTodoData: "", //今日待办集合
homeData: "", // 首页数据
pvChart: "", // 流量统计
orderChart: "", // 订单统计
historyMemberChart: "", // 最近会员流量统计
rangeKey: "LAST_SEVEN",
rangeMonth: "",
rangeOptions: [
{ title: "今天", searchType: "TODAY" },
{ title: "昨天", searchType: "YESTERDAY" },
{ title: "过去7天", searchType: "LAST_SEVEN" },
{ title: "过去30天", searchType: "LAST_THIRTY" },
],
};
},
computed: {
recentMonths() {
const list = [];
const now = new Date();
for (let i = 0; i < 12; i += 1) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const year = date.getFullYear();
const month = date.getMonth() + 1;
list.push({
value: `${year}-${month}`,
label: `${year}年${month}月`,
defineOptions({ name: "home" });
const router = useRouter();
const appStore = useAppStore();
const topHotGoodsData = ref([]); //热卖商品集合
const topHotShopsData = ref([]); //热卖店铺集合
const awaitTodoData = ref(""); //今日待办集合
const homeData = ref(""); // 首页数据
const pvChart = ref(""); // 流量统计
const orderChart = ref(""); // 订单统计
const historyMemberChart = ref(""); // 最近会员流量统计
const rangeKey = ref("LAST_SEVEN");
const rangeMonth = ref("");
const rangeOptions = ref([
{ title: "今天", searchType: "TODAY" },
{ title: "昨天", searchType: "YESTERDAY" },
{ title: "过去7天", searchType: "LAST_SEVEN" },
{ title: "过去30天", searchType: "LAST_THIRTY" },
]);
// 原代码未在 data 声明、直接挂 this 的字段,补声明为普通变量
let orderChartData = []; // 订单图表数据
let data = []; // 浏览量图表数据
let chartList = []; // 历史在线人数图表数据
const notices = computed(() => appStore.notices || {});
const recentMonths = computed(() => {
const list = [];
const now = new Date();
for (let i = 0; i < 12; i += 1) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const year = date.getFullYear();
const month = date.getMonth() + 1;
list.push({
value: `${year}-${month}`,
label: `${year}年${month}月`,
});
}
return list;
});
const rangeTitle = computed(() => {
if (rangeMonth.value) {
const found = recentMonths.value.find((item) => item.value === rangeMonth.value);
return found ? found.label : rangeMonth.value;
}
const found = rangeOptions.value.find((item) => item.searchType === rangeKey.value);
return found ? found.title : "过去7天";
});
// 路由跳转
function navigateTo(name) {
router.push({
name,
});
}
// 跳转到分销提现,并打开待处理标签页
function goDistributionCash() {
router.push({
name: 'distributionCash',
query: { tabStatus: 'APPLY' }
});
}
function goAppointmentWorkOrders() {
router.push({
name: "manager-appointment-work-orders",
query: { serviceStatus: "OPEN" },
});
}
function goProcurementAudit() {
router.push({
name: "manager-procurement-order-list",
query: { status: "SUBMITTED" },
});
}
function rangeQuery() {
if (rangeMonth.value) {
const [year, month] = rangeMonth.value.split("-");
return { year, month };
}
return { searchType: rangeKey.value || "LAST_SEVEN" };
}
function changeRange(item) {
rangeMonth.value = "";
rangeKey.value = item.searchType;
reloadRange();
}
function changeMonth(value) {
rangeMonth.value = value || "";
if (!rangeMonth.value) {
rangeKey.value = "LAST_SEVEN";
}
reloadRange();
}
function reloadRange() {
toHotGoods();
topHotShops();
getPvChart();
initOrderChartList();
}
// top10热卖商品
async function toHotGoods() {
let res = await hotGoods(rangeQuery());
topHotGoodsData.value = res.success ? res.result || [] : [];
}
// top10热卖店铺
async function topHotShops() {
let res = await hotShops(rangeQuery());
topHotShopsData.value = res.success ? res.result || [] : [];
}
// 今日待办
async function awaitTodo() {
let res = await getNoticePage();
res.success ? (awaitTodoData.value = res.result) : "";
}
//首页统计数据
async function getHomeData() {
let res = await homeStatistics();
if (res.success) {
const price = res.result.todayOrderPrice;
const amount = price === null || price === undefined || price === "" || price === "null"
? 0
: Number(price);
res.result.todayOrderPrice = Number.isNaN(amount) ? 0 : amount;
homeData.value = res.result;
}
}
// 实例化订单图表
async function initOrderChartList() {
orderChart.value ? orderChart.value.clear() : ''
const res = await API_Goods.getOrderChart(rangeQuery());
if (res.success) {
orderChartData = res.result || [];
if (!orderChart.value) {
orderChart.value = new Chart({
container: "orderChart",
autoFit: true,
height: 500,
padding: [70, 70, 70, 70],
});
}
initOrderChart(); //订单表
}
}
// 订单表
function initOrderChart() {
let chartData = orderChartData || [];
chartData.forEach((item) => {
const time = item.createTime ? String(item.createTime) : "";
item.createTime = time.includes(" ") ? time.split(" ")[0] : time;
item.title = "交易额";
});
orderChart.value.data(chartData);
orderChart.value.tooltip({
showCrosshairs: true,
shared: true,
});
orderChart.value
.line()
.position("createTime*price")
.label("price")
.color("title")
.shape("smooth");
orderChart.value
.point()
.position("createTime*price")
.label("price")
.color("title")
.shape("circle")
.style({
stroke: "#fff",
lineWidth: 1,
});
orderChart.value.area().position("createTime*price").color("title").shape("smooth");
orderChart.value.render();
}
// 浏览量统计图
function initPvChart() {
let uv = [];
let pv = [];
const rows = Array.isArray(data) ? data : [];
rows.forEach((item) => {
uv.push({
date: item.date,
uvNum: item.uvNum,
title: "访客数UV",
pv: item.uvNum,
});
pv.push({
date: item.date,
pvNum: item.pvNum,
pv: item.pvNum,
title: "浏览量PV",
});
});
let chartData = [...uv, ...pv];
pvChart.value.data(chartData);
pvChart.value.tooltip({
showCrosshairs: true,
shared: true,
});
pvChart.value
.line()
.position("date*pv")
.color("title")
.label("pv")
.shape("smooth");
pvChart.value
.point()
.position("date*pv")
.color("title")
.label("pv")
.shape("circle")
.style({
stroke: "#fff",
lineWidth: 1,
});
pvChart.value.area().position("date*pv").color("title").shape("smooth");
pvChart.value.render();
}
// 浏览量
async function getPvChart() {
pvChart.value ? pvChart.value.clear() : ''
API_Member.getStatisticsList(rangeQuery()).then((res) => {
if (res.result) {
data = res.result || [];
if (!pvChart.value) {
pvChart.value = new Chart({
container: "pvChart",
autoFit: true,
height: 500,
padding: [70, 70, 70, 70],
});
}
return list;
},
rangeTitle() {
if (this.rangeMonth) {
const found = this.recentMonths.find((item) => item.value === this.rangeMonth);
return found ? found.label : this.rangeMonth;
}
const found = this.rangeOptions.find((item) => item.searchType === this.rangeKey);
return found ? found.title : "过去7天";
},
},
methods: {
// 路由跳转
navigateTo(name) {
this.$router.push({
name,
initPvChart();
}
});
}
// 实例化会员流量图表
async function initHistoryMemberChartList() {
historyMemberChart.value ? historyMemberChart.value.clear() : ''
const res = await API_Member.historyMemberChartList();
if (res.success) {
chartList = res.result;
if (!historyMemberChart.value) {
historyMemberChart.value = new Chart({
container: "historyMemberChart",
autoFit: true,
height: 500,
padding: [70, 70, 70, 70],
});
},
// 跳转到分销提现,并打开待处理标签页
goDistributionCash() {
this.$router.push({
name: 'distributionCash',
query: { tabStatus: 'APPLY' }
});
},
goAppointmentWorkOrders() {
this.$router.push({
name: "manager-appointment-work-orders",
query: { serviceStatus: "OPEN" },
});
},
goProcurementAudit() {
this.$router.push({
name: "manager-procurement-order-list",
query: { status: "SUBMITTED" },
});
},
rangeQuery() {
if (this.rangeMonth) {
const [year, month] = this.rangeMonth.split("-");
return { year, month };
}
return { searchType: this.rangeKey || "LAST_SEVEN" };
},
changeRange(item) {
this.rangeMonth = "";
this.rangeKey = item.searchType;
this.reloadRange();
},
changeMonth(value) {
this.rangeMonth = value || "";
if (!this.rangeMonth) {
this.rangeKey = "LAST_SEVEN";
}
this.reloadRange();
},
reloadRange() {
this.toHotGoods();
this.topHotShops();
this.getPvChart();
this.initOrderChartList();
},
// top10热卖商品
async toHotGoods() {
let res = await hotGoods(this.rangeQuery());
this.topHotGoodsData = res.success ? res.result || [] : [];
},
}
// top10热卖店铺
async topHotShops() {
let res = await hotShops(this.rangeQuery());
this.topHotShopsData = res.success ? res.result || [] : [];
},
// 今日待办
async awaitTodo() {
let res = await getNoticePage();
res.success ? (this.awaitTodoData = res.result) : "";
},
initHistoryMemberChart();
}
}
//首页统计数据
async getHomeData() {
let res = await homeStatistics();
if (res.success) {
const price = res.result.todayOrderPrice;
const amount = price === null || price === undefined || price === "" || price === "null"
? 0
: Number(price);
res.result.todayOrderPrice = Number.isNaN(amount) ? 0 : amount;
this.homeData = res.result;
}
},
// 历史在线人数
function initHistoryMemberChart() {
// 默认已经加载 legend-filter 交互
let chartData = chartList;
let num = [];
let lastNum = [];
chartData.forEach((item) => {
num.push({
date: item.date.substring(5),
title: "最近48小时",
num: item.num,
res: item.num,
});
// 实例化订单图表
async initOrderChartList() {
this.orderChart ? this.orderChart.clear() : ''
const res = await API_Goods.getOrderChart(this.rangeQuery());
if (res.success) {
this.orderChartData = res.result || [];
lastNum.push({
date: item.date.substring(5),
title: "上一周期",
lastNum: item.lastNum || 0,
res: item.lastNum || 0,
});
});
let params = [...num, ...lastNum];
historyMemberChart.value.data(params);
if (!this.orderChart) {
this.orderChart = new Chart({
container: "orderChart",
autoFit: true,
height: 500,
padding: [70, 70, 70, 70],
});
}
historyMemberChart.value.tooltip({
showCrosshairs: true,
shared: true,
});
this.initOrderChart(); //订单表
}
},
historyMemberChart.value
.line()
.position("date*res")
.color("title")
.label("res")
.shape("smooth");
historyMemberChart.value
.point()
.position("date*res")
.color("title")
.label("res")
.shape("circle");
// 订单表
initOrderChart() {
let data = this.orderChartData || [];
historyMemberChart.value
.area()
.position("date*res")
.color("title")
.shape("smooth");
data.forEach((item) => {
const time = item.createTime ? String(item.createTime) : "";
item.createTime = time.includes(" ") ? time.split(" ")[0] : time;
item.title = "交易额";
});
this.orderChart.data(data);
historyMemberChart.value.render();
}
this.orderChart.tooltip({
showCrosshairs: true,
shared: true,
});
// 初始化信息
function init() {
toHotGoods();
topHotShops();
awaitTodo();
getHomeData();
getPvChart();
initOrderChartList();
initHistoryMemberChartList();
}
this.orderChart
.line()
.position("createTime*price")
.label("price")
.color("title")
.shape("smooth");
this.orderChart
.point()
.position("createTime*price")
.label("price")
.color("title")
.shape("circle")
.style({
stroke: "#fff",
lineWidth: 1,
});
this.orderChart.area().position("createTime*price").color("title").shape("smooth");
this.orderChart.render();
},
// 浏览量统计图
initPvChart() {
let uv = [];
let pv = [];
const rows = Array.isArray(this.data) ? this.data : [];
rows.forEach((item) => {
uv.push({
date: item.date,
uvNum: item.uvNum,
title: "访客数UV",
pv: item.uvNum,
});
pv.push({
date: item.date,
pvNum: item.pvNum,
pv: item.pvNum,
title: "浏览量PV",
});
});
let data = [...uv, ...pv];
this.pvChart.data(data);
this.pvChart.tooltip({
showCrosshairs: true,
shared: true,
});
this.pvChart
.line()
.position("date*pv")
.color("title")
.label("pv")
.shape("smooth");
this.pvChart
.point()
.position("date*pv")
.color("title")
.label("pv")
.shape("circle")
.style({
stroke: "#fff",
lineWidth: 1,
});
this.pvChart.area().position("date*pv").color("title").shape("smooth");
this.pvChart.render();
},
// 浏览量
async getPvChart() {
this.pvChart ? this.pvChart.clear() : ''
API_Member.getStatisticsList(this.rangeQuery()).then((res) => {
if (res.result) {
this.data = res.result || [];
if (!this.pvChart) {
this.pvChart = new Chart({
container: "pvChart",
autoFit: true,
height: 500,
padding: [70, 70, 70, 70],
});
}
this.initPvChart();
}
});
},
// 实例化会员流量图表
async initHistoryMemberChartList() {
this.historyMemberChart ? this.historyMemberChart.clear() : ''
const res = await API_Member.historyMemberChartList();
if (res.success) {
this.chartList = res.result;
if (!this.historyMemberChart) {
this.historyMemberChart = new Chart({
container: "historyMemberChart",
autoFit: true,
height: 500,
padding: [70, 70, 70, 70],
});
}
this.initHistoryMemberChart();
}
},
// 历史在线人数
initHistoryMemberChart() {
// 默认已经加载 legend-filter 交互
let data = this.chartList;
let num = [];
let lastNum = [];
data.forEach((item) => {
num.push({
date: item.date.substring(5),
title: "最近48小时",
num: item.num,
res: item.num,
});
lastNum.push({
date: item.date.substring(5),
title: "上一周期",
lastNum: item.lastNum || 0,
res: item.lastNum || 0,
});
});
let params = [...num, ...lastNum];
this.historyMemberChart.data(params);
this.historyMemberChart.tooltip({
showCrosshairs: true,
shared: true,
});
this.historyMemberChart
.line()
.position("date*res")
.color("title")
.label("res")
.shape("smooth");
this.historyMemberChart
.point()
.position("date*res")
.color("title")
.label("res")
.shape("circle");
this.historyMemberChart
.area()
.position("date*res")
.color("title")
.shape("smooth");
this.historyMemberChart.render();
},
// 初始化信息
init() {
this.toHotGoods();
this.topHotShops();
this.awaitTodo();
this.getHomeData();
this.getPvChart();
this.initOrderChartList();
this.initHistoryMemberChartList();
},
},
mounted() {
this.init();
},
};
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
@import "./home.scss";

View File

@@ -4,7 +4,7 @@
<template #header>
<span class="page-title">创建直播</span>
</template>
<el-form ref="form" :model="form" :rules="rules" label-width="100px" class="live-form">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="live-form">
<el-form-item label="直播标题" prop="title">
<el-input
v-model="form.title"
@@ -68,7 +68,7 @@
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
<ossManage
ref="ossManage"
ref="ossManageRef"
:is-component="true"
:initialize="picModelFlag"
@callback="callbackSelected"
@@ -77,11 +77,16 @@
</div>
</template>
<script>
<script setup>
import { nextTick, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { Picture } from "@element-plus/icons-vue";
import { createLive } from "@/api/live";
import { Message } from "@/utils/message";
import ossManage from "@/views/sys/oss-manage/ossManage";
defineOptions({ name: "live-add" });
function getCharLength(str) {
if (!str) return 0;
let len = 0;
@@ -98,88 +103,85 @@ function buildEndTime(startTime, hours = 24) {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
export default {
name: "live-add",
components: { Picture, ossManage },
data() {
const validateTitle = (rule, value, callback) => {
const len = getCharLength(value);
if (len < 6) {
callback(new Error("直播间名字最短3个汉字"));
return;
}
if (len > 34) {
callback(new Error("直播间名字最长17个汉字"));
return;
}
callback();
};
return {
picModelFlag: false,
submitting: false,
form: {
title: "",
displayMode: "0",
startTime: "",
coverImg: "",
liveIntroduce: "",
},
rules: {
title: [
{ required: true, message: "请输入直播标题", trigger: "blur" },
{ validator: validateTitle, trigger: "blur" },
],
displayMode: [{ required: true, message: "请选择显示模式", trigger: "change" }],
startTime: [{ required: true, message: "请选择直播开始时间", trigger: "change" }],
coverImg: [{ required: true, message: "请上传直播封面", trigger: "change" }],
},
};
},
methods: {
openCoverPicker() {
this.picModelFlag = true;
this.$nextTick(() => {
if (this.$refs.ossManage) {
this.$refs.ossManage.selectImage = true;
}
});
},
callbackSelected(val) {
this.picModelFlag = false;
if (val?.url) {
this.form.coverImg = val.url;
this.$refs.form?.validateField("coverImg");
}
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return;
this.submitting = true;
createLive({
title: this.form.title,
displayMode: this.form.displayMode,
coverImg: this.form.coverImg,
startTime: this.form.startTime,
endTime: buildEndTime(this.form.startTime),
liveIntroduce: this.form.liveIntroduce,
})
.then((res) => {
this.submitting = false;
if (res.success) {
this.$Message.success("创建成功");
this.$router.push({ path: "/live-list" });
}
})
.catch(() => {
this.submitting = false;
});
});
},
handleBack() {
this.$router.push({ path: "/live-list" });
},
},
const router = useRouter();
const formRef = ref(null);
const ossManageRef = ref(null);
const picModelFlag = ref(false);
const submitting = ref(false);
const form = reactive({
title: "",
displayMode: "0",
startTime: "",
coverImg: "",
liveIntroduce: "",
});
const validateTitle = (rule, value, callback) => {
const len = getCharLength(value);
if (len < 6) {
callback(new Error("直播间名字最短3个汉字"));
return;
}
if (len > 34) {
callback(new Error("直播间名字最长17个汉字"));
return;
}
callback();
};
const rules = {
title: [
{ required: true, message: "请输入直播标题", trigger: "blur" },
{ validator: validateTitle, trigger: "blur" },
],
displayMode: [{ required: true, message: "请选择显示模式", trigger: "change" }],
startTime: [{ required: true, message: "请选择直播开始时间", trigger: "change" }],
coverImg: [{ required: true, message: "请上传直播封面", trigger: "change" }],
};
function openCoverPicker() {
picModelFlag.value = true;
nextTick(() => {
if (ossManageRef.value) {
ossManageRef.value.selectImage = true;
}
});
}
function callbackSelected(val) {
picModelFlag.value = false;
if (val?.url) {
form.coverImg = val.url;
formRef.value?.validateField("coverImg");
}
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (!valid) return;
submitting.value = true;
createLive({
title: form.title,
displayMode: form.displayMode,
coverImg: form.coverImg,
startTime: form.startTime,
endTime: buildEndTime(form.startTime),
liveIntroduce: form.liveIntroduce,
})
.then((res) => {
submitting.value = false;
if (res.success) {
Message.success("创建成功");
router.push({ path: "/live-list" });
}
})
.catch(() => {
submitting.value = false;
});
});
}
function handleBack() {
router.push({ path: "/live-list" });
}
</script>
<style lang="scss" scoped>

View File

@@ -193,7 +193,8 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, watch, onMounted } from "vue";
import { Refresh } from "@element-plus/icons-vue";
import {
authLiveMessage,
@@ -204,243 +205,261 @@ import {
queryLiveMessagePage,
unblockUser,
} from "@/api/live";
import { Message } from "@/utils/message";
import { unixToDate } from "@/utils/filters";
export default {
name: "CommentReview",
components: { Refresh },
props: {
liveId: {
type: String,
required: true,
},
embedded: {
type: Boolean,
default: false,
},
defineOptions({ name: "CommentReview" });
const props = defineProps({
liveId: {
type: String,
required: true,
},
data() {
return {
activeTab: "message",
searchKeyword: "",
messageLoading: false,
userLoading: false,
blockLoading: false,
messageList: [],
userList: [],
blockList: [],
messagePagination: {
pageNumber: 1,
pageSize: 10,
total: 0,
},
userPagination: {
pageNumber: 1,
pageSize: 10,
total: 0,
},
blockPagination: {
pageNumber: 1,
pageSize: 10,
total: 0,
},
blockDialogVisible: false,
blockTarget: null,
blockReason: "",
blockSubmitting: false,
};
embedded: {
type: Boolean,
default: false,
},
watch: {
liveId() {
this.loadMessages();
},
},
mounted() {
this.loadMessages();
},
methods: {
formatTime(time) {
if (!time) return "-";
return this.$filters.unixToDate(time);
},
authStatusText(status) {
const map = { PASS: "已通过", REFUSE: "已拒绝", TOBEAUDITED: "待审核" };
return map[status] || status || "待审核";
},
authStatusTagType(status) {
const map = { PASS: "success", REFUSE: "danger", TOBEAUDITED: "warning" };
return map[status] || "info";
},
canPassAuth(row) {
const status = row?.authStatus;
return status === "TOBEAUDITED" || status === "REFUSE" || !status;
},
canRejectAuth(row) {
const status = row?.authStatus;
return status === "TOBEAUDITED" || status === "PASS" || !status;
},
parsePageResult(res) {
if (!res) return { records: [], total: 0 };
if (res.records) {
return { records: res.records || [], total: res.total || 0 };
});
const activeTab = ref("message");
const searchKeyword = ref("");
const messageLoading = ref(false);
const userLoading = ref(false);
const blockLoading = ref(false);
const messageList = ref([]);
const userList = ref([]);
const blockList = ref([]);
const messagePagination = reactive({
pageNumber: 1,
pageSize: 10,
total: 0,
});
const userPagination = reactive({
pageNumber: 1,
pageSize: 10,
total: 0,
});
const blockPagination = reactive({
pageNumber: 1,
pageSize: 10,
total: 0,
});
const blockDialogVisible = ref(false);
const blockTarget = ref(null);
const blockReason = ref("");
const blockSubmitting = ref(false);
watch(
() => props.liveId,
() => {
loadMessages();
}
);
function formatTime(time) {
if (!time) return "-";
return unixToDate(time);
}
function authStatusText(status) {
const map = { PASS: "已通过", REFUSE: "已拒绝", TOBEAUDITED: "待审核" };
return map[status] || status || "待审核";
}
function authStatusTagType(status) {
const map = { PASS: "success", REFUSE: "danger", TOBEAUDITED: "warning" };
return map[status] || "info";
}
function canPassAuth(row) {
const status = row?.authStatus;
return status === "TOBEAUDITED" || status === "REFUSE" || !status;
}
function canRejectAuth(row) {
const status = row?.authStatus;
return status === "TOBEAUDITED" || status === "PASS" || !status;
}
function parsePageResult(res) {
if (!res) return { records: [], total: 0 };
if (res.records) {
return { records: res.records || [], total: res.total || 0 };
}
if (res.result?.records) {
return { records: res.result.records || [], total: res.result.total || 0 };
}
if (res.success && res.result) {
return { records: res.result.records || [], total: res.result.total || 0 };
}
return { records: [], total: 0 };
}
function onTabChange(tab) {
if (tab === "message") loadMessages();
if (tab === "user") loadUsers();
if (tab === "block") loadBlocks();
}
function loadMessages() {
if (!props.liveId) return;
messageLoading.value = true;
const params = {
liveStreamId: props.liveId,
pageNumber: messagePagination.pageNumber,
pageSize: messagePagination.pageSize,
};
if (searchKeyword.value?.trim()) {
params.keyword = searchKeyword.value.trim();
}
queryLiveMessagePage(params)
.then((res) => {
messageLoading.value = false;
if (res.success) {
const page = parsePageResult(res);
messageList.value = page.records;
messagePagination.total = page.total;
}
if (res.result?.records) {
return { records: res.result.records || [], total: res.result.total || 0 };
})
.catch(() => {
messageLoading.value = false;
});
}
function loadUsers() {
if (!props.liveId) return;
userLoading.value = true;
liveUserList({
liveRoomId: props.liveId,
pageNumber: userPagination.pageNumber,
pageSize: userPagination.pageSize,
})
.then((res) => {
userLoading.value = false;
if (res.success) {
userList.value = res.result?.records || [];
userPagination.total = res.result?.total || 0;
}
if (res.success && res.result) {
return { records: res.result.records || [], total: res.result.total || 0 };
})
.catch(() => {
userLoading.value = false;
});
}
function loadBlocks() {
if (!props.liveId) return;
blockLoading.value = true;
getLiveBlockPage({
liveRoomId: props.liveId,
pageNumber: blockPagination.pageNumber,
pageSize: blockPagination.pageSize,
})
.then((res) => {
blockLoading.value = false;
const page = parsePageResult(res);
blockList.value = page.records;
blockPagination.total = page.total;
})
.catch(() => {
blockLoading.value = false;
});
}
function handleAuth(record, authStatus) {
authLiveMessage({
...record,
authStatus,
}).then((res) => {
if (res.success) {
Message.success("操作成功");
loadMessages();
}
});
}
function handleToggleMute(record) {
const muteFlag = !record.muteFlag;
editLiveUserMute({
liveUserid: record.id,
muteFlag,
}).then((res) => {
if (res.success) {
Message.success(muteFlag ? "已禁言" : "已解除禁言");
loadUsers();
}
});
}
function handleBlock(record) {
blockTarget.value = record;
blockReason.value = "";
blockDialogVisible.value = true;
}
function confirmBlock() {
if (!blockReason.value?.trim()) {
Message.warning("请输入拉黑原因");
return;
}
blockSubmitting.value = true;
blockUser({
liveRoomId: props.liveId,
userId: blockTarget.value?.userId || blockTarget.value?.id,
reason: blockReason.value.trim(),
})
.then((res) => {
blockSubmitting.value = false;
if (res.success) {
Message.success("拉黑成功");
blockDialogVisible.value = false;
loadUsers();
loadBlocks();
}
return { records: [], total: 0 };
},
onTabChange(tab) {
if (tab === "message") this.loadMessages();
if (tab === "user") this.loadUsers();
if (tab === "block") this.loadBlocks();
},
loadMessages() {
if (!this.liveId) return;
this.messageLoading = true;
const params = {
liveStreamId: this.liveId,
pageNumber: this.messagePagination.pageNumber,
pageSize: this.messagePagination.pageSize,
};
if (this.searchKeyword?.trim()) {
params.keyword = this.searchKeyword.trim();
}
queryLiveMessagePage(params)
.then((res) => {
this.messageLoading = false;
if (res.success) {
const page = this.parsePageResult(res);
this.messageList = page.records;
this.messagePagination.total = page.total;
}
})
.catch(() => {
this.messageLoading = false;
});
},
loadUsers() {
if (!this.liveId) return;
this.userLoading = true;
liveUserList({
liveRoomId: this.liveId,
pageNumber: this.userPagination.pageNumber,
pageSize: this.userPagination.pageSize,
})
.then((res) => {
this.userLoading = false;
if (res.success) {
this.userList = res.result?.records || [];
this.userPagination.total = res.result?.total || 0;
}
})
.catch(() => {
this.userLoading = false;
});
},
loadBlocks() {
if (!this.liveId) return;
this.blockLoading = true;
getLiveBlockPage({
liveRoomId: this.liveId,
pageNumber: this.blockPagination.pageNumber,
pageSize: this.blockPagination.pageSize,
})
.then((res) => {
this.blockLoading = false;
const page = this.parsePageResult(res);
this.blockList = page.records;
this.blockPagination.total = page.total;
})
.catch(() => {
this.blockLoading = false;
});
},
handleAuth(record, authStatus) {
authLiveMessage({
...record,
authStatus,
}).then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadMessages();
}
});
},
handleToggleMute(record) {
const muteFlag = !record.muteFlag;
editLiveUserMute({
liveUserid: record.id,
muteFlag,
}).then((res) => {
if (res.success) {
this.$Message.success(muteFlag ? "已禁言" : "已解除禁言");
this.loadUsers();
}
});
},
handleBlock(record) {
this.blockTarget = record;
this.blockReason = "";
this.blockDialogVisible = true;
},
confirmBlock() {
if (!this.blockReason?.trim()) {
this.$Message.warning("请输入拉黑原因");
return;
}
this.blockSubmitting = true;
blockUser({
liveRoomId: this.liveId,
userId: this.blockTarget?.userId || this.blockTarget?.id,
reason: this.blockReason.trim(),
})
.then((res) => {
this.blockSubmitting = false;
if (res.success) {
this.$Message.success("拉黑成功");
this.blockDialogVisible = false;
this.loadUsers();
this.loadBlocks();
}
})
.catch(() => {
this.blockSubmitting = false;
});
},
handleUnblock(record) {
unblockUser({
liveRoomId: this.liveId,
userId: record.userId || record.id,
}).then((res) => {
if (res.success) {
this.$Message.success("已解除拉黑");
this.loadBlocks();
}
});
},
onMessagePageSizeChange() {
this.messagePagination.pageNumber = 1;
this.loadMessages();
},
onUserPageSizeChange() {
this.userPagination.pageNumber = 1;
this.loadUsers();
},
onBlockPageSizeChange() {
this.blockPagination.pageNumber = 1;
this.loadBlocks();
},
handleSearch() {
this.messagePagination.pageNumber = 1;
this.loadMessages();
},
handleRefresh() {
this.loadMessages();
},
},
};
})
.catch(() => {
blockSubmitting.value = false;
});
}
function handleUnblock(record) {
unblockUser({
liveRoomId: props.liveId,
userId: record.userId || record.id,
}).then((res) => {
if (res.success) {
Message.success("已解除拉黑");
loadBlocks();
}
});
}
function onMessagePageSizeChange() {
messagePagination.pageNumber = 1;
loadMessages();
}
function onUserPageSizeChange() {
userPagination.pageNumber = 1;
loadUsers();
}
function onBlockPageSizeChange() {
blockPagination.pageNumber = 1;
loadBlocks();
}
function handleSearch() {
messagePagination.pageNumber = 1;
loadMessages();
}
function handleRefresh() {
loadMessages();
}
onMounted(() => {
loadMessages();
});
</script>
<style lang="scss" scoped>

View File

@@ -105,7 +105,8 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import {
cancelRecommendLiveCoupon,
getLiveCouponList,
@@ -114,189 +115,192 @@ import {
setRecommendLiveCoupon,
} from "@/api/live";
import { getPlatformCouponList } from "@/api/promotion";
import { Message, Modal } from "@/utils/message";
import { unitPrice } from "@/utils/filters";
export default {
name: "LiveCoupon",
props: {
liveId: {
type: String,
required: true,
},
liveRoomName: {
type: String,
default: "",
},
defineOptions({ name: "LiveCoupon" });
const props = defineProps({
liveId: {
type: String,
required: true,
},
data() {
return {
loading: false,
couponList: [],
selectedIds: [],
pickerVisible: false,
pickerLoading: false,
pickerSubmitting: false,
pickerList: [],
pickerTotal: 0,
pickerSelected: [],
pickerSearch: {
pageNumber: 1,
pageSize: 10,
couponName: "",
promotionStatus: "START",
sort: "create_time",
order: "desc",
},
};
liveRoomName: {
type: String,
default: "",
},
watch: {
liveId() {
this.loadCoupons();
},
},
mounted() {
this.loadCoupons();
},
methods: {
formatPrice(price) {
return this.$filters.unitPrice(price, "¥");
},
promotionStatusText(status) {
const map = { NEW: "未开始", START: "已开始", END: "已结束", CLOSE: "已关闭" };
return map[status] || "未知";
},
promotionStatusTagType(status) {
const map = { NEW: "info", START: "success", END: "warning", CLOSE: "danger" };
return map[status] || "info";
},
loadCoupons() {
if (!this.liveId) return;
this.loading = true;
getLiveCouponList(this.liveId)
.then((res) => {
this.loading = false;
if (res.success) {
this.couponList = res.result || [];
}
})
.catch(() => {
this.loading = false;
});
},
handleSelectionChange(rows) {
this.selectedIds = (rows || []).map((r) => r.id);
},
openCouponSelector() {
this.pickerVisible = true;
this.pickerSelected = [];
this.pickerSearch.pageNumber = 1;
this.loadPickerCoupons();
},
loadPickerCoupons() {
this.pickerLoading = true;
getPlatformCouponList(this.pickerSearch)
.then((res) => {
this.pickerLoading = false;
if (res.success) {
this.pickerList = res.result?.records || [];
this.pickerTotal = res.result?.total || 0;
}
})
.catch(() => {
this.pickerLoading = false;
});
},
onPickerPageSizeChange() {
this.pickerSearch.pageNumber = 1;
this.loadPickerCoupons();
},
handlePickerSelectionChange(rows) {
this.pickerSelected = rows || [];
},
buildLiveCouponPayload(item) {
return {
couponId: item.id,
couponPrice: Number(item.price ?? 0),
couponName: item.couponName || "",
liveRoomId: this.liveId,
liveRoomName: this.liveRoomName || "",
hideFlag: false,
recommend: false,
};
},
confirmPicker() {
if (!this.pickerSelected.length) {
this.$Message.warning("请选择优惠券");
return;
});
const table = ref(null);
const pickerTable = ref(null);
const loading = ref(false);
const couponList = ref([]);
const selectedIds = ref([]);
const pickerVisible = ref(false);
const pickerLoading = ref(false);
const pickerSubmitting = ref(false);
const pickerList = ref([]);
const pickerTotal = ref(0);
const pickerSelected = ref([]);
const pickerSearch = reactive({
pageNumber: 1,
pageSize: 10,
couponName: "",
promotionStatus: "START",
sort: "create_time",
order: "desc",
});
watch(
() => props.liveId,
() => {
loadCoupons();
}
);
onMounted(() => {
loadCoupons();
});
function formatPrice(price) {
return unitPrice(price, "¥");
}
function promotionStatusText(status) {
const map = { NEW: "未开始", START: "已开始", END: "已结束", CLOSE: "已关闭" };
return map[status] || "未知";
}
function promotionStatusTagType(status) {
const map = { NEW: "info", START: "success", END: "warning", CLOSE: "danger" };
return map[status] || "info";
}
function loadCoupons() {
if (!props.liveId) return;
loading.value = true;
getLiveCouponList(props.liveId)
.then((res) => {
loading.value = false;
if (res.success) {
couponList.value = res.result || [];
}
const existingCouponIds = new Set(this.couponList.map((c) => String(c.couponId)));
const payload = this.pickerSelected
.filter((item) => !existingCouponIds.has(String(item.id)))
.map((item) => this.buildLiveCouponPayload(item));
if (!payload.length) {
this.$Message.warning("所选优惠券均已添加");
return;
})
.catch(() => {
loading.value = false;
});
}
function handleSelectionChange(rows) {
selectedIds.value = (rows || []).map((r) => r.id);
}
function openCouponSelector() {
pickerVisible.value = true;
pickerSelected.value = [];
pickerSearch.pageNumber = 1;
loadPickerCoupons();
}
function loadPickerCoupons() {
pickerLoading.value = true;
getPlatformCouponList(pickerSearch)
.then((res) => {
pickerLoading.value = false;
if (res.success) {
pickerList.value = res.result?.records || [];
pickerTotal.value = res.result?.total || 0;
}
this.pickerSubmitting = true;
saveBatchLiveCoupon(payload)
.then((res) => {
this.pickerSubmitting = false;
if (res.success) {
this.$Message.success("添加成功");
this.pickerVisible = false;
this.loadCoupons();
}
})
.catch(() => {
this.pickerSubmitting = false;
});
},
handleRemove(record) {
this.$Modal.confirm({
title: "确认移除",
content: "确定要移除该优惠券吗?",
onOk: () => {
return removeBatchLiveCoupon([record.id]).then((res) => {
if (res.success) {
this.$Message.success("移除成功");
this.loadCoupons();
}
});
},
});
},
handleBatchRemove() {
this.$Modal.confirm({
title: "确认批量移除",
content: `确定要移除选中的 ${this.selectedIds.length} 个优惠券吗?`,
onOk: () => {
return removeBatchLiveCoupon(this.selectedIds).then((res) => {
if (res.success) {
this.$Message.success("移除成功");
this.selectedIds = [];
this.loadCoupons();
}
});
},
});
},
handleRecommend(record) {
setRecommendLiveCoupon(record.id).then((res) => {
})
.catch(() => {
pickerLoading.value = false;
});
}
function onPickerPageSizeChange() {
pickerSearch.pageNumber = 1;
loadPickerCoupons();
}
function handlePickerSelectionChange(rows) {
pickerSelected.value = rows || [];
}
function buildLiveCouponPayload(item) {
return {
couponId: item.id,
couponPrice: Number(item.price ?? 0),
couponName: item.couponName || "",
liveRoomId: props.liveId,
liveRoomName: props.liveRoomName || "",
hideFlag: false,
recommend: false,
};
}
function confirmPicker() {
if (!pickerSelected.value.length) {
Message.warning("请选择优惠券");
return;
}
const existingCouponIds = new Set(couponList.value.map((c) => String(c.couponId)));
const payload = pickerSelected.value
.filter((item) => !existingCouponIds.has(String(item.id)))
.map((item) => buildLiveCouponPayload(item));
if (!payload.length) {
Message.warning("所选优惠券均已添加");
return;
}
pickerSubmitting.value = true;
saveBatchLiveCoupon(payload)
.then((res) => {
pickerSubmitting.value = false;
if (res.success) {
Message.success("添加成功");
pickerVisible.value = false;
loadCoupons();
}
})
.catch(() => {
pickerSubmitting.value = false;
});
}
function handleRemove(record) {
Modal.confirm({
title: "确认移除",
content: "确定要移除该优惠券吗?",
onOk: () => {
return removeBatchLiveCoupon([record.id]).then((res) => {
if (res.success) {
this.$Message.success("已设为推荐");
this.loadCoupons();
Message.success("移除成功");
loadCoupons();
}
});
},
handleCancelRecommend(record) {
cancelRecommendLiveCoupon(record.id).then((res) => {
});
}
function handleBatchRemove() {
Modal.confirm({
title: "确认批量移除",
content: `确定要移除选中的 ${selectedIds.value.length} 个优惠券吗?`,
onOk: () => {
return removeBatchLiveCoupon(selectedIds.value).then((res) => {
if (res.success) {
this.$Message.success("已取消推荐");
this.loadCoupons();
Message.success("移除成功");
selectedIds.value = [];
loadCoupons();
}
});
},
},
};
});
}
function handleRecommend(record) {
setRecommendLiveCoupon(record.id).then((res) => {
if (res.success) {
Message.success("已设为推荐");
loadCoupons();
}
});
}
function handleCancelRecommend(record) {
cancelRecommendLiveCoupon(record.id).then((res) => {
if (res.success) {
Message.success("已取消推荐");
loadCoupons();
}
});
}
</script>
<style lang="scss" scoped>

View File

@@ -30,136 +30,155 @@
</div>
</template>
<script>
<script setup>
import { computed, onBeforeUnmount, ref, toRef, watch } from "vue";
import Cookies from "js-cookie";
import { toRef } from "vue";
import { useChat } from "../composables/useChat";
import { resolveImConfigFromDetail } from "../composables/useLiveSetting";
export default {
defineOptions({
name: "LiveChatPanel",
props: {
liveId: {
type: String,
required: true,
},
liveDetail: {
type: Object,
default: () => ({}),
},
active: {
type: Boolean,
default: true,
},
});
const props = defineProps({
liveId: {
type: String,
required: true,
},
setup(props) {
let userInfo = {};
try {
userInfo = JSON.parse(Cookies.get("userInfoManager") || "{}");
} catch {
userInfo = {};
liveDetail: {
type: Object,
default: () => ({}),
},
active: {
type: Boolean,
default: true,
},
});
let userInfo = {};
try {
userInfo = JSON.parse(Cookies.get("userInfoManager") || "{}");
} catch {
userInfo = {};
}
const {
messageList,
chatListRef,
bottomAnchorRef,
unreadCount,
isLoadingHistory,
initError,
initTencentIm,
scrollToBottomAndClearUnread,
onScroll,
setGroupID,
cleanup,
} = useChat(toRef(props, "liveId"), userInfo, toRef(props, "liveDetail"));
const initializing = ref(false);
const chatInited = ref(false);
/** 腾讯云 IM 群组:优先后端字段,否则用直播间 ID(与旧版中控台一致) */
const imGroupId = computed(() => {
return props.liveDetail?.imGroupId || props.liveDetail?.groupId || props.liveId || "";
});
const imSdkAppId = computed(() => {
return resolveImConfigFromDetail(props.liveDetail).sdkAppId;
});
const canInitChat = computed(() => {
const { sdkAppId, secretKey, userSig } = resolveImConfigFromDetail(props.liveDetail);
return !!(imGroupId.value && sdkAppId && (userSig || secretKey));
});
watch(
canInitChat,
(ready) => {
if (!ready) return;
setGroupID(imGroupId.value);
if (props.active) {
bootstrapChat();
}
},
{ immediate: true }
);
return useChat(toRef(props, "liveId"), userInfo, toRef(props, "liveDetail"));
},
data() {
return {
initializing: false,
chatInited: false,
};
},
computed: {
/** 腾讯云 IM 群组:优先后端字段,否则用直播间 ID(与旧版中控台一致) */
imGroupId() {
return this.liveDetail?.imGroupId || this.liveDetail?.groupId || this.liveId || "";
},
imSdkAppId() {
return resolveImConfigFromDetail(this.liveDetail).sdkAppId;
},
canInitChat() {
const { sdkAppId, secretKey, userSig } = resolveImConfigFromDetail(this.liveDetail);
return !!(this.imGroupId && sdkAppId && (userSig || secretKey));
},
},
watch: {
canInitChat: {
immediate: true,
handler(ready) {
if (!ready) return;
this.setGroupID(this.imGroupId);
if (this.active) {
this.bootstrapChat();
}
},
},
imGroupId(id) {
if (!id) return;
this.setGroupID(id);
},
active(val) {
if (val && this.canInitChat && !this.chatInited) {
this.bootstrapChat();
}
},
liveId() {
this.chatInited = false;
this.cleanup();
if (this.active && this.canInitChat) {
this.bootstrapChat();
}
},
liveDetail: {
deep: true,
handler(detail, oldDetail) {
const nextConfig = resolveImConfigFromDetail(detail);
const prevConfig = resolveImConfigFromDetail(oldDetail);
if (
nextConfig.sdkAppId &&
(nextConfig.sdkAppId !== prevConfig.sdkAppId ||
nextConfig.secretKey !== prevConfig.secretKey ||
nextConfig.userSig !== prevConfig.userSig)
) {
this.chatInited = false;
this.cleanup();
if (this.active && this.canInitChat) {
this.bootstrapChat();
}
}
},
},
},
beforeUnmount() {
this.cleanup();
},
methods: {
async bootstrapChat() {
if (!this.liveId || this.chatInited || this.initializing || !this.canInitChat) {
return;
}
watch(imGroupId, (id) => {
if (!id) return;
setGroupID(id);
});
const groupId = this.imGroupId;
if (!groupId) {
this.initError = "缺少直播间 ID,无法初始化聊天";
return;
}
this.setGroupID(groupId);
watch(
() => props.active,
(val) => {
if (val && canInitChat.value && !chatInited.value) {
bootstrapChat();
}
}
);
this.initializing = true;
try {
await this.initTencentIm();
if (!this.initError) {
this.chatInited = true;
}
} catch (error) {
console.error("初始化直播聊天失败:", error);
this.initError = "聊天初始化失败,请刷新重试";
this.chatInited = false;
} finally {
this.initializing = false;
watch(
() => props.liveId,
() => {
chatInited.value = false;
cleanup();
if (props.active && canInitChat.value) {
bootstrapChat();
}
}
);
watch(
() => props.liveDetail,
(detail, oldDetail) => {
const nextConfig = resolveImConfigFromDetail(detail);
const prevConfig = resolveImConfigFromDetail(oldDetail);
if (
nextConfig.sdkAppId &&
(nextConfig.sdkAppId !== prevConfig.sdkAppId ||
nextConfig.secretKey !== prevConfig.secretKey ||
nextConfig.userSig !== prevConfig.userSig)
) {
chatInited.value = false;
cleanup();
if (props.active && canInitChat.value) {
bootstrapChat();
}
},
}
},
};
{ deep: true }
);
onBeforeUnmount(() => {
cleanup();
});
async function bootstrapChat() {
if (!props.liveId || chatInited.value || initializing.value || !canInitChat.value) {
return;
}
const groupId = imGroupId.value;
if (!groupId) {
initError.value = "缺少直播间 ID,无法初始化聊天";
return;
}
setGroupID(groupId);
initializing.value = true;
try {
await initTencentIm();
if (!initError.value) {
chatInited.value = true;
}
} catch (error) {
console.error("初始化直播聊天失败:", error);
initError.value = "聊天初始化失败,请刷新重试";
chatInited.value = false;
} finally {
initializing.value = false;
}
}
</script>
<style lang="scss" scoped>

View File

@@ -76,7 +76,7 @@
</div>
<el-empty v-if="!loading && !goodsList.length" description="暂无直播商品" />
<sku-select ref="skuSelect" @selectedGoodsData="selectedGoodsData" />
<sku-select ref="skuSelectRef" @selectedGoodsData="selectedGoodsData" />
<el-dialog v-model="popularityVisible" title="设置商品热度" width="400px">
<el-form label-width="80px">
@@ -92,7 +92,8 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import {
cancelRecommendLiveGoods,
getLiveGoodsList,
@@ -104,193 +105,208 @@ import {
setRecommendLiveGoods,
} from "@/api/live";
import skuSelect from "@/components/lili-dialog";
import { Message, Modal } from "@/utils/message";
import { unitPrice } from "@/utils/filters";
export default {
name: "LiveGoods",
components: { skuSelect },
props: {
liveId: {
type: String,
required: true,
},
readonly: {
type: Boolean,
default: false,
},
defineOptions({ name: "LiveGoods" });
const props = defineProps({
liveId: {
type: String,
required: true,
},
data() {
return {
loading: false,
goodsList: [],
selectedIds: [],
popularityVisible: false,
currentGoods: null,
popularityForm: {
popularity: 0,
},
};
readonly: {
type: Boolean,
default: false,
},
watch: {
liveId() {
this.loadGoods();
},
},
mounted() {
this.loadGoods();
},
methods: {
formatPrice(price) {
return this.$filters.unitPrice(price, "¥");
},
isSellOut(row) {
return !!row.soldOutFlag;
},
buildLiveGoodsPayload(item) {
const price = Number(item.price ?? 0);
const originPrice = Number(
item.originPrice ?? item.cost ?? item.originalPrice ?? price
);
return {
liveRoomId: this.liveId,
goodsId: item.goodsId,
skuId: item.id || item.skuId,
goodsName: item.goodsName || "",
stock: String(item.quantity ?? item.stock ?? ""),
price,
thumbnail: item.thumbnail || "",
salesCount: Number(item.salesCount ?? item.buyCount ?? 0),
storeId: item.storeId || "",
storeName: item.storeName || "",
originPrice,
sellPoint: item.sellPoint || item.sellingPoint || "",
popularity: 0,
hideFlag: false,
canBuyFlag: true,
soldOutFlag: false,
recommend: false,
};
},
loadGoods() {
if (!this.liveId) return;
this.loading = true;
getLiveGoodsList(this.liveId)
.then((res) => {
this.loading = false;
if (res.success) {
this.goodsList = res.result || [];
}
})
.catch(() => {
this.loading = false;
});
},
openChooseGoods() {
this.$refs.skuSelect.open("goods");
const data = JSON.parse(JSON.stringify(this.goodsList));
data.forEach((e) => {
if (e.skuId) e.id = e.skuId;
});
this.$refs.skuSelect.goodsData = data;
},
selectedGoodsData(selected) {
if (!selected?.length) return;
const existingSkuIds = new Set(this.goodsList.map((g) => String(g.skuId)));
const payload = selected
.filter((item) => !existingSkuIds.has(String(item.id || item.skuId)))
.map((item) => this.buildLiveGoodsPayload(item));
if (!payload.length) {
this.$Message.warning("所选商品均已添加");
return;
});
const loading = ref(false);
const goodsList = ref([]);
const selectedIds = ref([]);
const popularityVisible = ref(false);
const currentGoods = ref(null);
const popularityForm = reactive({
popularity: 0,
});
// 模板 ref:原 ref="skuSelect" 与 import 的组件同名,改名避免互相覆盖
const skuSelectRef = ref(null);
watch(
() => props.liveId,
() => {
loadGoods();
}
);
function formatPrice(price) {
return unitPrice(price, "¥");
}
function isSellOut(row) {
return !!row.soldOutFlag;
}
function buildLiveGoodsPayload(item) {
const price = Number(item.price ?? 0);
const originPrice = Number(
item.originPrice ?? item.cost ?? item.originalPrice ?? price
);
return {
liveRoomId: props.liveId,
goodsId: item.goodsId,
skuId: item.id || item.skuId,
goodsName: item.goodsName || "",
stock: String(item.quantity ?? item.stock ?? ""),
price,
thumbnail: item.thumbnail || "",
salesCount: Number(item.salesCount ?? item.buyCount ?? 0),
storeId: item.storeId || "",
storeName: item.storeName || "",
originPrice,
sellPoint: item.sellPoint || item.sellingPoint || "",
popularity: 0,
hideFlag: false,
canBuyFlag: true,
soldOutFlag: false,
recommend: false,
};
}
function loadGoods() {
if (!props.liveId) return;
loading.value = true;
getLiveGoodsList(props.liveId)
.then((res) => {
loading.value = false;
if (res.success) {
goodsList.value = res.result || [];
}
saveBatchLiveGoods(payload).then((res) => {
})
.catch(() => {
loading.value = false;
});
}
function openChooseGoods() {
skuSelectRef.value.open("goods");
const data = JSON.parse(JSON.stringify(goodsList.value));
data.forEach((e) => {
if (e.skuId) e.id = e.skuId;
});
skuSelectRef.value.goodsData = data;
}
function selectedGoodsData(selected) {
if (!selected?.length) return;
const existingSkuIds = new Set(goodsList.value.map((g) => String(g.skuId)));
const payload = selected
.filter((item) => !existingSkuIds.has(String(item.id || item.skuId)))
.map((item) => buildLiveGoodsPayload(item));
if (!payload.length) {
Message.warning("所选商品均已添加");
return;
}
saveBatchLiveGoods(payload).then((res) => {
if (res.success) {
Message.success("添加成功");
loadGoods();
}
});
}
function handleSelectionChange(rows) {
selectedIds.value = (rows || []).map((r) => r.id);
}
function handleRemove(record) {
Modal.confirm({
title: "确认移除",
content: "确定要移除该商品吗?",
onOk: () => {
return removeBatchLiveGoods([record.id]).then((res) => {
if (res.success) {
this.$Message.success("添加成功");
this.loadGoods();
Message.success("移除成功");
loadGoods();
}
});
},
handleSelectionChange(rows) {
this.selectedIds = (rows || []).map((r) => r.id);
},
handleRemove(record) {
this.$Modal.confirm({
title: "确认移除",
content: "确定要移除该商品吗?",
onOk: () => {
return removeBatchLiveGoods([record.id]).then((res) => {
if (res.success) {
this.$Message.success("移除成功");
this.loadGoods();
}
});
},
});
},
handleBatchRemove() {
this.$Modal.confirm({
title: "确认批量移除",
content: `确定要移除选中的 ${this.selectedIds.length} 个商品吗?`,
onOk: () => {
return removeBatchLiveGoods(this.selectedIds).then((res) => {
if (res.success) {
this.$Message.success("移除成功");
this.selectedIds = [];
this.loadGoods();
}
});
},
});
},
handleRecommend(record) {
setRecommendLiveGoods(record.id).then((res) => {
});
}
function handleBatchRemove() {
Modal.confirm({
title: "确认批量移除",
content: `确定要移除选中的 ${selectedIds.value.length} 个商品吗?`,
onOk: () => {
return removeBatchLiveGoods(selectedIds.value).then((res) => {
if (res.success) {
this.$Message.success("已设为推荐");
this.loadGoods();
Message.success("移除成功");
selectedIds.value = [];
loadGoods();
}
});
},
handleCancelRecommend(record) {
cancelRecommendLiveGoods(record.id).then((res) => {
if (res.success) {
this.$Message.success("已取消推荐");
this.loadGoods();
}
});
},
handleToggleSoldOut(record) {
const soldOutFlag = !this.isSellOut(record);
setLiveGoodsSoldOut(record.id, soldOutFlag).then((res) => {
if (res.success) {
this.$Message.success(soldOutFlag ? "已设为售罄" : "已取消售罄");
this.loadGoods();
}
});
},
openPopularityModal(record) {
this.currentGoods = record;
getLiveGoodsPopularity(record.id, this.liveId).then((res) => {
this.popularityForm.popularity = res.success
? res.result?.popularity ?? record.popularity ?? 0
: record.popularity ?? 0;
this.popularityVisible = true;
});
},
handleSavePopularity() {
if (!this.currentGoods) return;
setLiveGoodsPopularity(
this.currentGoods.id,
this.liveId,
this.popularityForm.popularity
).then((res) => {
if (res.success) {
this.$Message.success("热度设置成功");
this.popularityVisible = false;
this.loadGoods();
}
});
},
},
};
});
}
function handleRecommend(record) {
setRecommendLiveGoods(record.id).then((res) => {
if (res.success) {
Message.success("已设为推荐");
loadGoods();
}
});
}
function handleCancelRecommend(record) {
cancelRecommendLiveGoods(record.id).then((res) => {
if (res.success) {
Message.success("已取消推荐");
loadGoods();
}
});
}
function handleToggleSoldOut(record) {
const soldOutFlag = !isSellOut(record);
setLiveGoodsSoldOut(record.id, soldOutFlag).then((res) => {
if (res.success) {
Message.success(soldOutFlag ? "已设为售罄" : "已取消售罄");
loadGoods();
}
});
}
function openPopularityModal(record) {
currentGoods.value = record;
getLiveGoodsPopularity(record.id, props.liveId).then((res) => {
popularityForm.popularity = res.success
? res.result?.popularity ?? record.popularity ?? 0
: record.popularity ?? 0;
popularityVisible.value = true;
});
}
function handleSavePopularity() {
if (!currentGoods.value) return;
setLiveGoodsPopularity(
currentGoods.value.id,
props.liveId,
popularityForm.popularity
).then((res) => {
if (res.success) {
Message.success("热度设置成功");
popularityVisible.value = false;
loadGoods();
}
});
}
onMounted(() => {
loadGoods();
});
</script>
<style lang="scss" scoped>

View File

@@ -76,142 +76,144 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { DocumentCopy } from "@element-plus/icons-vue";
import { queryLiveOrderPage } from "@/api/live";
import { Message } from "@/utils/message";
import { unixToDate, unitPrice } from "@/utils/filters";
export default {
name: "LivePurchaseRecords",
components: { DocumentCopy },
props: {
liveId: {
type: String,
required: true,
},
defineOptions({ name: "LivePurchaseRecords" });
const props = defineProps({
liveId: {
type: String,
required: true,
},
data() {
return {
loading: false,
orderList: [],
pagination: {
pageNumber: 1,
pageSize: 5,
total: 0,
});
const loading = ref(false);
const orderList = ref([]);
const pagination = reactive({
pageNumber: 1,
pageSize: 5,
total: 0,
});
watch(
() => props.liveId,
() => {
pagination.pageNumber = 1;
loadData();
}
);
onMounted(() => {
loadData();
});
function refresh() {
loadData();
}
function formatTime(time) {
if (!time) return "-";
if (typeof time === "string" && time.includes("-")) return time;
const ts = typeof time === "number" ? (time > 1e12 ? time / 1000 : time) : time;
return unixToDate(ts);
}
function formatPrice(val) {
return unitPrice(val, "¥");
}
function getOrderItems(order) {
if (order.orderItems?.length) return order.orderItems;
if (order.groupGoodsId) {
return [
{
image: order.groupImages,
name: order.groupName,
goodsPrice: order.groupGoodsPrice,
num: order.groupNum,
},
};
},
watch: {
liveId() {
this.pagination.pageNumber = 1;
this.loadData();
},
},
mounted() {
this.loadData();
},
methods: {
refresh() {
this.loadData();
},
formatTime(time) {
if (!time) return "-";
if (typeof time === "string" && time.includes("-")) return time;
const ts = typeof time === "number" ? (time > 1e12 ? time / 1000 : time) : time;
return this.$filters.unixToDate(ts);
},
formatPrice(val) {
return this.$filters.unitPrice(val, "¥");
},
getOrderItems(order) {
if (order.orderItems?.length) return order.orderItems;
if (order.groupGoodsId) {
return [
{
image: order.groupImages,
name: order.groupName,
goodsPrice: order.groupGoodsPrice,
num: order.groupNum,
},
];
];
}
return [];
}
function clientTypeText(v) {
const map = { H5: "移动端", PC: "PC端", WECHAT_MP: "小程序端", APP: "移动应用端" };
return map[v] || v || "-";
}
function clientTypeDotClass(v) {
const map = { H5: "warning", PC: "primary", WECHAT_MP: "success", APP: "info" };
return map[v] || "info";
}
function orderPromotionText(v) {
const map = {
NORMAL: "普通订单",
PINTUAN: "拼团订单",
GIFT: "赠品订单",
POINTS: "积分订单",
KANJIA: "砍价订单",
};
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 orderStatusDotClass(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 copySn(sn) {
if (!sn) return;
navigator.clipboard.writeText(sn);
Message.success("订单编号已复制");
}
function onPageSizeChange() {
pagination.pageNumber = 1;
loadData();
}
function loadData() {
if (!props.liveId) return;
loading.value = true;
queryLiveOrderPage({
liveRoomId: props.liveId,
pageNumber: pagination.pageNumber,
pageSize: pagination.pageSize,
})
.then((res) => {
loading.value = false;
if (res.success) {
orderList.value = res.result?.records || [];
pagination.total = res.result?.total || 0;
}
return [];
},
clientTypeText(v) {
const map = { H5: "移动端", PC: "PC端", WECHAT_MP: "小程序端", APP: "移动应用端" };
return map[v] || v || "-";
},
clientTypeDotClass(v) {
const map = { H5: "warning", PC: "primary", WECHAT_MP: "success", APP: "info" };
return map[v] || "info";
},
orderPromotionText(v) {
const map = {
NORMAL: "普通订单",
PINTUAN: "拼团订单",
GIFT: "赠品订单",
POINTS: "积分订单",
KANJIA: "砍价订单",
};
return map[v] || v || "-";
},
orderStatusText(v) {
const map = {
UNPAID: "未付款",
PAID: "已付款",
UNDELIVERED: "待发货",
STAY_PICKED_UP: "待自提",
PARTS_DELIVERED: "部分发货",
DELIVERED: "已发货",
COMPLETED: "已完成",
TAKE: "待核验",
CANCELLED: "已取消",
};
return map[v] || v || "-";
},
orderStatusDotClass(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";
},
copySn(sn) {
if (!sn) return;
navigator.clipboard.writeText(sn);
this.$Message.success("订单编号已复制");
},
onPageSizeChange() {
this.pagination.pageNumber = 1;
this.loadData();
},
loadData() {
if (!this.liveId) return;
this.loading = true;
queryLiveOrderPage({
liveRoomId: this.liveId,
pageNumber: this.pagination.pageNumber,
pageSize: this.pagination.pageSize,
})
.then((res) => {
this.loading = false;
if (res.success) {
this.orderList = res.result?.records || [];
this.pagination.total = res.result?.total || 0;
}
})
.catch(() => {
this.loading = false;
});
},
},
};
})
.catch(() => {
loading.value = false;
});
}
defineExpose({ refresh });
</script>
<style lang="scss" scoped>

View File

@@ -238,7 +238,9 @@
</div>
</template>
<script>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import {
endLive,
getLiveInfo,
@@ -248,450 +250,450 @@ import {
} from "@/api/live";
import config from "@/config";
import { DocumentCopy, Refresh } from "@element-plus/icons-vue";
import { Message, Modal } from "@/utils/message";
import { unixToDate } from "@/utils/filters";
import CommentReview from "./components/CommentReview.vue";
import Coupon from "./components/Coupon.vue";
import LiveChatPanel from "./components/LiveChatPanel.vue";
import LiveGoods from "./components/LiveGoods.vue";
import LivePurchaseRecords from "./components/LivePurchaseRecords.vue";
export default {
name: "live-control-panel",
components: { CommentReview, Coupon, LiveChatPanel, LiveGoods, LivePurchaseRecords, DocumentCopy, Refresh },
data() {
return {
loading: false,
liveId: "",
videoUrl: "",
wapUrl: "",
aspectRatio: "16:9",
chatTab: "chat",
statsTab: "general",
manageTab: "comment",
liveDetail: {},
orderStats: {},
onlineUserList: [],
onlineLoading: false,
onlinePagination: {
pageNumber: 1,
pageSize: 10,
total: 0,
},
nowTick: Date.now(),
durationTimer: null,
pollTimer: null,
hlsHttpFallbackUsed: false,
lastHlsPullUrl: "",
};
},
created() {
this.hlsInstance = null;
this.hlsVideoElement = null;
this._hlsVideoClickHandler = null;
},
computed: {
liveStatus() {
return this.liveDetail.liveStatus || this.liveDetail.status || "";
},
isLiving() {
return this.liveStatus === "LIVING";
},
shouldPlayStream() {
return this.isLiving && !!this.videoUrl;
},
showPlayerCover() {
return !this.shouldPlayStream;
},
cumulativeViewers() {
return Number(this.liveDetail.actualViewNumber ?? 0);
},
currentViewers() {
return this.cumulativeViewers;
},
commentPeopleNumber() {
return Number(this.liveDetail.commentPeopleNumber ?? 0);
},
interactionRate() {
if (!this.cumulativeViewers) return "0.00%";
return `${((this.commentPeopleNumber / this.cumulativeViewers) * 100).toFixed(2)}%`;
},
dealConversionRate() {
if (!this.cumulativeViewers) return "0%";
const dealCount = Number(this.orderStats.dealUserCount ?? 0);
return `${((dealCount / this.cumulativeViewers) * 100).toFixed(0)}%`;
},
liveDuration() {
void this.nowTick;
const startMs = this.parseDateTime(this.liveDetail.startTime);
if (!startMs || Date.now() < startMs) return "00:00:00";
let endMs = Date.now();
if (this.liveStatus === "ENDED") {
const endTimeMs = this.parseDateTime(this.liveDetail.endTime);
if (endTimeMs && endTimeMs > startMs) {
endMs = endTimeMs;
}
defineOptions({ name: "live-control-panel" });
const route = useRoute();
const purchaseRecords = ref(null);
const hlsContainer = ref(null);
const loading = ref(false);
const liveId = ref("");
const videoUrl = ref("");
const wapUrl = ref("");
const aspectRatio = ref("16:9");
const chatTab = ref("chat");
const statsTab = ref("general");
const manageTab = ref("comment");
const liveDetail = ref({});
const orderStats = ref({});
const onlineUserList = ref([]);
const onlineLoading = ref(false);
const onlinePagination = reactive({
pageNumber: 1,
pageSize: 10,
total: 0,
});
const nowTick = ref(Date.now());
const durationTimer = ref(null);
const pollTimer = ref(null);
const hlsHttpFallbackUsed = ref(false);
const lastHlsPullUrl = ref("");
// 原 created 中直接挂到 this 上的字段(未在 data 声明),保持非响应式
let hlsInstance = null;
let hlsVideoElement = null;
let _hlsVideoClickHandler = null;
const liveStatus = computed(() => {
return liveDetail.value.liveStatus || liveDetail.value.status || "";
});
const isLiving = computed(() => {
return liveStatus.value === "LIVING";
});
const shouldPlayStream = computed(() => {
return isLiving.value && !!videoUrl.value;
});
const showPlayerCover = computed(() => {
return !shouldPlayStream.value;
});
const cumulativeViewers = computed(() => {
return Number(liveDetail.value.actualViewNumber ?? 0);
});
const currentViewers = computed(() => {
return cumulativeViewers.value;
});
const commentPeopleNumber = computed(() => {
return Number(liveDetail.value.commentPeopleNumber ?? 0);
});
const interactionRate = computed(() => {
if (!cumulativeViewers.value) return "0.00%";
return `${((commentPeopleNumber.value / cumulativeViewers.value) * 100).toFixed(2)}%`;
});
const dealConversionRate = computed(() => {
if (!cumulativeViewers.value) return "0%";
const dealCount = Number(orderStats.value.dealUserCount ?? 0);
return `${((dealCount / cumulativeViewers.value) * 100).toFixed(0)}%`;
});
const liveDuration = computed(() => {
void nowTick.value;
const startMs = parseDateTime(liveDetail.value.startTime);
if (!startMs || Date.now() < startMs) return "00:00:00";
let endMs = Date.now();
if (liveStatus.value === "ENDED") {
const endTimeMs = parseDateTime(liveDetail.value.endTime);
if (endTimeMs && endTimeMs > startMs) {
endMs = endTimeMs;
}
}
return formatDuration(endMs - startMs);
});
watch(videoUrl, () => {
syncH5Player();
});
watch(isLiving, () => {
syncH5Player();
if (isLiving.value) {
startPolling();
} else {
stopPolling();
}
});
watch(aspectRatio, () => {
lastHlsPullUrl.value = "";
nextTick(() => syncH5Player());
});
watch(chatTab, (tab) => {
if (tab === "online") loadOnlineUsers();
});
watch(statsTab, (tab) => {
if (tab === "purchase") purchaseRecords.value?.refresh();
});
onMounted(() => {
liveId.value = String(route.query.id || "");
if (!liveId.value) {
Message.error("缺少直播ID");
return;
}
refreshAll();
startDurationTimer();
});
onBeforeUnmount(() => {
destroyH5HlsPlayer();
stopPolling();
stopDurationTimer();
});
function parseDateTime(value) {
if (!value) return null;
if (typeof value === "string" && value.includes("-")) {
return new Date(value.replace(/-/g, "/")).getTime();
}
if (typeof value === "number") {
return value > 1e12 ? value : value * 1000;
}
return null;
}
function formatDuration(ms) {
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const pad = (n) => String(n).padStart(2, "0");
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}
function startDurationTimer() {
stopDurationTimer();
durationTimer.value = setInterval(() => {
nowTick.value = Date.now();
}, 1000);
}
function stopDurationTimer() {
if (durationTimer.value) {
clearInterval(durationTimer.value);
durationTimer.value = null;
}
}
function formatTime(time) {
if (!time) return "-";
if (typeof time === "string" && time.includes("-")) return time;
const ts = typeof time === "number" ? (time > 1e12 ? time / 1000 : time) : time;
return unixToDate(ts);
}
function formatAmount(val) {
return Number(val || 0).toFixed(2);
}
function liveStatusText(status) {
const map = { NEW: "未开始", LIVING: "直播中", PAUSED: "已暂停", ENDED: "已结束" };
return map[status] || "未知";
}
function liveStatusTagType(status) {
const map = { NEW: "info", LIVING: "success", PAUSED: "warning", ENDED: "warning" };
return map[status] || "info";
}
function startPolling() {
stopPolling();
if (!isLiving.value) return;
pollTimer.value = setInterval(() => {
loadDetail(false);
}, 5000);
}
function stopPolling() {
if (pollTimer.value) {
clearInterval(pollTimer.value);
pollTimer.value = null;
}
}
function refreshAll() {
loadDetail();
loadOrderStats();
purchaseRecords.value?.refresh();
if (chatTab.value === "online") loadOnlineUsers();
}
function buildHlsPlayUrl(url, useHttpFallback = false) {
if (!url) return "";
const httpUrl =
useHttpFallback && url.startsWith("https://")
? url.replace("https://", "http://")
: url;
return httpUrl.replace(".flv", ".m3u8");
}
function syncH5Player(retry = 0) {
const shouldPlay = shouldPlayStream.value;
if (!shouldPlay) {
destroyH5HlsPlayer();
lastHlsPullUrl.value = "";
return;
}
if (
videoUrl.value === lastHlsPullUrl.value &&
hlsInstance &&
hlsVideoElement &&
!hlsVideoElement.paused
) {
return;
}
if (videoUrl.value === lastHlsPullUrl.value && hlsInstance) {
ensureH5VideoPlaying(hlsVideoElement);
return;
}
lastHlsPullUrl.value = videoUrl.value;
hlsHttpFallbackUsed.value = false;
nextTick(() => {
const container = hlsContainer.value;
if (!container) {
if (retry < 8) {
setTimeout(() => syncH5Player(retry + 1), 80);
}
return this.formatDuration(endMs - startMs);
},
},
watch: {
videoUrl() {
this.syncH5Player();
},
isLiving() {
this.syncH5Player();
if (this.isLiving) {
this.startPolling();
} else {
this.stopPolling();
}
},
aspectRatio() {
this.lastHlsPullUrl = "";
this.$nextTick(() => this.syncH5Player());
},
chatTab(tab) {
if (tab === "online") this.loadOnlineUsers();
},
statsTab(tab) {
if (tab === "purchase") this.$refs.purchaseRecords?.refresh();
},
},
mounted() {
this.liveId = String(this.$route.query.id || "");
if (!this.liveId) {
this.$Message.error("缺少直播ID");
return;
}
this.refreshAll();
this.startDurationTimer();
},
beforeUnmount() {
this.destroyH5HlsPlayer();
this.stopPolling();
this.stopDurationTimer();
},
methods: {
parseDateTime(value) {
if (!value) return null;
if (typeof value === "string" && value.includes("-")) {
return new Date(value.replace(/-/g, "/")).getTime();
}
if (typeof value === "number") {
return value > 1e12 ? value : value * 1000;
}
return null;
},
formatDuration(ms) {
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const pad = (n) => String(n).padStart(2, "0");
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
},
startDurationTimer() {
this.stopDurationTimer();
this.durationTimer = setInterval(() => {
this.nowTick = Date.now();
}, 1000);
},
stopDurationTimer() {
if (this.durationTimer) {
clearInterval(this.durationTimer);
this.durationTimer = null;
}
},
formatTime(time) {
if (!time) return "-";
if (typeof time === "string" && time.includes("-")) return time;
const ts = typeof time === "number" ? (time > 1e12 ? time / 1000 : time) : time;
return this.$filters.unixToDate(ts);
},
formatAmount(val) {
return Number(val || 0).toFixed(2);
},
liveStatusText(status) {
const map = { NEW: "未开始", LIVING: "直播中", PAUSED: "已暂停", ENDED: "已结束" };
return map[status] || "未知";
},
liveStatusTagType(status) {
const map = { NEW: "info", LIVING: "success", PAUSED: "warning", ENDED: "warning" };
return map[status] || "info";
},
startPolling() {
this.stopPolling();
if (!this.isLiving) return;
this.pollTimer = setInterval(() => {
this.loadDetail(false);
}, 5000);
},
stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
},
refreshAll() {
this.loadDetail();
this.loadOrderStats();
this.$refs.purchaseRecords?.refresh();
if (this.chatTab === "online") this.loadOnlineUsers();
},
buildHlsPlayUrl(url, useHttpFallback = false) {
if (!url) return "";
const httpUrl =
useHttpFallback && url.startsWith("https://")
? url.replace("https://", "http://")
: url;
return httpUrl.replace(".flv", ".m3u8");
},
syncH5Player(retry = 0) {
const shouldPlay = this.shouldPlayStream;
if (!shouldPlay) {
this.destroyH5HlsPlayer();
this.lastHlsPullUrl = "";
return;
}
if (
this.videoUrl === this.lastHlsPullUrl &&
this.hlsInstance &&
this.hlsVideoElement &&
!this.hlsVideoElement.paused
) {
return;
}
if (this.videoUrl === this.lastHlsPullUrl && this.hlsInstance) {
this.ensureH5VideoPlaying(this.hlsVideoElement);
return;
}
this.lastHlsPullUrl = this.videoUrl;
this.hlsHttpFallbackUsed = false;
this.$nextTick(() => {
const container = this.$refs.hlsContainer;
if (!container) {
if (retry < 8) {
setTimeout(() => this.syncH5Player(retry + 1), 80);
initH5HlsPlayer();
});
}
function ensureH5VideoPlaying(video) {
if (!video) return Promise.resolve();
const tryPlay = (muted) => {
video.muted = muted;
return video.play().catch(() => Promise.reject());
};
return tryPlay(false)
.catch(() => tryPlay(true))
.catch(() => {
console.warn("[HLS] 自动播放被阻止,请点击画面播放");
});
}
async function initH5HlsPlayer(useHttpFallback = false) {
const originalUrl = videoUrl.value;
const container = hlsContainer.value;
if (!originalUrl || !container) return;
const url = buildHlsPlayUrl(originalUrl, useHttpFallback);
destroyH5HlsPlayer();
const video = document.createElement("video");
video.style.cssText = "width:100%;height:100%;object-fit:cover;background:#000;";
video.autoplay = true;
video.muted = true;
video.defaultMuted = true;
video.playsInline = true;
video.setAttribute("autoplay", "");
video.setAttribute("muted", "");
video.setAttribute("playsinline", "");
video.setAttribute("webkit-playsinline", "");
video.controls = false;
const onVideoClick = () => {
if (video.muted) {
video.muted = false;
}
video.play().catch(() => {});
};
video.addEventListener("click", onVideoClick);
container.appendChild(video);
hlsVideoElement = video;
_hlsVideoClickHandler = onVideoClick;
const onReadyPlay = () => {
ensureH5VideoPlaying(video);
};
let Hls = null;
try {
const hlsModule = await import("hls.js");
Hls = hlsModule.default;
} catch (error) {
console.error("[HLS] 加载播放器失败:", error);
Message.error("直播播放器加载失败,请刷新页面重试");
return;
}
if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
liveSyncDurationCount: 3,
liveMaxLatencyDurationCount: 6,
maxBufferLength: 10,
maxMaxBufferLength: 30,
});
hlsInstance = hls;
hls.on(Hls.Events.MEDIA_ATTACHED, onReadyPlay);
hls.on(Hls.Events.MANIFEST_PARSED, onReadyPlay);
hls.attachMedia(video);
hls.loadSource(url);
hls.on(Hls.Events.ERROR, (_event, data) => {
if (!data.fatal) return;
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
if (!hlsHttpFallbackUsed.value && originalUrl.startsWith("https://")) {
console.warn("[HLS] HTTPS 加载失败,尝试 HTTP 降级...");
hlsHttpFallbackUsed.value = true;
destroyH5HlsPlayer();
nextTick(() => initH5HlsPlayer(true));
} else {
console.error("[HLS] 网络错误,尝试重连...");
hls.startLoad();
}
return;
}
this.initH5HlsPlayer();
});
},
ensureH5VideoPlaying(video) {
if (!video) return Promise.resolve();
const tryPlay = (muted) => {
video.muted = muted;
return video.play().catch(() => Promise.reject());
};
return tryPlay(false)
.catch(() => tryPlay(true))
.catch(() => {
console.warn("[HLS] 自动播放被阻止,请点击画面播放");
});
},
async initH5HlsPlayer(useHttpFallback = false) {
const originalUrl = this.videoUrl;
const container = this.$refs.hlsContainer;
if (!originalUrl || !container) return;
const url = this.buildHlsPlayUrl(originalUrl, useHttpFallback);
this.destroyH5HlsPlayer();
const video = document.createElement("video");
video.style.cssText = "width:100%;height:100%;object-fit:cover;background:#000;";
video.autoplay = true;
video.muted = true;
video.defaultMuted = true;
video.playsInline = true;
video.setAttribute("autoplay", "");
video.setAttribute("muted", "");
video.setAttribute("playsinline", "");
video.setAttribute("webkit-playsinline", "");
video.controls = false;
const onVideoClick = () => {
if (video.muted) {
video.muted = false;
}
video.play().catch(() => {});
};
video.addEventListener("click", onVideoClick);
container.appendChild(video);
this.hlsVideoElement = video;
this._hlsVideoClickHandler = onVideoClick;
const onReadyPlay = () => {
this.ensureH5VideoPlaying(video);
};
let Hls = null;
try {
const hlsModule = await import("hls.js");
Hls = hlsModule.default;
} catch (error) {
console.error("[HLS] 加载播放器失败:", error);
this.$Message.error("直播播放器加载失败,请刷新页面重试");
return;
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.error("[HLS] 媒体错误,尝试恢复...");
hls.recoverMediaError();
break;
default:
console.error("[HLS] 致命错误:", data);
destroyH5HlsPlayer();
Message.error("直播加载失败");
break;
}
if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
liveSyncDurationCount: 3,
liveMaxLatencyDurationCount: 6,
maxBufferLength: 10,
maxMaxBufferLength: 30,
});
this.hlsInstance = hls;
hls.on(Hls.Events.MEDIA_ATTACHED, onReadyPlay);
hls.on(Hls.Events.MANIFEST_PARSED, onReadyPlay);
hls.attachMedia(video);
hls.loadSource(url);
hls.on(Hls.Events.ERROR, (_event, data) => {
if (!data.fatal) return;
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
if (!this.hlsHttpFallbackUsed && originalUrl.startsWith("https://")) {
console.warn("[HLS] HTTPS 加载失败,尝试 HTTP 降级...");
this.hlsHttpFallbackUsed = true;
this.destroyH5HlsPlayer();
this.$nextTick(() => this.initH5HlsPlayer(true));
} else {
console.error("[HLS] 网络错误,尝试重连...");
hls.startLoad();
}
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.error("[HLS] 媒体错误,尝试恢复...");
hls.recoverMediaError();
break;
default:
console.error("[HLS] 致命错误:", data);
this.destroyH5HlsPlayer();
this.$Message.error("直播加载失败");
break;
}
});
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = url;
video.addEventListener("loadedmetadata", onReadyPlay);
video.addEventListener("canplay", onReadyPlay);
} else {
this.$Message.error("当前浏览器不支持 HLS 播放");
});
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = url;
video.addEventListener("loadedmetadata", onReadyPlay);
video.addEventListener("canplay", onReadyPlay);
} else {
Message.error("当前浏览器不支持 HLS 播放");
}
}
function destroyH5HlsPlayer() {
if (hlsInstance) {
hlsInstance.destroy();
hlsInstance = null;
}
if (hlsVideoElement) {
if (_hlsVideoClickHandler) {
hlsVideoElement.removeEventListener("click", _hlsVideoClickHandler);
_hlsVideoClickHandler = null;
}
hlsVideoElement.remove();
hlsVideoElement = null;
}
const container = hlsContainer.value;
if (container) {
container.innerHTML = "";
}
}
function loadDetail(showLoading = true) {
if (showLoading) loading.value = true;
getLiveInfo(liveId.value)
.then((res) => {
if (showLoading) loading.value = false;
if (res.success) {
const data = res.result || {};
liveDetail.value = data;
videoUrl.value = data.pullStreamUrl || data.streamUrl || "";
syncH5Player();
}
},
destroyH5HlsPlayer() {
if (this.hlsInstance) {
this.hlsInstance.destroy();
this.hlsInstance = null;
})
.catch(() => {
if (showLoading) loading.value = false;
});
}
function loadOrderStats() {
getLiveOrderStatistics(liveId.value).then((res) => {
if (res.success) {
orderStats.value = res.result || {};
}
});
}
function loadOnlineUsers() {
onlineLoading.value = true;
liveUserList({
liveRoomId: liveId.value,
pageNumber: onlinePagination.pageNumber,
pageSize: onlinePagination.pageSize,
})
.then((res) => {
onlineLoading.value = false;
if (res.success) {
onlineUserList.value = res.result?.records || [];
onlinePagination.total = res.result?.total || 0;
}
if (this.hlsVideoElement) {
if (this._hlsVideoClickHandler) {
this.hlsVideoElement.removeEventListener("click", this._hlsVideoClickHandler);
this._hlsVideoClickHandler = null;
}
this.hlsVideoElement.remove();
this.hlsVideoElement = null;
}
const container = this.$refs.hlsContainer;
if (container) {
container.innerHTML = "";
}
},
loadDetail(showLoading = true) {
if (showLoading) this.loading = true;
getLiveInfo(this.liveId)
.then((res) => {
if (showLoading) this.loading = false;
if (res.success) {
const data = res.result || {};
this.liveDetail = data;
this.videoUrl = data.pullStreamUrl || data.streamUrl || "";
this.syncH5Player();
}
})
.catch(() => {
if (showLoading) this.loading = false;
});
},
loadOrderStats() {
getLiveOrderStatistics(this.liveId).then((res) => {
})
.catch(() => {
onlineLoading.value = false;
});
}
function buildShareUrl() {
const base = (wapUrl.value || BASE?.WAP_URL || config.website || "").replace(/\/?$/, "/");
return `${base}pages/promotion/live/room?id=${liveId.value}`;
}
function handleShare() {
const url = buildShareUrl();
if (!url) {
Message.warning("未配置移动端地址");
return;
}
copyText(url, "分享链接");
}
function copyText(text, label = "内容") {
if (!text) {
Message.warning(`${label}为空,无法复制`);
return;
}
navigator.clipboard.writeText(text);
Message.success(`${label}已复制`);
}
function handleStart() {
Modal.confirm({
title: "确认开播",
content: "确定要开始该直播吗?",
onOk: () => {
return startLive(liveId.value).then((res) => {
if (res.success) {
this.orderStats = res.result || {};
Message.success("开播成功");
refreshAll();
}
});
},
loadOnlineUsers() {
this.onlineLoading = true;
liveUserList({
liveRoomId: this.liveId,
pageNumber: this.onlinePagination.pageNumber,
pageSize: this.onlinePagination.pageSize,
})
.then((res) => {
this.onlineLoading = false;
if (res.success) {
this.onlineUserList = res.result?.records || [];
this.onlinePagination.total = res.result?.total || 0;
}
})
.catch(() => {
this.onlineLoading = false;
});
},
buildShareUrl() {
const base = (this.wapUrl || BASE?.WAP_URL || config.website || "").replace(/\/?$/, "/");
return `${base}pages/promotion/live/room?id=${this.liveId}`;
},
handleShare() {
const url = this.buildShareUrl();
if (!url) {
this.$Message.warning("未配置移动端地址");
return;
}
this.copyText(url, "分享链接");
},
copyText(text, label = "内容") {
if (!text) {
this.$Message.warning(`${label}为空,无法复制`);
return;
}
navigator.clipboard.writeText(text);
this.$Message.success(`${label}已复制`);
},
handleStart() {
this.$Modal.confirm({
title: "确认开播",
content: "确定要开始该直播吗?",
onOk: () => {
return startLive(this.liveId).then((res) => {
if (res.success) {
this.$Message.success("开播成功");
this.refreshAll();
}
});
},
});
}
function handleEnd() {
Modal.confirm({
title: "确认结束",
content: "确定要结束该直播吗?",
onOk: () => {
return endLive(liveId.value).then((res) => {
if (res.success) {
Message.success("直播已结束");
destroyH5HlsPlayer();
stopPolling();
refreshAll();
}
});
},
handleEnd() {
this.$Modal.confirm({
title: "确认结束",
content: "确定要结束该直播吗?",
onOk: () => {
return endLive(this.liveId).then((res) => {
if (res.success) {
this.$Message.success("直播已结束");
this.destroyH5HlsPlayer();
this.stopPolling();
this.refreshAll();
}
});
},
});
},
},
};
});
}
</script>
<style lang="scss" scoped>

View File

@@ -42,66 +42,66 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getLiveInfo } from "@/api/live";
import { Message } from "@/utils/message";
import { unixToDate } from "@/utils/filters";
import LiveGoods from "./components/LiveGoods.vue";
export default {
name: "live-detail",
components: { LiveGoods },
data() {
return {
loading: false,
liveId: "",
detail: {},
};
},
computed: {
liveStatus() {
return this.detail.liveStatus || this.detail.status || "";
},
},
mounted() {
this.liveId = String(this.$route.query.id || "");
if (!this.liveId) {
this.$Message.error("缺少直播ID");
this.handleBack();
return;
}
this.loadDetail();
},
methods: {
formatTime(time) {
if (!time) return "-";
return this.$filters.unixToDate(time);
},
liveStatusText(status) {
const map = { NEW: "未开始", LIVING: "直播中", ENDED: "已结束" };
return map[status] || "未知";
},
liveStatusTagType(status) {
const map = { NEW: "info", LIVING: "success", ENDED: "warning" };
return map[status] || "info";
},
loadDetail() {
this.loading = true;
getLiveInfo(this.liveId)
.then((res) => {
this.loading = false;
if (res.success) {
this.detail = res.result || {};
}
})
.catch(() => {
this.loading = false;
this.handleBack();
});
},
handleBack() {
this.$router.push({ path: "/live-list" });
},
},
};
defineOptions({ name: "live-detail" });
const route = useRoute();
const router = useRouter();
const loading = ref(false);
const liveId = ref("");
const detail = ref({});
const liveStatus = computed(() => {
return detail.value.liveStatus || detail.value.status || "";
});
onMounted(() => {
liveId.value = String(route.query.id || "");
if (!liveId.value) {
Message.error("缺少直播ID");
handleBack();
return;
}
loadDetail();
});
function formatTime(time) {
if (!time) return "-";
return unixToDate(time);
}
function liveStatusText(status) {
const map = { NEW: "未开始", LIVING: "直播中", ENDED: "已结束" };
return map[status] || "未知";
}
function liveStatusTagType(status) {
const map = { NEW: "info", LIVING: "success", ENDED: "warning" };
return map[status] || "info";
}
function loadDetail() {
loading.value = true;
getLiveInfo(liveId.value)
.then((res) => {
loading.value = false;
if (res.success) {
detail.value = res.result || {};
}
})
.catch(() => {
loading.value = false;
handleBack();
});
}
function handleBack() {
router.push({ path: "/live-list" });
}
</script>
<style lang="scss" scoped>

View File

@@ -4,7 +4,7 @@
<template #header>
<span class="page-title">编辑直播</span>
</template>
<el-form ref="form" :model="form" :rules="rules" label-width="100px" class="live-form">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="live-form">
<el-form-item v-if="form.liveStatus !== 'NEW'" label="直播状态">
<el-tag :type="liveStatusTagType(form.liveStatus)">{{ liveStatusText(form.liveStatus) }}</el-tag>
</el-form-item>
@@ -81,7 +81,7 @@
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
<ossManage
ref="ossManage"
ref="ossManageRef"
:is-component="true"
:initialize="picModelFlag"
@callback="callbackSelected"
@@ -90,11 +90,22 @@
</div>
</template>
<script>
<script setup>
import { nextTick, onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Picture } from "@element-plus/icons-vue";
import { getLiveInfo, updateLive } from "@/api/live";
import { Message } from "@/utils/message";
import ossManage from "@/views/sys/oss-manage/ossManage";
defineOptions({ name: "live-edit" });
const route = useRoute();
const router = useRouter();
const formRef = ref(null);
const ossManageRef = ref(null);
function getCharLength(str) {
if (!str) return 0;
let len = 0;
@@ -130,133 +141,126 @@ function formatDateTime(value) {
return String(value);
}
export default {
name: "live-edit",
components: { Picture, ossManage },
data() {
const validateTitle = (rule, value, callback) => {
const len = getCharLength(value);
if (len < 6) {
callback(new Error("直播间名字最短3个汉字"));
return;
}
if (len > 34) {
callback(new Error("直播间名字最长17个汉字"));
return;
}
callback();
};
return {
picModelFlag: false,
loading: false,
submitting: false,
liveId: "",
endTime: "",
form: {
title: "",
displayMode: "0",
startTime: "",
coverImg: "",
liveIntroduce: "",
liveStatus: "NEW",
},
rules: {
title: [
{ required: true, message: "请输入直播标题", trigger: "blur" },
{ validator: validateTitle, trigger: "blur" },
],
displayMode: [{ required: true, message: "请选择显示模式", trigger: "change" }],
startTime: [{ required: true, message: "请选择直播开始时间", trigger: "change" }],
coverImg: [{ required: true, message: "请上传直播封面", trigger: "change" }],
},
};
},
mounted() {
this.liveId = String(this.$route.query.id || "");
if (!this.liveId) {
this.$Message.error("缺少直播ID");
this.handleBack();
return;
}
this.loadDetail();
},
methods: {
liveStatusText(status) {
const map = { NEW: "未开始", LIVING: "直播中", ENDED: "已结束" };
return map[status] || "未知";
},
liveStatusTagType(status) {
const map = { NEW: "info", LIVING: "success", ENDED: "warning" };
return map[status] || "info";
},
openCoverPicker() {
this.picModelFlag = true;
this.$nextTick(() => {
if (this.$refs.ossManage) {
this.$refs.ossManage.selectImage = true;
}
});
},
callbackSelected(val) {
this.picModelFlag = false;
if (val?.url) {
this.form.coverImg = val.url;
this.$refs.form?.validateField("coverImg");
}
},
loadDetail() {
this.loading = true;
getLiveInfo(this.liveId)
.then((res) => {
this.loading = false;
if (res.success) {
const data = res.result || {};
this.form.title = data.title || "";
this.form.displayMode = normalizeDisplayMode(data.displayMode);
this.form.coverImg = data.coverImg || "";
this.form.liveIntroduce = data.liveIntroduce || "";
this.form.liveStatus = data.liveStatus || data.status || "NEW";
this.endTime = formatDateTime(data.endTime);
this.form.startTime = formatDateTime(data.startTime);
}
})
.catch(() => {
this.loading = false;
});
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return;
let endTime = this.endTime;
if (!endTime || endTime <= this.form.startTime) {
endTime = buildEndTime(this.form.startTime);
}
this.submitting = true;
updateLive(this.liveId, {
title: this.form.title,
displayMode: this.form.displayMode,
coverImg: this.form.coverImg,
startTime: this.form.startTime,
endTime,
liveIntroduce: this.form.liveIntroduce,
})
.then((res) => {
this.submitting = false;
if (res.success) {
this.$Message.success("保存成功");
this.$router.push({ path: "/live-list" });
}
})
.catch(() => {
this.submitting = false;
});
});
},
handleBack() {
this.$router.push({ path: "/live-list" });
},
},
const validateTitle = (rule, value, callback) => {
const len = getCharLength(value);
if (len < 6) {
callback(new Error("直播间名字最短3个汉字"));
return;
}
if (len > 34) {
callback(new Error("直播间名字最长17个汉字"));
return;
}
callback();
};
const picModelFlag = ref(false);
const loading = ref(false);
const submitting = ref(false);
const liveId = ref("");
const endTime = ref("");
const form = reactive({
title: "",
displayMode: "0",
startTime: "",
coverImg: "",
liveIntroduce: "",
liveStatus: "NEW",
});
const rules = reactive({
title: [
{ required: true, message: "请输入直播标题", trigger: "blur" },
{ validator: validateTitle, trigger: "blur" },
],
displayMode: [{ required: true, message: "请选择显示模式", trigger: "change" }],
startTime: [{ required: true, message: "请选择直播开始时间", trigger: "change" }],
coverImg: [{ required: true, message: "请上传直播封面", trigger: "change" }],
});
onMounted(() => {
liveId.value = String(route.query.id || "");
if (!liveId.value) {
Message.error("缺少直播ID");
handleBack();
return;
}
loadDetail();
});
function liveStatusText(status) {
const map = { NEW: "未开始", LIVING: "直播中", ENDED: "已结束" };
return map[status] || "未知";
}
function liveStatusTagType(status) {
const map = { NEW: "info", LIVING: "success", ENDED: "warning" };
return map[status] || "info";
}
function openCoverPicker() {
picModelFlag.value = true;
nextTick(() => {
if (ossManageRef.value) {
ossManageRef.value.selectImage = true;
}
});
}
function callbackSelected(val) {
picModelFlag.value = false;
if (val?.url) {
form.coverImg = val.url;
formRef.value?.validateField("coverImg");
}
}
function loadDetail() {
loading.value = true;
getLiveInfo(liveId.value)
.then((res) => {
loading.value = false;
if (res.success) {
const data = res.result || {};
form.title = data.title || "";
form.displayMode = normalizeDisplayMode(data.displayMode);
form.coverImg = data.coverImg || "";
form.liveIntroduce = data.liveIntroduce || "";
form.liveStatus = data.liveStatus || data.status || "NEW";
endTime.value = formatDateTime(data.endTime);
form.startTime = formatDateTime(data.startTime);
}
})
.catch(() => {
loading.value = false;
});
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (!valid) return;
let endTimeVal = endTime.value;
if (!endTimeVal || endTimeVal <= form.startTime) {
endTimeVal = buildEndTime(form.startTime);
}
submitting.value = true;
updateLive(liveId.value, {
title: form.title,
displayMode: form.displayMode,
coverImg: form.coverImg,
startTime: form.startTime,
endTime: endTimeVal,
liveIntroduce: form.liveIntroduce,
})
.then((res) => {
submitting.value = false;
if (res.success) {
Message.success("保存成功");
router.push({ path: "/live-list" });
}
})
.catch(() => {
submitting.value = false;
});
});
}
function handleBack() {
router.push({ path: "/live-list" });
}
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<div class="live-list search">
<el-card>
<el-form
ref="searchForm"
ref="searchFormRef"
:model="searchForm"
inline
label-width="90px"
@@ -135,202 +135,206 @@
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { queryLivePage, startLive, endLive } from "@/api/live";
import { getSetting } from "@/api/index";
import { Message, Modal } from "@/utils/message";
import { unixToDate } from "@/utils/filters";
import config from "@/config";
export default {
name: "live-list",
data() {
return {
loading: false,
data: [],
total: 0,
activeStatus: "",
searchForm: {
pageNumber: 1,
pageSize: 20,
title: "",
storeName: "",
},
shareVisible: false,
shareTitle: "",
shareH5Url: "",
wapUrl: "",
};
},
mounted() {
this.loadWapSetting();
this.getData();
},
methods: {
formatTime(time) {
if (!time) return "-";
return this.$filters.unixToDate(time);
},
liveStatusText(status) {
const map = { NEW: "未开始", LIVING: "直播中", ENDED: "已结束" };
return map[status] || "未知";
},
liveStatusTagType(status) {
const map = { NEW: "info", LIVING: "success", ENDED: "warning" };
return map[status] || "info";
},
buildParams() {
const params = {
pageNumber: this.searchForm.pageNumber,
pageSize: this.searchForm.pageSize,
};
if (this.searchForm.title) params.title = this.searchForm.title;
if (this.searchForm.storeName) params.storeName = this.searchForm.storeName;
if (this.activeStatus) params.liveStatus = this.activeStatus;
return params;
},
getData() {
this.loading = true;
queryLivePage(this.buildParams())
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result?.records || [];
this.total = res.result?.total || 0;
}
})
.catch(() => {
this.loading = false;
});
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getData();
},
handleReset() {
this.searchForm.title = "";
this.searchForm.storeName = "";
this.searchForm.pageNumber = 1;
this.getData();
},
handleTabChange() {
this.searchForm.pageNumber = 1;
this.getData();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getData();
},
handleAdd() {
this.$router.push({ path: "/live-add" });
},
handleView(row) {
this.$router.push({ path: "/live-manage-detail", query: { id: row.id } });
},
handleEdit(row) {
this.$router.push({ path: "/live-edit", query: { id: row.id } });
},
handleControl(row) {
const route = this.$router.resolve({
path: "/live-control-panel",
query: { id: row.id },
});
window.open(route.href, "_blank");
},
buildH5ShareUrl(liveId) {
const base = (this.wapUrl || BASE?.WAP_URL || config.website || "").replace(/\/?$/, "/");
return `${base}pages/promotion/live/room?id=${liveId}`;
},
handleShare(row) {
this.shareTitle = row.title || "";
this.shareH5Url = this.buildH5ShareUrl(row.id);
this.shareVisible = true;
},
copyShareUrl() {
if (!this.shareH5Url) return;
this.copyText(this.shareH5Url);
},
copyText(text) {
const onSuccess = () => this.$Message.success("链接已复制");
const onFail = () => this.$Message.error("复制失败,请手动复制");
defineOptions({ name: "live-list" });
if (navigator.clipboard?.writeText) {
navigator.clipboard
.writeText(text)
.then(onSuccess)
.catch(() => {
this.copyTextFallback(text) ? onSuccess() : onFail();
});
return;
}
const router = useRouter();
this.copyTextFallback(text) ? onSuccess() : onFail();
},
copyTextFallback(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-9999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch (e) {
ok = false;
const searchFormRef = ref(null);
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const activeStatus = ref("");
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
title: "",
storeName: "",
});
const shareVisible = ref(false);
const shareTitle = ref("");
const shareH5Url = ref("");
const wapUrl = ref("");
onMounted(() => {
loadWapSetting();
getData();
});
function formatTime(time) {
if (!time) return "-";
return unixToDate(time);
}
function liveStatusText(status) {
const map = { NEW: "未开始", LIVING: "直播中", ENDED: "已结束" };
return map[status] || "未知";
}
function liveStatusTagType(status) {
const map = { NEW: "info", LIVING: "success", ENDED: "warning" };
return map[status] || "info";
}
function buildParams() {
const params = {
pageNumber: searchForm.pageNumber,
pageSize: searchForm.pageSize,
};
if (searchForm.title) params.title = searchForm.title;
if (searchForm.storeName) params.storeName = searchForm.storeName;
if (activeStatus.value) params.liveStatus = activeStatus.value;
return params;
}
function getData() {
loading.value = true;
queryLivePage(buildParams())
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result?.records || [];
total.value = res.result?.total || 0;
}
document.body.removeChild(textArea);
return ok;
},
handleStart(row) {
if (row.liveStatus === "LIVING") {
this.$Message.warning("直播间已在直播中");
return;
}
if (row.liveStatus === "ENDED") {
this.$Message.warning("直播已结束,无法开播");
return;
}
this.$Modal.confirm({
title: "确认开播",
content: "确定要开始该直播吗?",
onOk: () => {
return startLive(row.id).then((res) => {
if (res.success) {
this.$Message.success("开播成功");
this.getData();
}
});
},
})
.catch(() => {
loading.value = false;
});
}
function handleSearch() {
searchForm.pageNumber = 1;
getData();
}
function handleReset() {
searchForm.title = "";
searchForm.storeName = "";
searchForm.pageNumber = 1;
getData();
}
function handleTabChange() {
searchForm.pageNumber = 1;
getData();
}
function changePageSize() {
searchForm.pageNumber = 1;
getData();
}
function handleAdd() {
router.push({ path: "/live-add" });
}
function handleView(row) {
router.push({ path: "/live-manage-detail", query: { id: row.id } });
}
function handleEdit(row) {
router.push({ path: "/live-edit", query: { id: row.id } });
}
function handleControl(row) {
const route = router.resolve({
path: "/live-control-panel",
query: { id: row.id },
});
window.open(route.href, "_blank");
}
function buildH5ShareUrl(liveId) {
// 历史遗留:BASE 未定义,wapUrl 为空时会抛 ReferenceError,保持原样
const base = (wapUrl.value || BASE?.WAP_URL || config.website || "").replace(/\/?$/, "/");
return `${base}pages/promotion/live/room?id=${liveId}`;
}
function handleShare(row) {
shareTitle.value = row.title || "";
shareH5Url.value = buildH5ShareUrl(row.id);
shareVisible.value = true;
}
function copyShareUrl() {
if (!shareH5Url.value) return;
copyText(shareH5Url.value);
}
function copyText(text) {
const onSuccess = () => Message.success("链接已复制");
const onFail = () => Message.error("复制失败,请手动复制");
if (navigator.clipboard?.writeText) {
navigator.clipboard
.writeText(text)
.then(onSuccess)
.catch(() => {
copyTextFallback(text) ? onSuccess() : onFail();
});
return;
}
copyTextFallback(text) ? onSuccess() : onFail();
}
function copyTextFallback(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-9999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch (e) {
ok = false;
}
document.body.removeChild(textArea);
return ok;
}
function handleStart(row) {
if (row.liveStatus === "LIVING") {
Message.warning("直播间已在直播中");
return;
}
if (row.liveStatus === "ENDED") {
Message.warning("直播已结束,无法开播");
return;
}
Modal.confirm({
title: "确认开播",
content: "确定要开始该直播吗?",
onOk: () => {
return startLive(row.id).then((res) => {
if (res.success) {
Message.success("开播成功");
getData();
}
});
},
handleEnd(row) {
if (row.liveStatus !== "LIVING") {
this.$Message.warning("仅直播中的直播间可以结束");
return;
}
this.$Modal.confirm({
title: "确认结束",
content: "确定要结束该直播吗?",
onOk: () => {
return endLive(row.id).then((res) => {
if (res.success) {
this.$Message.success("直播已结束");
this.getData();
}
});
},
});
}
function handleEnd(row) {
if (row.liveStatus !== "LIVING") {
Message.warning("仅直播中的直播间可以结束");
return;
}
Modal.confirm({
title: "确认结束",
content: "确定要结束该直播吗?",
onOk: () => {
return endLive(row.id).then((res) => {
if (res.success) {
Message.success("直播已结束");
getData();
}
});
},
loadWapSetting() {
getSetting("BASE_SETTING")
.then((res) => {
if (res.success && res.result?.buyerDomain) {
this.wapUrl = res.result.buyerDomain;
}
});
},
},
};
});
}
function loadWapSetting() {
getSetting("BASE_SETTING")
.then((res) => {
if (res.success && res.result?.buyerDomain) {
wapUrl.value = res.result.buyerDomain;
}
});
}
</script>
<style lang="scss" scoped>

View File

@@ -1,5 +1,5 @@
<template>
<div class="login" @click="$refs.verify.show = false">
<div class="login" @click="hideVerify">
<div class="login-brand">
<div class="brand-inner">
<p class="brand-name">LILISHOP</p>
@@ -24,7 +24,7 @@
</div>
<el-form
ref="usernameLoginForm"
ref="usernameLoginFormRef"
:model="form"
:rules="rules"
class="form"
@@ -70,7 +70,7 @@
</el-button>
<verify
ref="verify"
ref="verifyRef"
class="verify-con"
verifyType="LOGIN"
@change="verifyChange"
@@ -81,7 +81,9 @@
</div>
</template>
<script>
<script setup>
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { User, Lock } from "@element-plus/icons-vue";
import { login, userInfo } from "@/api/index";
import Cookies from "js-cookie";
@@ -89,82 +91,79 @@ import Header from "@/views/main-parts/header";
import Footer from "@/views/main-parts/footer";
import util from "@/libs/util.js";
import verify from "@/components/verify";
import { setStore } from "@/libs/storage";
import { md5 } from "@/utils/md5.js";
import { useAppStore } from "@/stores/app";
export default {
components: {
Header,
Footer,
verify,
User,
Lock,
},
data() {
return {
year: new Date().getFullYear(),
loading: false,
form: {
username: "",
password: "",
mobile: "",
code: "",
},
rules: {
username: [
{ required: true, message: "账号不能为空", trigger: "blur" },
],
password: [
{ required: true, message: "密码不能为空", trigger: "blur" },
],
},
};
},
methods: {
afterLogin(res) {
const accessToken = res.result.accessToken;
const refreshToken = res.result.refreshToken;
this.setStore("accessToken", accessToken);
this.setStore("refreshToken", refreshToken);
userInfo().then((res) => {
if (res.success) {
Cookies.set("userInfoManager", JSON.stringify(res.result));
this.$store.commit("setAvatarPath", res.result.avatar);
util.initRouter(this);
this.$store.commit("setOpenedList");
this.$store.commit("initCachePage");
this.$router.push({ name: "home_index" });
} else {
this.loading = false;
}
});
},
submitLogin() {
this.$refs.usernameLoginForm.validate((valid) => {
if (valid) {
this.$refs.verify.init();
}
});
},
verifyChange(con) {
if (!con.status) return;
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) => {
if (res && res.success) {
this.afterLogin(res);
} else {
this.loading = false;
}
})
.catch(() => {
this.loading = false;
});
this.$refs.verify.show = false;
},
},
const router = useRouter();
const appStore = useAppStore();
const year = ref(new Date().getFullYear());
const loading = ref(false);
const form = reactive({
username: "",
password: "",
mobile: "",
code: "",
});
const rules = {
username: [{ required: true, message: "账号不能为空", trigger: "blur" }],
password: [{ required: true, message: "密码不能为空", trigger: "blur" }],
};
const usernameLoginFormRef = ref(null);
const verifyRef = ref(null);
function hideVerify() {
verifyRef.value.show = false;
}
function afterLogin(res) {
const accessToken = res.result.accessToken;
const refreshToken = res.result.refreshToken;
setStore("accessToken", accessToken);
setStore("refreshToken", refreshToken);
userInfo().then((res) => {
if (res.success) {
Cookies.set("userInfoManager", JSON.stringify(res.result));
appStore.setAvatarPath(res.result.avatar);
util.initRouter();
appStore.setOpenedList();
appStore.initCachePage();
router.push({ name: "home_index" });
} else {
loading.value = false;
}
});
}
function submitLogin() {
usernameLoginFormRef.value.validate((valid) => {
if (valid) {
verifyRef.value.init();
}
});
}
function verifyChange(con) {
if (!con.status) return;
loading.value = true;
const fd = new FormData();
fd.append("username", form.username);
fd.append("password", md5(form.password));
login(fd)
.then((res) => {
if (res && res.success) {
afterLogin(res);
} else {
loading.value = false;
}
})
.catch(() => {
loading.value = false;
});
verifyRef.value.show = false;
}
</script>
<style lang="scss" scoped>

View File

@@ -59,7 +59,7 @@
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="form" :model="form" :rules="formValidate" label-width="120px">
<el-form ref="formRef" :model="form" :rules="formValidate" label-width="120px">
<el-form-item label="物流公司名称" prop="name">
<el-input v-model="form.name" clearable style="width: 100%" />
</el-form-item>
@@ -97,13 +97,17 @@
</div>
</template>
<script>
<script setup>
import { nextTick, onMounted, reactive, ref } from "vue";
import {
getLogisticsPage,
updateLogistics,
addLogistics,
delLogistics,
} from "@/api/logistics";
import { Message, Modal } from "@/utils/message";
defineOptions({ name: "logistics" });
const defaultForm = () => ({
name: "",
@@ -114,156 +118,152 @@ const defaultForm = () => ({
const normalizeStandBy = (value) => (value === true || value === "true" ? "true" : "false");
export default {
name: "logistics",
data() {
return {
loading: true,
modalVisible: false,
modalTitle: "",
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
name: "",
},
form: defaultForm(),
formValidate: {
name: [
{
required: true,
message: "请输入物流公司名称",
trigger: "blur",
},
],
code: [
{
required: true,
message: "请输入物流公司编码",
trigger: "blur",
},
],
},
submitLoading: false,
data: [],
total: 0,
id: "",
};
},
methods: {
init() {
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getLogisticsPage(this.searchForm).then((res) => {
this.loading = false;
if (res.success) {
const data = res.result.records;
data.forEach((e) => {
e.switch = e.disabled === "OPEN";
e.standBy = normalizeStandBy(e.standBy);
});
this.data = data;
this.total = res.result.total;
}
});
},
changeSwitch(row) {
updateLogistics(row.id, {
name: row.name,
code: row.code,
standBy: normalizeStandBy(row.standBy),
disabled: row.disabled === "CLOSE" ? "OPEN" : "CLOSE",
}).then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
} else {
row.switch = !row.switch;
}
});
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return;
this.submitLoading = true;
const table = ref(null);
const formRef = ref(null);
if (this.modalTitle === "添加") {
const payload = {
name: this.form.name,
code: this.form.code,
disabled: this.form.disabled,
standBy: normalizeStandBy(this.form.standBy),
formItems: this.form.formItems,
};
addLogistics(payload).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
this.modalVisible = false;
}
});
} else {
updateLogistics(this.id, {
...this.form,
standBy: normalizeStandBy(this.form.standBy),
}).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
this.modalVisible = false;
}
});
const loading = ref(true);
const modalVisible = ref(false);
const modalTitle = ref("");
const searchForm = reactive({
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
name: "",
});
const form = ref(defaultForm());
const formValidate = reactive({
name: [
{
required: true,
message: "请输入物流公司名称",
trigger: "blur",
},
],
code: [
{
required: true,
message: "请输入物流公司编码",
trigger: "blur",
},
],
});
const submitLoading = ref(false);
const data = ref([]);
const total = ref(0);
const id = ref("");
function init() {
getDataList();
}
function changePage() {
getDataList();
}
function changePageSize() {
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
getLogisticsPage(searchForm).then((res) => {
loading.value = false;
if (res.success) {
const records = res.result.records;
records.forEach((e) => {
e.switch = e.disabled === "OPEN";
e.standBy = normalizeStandBy(e.standBy);
});
data.value = records;
total.value = res.result.total;
}
});
}
function changeSwitch(row) {
updateLogistics(row.id, {
name: row.name,
code: row.code,
standBy: normalizeStandBy(row.standBy),
disabled: row.disabled === "CLOSE" ? "OPEN" : "CLOSE",
}).then((res) => {
if (res.success) {
Message.success("操作成功");
getDataList();
} else {
row.switch = !row.switch;
}
});
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (!valid) return;
submitLoading.value = true;
if (modalTitle.value === "添加") {
const payload = {
name: form.value.name,
code: form.value.code,
disabled: form.value.disabled,
standBy: normalizeStandBy(form.value.standBy),
formItems: form.value.formItems,
};
addLogistics(payload).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("操作成功");
getDataList();
modalVisible.value = false;
}
});
} else {
updateLogistics(id.value, {
...form.value,
standBy: normalizeStandBy(form.value.standBy),
}).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("操作成功");
getDataList();
modalVisible.value = false;
}
});
}
});
}
function add() {
modalTitle.value = "添加";
form.value = defaultForm();
nextTick(() => {
formRef.value?.resetFields();
});
modalVisible.value = true;
}
function detail(v) {
id.value = v.id;
modalTitle.value = "修改";
form.value = {
name: v.name,
code: v.code,
standBy: normalizeStandBy(v.standBy),
disabled: v.disabled,
};
modalVisible.value = true;
}
function remove(v) {
Modal.confirm({
title: "确认删除",
content: "您确认要删除 " + v.name + " ?",
onOk: () => {
return delLogistics(v.id).then((res) => {
if (res.success) {
Message.success("操作成功");
getDataList();
}
});
},
add() {
this.modalTitle = "添加";
this.form = defaultForm();
this.$nextTick(() => {
this.$refs.form?.resetFields();
});
this.modalVisible = true;
},
detail(v) {
this.id = v.id;
this.modalTitle = "修改";
this.form = {
name: v.name,
code: v.code,
standBy: normalizeStandBy(v.standBy),
disabled: v.disabled,
};
this.modalVisible = true;
},
remove(v) {
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除 " + v.name + " ?",
onOk: () => {
return delLogistics(v.id).then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
});
},
},
mounted() {
this.init();
},
};
});
}
onMounted(() => {
init();
});
</script>

View File

@@ -12,17 +12,11 @@
</div>
</template>
<script>
<script setup>
import { ref } from "vue";
import config from "@/config/index";
export default {
data() {
return {
config,
year: new Date().getFullYear(),
};
},
};
const year = ref(new Date().getFullYear());
</script>
<style lang="scss" scoped>

View File

@@ -6,50 +6,46 @@
</div>
</template>
<script>
<script setup>
import { ref, onMounted } from "vue";
import { getBaseSite } from "@/api/common.js";
export default {
data() {
return {
domainLogo: "",
};
},
methods: {
applyFavicon(href) {
if (!href) return;
let 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 && res.result.settingValue) {
const { domainLogo, domainIcon, siteName } = JSON.parse(
res.result.settingValue
);
this.domainLogo = domainLogo || "";
const expirationTime = new Date().setHours(new Date().getHours() + 1);
localStorage.setItem("icontitle_expiration_time", expirationTime);
localStorage.setItem("icon", domainLogo || "");
localStorage.setItem("domainIcon", domainIcon || "");
localStorage.setItem("title", siteName || "");
this.applyFavicon(domainIcon || domainLogo);
if (siteName) {
window.document.title = siteName + " - 运营后台";
}
}
});
},
},
mounted() {
this.getSite();
},
};
const domainLogo = ref("");
function applyFavicon(href) {
if (!href) return;
let 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 && res.result.settingValue) {
const { domainLogo: logo, domainIcon, siteName } = JSON.parse(
res.result.settingValue
);
domainLogo.value = logo || "";
const expirationTime = new Date().setHours(new Date().getHours() + 1);
localStorage.setItem("icontitle_expiration_time", expirationTime);
localStorage.setItem("icon", logo || "");
localStorage.setItem("domainIcon", domainIcon || "");
localStorage.setItem("title", siteName || "");
applyFavicon(domainIcon || logo);
if (siteName) {
window.document.title = siteName + " - 运营后台";
}
}
});
}
onMounted(() => {
getSite();
});
</script>
<style lang="scss" scoped>

View File

@@ -12,19 +12,19 @@
</div>
</template>
<script>
<script setup>
import { Platform } from "@element-plus/icons-vue";
import i18n from "@/locale";
import { useAppStore } from "@/stores/app";
export default {
name: "langSwitch",
components: { Platform },
methods: {
langChange(v) {
this.$i18n.locale = v;
this.$store.commit("switchLang", v);
},
},
};
defineOptions({ name: "langSwitch" });
const appStore = useAppStore();
function langChange(v) {
i18n.global.locale = v;
appStore.switchLang(v);
}
</script>
<style lang="scss" scoped>

View File

@@ -40,42 +40,42 @@
</div>
</template>
<script>
<script setup>
import { computed } from "vue";
import { useRouter } from "vue-router";
import { ArrowDown } from "@element-plus/icons-vue";
export default {
name: "messageTip",
components: { ArrowDown },
props: {
res: {
type: Object,
default: () => ({}),
},
defineOptions({ name: "messageTip" });
const props = defineProps({
res: {
type: Object,
default: () => ({}),
},
computed: {
value() {
let count = 0;
const r = this.res || {};
Object.keys(r).forEach((k) => {
if (r[k]) count += r[k];
});
return count;
},
},
methods: {
navigateTo(name) {
if (name === "manager-appointment-work-orders") {
this.$router.push({ name, query: { serviceStatus: "OPEN" } });
return;
}
if (name === "manager-procurement-order-list") {
this.$router.push({ name, query: { status: "SUBMITTED" } });
return;
}
this.$router.push({ name });
},
},
};
});
const router = useRouter();
const value = computed(() => {
let count = 0;
const r = props.res || {};
Object.keys(r).forEach((k) => {
if (r[k]) count += r[k];
});
return count;
});
function navigateTo(name) {
if (name === "manager-appointment-work-orders") {
router.push({ name, query: { serviceStatus: "OPEN" } });
return;
}
if (name === "manager-procurement-order-list") {
router.push({ name, query: { status: "SUBMITTED" } });
return;
}
router.push({ name });
}
</script>
<style scoped>
@@ -109,4 +109,3 @@ export default {
transform: translateY(-50%) translateX(50%);
}
</style>

View File

@@ -18,8 +18,17 @@
</el-breadcrumb>
</template>
<script>
<script setup>
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { otherRouter } from "@/router/router";
import { useAppStore } from "@/stores/app";
defineOptions({ name: "pageBreadcrumb" });
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const stripLayout = (name) => (name || "").replace(/__layout$/, "");
@@ -61,105 +70,101 @@ const findFirstLeaf = (group) => {
return stripLayout(first?.name) || null;
};
export default {
name: "pageBreadcrumb",
computed: {
items() {
const route = this.$route;
if (!route || !route.name || route.name === "home_index") {
return [{ title: "首页" }];
}
return this.buildChain(stripLayout(route.name));
},
},
methods: {
go(target) {
if (!target || !target.name) return;
if (this.$route.name === target.name) return;
this.$router.push(target);
},
findInMenu(name) {
const menuList = this.$store.state.app.menuList || [];
for (const grp of menuList) {
for (const child of grp.children || []) {
if (stripLayout(child.name) === name) {
return { group: grp, child };
}
}
}
return null;
},
routerEntry(name) {
if (Array.isArray(otherRouter.children)) {
return otherRouter.children.find((c) => c.name === name);
}
return null;
},
topItem() {
const navList = this.$store.state.app.navList || [];
const menuList = this.$store.state.app.menuList || [];
const currNavTitle = this.$store.state.app.currNavTitle;
const firstName =
this.$route.meta && this.$route.meta.firstRouterName;
const items = computed(() => {
if (!route || !route.name || route.name === "home_index") {
return [{ title: "首页" }];
}
return buildChain(stripLayout(route.name));
});
let topTitle = currNavTitle;
if (firstName) {
const top = navList.find((n) => n.name === firstName);
if (top) topTitle = top.title;
function go(target) {
if (!target || !target.name) return;
if (route.name === target.name) return;
router.push(target);
}
function findInMenu(name) {
const menuList = appStore.menuList || [];
for (const grp of menuList) {
for (const child of grp.children || []) {
if (stripLayout(child.name) === name) {
return { group: grp, child };
}
if (!topTitle) return null;
}
}
return null;
}
let target = null;
if (menuList && menuList.length) {
const leafName = findFirstLeaf(menuList[0]);
if (leafName) target = { name: leafName };
}
return { title: topTitle, target };
},
buildChain(name, depth = 0) {
if (depth > 6) return [];
function routerEntry(name) {
if (Array.isArray(otherRouter.children)) {
return otherRouter.children.find((c) => c.name === name);
}
return null;
}
const inMenu = this.findInMenu(name);
if (inMenu) {
const list = [];
const top = this.topItem();
if (top) list.push(top);
const groupLeaf = findFirstLeaf(inMenu.group);
list.push({
title: inMenu.group.title,
target: groupLeaf ? { name: groupLeaf } : null,
});
list.push({ title: inMenu.child.title });
return list;
}
function topItem() {
const navList = appStore.navList || [];
const menuList = appStore.menuList || [];
const currNavTitle = appStore.currNavTitle;
const firstName = route.meta && route.meta.firstRouterName;
const parentName = PARENT_MAP[name];
const entry = this.routerEntry(name);
const selfTitle = entry?.title
? entry.title
: (this.$route.meta && this.$route.meta.title
? String(this.$route.meta.title).replace(/\s*-\s*.+$/, "")
: "");
let topTitle = currNavTitle;
if (firstName) {
const top = navList.find((n) => n.name === firstName);
if (top) topTitle = top.title;
}
if (!topTitle) return null;
if (parentName) {
const parentChain = this.buildChain(parentName, depth + 1).map(
(it, idx, arr) =>
idx === arr.length - 1
? { title: it.title, target: { name: parentName } }
: it
);
if (selfTitle) parentChain.push({ title: selfTitle });
return parentChain;
}
let target = null;
if (menuList && menuList.length) {
const leafName = findFirstLeaf(menuList[0]);
if (leafName) target = { name: leafName };
}
return { title: topTitle, target };
}
const list = [];
const top = this.topItem();
if (top) list.push(top);
if (selfTitle) list.push({ title: selfTitle });
return list;
},
},
};
function buildChain(name, depth = 0) {
if (depth > 6) return [];
const inMenu = findInMenu(name);
if (inMenu) {
const list = [];
const top = topItem();
if (top) list.push(top);
const groupLeaf = findFirstLeaf(inMenu.group);
list.push({
title: inMenu.group.title,
target: groupLeaf ? { name: groupLeaf } : null,
});
list.push({ title: inMenu.child.title });
return list;
}
const parentName = PARENT_MAP[name];
const entry = routerEntry(name);
const selfTitle = entry?.title
? entry.title
: (route.meta && route.meta.title
? String(route.meta.title).replace(/\s*-\s*.+$/, "")
: "");
if (parentName) {
const parentChain = buildChain(parentName, depth + 1).map(
(it, idx, arr) =>
idx === arr.length - 1
? { title: it.title, target: { name: parentName } }
: it
);
if (selfTitle) parentChain.push({ title: selfTitle });
return parentChain;
}
const list = [];
const top = topItem();
if (top) list.push(top);
if (selfTitle) list.push({ title: selfTitle });
return list;
}
</script>
<style lang="scss" scoped>

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