feat: cache

This commit is contained in:
zc 2025-08-26 15:37:15 +08:00
parent af14f26096
commit d32d264066
12 changed files with 693 additions and 38 deletions

278
src/views/login/index.vue Normal file
View File

@ -0,0 +1,278 @@
<script setup>
import { ElMessage } from 'element-plus'
import { Encrypt } from '@/utils/crypto'
import { getCurrentTime } from '@/utils/config'
import useStore from '@/stores'
import { apiGetImgCode, apiGetSmsCode } from '@/api/login'
import DialogPassword from '@/components/DialogPassword/index.vue'
const { user } = useStore()
const router = useRouter()
// el-formref
const loginFormRef = ref(null)
const loginData = reactive({
userPhoneEn: '',
password: '',
imageCode: '',
uuid: '',
smsCode: ''
})
const loginRules = reactive({
userPhoneEn: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
],
imageCode: [
{ required: true, message: '请输入图像验证码', trigger: 'blur' },
{ min: 4, message: '图像验证码不能少于4位', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, message: '密码不能少于8位', trigger: 'blur' },
{ max: 16, message: '密码不能超过16位', trigger: 'blur' }
],
smsCode: [
{ required: true, message: '请输入短信验证码', trigger: 'blur' },
{ min: 6, message: '短信验证码不能少于6位', trigger: 'blur' }
]
})
const imageCodeUrl = ref('') //
const hasSendMsg = ref(false) //
const time = ref(0) //
const dialogPasswordVisible = ref(false)
//
const handleGetImgCode = () => {
apiGetImgCode().then((res) => {
if (res.code === 200) {
imageCodeUrl.value = `data:image/gif;base64,${res.data.img}`
loginData.uuid = res.data.uuid
}
})
}
handleGetImgCode()
//
const timer = ref()
const handleGetSmsCode = () => {
if (hasSendMsg.value) return
const emptyData = {
userPhoneEn: loginData.userPhoneEn,
password: loginData.password,
imageCode: loginData.imageCode,
uuid: loginData.uuid
}
if (hasEmptyValue(emptyData)) {
return ElMessage.error('请输入对应信息')
}
apiGetSmsCode({
userPhoneEn: loginData.userPhoneEn,
password: Encrypt(loginData.password),
imageCode: loginData.imageCode,
uuid: loginData.uuid
}).then((res) => {
if (res.code === 200) {
hasSendMsg.value = true
time.value = 60
timeCb()
} else {
ElMessage.error(res.msg)
setTimeout(() => {
handleGetImgCode()
loginData.imageCode = ''
}, 1000)
}
})
//
const timeCb = () => {
clearTimeout(timer)
time.value--
if (time.value < 1) {
return (hasSendMsg.value = false)
}
timer.value = setTimeout(timeCb, 1000)
}
}
//
const handleLogin = () => {
loginFormRef.value.validate((valid) => {
if (valid) {
user.onLogin(loginData).then((res) => {
if (res.code === 200) {
router.push({ path: '/home' })
} else if (res.code === 6005) {
dialogPasswordVisible.value = true
}
})
} else {
return false
}
})
}
//
const hasEmptyValue = (obj) => {
for (let key in obj) {
if (obj[key] == null || obj[key] === '') {
return true
}
}
return false
}
//
const currentDate = ref()
setInterval(() => {
currentDate.value = getCurrentTime()
})
</script>
<template>
<div id="login">
<el-card class="login_elCard">
<h2>东成工作台</h2>
<h3>{{ currentDate }}</h3>
<el-form
:model="loginData"
:rules="loginRules"
label-width="auto"
ref="loginFormRef"
@keyup.enter="handleLogin"
class="login_form"
>
<el-form-item label="手机号码" prop="userPhoneEn">
<el-input
v-model="loginData.userPhoneEn"
placeholder="请输入手机号码"
type="text"
maxlength="11"
class="login_form__input"
/>
</el-form-item>
<el-form-item label="登录密码" prop="password">
<el-input
v-model="loginData.password"
placeholder="请输入登录密码"
type="password"
maxlength="16"
minlength="8"
show-password
class="login_form__input"
/>
</el-form-item>
<el-form-item label="图像验证码" prop="imageCode">
<el-input
v-model="loginData.imageCode"
placeholder="请输入图像验证码"
type="text"
maxlength="4"
class="login_form__input login_form__img-code"
>
<template #append>
<img class="login_form__input-img" @click="handleGetImgCode" :src="imageCodeUrl" />
</template>
</el-input>
</el-form-item>
<el-form-item label="短信验证码" prop="smsCode">
<el-input
v-model="loginData.smsCode"
placeholder="请输入短信验证码"
type="text"
maxlength="6"
class="login_form__input"
>
<template #append>
<span class="cursor-pointer" @click="handleGetSmsCode">
{{ hasSendMsg ? time + 's后重发' : '发送验证码' }}
</span>
</template>
</el-input>
</el-form-item>
</el-form>
<div class="login_submit_btn">
<el-button @click="handleLogin" type="primary">登录</el-button>
</div>
</el-card>
<dialog-password
ref="dialogPasswordRef"
v-model:dialogPasswordVisible="dialogPasswordVisible"
:loginData="loginData"
:type="'init'"
/>
</div>
</template>
<style lang="scss" scoped>
#login {
width: 100%;
height: 100%;
background-image: url('@/assets/images/login_bg1.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
.login_tips {
position: absolute;
top: 20%;
left: 20%;
h3,
p {
color: #5d5d5e;
}
}
.login_elCard {
// width: 420px;
// height: 430px;
// background-color: #ecf5ff;
border-radius: 30px;
text-align: center;
padding: 10px 20px;
position: absolute;
top: 20%;
right: 10%;
}
.login_form {
&__input {
::v-deep(.el-input__inner) {
height: 40px;
line-height: 40px;
font-size: 13px;
}
}
&__img-code {
.el-input__wrapper {
padding-right: 1px;
}
::v-deep(.el-input-group__append) {
padding: 0 !important;
}
}
&__input-img {
height: 40px;
cursor: pointer;
}
.el-input-group__append {
cursor: pointer;
}
::v-deep(.el-form-item__label) {
height: 40px !important;
line-height: 40px !important;
}
}
.login_submit_btn {
margin: 30px 0;
.el-button {
width: 100%;
height: 40px;
line-height: 40px;
}
}
}
</style>

