Merge branch 'dev-sws' into dev-yxl

v1.0.0
于肖磊 2024-08-14 18:38:54 +08:00
commit b8d8859c84
14 changed files with 388 additions and 148 deletions

View File

@ -6,4 +6,4 @@ NODE_ENV='dev'
VITE_APP_TITLE = 'shopxo'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api'
VITE_APP_BASE_API_URL = 'http://shopxo.com/'
VITE_APP_BASE_API_URL = 'http://shopxo.com/admin.php/'

57
src/api/upload.ts Normal file
View File

@ -0,0 +1,57 @@
import request from '@/utils/request';
class UploadAPI {
/** 分类查询接口*/
static getTree() {
return request({
url: `diyapi/attachmentinit`,
method: 'post',
});
}
/** 分类新增,修改接口 */
static saveTree(data: any) {
return request({
url: `diyapi/attachmentcategorysave`,
method: 'post',
data,
});
}
/** 分类删除接口 */
static delTree(data: any) {
return request({
url: `diyapi/attachmentcategorydelete`,
method: 'post',
data,
});
}
/** 附件移动分类 */
static moveTree(data: any) {
return request({
url: `diyapi/attachmentmovecategory`,
method: 'post',
data,
});
}
}
export default UploadAPI;
// 分类树结构
export interface Tree {
/** 主键 */
id: string;
/** 父级id */
pid: string;
/** 名称 */
name: string;
/** 路径 */
path: string;
/** 是否开启 */
is_enable: boolean;
/** 排序 */
sort: number;
/** 下级 */
items?: Tree[];
/** 图标 */
icon?: string;
}

View File

@ -133,6 +133,9 @@
.name {
width: 9.8rem;
}
.add_hot {
width: 8.8rem;
}
}
}
}

View File

