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

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

View File

@@ -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>