View File

@ -0,0 +1,21 @@
const configData = ref()
export const initConfig = () => {
configData.value = pageConfig({
search: {
comTitle: { label: '标题', clearable: true },
comCategoryId: { label: '类目', option: [], clearable: true }
},
table: {
id: { label: '产品ID' },
comTitle: { label: '标题' },
sales: { label: '销量' },
price: { label: '价格' },
btn: {
types: ['primary', 'info', 'warning', 'success', 'danger'],
names: ['编辑', '复制', '加入首页', '上下架', '删除'],
width: 230
}
}
})
return configData
}

View File

@ -0,0 +1,53 @@
<template>
<div>
<search-module :search="search" :table="table">
<template #btn>
<el-button @click="onAddOrEdit('add', null)" type="success">新增</el-button>
</template>
</search-module>
<div id="table-drag-wrap">
<table-module :table="table"> </table-module>
</div>
</div>
</template>
<script lang="ts" setup>
import { useDraggable } from 'vue-draggable-plus'
import { initConfig } from './config'
/** 新增&编辑 */
const onAddOrEdit = (type: string, row: any) => {}
/** 复制 */
const onCopy = (row: any) => {}
//
const onAddHome = (row: any) => {}
//
const onDelete = (row: any) => {
handleMessageBox({
msg: `是否确认删除吗?`,
success: api.commodity.delCommodity.post!,
data: { id: row.id }
}).then(() => {
table.value.$onGetData(table.value)
})
}
/** 初始化页面 */
const ConfigData = initConfig()
const { search, table } = handleInit(ConfigData, api.commodity.getCommodityList.post, [
onAddOrEdit.bind(null, 'edit'),
onCopy,
onAddHome,
onDelete
])
table.value.$onGetData(table.value, 1, search)
setTimeout(() => {
useDraggable('#table-drag-wrap .el-table__body tbody', table.value.$data)
}, 2000)
/** 页面缓存 */
defineOptions({ name: 'supplier-config' })
</script>