@ -39,13 +39,14 @@
</div>
<div class="right-content flex-1 pa-20">
<div class="size-16 fw mb-10">图片热区</div>
<div class="size-12 cr-9 mb-20">框选热区范围双击设置热区信息</div>
<div class="flex-col gap-20 item">
<div v-for="(item, index) in hot_list.data" :key="index" class="flex-row align-c gap-10">
<el-input v-model="item.name" class="name" placeholder="名称"></el-input>
<url-value v-model="item.link"></url-value>
<icon name="del" size="20" @click="del_event(index)"></icon>
</div>
<el-button type="primary" class="add_hot" @click="add_event"></el-button>
<div class="size-12 cr-9">框选热区范围双击设置热区信息</div>
</div>
</div>
</div>
@ -142,7 +143,7 @@ const end_drag = (event: MouseEvent) => {
if (rect_end.value.width > 16 && rect_end.value.height > 16) {
hot_list.value.data.push({
name: '热区' + (hot_list.value.data.length + 1),
link: { name: '', link: '' },
link: {},
drag_start: cloneDeep(rect_start.value),
drag_end: cloneDeep(rect_end.value),
});
@ -332,6 +333,24 @@ const rect_style = computed(() => {
const del_event = (index: number) => {
hot_list.value.data.splice(index, 1);
};
const add_event = () => {
hot_list.value.data.push({
name: '热区' + (hot_list.value.data.length + 1),
link: {},
drag_start: {
x: 0,
y: 0,
width: 0,
height: 0,
},
drag_end: {
x: 100,
y: 100,
width: 100,
height: 100,
},
});
};
//#endregion -----------------------------------------------end
//#region -----------------------------------------------start

View File

@ -32,11 +32,14 @@
<script setup lang="ts">
import { FormInstance, FormRules } from 'element-plus';
import { cloneDeep } from 'lodash';
import UploadAPI, { Tree } from '@/api/upload';
/**
* @description: 分类操作
* @param modelValue{uploadList[]} 默认值
* @param visibleDialog{Boolean} 弹窗开启关闭
* @param type{String} 新增add编辑edit
* @param categoryId{String} 分类id
* @param categoryPid{String} 分类父id
* @return {*} update:modelValue confirm
*/
const props = defineProps({
@ -52,21 +55,36 @@ const props = defineProps({
type: [String, Number],
default: '',
},
categoryPid: {
type: [String, Number],
default: '',
},
});
const dialog_visible_category_oprate = defineModel({ type: Boolean, default: false });
const form = ref<Tree>({
id: '',
pid: '',
name: '',
path: '',
sort: 0,
is_enable: true,
children: [],
items: [],
});
watch(
() => dialog_visible_category_oprate.value,
(newValue) => {
if (newValue && props.type !== 'add') {
form.value = cloneDeep(props.value);
} else {
form.value = {
id: '',
pid: '',
name: '',
path: '',
sort: 0,
is_enable: true,
items: [],
};
}
}
);
@ -97,7 +115,20 @@ const confirm_event = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
emit('confirm', form.value, props.categoryId);
const new_data = {
...form.value,
pid: props.categoryPid,
};
UploadAPI.saveTree(new_data).then((res) => {
if (props.type == 'add') {
ElMessage.success('添加成功');
} else if (props.type == 'edit') {
ElMessage.success('编辑成功');
} else {
ElMessage.success('操作成功');
}
emit('confirm');
});
cancel_event(formEl);
} else {
console.log('error submit!', fields);

View File

@ -1,9 +0,0 @@
// 分类树结构
type Tree = {
id: number | string;
name: string;
path: string;
is_enable: boolean;
sort: number;
children: Tree[];
};

View File

@ -24,14 +24,14 @@
<el-scrollbar height="490px">
<el-tree ref="treeRef" class="filter-tree" :data="type_data" node-key="id" highlight-current :expand-on-click-node="false" :props="defaultProps" empty-text="" default-expand-all :filter-node-method="filter_node" @node-click="tree_node_event">
<template #default="{ node, data }">
<span class="custom-tree-node flex-row jc-sb align-c w pr-10">
<span>{{ data.name }}</span>
<span class="flex-row gap-10">
<icon name="add" size="12" color="primary" @click="append_type_event(data)"></icon>
<div class="custom-tree-node flex-row jc-sb gap-10 align-c w pr-10">
<div class="flex-1 flex-width text-line-1 block">{{ data.name }}</div>
<div class="flex-row gap-10">
<icon v-if="data.pid == 0" name="add" size="12" color="primary" @click="append_type_event(data)"></icon>
<icon name="edit" size="12" color="primary" @click="edit_type_event(data)"></icon>
<icon name="del" size="12" color="primary" @click="remove_type_event(node, data)"></icon>
</span>
</span>
</div>
</div>
</template>
</el-tree>
</el-scrollbar>
@ -41,7 +41,10 @@
<div class="right-oprate flex-row">
<el-button type="primary" plain @click="upload_model_open">{{ upload_type_name }}</el-button>
<el-button @click="mult_del_event">{{ upload_type_name }}</el-button>
<el-cascader class="right-classify ml-12" :options="category_list" :placeholder="upload_type_name + '移动至'" :show-all-levels="false"></el-cascader>
<!-- <el-cascader :show-all-levels="false" clearable></el-cascader> -->
<div class="right-classify ml-12">
<transform-category :data="type_data" :check-img-ids="check_img_ids" :placeholder="upload_type_name + '移动至'"></transform-category>
</div>
</div>
<div class="right-search">
<el-input v-model="search_name" :placeholder="'请输入' + upload_type_name + '名称'" @input="get_list">
@ -170,10 +173,13 @@
<!-- 图片预览 -->
<el-image-viewer v-if="preview_switch_img && upload_type == 'img'" :z-index="999999" :url-list="[preview_url]" :hide-on-click-modal="true" @close="preview_close"></el-image-viewer>
<upload-model v-model="upload_model_visible" :type="upload_type" :exts="props.type == 'img' ? ext_img_name_list : props.type == 'video' ? ext_video_name_list : ext_file_name_list" @close="close_upload_model"></upload-model>
<form-upload-category v-model="upload_category_model_visible" :value="upload_category_model" :type="upload_category_type" :category-id="upload_category_id" @confirm="upload_category_confirm"></form-upload-category>
<form-upload-category v-model="upload_category_model_visible" :value="upload_category_model" :type="upload_category_type" :category-id="upload_category_id" :category-pid="upload_category_pid" @confirm="upload_category_confirm"></form-upload-category>
</template>
<script lang="ts" setup>
import { get_math } from '@/utils/index';
import UploadAPI, { Tree } from '@/api/upload';
import { uploadrStore } from '@/store';
const upload_store = uploadrStore();
const app = getCurrentInstance();
/**
* @description: 图片上传
@ -230,6 +236,14 @@ const view_list_value = ref<uploadList[]>([]);
//
// const dialog_visible = ref(props.visibleDialog);
const dialog_visible = defineModel('visibleDialog', { type: Boolean, default: false });
watch(
() => dialog_visible.value,
(val) => {
if (val) {
type_data.value = upload_store.category;
}
}
);
//
const ext_img_name_list = ref(['.png', '.jpg', '.jpeg', '.bmp', '.webp', '.gif']);
@ -297,17 +311,6 @@ const upload_type_name = computed(() => {
const upload_type_change = (type: any) => {
view_list_value.value = [];
};
const treeRef = ref();
const defaultProps = {
children: 'children',
label: 'name',
};
//
const search_filter = ref('');
watch(search_filter, (val) => {
treeRef.value!.filter(val);
});
//
const search_name = ref('');
//
@ -316,55 +319,6 @@ const search_name = ref('');
const page = ref(1);
//
const data_total = ref(0);
const filter_node = (value: string, data: any): boolean => {
if (!value) return true;
return data.name.indexOf(value) !== -1;
};
const type_data = ref<Tree[]>([
{
id: 0,
name: '全部图片',
path: '全部',
is_enable: true,
sort: 1,
children: [],
},
{
id: 1,
name: '全部视频',
path: '全部',
is_enable: true,
sort: 2,
children: [],
},
{
id: 2,
name: '全部文件',
path: '全部',
is_enable: true,
sort: 3,
children: [],
},
]);
// //
const category_list = [
{
value: 'component',
label: 'Component',
children: [
{
value: 'basic',
label: 'Basic',
children: [
{
value: 'layout',
label: 'Layout',
},
],
},
],
},
];
//
const upload_list = ref<uploadList[]>([
@ -375,6 +329,7 @@ const upload_list = ref<uploadList[]>([
{ id: 5, url: '/src/assets/movie.mp4', original: '头像5', title: '头像5', ext: '.mp4', type: 'video' },
{ id: 6, url: '/src/assets/movie.mp4', original: '头像6', title: '头像6', ext: '.docx', type: '.docx' },
]);
const check_img_ids = ref('');
//
const check_img_event = (item: any) => {
const item_id = item.id;
@ -392,6 +347,7 @@ const check_img_event = (item: any) => {
}
}
}
check_img_ids.value = view_list_value.value.map((item: any) => item.id).join(',');
};
//
const preview_switch_img = ref(false);
@ -476,14 +432,47 @@ const search_data = ref({
const get_list = () => {
console.log('查询接口', search_data);
};
//#region ----------------------------------------------------------start
const treeRef = ref();
const defaultProps = {
children: 'items',
label: 'name',
};
//
const search_filter = ref('');
watch(search_filter, (val) => {
treeRef.value!.filter(val);
});
const filter_node = (value: string, data: any): boolean => {
if (!value) return true;
return data.name.indexOf(value) !== -1;
};
const type_data = ref<Tree[]>([]);
onMounted(() => {
if (!upload_store.is_category) {
upload_store.set_is_category(true);
get_tree();
} else {
type_data.value = upload_store.category;
}
});
//
const get_tree = () => {
UploadAPI.getTree().then((res) => {
type_data.value = res.data.category_list;
upload_store.set_category(type_data.value);
});
};
//
const upload_category_model = ref<Tree>({
id: '',
pid: '',
name: '',
path: '',
sort: 0,
is_enable: false,
children: [],
is_enable: true,
items: [],
});
//
const upload_category_type = ref('add');
@ -494,36 +483,11 @@ const add_type = () => {
upload_category_type.value = 'add';
upload_category_model_visible.value = true;
upload_category_id.value = '';
upload_category_pid.value = '';
};
//
const upload_category_confirm = (data: any) => {
if (upload_category_type.value == 'add') {
//
//
if (upload_category_id.value == '') {
//
console.log('添加子级分类');
// type_data.value = type_data.value.map((item: any) => {
// if (item.id == upload_category_id.value) {
// item.children.push(data);
// }
// return item;
// });
} else {
//
console.log('添加一级分类');
// type_data.value = [...type_data.value, data];
}
} else if (upload_category_type.value == 'edit') {
//
console.log('编辑分类');
// type_data.value = type_data.value.map((item: any) => {
// if (item.id == data.id) {
// item = data;
// }
// return item;
// });
}
const upload_category_confirm = () => {
get_tree();
};
//
const tree_node_event = (data: any) => {
@ -531,27 +495,39 @@ const tree_node_event = (data: any) => {
get_list();
};
const upload_category_id = ref<number | string>('');
const upload_category_pid = ref<number | string>('');
//
const append_type_event = (data: Tree) => {
upload_category_type.value = 'add';
upload_category_id.value = data.id;
upload_category_id.value = '';
upload_category_pid.value = data.id;
upload_category_model_visible.value = true;
};
//
const edit_type_event = (data: Tree) => {
upload_category_type.value = 'edit';
upload_category_id.value = data.id;
upload_category_pid.value = data.pid;
upload_category_model_visible.value = true;
upload_category_model.value = data;
};
// Nodenode使any
const remove_type_event = (node: any, data: Tree) => {
const parent = node.parent;
const children: Tree[] = parent.data.children || parent.data;
const index = children.findIndex((d) => d.id === data.id);
children.splice(index, 1);
// type_data.value = [...type_data.value];
app?.appContext.config.globalProperties.$common.message_box('删除后不可恢复,确定继续吗?', 'warning').then(() => {
UploadAPI.delTree({ id: data.id }).then((res) => {
const parent = node.parent;
const children: Tree[] = parent.data.items || parent.data;
const index = children.findIndex((d) => d.id === data.id);
children.splice(index, 1);
ElMessage({
type: 'success',
message: '删除成功!',
});
});
});
};
//#endregion ----------------------------------------------------------end
//
const confirm_event = () => {
dialog_visible.value = false;

View File

@ -0,0 +1,133 @@
<template>
<el-popover v-model:visible="visible_dialog" placement="bottom" width="400" trigger="click">
<template #reference>
<div class="flex-row align-c gap-10 br-d radius-sm plr-11 value-input">
<div class="flex-1 flex-width size-12 text-line-1">
<text v-if="label">{{ label }}</text>
<text v-else class="cr-9">{{ placeholder }}</text>
</div>
<div class="value-input-icon">
<template v-if="!label">
<icon name="arrow-right" size="12" color="9"></icon>
</template>
<template v-else>
<div @click.stop="clear_model_value">
<icon name="close-o" size="12" color="c"></icon>
</div>
</template>
</div>
</div>
</template>
<div class="flex-col gap-10">
<div>
<el-cascader-panel v-model="cascader_val" :options="cascader_data" @change="cascader_change"></el-cascader-panel>
</div>
<div class="flex-row jc-e">
<el-button type="primary" @click="visible_dialog = false">取消</el-button>
<el-button type="primary" @click="confirm"></el-button>
</div>
</div>
</el-popover>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash';
import UploadAPI, { Tree } from '@/api/upload';
const app = getCurrentInstance();
const props = defineProps({
data: {
type: Array as PropType<Tree[]>,
default: () => [],
},
placeholder: {
type: String,
default: '请选择',
},
checkImgIds: {
type: String,
default: () => '',
},
});
// 使
const cascader_data = computed(() => {
return props.data.map((tree) => ({
value: tree.id,
label: tree.name,
children: tree.items?.map((item) => ({
value: item.id,
label: item.name,
})),
}));
});
const visible_dialog = ref(false);
watch(
() => visible_dialog.value,
(val) => {
if (val && label.value == '') {
temp_label.value = '';
cascader_val.value = '';
}
}
);
const emit = defineEmits(['update:modelValue']);
const clear_model_value = () => {
label.value = '';
temp_label.value = '';
cascader_val.value = '';
console.log('清空', label.value);
};
const temp_label = ref('');
const cascader_val = ref('');
const label = ref('');
const category_id = ref('');
// change
const cascader_change = (val: any) => {
// valdatalabel
category_id.value = val[val.length - 1];
temp_label.value = cascader_data.value.find((item: any) => item.value == category_id.value)?.label || '';
};
//
const confirm = () => {
console.log(props.checkImgIds);
if (props.checkImgIds && category_id.value) {
app?.appContext.config.globalProperties.$common.message_box('确定转移吗?', 'warning').then(() => {
//
const new_data = {
ids: props.checkImgIds,
category_id: category_id.value,
};
UploadAPI.moveTree(new_data).then((res) => {
visible_dialog.value = false;
label.value = cloneDeep(temp_label.value);
ElMessage.success('转移成功!');
});
});
} else {
if (!props.checkImgIds) {
ElMessage.warning('请先选择图片!');
}
if (!category_id.value) {
ElMessage.warning('请先选择分组!');
}
}
};
</script>
<style lang="scss" scoped>
.value-input {
width: 100%;
height: 3.2rem;
line-height: 3.2rem;
cursor: pointer;
position: relative;
.value-input-icon {
position: absolute;
right: 0;
width: 3.4rem;
z-index: 1;
text-align: center;
}
}
:deep(.el-cascader-menu) {
flex: 1;
}
</style>

View File

@ -15,7 +15,7 @@
</template>
<script setup lang="ts">
import { common_styles_computer } from '@/utils';
import { footerNavCounterStore } from '@/store/modules/footer-nav-content';
import { footerNavCounterStore } from '@/store';
const footer_nav_counter_store = footerNavCounterStore();
const props = defineProps({
showFooter: {

View File

@ -5,6 +5,8 @@ const store = createPinia();
// 全局注册 store
export function setupStore(app: App<Element>) {
app.use(store)
app.use(store);
}
export { store };
export * from './modules/footer-nav-content';
export * from './modules/upload';
export { store };

View File

@ -1,7 +1,7 @@
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export const footerNavCounterStore = defineStore('counter', () => {
export const footerNavCounterStore = defineStore('footerNavCounter', () => {
const padding_footer = ref(70);
function padding_footer_computer(num: number) {
padding_footer.value = num;

View File

@ -0,0 +1,25 @@
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export const uploadrStore = defineStore('upload', () => {
// 上传分类列表
const category = ref<any[]>([]);
// 上传分类是否有数据的判断
const is_category = ref(false);
// 存储上传分类列表
function set_category(data: any[]) {
category.value = data;
is_category.value = true;
}
// 如果为false 则转为true
function set_is_category(bool: boolean) {
is_category.value = bool;
}
return {
category,
is_category,
set_category,
set_is_category,
};
});

View File

@ -106,7 +106,7 @@ declare global {
*/
type linkData = {
id?: number;
name: string;
name?: string;
link?: String;
data?: Data[];
icon?: string;

View File

@ -1,57 +1,60 @@
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
headers: { 'Content-Type': 'application/json;charset=utf-8' }
})
headers: { 'Content-Type': 'application/json;charset=utf-8' },
});
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// const userStore = useUserStoreHook();
const userStore = { token: 1 }
if (userStore.token) {
config.headers.Authorization = userStore.token
// const accessToken = localStorage.getItem(TOKEN_KEY);
const accessToken = { token: '100a1e5ba70b3920a919f85ac10b7bb6' };
if (accessToken.token) {
// config.headers.Authorization = accessToken.token;
// config.data = { ...config.data, token: accessToken.token };
config.url = config.url + '?token=' + accessToken.token;
}
return config
return config;
},
(error: any) => {
return Promise.reject(error)
return Promise.reject(error);
}
)
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const { code, msg } = response.data
const { code, msg } = response.data;
// 登录成功
if (code === '00000') {
return response.data
if (code == '0') {
return response.data;
}
ElMessage.error(msg || '系统出错')
return Promise.reject(new Error(msg || 'Error'))
ElMessage.error(msg || '系统出错');
return Promise.reject(new Error(msg || 'Error'));
},
(error: any) => {
if (error.response.data) {
const { code, msg } = error.response.data
const { code, msg } = error.response.data;
// token 过期,跳转登录页
if (code === 'A0230') {
if (code === '-400') {
ElMessageBox.confirm('当前页面已失效,请重新登录', '提示', {
confirmButtonText: '确定',
type: 'warning'
type: 'warning',
}).then(() => {
localStorage.clear() // @vueuse/core 自动导入
window.location.href = '/'
})
localStorage.clear(); // @vueuse/core 自动导入
window.location.href = '/';
});
} else {
ElMessage.error(msg || '系统出错')
ElMessage.error(msg || '系统出错');
}
}
return Promise.reject(error.message)
return Promise.reject(error.message);
}
)
);
// 导出 axios 实例
export default service
export default service;