View File

@ -1,6 +1,8 @@
type TCommodityType = 'online' | 'quit' | 'edit' | 'delete'
export const useCommodityType = (search: any, table: any) => {
let beforeValue = 'online'
const commodityType = ref('online')
const commodityType = ref<TCommodityType>('online')
const commodityTypeOptions = [
{ label: '在线商品', value: 'online' },
{ label: '下架商品', value: 'quit' },

View File

@ -0,0 +1,128 @@
<template>
<div id="category-tree" class="p-2 bg-white">
<p>类目展示</p>
<el-tree
style="max-width: 600px"
:data="dataSource"
node-key="id"
draggable
default-expand-all
:expand-on-click-node="false"
@node-drag-start="handleDragStart"
@node-drag-enter="handleDragEnter"
@node-drag-leave="handleDragLeave"
@node-drag-over="handleDragOver"
@node-drag-end="handleDragEnd"
@node-drop="handleDrop"
>
<template #default="{ node, data }">
<div class="category-tree-node">
<span v-if="curId !== data.id">{{ data.label }}</span>
<el-input
ref="inputRef"
v-else
v-model="data.label2"
size="small"
class="w-40"
@keyup.enter="handleInputConfirm(data)"
@blur="handleInputCancel(node, data)"
/>
<div>
<el-button type="primary" link @click="edit(data)" size="small">编辑 </el-button>
<el-button type="primary" link @click="append(data)" size="small"> 添加 </el-button>
<el-button type="danger" size="small" link @click="remove(node, data)">
删除
</el-button>
</div>
</div>
</template>
</el-tree>
</div>
</template>
<script lang="ts" setup>
import { ElButton } from 'element-plus'
import type { RenderContentContext } from 'element-plus'
import { dataSource } from './mock'
import {
handleDragStart,
handleDragEnter,
handleDragOver,
handleDragLeave,
handleDragEnd,
handleDrop
} from './use-drag'
interface Tree {
id: number
label: string
children?: Tree[]
}
type Node = RenderContentContext['node']
type Data = RenderContentContext['data']
const curId = ref(NaN)
const inputRef = ref()
const edit = (data: Data) => {
data.label2 = data.label
curId.value = data.id
nextTick(() => {
inputRef.value.focus()
})
}
//
const handleInputConfirm = (data: any) => {
if (curId.value === Infinity) {
//
} else {
//
}
data.label = data.label2
data.label2 = ''
curId.value = NaN
}
//
const handleInputCancel = (node: Node, data: Data) => {
if (data.id === Infinity) {
remove(node, data)
}
curId.value = NaN
}
const append = (data: Data) => {
curId.value = Infinity
const newChild = { id: curId.value, label2: 'test', children: [] }
if (!data.children) {
data.children = []
}
data.children.push(newChild)
dataSource.value = [...dataSource.value]
nextTick(() => {
inputRef.value.focus()
})
}
const remove = (node: Node, data: Data) => {
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)
dataSource.value = [...dataSource.value]
}
</script>
<style lang="scss" scoped>
#category-tree {
.category-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
}
</style>

View File

@ -0,0 +1,56 @@
interface Tree {
id: number
label: string
children?: Tree[]
}
export const dataSource = ref<Tree[]>([
{
id: 1,
label: 'Level one 1',
children: [
{
id: 4,
label: 'Level two 1-1',
children: [
{
id: 9,
label: 'Level three 1-1-1'
},
{
id: 10,
label: 'Level three 1-1-2'
}
]
}
]
},
{
id: 2,
label: 'Level one 2',
children: [
{
id: 5,
label: 'Level two 2-1'
},
{
id: 6,
label: 'Level two 2-2'
}
]
},
{
id: 3,
label: 'Level one 3',
children: [
{
id: 7,
label: 'Level two 3-1'
},
{
id: 8,
label: 'Level two 3-2'
}
]
}
])

View File

@ -0,0 +1,39 @@
import type { DragEvents } from 'element-plus/es/components/tree/src/model/useDragNode'
import type { NodeDropType, RenderContentContext } from 'element-plus'
type Node = RenderContentContext['node']
// 开始拖拽
export const handleDragStart = (node: Node, ev: DragEvents) => {
console.log('drag start', node)
}
// 拖拽进入其他节点时触发的事件
export const handleDragEnter = (draggingNode: Node, dropNode: Node, ev: DragEvents) => {
console.log('tree drag enter:', dropNode.label)
}
// 拖拽离开某个节点时触发的事件
export const handleDragLeave = (draggingNode: Node, dropNode: Node, ev: DragEvents) => {
console.log('tree drag leave:', dropNode.label)
}
// 在拖拽节点时触发的事件(类似浏览器的 mouseover 事件)
export const handleDragOver = (draggingNode: Node, dropNode: Node, ev: DragEvents) => {
console.log('tree drag over:', dropNode.label)
}
// 拖拽结束时(可能未成功)触发的事件
export const handleDragEnd = (
draggingNode: Node,
dropNode: Node,
dropType: NodeDropType,
ev: DragEvents
) => {
console.log('tree drag end:', dropNode && dropNode.label, dropType)
}
// 拖拽成功完成时触发的事件
export const handleDrop = (
draggingNode: Node,
dropNode: Node,
dropType: NodeDropType,
ev: DragEvents
) => {
console.log('tree drop:', dropNode.label, dropType)
}

View File

@ -24,5 +24,5 @@
</template>
<script setup lang="ts">
const props = defineProps<{ list: [] }>()
const props = defineProps<{ list: any[] }>()
</script>

View File

@ -0,0 +1,44 @@
<template>
<div style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:defaultConfig="{}"
mode="default"
/>
<Editor
style="height: 500px; overflow-y: hidden"
v-model="model"
:defaultConfig="{ placeholder: '请输入内容...' }"
mode="default"
@onCreated="
(editor: any) => {
editorRef = editor
}
"
/>
</div>
</template>
<script setup lang="ts">
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
const model = defineModel()
// ajax
/* onMounted(() => {
setTimeout(() => {
// model.value = '<p> Ajax </p>'
}, 1500)
}) */
// shallowRef
const editorRef = shallowRef()
//
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
</script>

View File

@ -19,33 +19,35 @@
</el-card>
<el-card class="mt-2 sku-info">
<h4 class="font-bold mb-2">SKU信息:</h4>
<dl v-for="(item, index) in categoryData" :key="item.typeId" class="flex items-center">
<dt class="text-sm text-[#666] mb-2">{{ item.typeName }}</dt>
<dd class="flex mb-2">
<div v-for="child in item.options" :key="item.typeId + '-' + child.id">
<el-tag closable effect="plain" :type="colors[index % 4]" class="mr-1">{{
child.name
}}</el-tag>
</div>
<el-input
v-if="inputVisibles[index]"
ref="InputRefs"
v-model="inputValue"
class="w-20"
size="small"
@keyup.enter="handleInputConfirm(index)"
@blur="handleInputConfirm(index)"
/>
<el-button
v-else
class="button-new-tag ml-1"
size="small"
@click="onAddCategoryDataOption(index)"
>
+ 新增
</el-button>
</dd>
</dl>
<div ref="dragRef">
<dl v-for="(item, index) in categoryData" :key="item.typeId" class="flex items-center">
<dt class="text-sm text-[#666] mb-2">{{ item.typeName }}</dt>
<dd class="flex mb-2">
<div v-for="child in item.options" :key="item.typeId + '-' + child.id">
<el-tag closable effect="plain" :type="colors[index % 4]" class="mr-1">{{
child.name
}}</el-tag>
</div>
<el-input
v-if="inputVisibles[index]"
ref="InputRefs"
v-model="inputValue"
class="w-20"
size="small"
@keyup.enter="handleInputConfirm(index)"
@blur="handleInputConfirm(index)"
/>
<el-button
v-else
class="button-new-tag ml-1"
size="small"
@click="onAddCategoryDataOption(index)"
>
+ 新增
</el-button>
</dd>
</dl>
</div>
<h4 class="my-1">价格配置</h4>
<div
@ -57,16 +59,19 @@
</div>
</el-card>
<el-card>
<h4 class="font-bold mb-2">SKU信息:</h4>
<h4 class="font-bold mb-2">内容编辑:</h4>
<wan-editor v-model="htmlContent"></wan-editor>
</el-card>
<el-button type="primary" @click="onSubmit()">提交</el-button>
</div>
</template>
<script setup lang="ts">
import { initConfig } from './config'
import { categoryDataMock, categoryOptionsMock } from './mock'
import { generateTargetData, useCategoryAddOption, colors } from './use-method'
import { generateTargetData, useCategoryAddOption, colors, useDrag } from './use-method'
import categoryConfig from './category-config.vue'
import wanEditor from './editor.vue'
const { $dialog, dialog1 } = toRefs(initConfig().value!)
$dialog.value.config = dialog1.value
@ -76,8 +81,16 @@ const categoryData = ref(categoryDataMock)
const { inputValue, InputRefs, inputVisibles, onAddCategoryDataOption, handleInputConfirm } =
useCategoryAddOption(categoryData)
const { dragRef, createDrag } = useDrag(categoryData)
createDrag()
const list = computed(() => generateTargetData(categoryData.value))
console.warn('----- my data is list.value: ', list.value)
const htmlContent = ref('自定义')
const onSubmit = () => {
console.warn('----- my data is htmlContent: ', htmlContent.value)
}
</script>
<style scoped lang="scss">

View File

@ -1,3 +1,4 @@
import { useDraggable } from 'vue-draggable-plus'
// 定义源数据的结构类型
interface Option {
id: number
@ -50,11 +51,10 @@ export const generateTargetData = (categoryData: CategoryDataMock): TargetData[]
return result
}
//
// 添加类目
export const useCategoryAddOption = (categoryData: any) => {
const inputVisibles = ref(Array.from({length: categoryData.value.length }, (_) => false))
const inputVisibles = ref(Array.from({ length: categoryData.value.length }, (_) => false))
const InputRefs = ref()
const inputValue = ref('')
const onAddCategoryDataOption = (index: number) => {
@ -65,12 +65,33 @@ export const useCategoryAddOption = (categoryData: any) => {
}
const handleInputConfirm = (index: number) => {
if (inputValue.value) {
categoryData.value[index].options.push({ id: inputValue.value, name: inputValue.value, isAdd: true })
categoryData.value[index].options.push({
id: inputValue.value,
name: inputValue.value,
isAdd: true
})
}
inputVisibles.value[index] = false
inputValue.value = ''
}
return { inputVisibles, InputRefs, inputValue, onAddCategoryDataOption, handleInputConfirm}
return { inputVisibles, InputRefs, inputValue, onAddCategoryDataOption, handleInputConfirm }
}
export const colors = ['primary', 'success', 'warning', 'danger', 'info']
export const useDrag = (list: any) => {
const dragRef = ref()
const createDrag = () => {
const { start } = useDraggable(dragRef, list, {
animation: 150,
ghostClass: 'ghost',
onStart() {
console.log('start')
},
onUpdate() {
console.log('update')
}
})
}
return { dragRef, createDrag }
}

View File

@ -1170,7 +1170,7 @@
"@wangeditor/editor-for-vue@^5.1.12":
version "5.1.12"
resolved "https://registry.yarnpkg.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz#f7d5f239b39cdfc01d31151488de8443fe6edc64"
resolved "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz#f7d5f239b39cdfc01d31151488de8443fe6edc64"
integrity sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==
"@wangeditor/editor@^5.1.23":