1.shopxo-diy装修项目,代码引入

sws 2024-08-12
This commit is contained in:
sws
2024-08-12 10:18:11 +08:00
commit 39e3cadb16
234 changed files with 27732 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.history
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
package-lock.json
yarn.lock
pnpm-lock.yaml
stats.html

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# vue3-vite
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en" style="overflow: hidden;">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>装修</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

57
package.json Normal file
View File

@ -0,0 +1,57 @@
{
"name": "vue3-vite",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"build-pro": "vite build --mode production",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"@types/dompurify": "^3.0.5",
"@vueuse/core": "^10.2.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.4.0",
"dompurify": "^3.1.6",
"element-plus": "^2.3.7",
"pinia": "^2.1.3",
"qrcode": "^1.5.3",
"swiper": "^11.1.5",
"terser": "^5.31.5",
"tsparticles": "^2.11.0",
"unocss": "^0.53.5",
"vue": "^3.3.4",
"vue-draggable-plus": "^0.5.0",
"vue-router": "^4.0.13",
"vue3-draggable-resizable": "^1.6.5"
},
"devDependencies": {
"@iconify-json/ep": "^1.1.11",
"@rushstack/eslint-patch": "^1.2.0",
"@tsconfig/node18": "^2.0.1",
"@types/sortablejs": "^1.15.8",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.39.0",
"eslint-plugin-vue": "^9.11.0",
"fast-glob": "3.2.11",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.0",
"sass": "^1.63.6",
"typescript": "~5.0.4",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.3",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.3.9",
"vite-plugin-svg-icons": "2.0.1",
"vue-tsc": "^1.6.5",
"vue3-particles": "^2.10.1"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

6
src/App.vue Normal file
View File

@ -0,0 +1,6 @@
<template>
<router-view />
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss"></style>

23
src/api/auth/types.ts Normal file
View File

@ -0,0 +1,23 @@
/**
* 登录请求参数
*/
export interface LoginData {
/**
* 用户名
*/
username: string;
/**
* 密码
*/
password: string;
/**
* 验证码缓存key
*/
// verifyCodeKey?: string;
/**
* 验证码
*/
// verifyCode?: string;
}

539
src/assets/icons/demo.css Normal file
View File

@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,320 @@
@font-face {
font-family: "iconfont"; /* Project id 4607934 */
src: url('iconfont.woff2?t=1723196559821') format('woff2'),
url('iconfont.woff?t=1723196559821') format('woff'),
url('iconfont.ttf?t=1723196559821') format('truetype'),
url('iconfont.svg?t=1723196559821#iconfont') format('svg');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-tips:before {
content: "\e74a";
}
.icon-arrow-right-dbl:before {
content: "\e7b6";
}
.icon-EXE:before {
content: "\e782";
}
.icon-txt:before {
content: "\e786";
}
.icon-a-1ge:before {
content: "\e794";
}
.icon-heng2:before {
content: "\e789";
}
.icon-a-4x4:before {
content: "\e78a";
}
.icon-zuo1you2:before {
content: "\e78b";
}
.icon-shu3:before {
content: "\e78c";
}
.icon-shang2xia1:before {
content: "\e78d";
}
.icon-zuo1youshang1youxia2:before {
content: "\e78e";
}
.icon-shang1xia2:before {
content: "\e78f";
}
.icon-tianzige:before {
content: "\e790";
}
.icon-shang2xia3:before {
content: "\e792";
}
.icon-zuo2you1:before {
content: "\e793";
}
.icon-shu2:before {
content: "\e77b";
}
.icon-upload:before {
content: "\e72b";
}
.icon-true:before {
content: "\e741";
}
.icon-cart:before {
content: "\e791";
}
.icon-pdf:before {
content: "\e787";
}
.icon-ppt:before {
content: "\e788";
}
.icon-word:before {
content: "\e785";
}
.icon-vsd:before {
content: "\e783";
}
.icon-apk:before {
content: "\e784";
}
.icon-vf:before {
content: "\e77d";
}
.icon-video:before {
content: "\e77e";
}
.icon-zip:before {
content: "\e77f";
}
.icon-ipa:before {
content: "\e780";
}
.icon-excel:before {
content: "\e781";
}
.icon-file:before {
content: "\e77c";
}
.icon-error-img:before {
content: "\e779";
}
.icon-folder:before {
content: "\e77a";
}
.icon-true-o:before {
content: "\e777";
}
.icon-checked-1:before {
content: "\e778";
}
.icon-eye-close:before {
content: "\e775";
}
.icon-eye:before {
content: "\e776";
}
.icon-position:before {
content: "\e6af";
}
.icon-add:before {
content: "\e71b";
}
.icon-arrow-left:before {
content: "\e73f";
}
.icon-arrow-right:before {
content: "\e737";
}
.icon-arrow-top:before {
content: "\e738";
}
.icon-radius-l-b:before {
content: "\e774";
}
.icon-radius-r-t:before {
content: "\e773";
}
.icon-img:before {
content: "\e768";
}
.icon-out-l:before {
content: "\e769";
}
.icon-out-b:before {
content: "\e76a";
}
.icon-del:before {
content: "\e76b";
}
.icon-drag:before {
content: "\e76c";
}
.icon-out-r:before {
content: "\e76d";
}
.icon-line-point:before {
content: "\e76e";
}
.icon-del-o:before {
content: "\e76f";
}
.icon-copy:before {
content: "\e770";
}
.icon-line:before {
content: "\e771";
}
.icon-close-o:before {
content: "\e772";
}
.icon-text:before {
content: "\e766";
}
.icon-auxiliary-line:before {
content: "\e767";
}
.icon-close:before {
content: "\e764";
}
.icon-edit:before {
content: "\e765";
}
.icon-radius-r-b:before {
content: "\e758";
}
.icon-arrow-bottom:before {
content: "\e759";
}
.icon-elliptic:before {
content: "\e75a";
}
.icon-out-t:before {
content: "\e75b";
}
.icon-enter-r:before {
content: "\e75c";
}
.icon-single-sheet:before {
content: "\e752";
}
.icon-spread-over:before {
content: "\e753";
}
.icon-enter-l:before {
content: "\e75d";
}
.icon-round-dot:before {
content: "\e75e";
}
.icon-enter-b:before {
content: "\e75f";
}
.icon-search:before {
content: "\e760";
}
.icon-tile:before {
content: "\e761";
}
.icon-center:before {
content: "\e762";
}
.icon-enter-t:before {
content: "\e763";
}
.icon-left:before {
content: "\e755";
}
.icon-reset:before {
content: "\e756";
}
.icon-radius-l-t:before {
content: "\e757";
}
.icon-right:before {
content: "\e754";
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,541 @@
{
"id": "4607934",
"name": "装修拖拽",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "web端",
"glyphs": [
{
"icon_id": "40405526",
"name": "问号",
"font_class": "tips",
"unicode": "e74a",
"unicode_decimal": 59210
},
{
"icon_id": "41252801",
"name": "双箭头 右",
"font_class": "arrow-right-dbl",
"unicode": "e7b6",
"unicode_decimal": 59318
},
{
"icon_id": "41231177",
"name": "EXE",
"font_class": "EXE",
"unicode": "e782",
"unicode_decimal": 59266
},
{
"icon_id": "41231176",
"name": "TXT",
"font_class": "txt",
"unicode": "e786",
"unicode_decimal": 59270
},
{
"icon_id": "41230988",
"name": "1个",
"font_class": "a-1ge",
"unicode": "e794",
"unicode_decimal": 59284
},
{
"icon_id": "41230997",
"name": "横2",
"font_class": "heng2",
"unicode": "e789",
"unicode_decimal": 59273
},
{
"icon_id": "41230995",
"name": "4x4",
"font_class": "a-4x4",
"unicode": "e78a",
"unicode_decimal": 59274
},
{
"icon_id": "41230998",
"name": "左1右2",
"font_class": "zuo1you2",
"unicode": "e78b",
"unicode_decimal": 59275
},
{
"icon_id": "41230996",
"name": "竖3",
"font_class": "shu3",
"unicode": "e78c",
"unicode_decimal": 59276
},
{
"icon_id": "41230994",
"name": "上2下1",
"font_class": "shang2xia1",
"unicode": "e78d",
"unicode_decimal": 59277
},
{
"icon_id": "41230990",
"name": "左1右上1右下2",
"font_class": "zuo1youshang1youxia2",
"unicode": "e78e",
"unicode_decimal": 59278
},
{
"icon_id": "41230993",
"name": "上1下2",
"font_class": "shang1xia2",
"unicode": "e78f",
"unicode_decimal": 59279
},
{
"icon_id": "41230992",
"name": "田字格",
"font_class": "tianzige",
"unicode": "e790",
"unicode_decimal": 59280
},
{
"icon_id": "41230989",
"name": "上2下3",
"font_class": "shang2xia3",
"unicode": "e792",
"unicode_decimal": 59282
},
{
"icon_id": "41230991",
"name": "左2右1",
"font_class": "zuo2you1",
"unicode": "e793",
"unicode_decimal": 59283
},
{
"icon_id": "41230999",
"name": "竖2",
"font_class": "shu2",
"unicode": "e77b",
"unicode_decimal": 59259
},
{
"icon_id": "38722414",
"name": "上传",
"font_class": "upload",
"unicode": "e72b",
"unicode_decimal": 59179
},
{
"icon_id": "40047122",
"name": "进销存对号",
"font_class": "true",
"unicode": "e741",
"unicode_decimal": 59201
},
{
"icon_id": "39755359",
"name": "购物车1",
"font_class": "cart",
"unicode": "e791",
"unicode_decimal": 59281
},
{
"icon_id": "41176633",
"name": "PDF",
"font_class": "pdf",
"unicode": "e787",
"unicode_decimal": 59271
},
{
"icon_id": "41176632",
"name": "ppt",
"font_class": "ppt",
"unicode": "e788",
"unicode_decimal": 59272
},
{
"icon_id": "41176635",
"name": "word",
"font_class": "word",
"unicode": "e785",
"unicode_decimal": 59269
},
{
"icon_id": "41176637",
"name": "vsd",
"font_class": "vsd",
"unicode": "e783",
"unicode_decimal": 59267
},
{
"icon_id": "41176636",
"name": "APK",
"font_class": "apk",
"unicode": "e784",
"unicode_decimal": 59268
},
{
"icon_id": "41176686",
"name": "音频",
"font_class": "vf",
"unicode": "e77d",
"unicode_decimal": 59261
},
{
"icon_id": "41176682",
"name": "视频1",
"font_class": "video",
"unicode": "e77e",
"unicode_decimal": 59262
},
{
"icon_id": "41176680",
"name": "压缩包",
"font_class": "zip",
"unicode": "e77f",
"unicode_decimal": 59263
},
{
"icon_id": "41176681",
"name": "IPA",
"font_class": "ipa",
"unicode": "e780",
"unicode_decimal": 59264
},
{
"icon_id": "41176679",
"name": "Excel",
"font_class": "excel",
"unicode": "e781",
"unicode_decimal": 59265
},
{
"icon_id": "41176712",
"name": "文件1",
"font_class": "file",
"unicode": "e77c",
"unicode_decimal": 59260
},
{
"icon_id": "41172227",
"name": "图片",
"font_class": "error-img",
"unicode": "e779",
"unicode_decimal": 59257
},
{
"icon_id": "41172226",
"name": "文件",
"font_class": "folder",
"unicode": "e77a",
"unicode_decimal": 59258
},
{
"icon_id": "41168417",
"name": "对号 线",
"font_class": "true-o",
"unicode": "e777",
"unicode_decimal": 59255
},
{
"icon_id": "41150407",
"name": "选中1",
"font_class": "checked-1",
"unicode": "e778",
"unicode_decimal": 59256
},
{
"icon_id": "41131422",
"name": "眼睛关闭",
"font_class": "eye-close",
"unicode": "e775",
"unicode_decimal": 59253
},
{
"icon_id": "41131421",
"name": "眼睛",
"font_class": "eye",
"unicode": "e776",
"unicode_decimal": 59254
},
{
"icon_id": "37137568",
"name": "icon-index-zxmd-dress",
"font_class": "position",
"unicode": "e6af",
"unicode_decimal": 59055
},
{
"icon_id": "38587548",
"name": "加号",
"font_class": "add",
"unicode": "e71b",
"unicode_decimal": 59163
},
{
"icon_id": "40047124",
"name": "进销存左箭头",
"font_class": "arrow-left",
"unicode": "e73f",
"unicode_decimal": 59199
},
{
"icon_id": "40047129",
"name": "进销存右箭头",
"font_class": "arrow-right",
"unicode": "e737",
"unicode_decimal": 59191
},
{
"icon_id": "40047131",
"name": "进销存上箭头",
"font_class": "arrow-top",
"unicode": "e738",
"unicode_decimal": 59192
},
{
"icon_id": "40964540",
"name": "左下圆角",
"font_class": "radius-l-b",
"unicode": "e774",
"unicode_decimal": 59252
},
{
"icon_id": "40964513",
"name": "右上圆角",
"font_class": "radius-r-t",
"unicode": "e773",
"unicode_decimal": 59251
},
{
"icon_id": "40963370",
"name": "图片icon",
"font_class": "img",
"unicode": "e768",
"unicode_decimal": 59240
},
{
"icon_id": "40963369",
"name": "外左",
"font_class": "out-l",
"unicode": "e769",
"unicode_decimal": 59241
},
{
"icon_id": "40963368",
"name": "外下",
"font_class": "out-b",
"unicode": "e76a",
"unicode_decimal": 59242
},
{
"icon_id": "40963367",
"name": "删除3",
"font_class": "del",
"unicode": "e76b",
"unicode_decimal": 59243
},
{
"icon_id": "40963365",
"name": "拖动",
"font_class": "drag",
"unicode": "e76c",
"unicode_decimal": 59244
},
{
"icon_id": "40963366",
"name": "外右",
"font_class": "out-r",
"unicode": "e76d",
"unicode_decimal": 59245
},
{
"icon_id": "40963361",
"name": "点线",
"font_class": "line-point",
"unicode": "e76e",
"unicode_decimal": 59246
},
{
"icon_id": "40963360",
"name": "删除2",
"font_class": "del-o",
"unicode": "e76f",
"unicode_decimal": 59247
},
{
"icon_id": "40963363",
"name": "复制",
"font_class": "copy",
"unicode": "e770",
"unicode_decimal": 59248
},
{
"icon_id": "40963364",
"name": "实线",
"font_class": "line",
"unicode": "e771",
"unicode_decimal": 59249
},
{
"icon_id": "40963362",
"name": "关闭1",
"font_class": "close-o",
"unicode": "e772",
"unicode_decimal": 59250
},
{
"icon_id": "40963407",
"name": "文本",
"font_class": "text",
"unicode": "e766",
"unicode_decimal": 59238
},
{
"icon_id": "40963406",
"name": "线条",
"font_class": "auxiliary-line",
"unicode": "e767",
"unicode_decimal": 59239
},
{
"icon_id": "40963287",
"name": "关闭",
"font_class": "close",
"unicode": "e764",
"unicode_decimal": 59236
},
{
"icon_id": "40963286",
"name": "编辑",
"font_class": "edit",
"unicode": "e765",
"unicode_decimal": 59237
},
{
"icon_id": "40963301",
"name": "右下圆角",
"font_class": "radius-r-b",
"unicode": "e758",
"unicode_decimal": 59224
},
{
"icon_id": "40963300",
"name": "下箭头",
"font_class": "arrow-bottom",
"unicode": "e759",
"unicode_decimal": 59225
},
{
"icon_id": "40963298",
"name": "椭圆",
"font_class": "elliptic",
"unicode": "e75a",
"unicode_decimal": 59226
},
{
"icon_id": "40963299",
"name": "外上",
"font_class": "out-t",
"unicode": "e75b",
"unicode_decimal": 59227
},
{
"icon_id": "40963297",
"name": "内右",
"font_class": "enter-r",
"unicode": "e75c",
"unicode_decimal": 59228
},
{
"icon_id": "40963296",
"name": "单张",
"font_class": "single-sheet",
"unicode": "e752",
"unicode_decimal": 59218
},
{
"icon_id": "40963295",
"name": "铺满",
"font_class": "spread-over",
"unicode": "e753",
"unicode_decimal": 59219
},
{
"icon_id": "40963292",
"name": "内左",
"font_class": "enter-l",
"unicode": "e75d",
"unicode_decimal": 59229
},
{
"icon_id": "40963294",
"name": "圆点",
"font_class": "round-dot",
"unicode": "e75e",
"unicode_decimal": 59230
},
{
"icon_id": "40963293",
"name": "内下",
"font_class": "enter-b",
"unicode": "e75f",
"unicode_decimal": 59231
},
{
"icon_id": "40963291",
"name": "搜索",
"font_class": "search",
"unicode": "e760",
"unicode_decimal": 59232
},
{
"icon_id": "40963290",
"name": "平铺",
"font_class": "tile",
"unicode": "e761",
"unicode_decimal": 59233
},
{
"icon_id": "40963289",
"name": "居中",
"font_class": "center",
"unicode": "e762",
"unicode_decimal": 59234
},
{
"icon_id": "40963288",
"name": "内上",
"font_class": "enter-t",
"unicode": "e763",
"unicode_decimal": 59235
},
{
"icon_id": "40963305",
"name": "左对齐",
"font_class": "left",
"unicode": "e755",
"unicode_decimal": 59221
},
{
"icon_id": "40963304",
"name": "重置",
"font_class": "reset",
"unicode": "e756",
"unicode_decimal": 59222
},
{
"icon_id": "40963303",
"name": "左上圆角",
"font_class": "radius-l-t",
"unicode": "e757",
"unicode_decimal": 59223
},
{
"icon_id": "40963302",
"name": "右对齐",
"font_class": "right",
"unicode": "e754",
"unicode_decimal": 59220
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

BIN
src/assets/images/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/assets/movie.mp4 Normal file

Binary file not shown.

View File

@ -0,0 +1,19 @@
<template>
<div class="card" :style="'padding:' + padding">
<slot></slot>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
// 内边距
padding: {
type: String,
default: "3rem",
},
});
</script>
<style lang="scss" scoped>
.card {
background-color: #fff;
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<div class="flex-row align-c gap-12">
<el-color-picker v-model="color" show-alpha :predefine="predefine_colors" @change="color_picker_change" />
<icon name="reset" color="primary" size="16" :class="['c-pointer', { disable: color == defaultColor }]" @click="reset_event"></icon>
</div>
</template>
<script setup lang="ts">
import { isEmpty } from 'lodash';
const predefine_colors = ref(['#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585', 'rgba(255, 69, 0, 0.68)', 'rgb(255, 120, 0)', 'hsv(51, 100, 98)', 'hsva(120, 40, 94, 0.5)', 'hsl(181, 100%, 37%)', 'hsla(209, 100%, 56%, 0.73)', '#c7158577']);
interface Props {
defaultColor?: string;
}
const props = withDefaults(defineProps<Props>(), {
defaultColor: '',
});
const color = defineModel({ type: String });
const emit = defineEmits(['update:value']);
const color_picker_change = (val: string | null) => {
emit('update:value', val);
};
const reset_event = () => {
if (color.value == props.defaultColor) {
return;
} else {
if (!isEmpty(props.defaultColor)) {
color.value = props.defaultColor;
} else {
color.value = '';
}
emit('update:value', color.value);
}
};
</script>
<style lang="scss" scoped>
.flex-row .disable {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<VueDraggable v-model="from" :animation="500" target=".sort-target" handle=".icon-drag" :scroll="true" :on-sort="on_sort">
<TransitionGroup type="transition" tag="ul" name="fade" class="sort-target flex-col gap-x-20">
<li v-for="(item, index) in from" :key="item.id" :class="className" class="flex gap-y-16 re" @click="on_click(item, index)">
<el-icon class="iconfont icon-drag size-16 cursor-move" />
<slot :row="item" :index="index" />
<el-icon v-if="type == 'line'" class="iconfont icon-del-o size-16" @click="remove(index)" />
<el-icon v-if="type == 'card'" class="iconfont icon-close-o size-16 abs cr-c top-de-5 right-de-5" @click="remove(index)" />
</li>
</TransitionGroup>
</VueDraggable>
</template>
<script setup lang="ts">
import { VueDraggable } from 'vue-draggable-plus';
const emits = defineEmits(['click', 'remove', 'onSort']);
interface Props {
data: any; // 拖拽列表数据
type?: string; // card: 卡片区域 line: 一行
spaceCol?: number; // 上下间距
iconPosition?: string; // top/bottom/center
}
const props = withDefaults(defineProps<Props>(), {
type: () => 'line',
spaceCol: () => 5,
iconPosition: 'center',
});
const className = ref('');
if (props.type == 'card') {
className.value += `card-background box-shadow-sm pt-${props.spaceCol} pb-${props.spaceCol}`;
if (props.iconPosition == 'top') {
className.value += '';
} else if (props.iconPosition == 'bottom') {
className.value += ' align-e';
} else {
className.value += ' align-c';
}
} else {
// 不是卡片类型的设置居中显示
className.value = 'align-c';
}
const from = ref(props.data);
const on_click = (item: any, index: number) => {
emits('click', item, index);
};
const remove = (index: number) => {
emits('remove', index);
};
// 拖拽更新之后用户更新数据
const on_sort = () => {
emits('onSort', from);
};
</script>
<style scoped>
.card-background {
background: #fff;
padding-left: 1.6rem;
padding-right: 2rem;
}
.size-16 {
font-size: 1.6rem !important;
}
.icon-del-o {
cursor: pointer;
}
.cursor-move {
color: #ddd;
cursor: move;
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<i class="iconfont" :class="className" :style="'font-size:' + size + 'px;' + (props.color.indexOf('#') !== -1 ? 'color:' + props.color : '') + styles" @click="onClick">
<slot></slot>
</i>
</template>
<script lang="ts" setup>
const props = defineProps({
// 通过传递类名,来自定义样式
name: {
type: String,
default: '',
},
color: {
type: String,
default: '',
},
size: {
type: String,
default: '',
},
styles: {
type: String,
default: '',
},
});
// const className = ref(`icon-${props.name} ${props.color ? (props.color.indexOf('#') == -1 ? `cr-${props.color}` : '') : ''}`);
const className = computed(() => {
return `icon-${props.name} ${props.color ? (props.color.indexOf('#') == -1 ? `cr-${props.color}` : '') : ''}`;
});
const emit = defineEmits(['click']);
function onClick() {
// 回调方法
emit('click');
}
</script>
<style lang="scss">
@import '@/assets/icons/iconfont.css';
i.iconfont {
display: inline-flex;
justify-content: center;
align-items: center;
font-size: inherit;
gap: 0.5rem;
&:hover {
opacity: 0.8;
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<el-image :src="image?.url || ''" class="flex align-c jc-c">
<template #error>
<div class="image-slot" :style="errorStyle">
<img :src="error_image" :style="errorImgStyle" />
</div>
</template>
</el-image>
</template>
<script setup lang="ts">
const props = defineProps({
errorImgStyle: {
type: String,
default: () => '',
},
errorStyle: {
type: String,
default: () => '',
},
});
const image = defineModel({ type: Object, default: () => {} });
const error_image = ref(new URL(`../../../assets/images/empty.png`, import.meta.url).href);
</script>
<style lang="scss" scoped>
.image-slot {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f4fcff;
}
</style>

View File

@ -0,0 +1,46 @@
<!-- 数字输入框 -->
<template>
<div class="input-number" :class="iconName ? 'has-icon' : ''">
<icon v-if="iconName" :name="iconName" size="14" color="3" class="input-icon"></icon>
<el-input-number v-model="internal_value" :min="min" :max="max" type="number" placeholder="0" controls-position="right"></el-input-number>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
iconName: {
type: String,
default: () => '',
},
min: {
type: Number,
default: () => 0,
},
max: {
type: Number,
default: () => 100,
},
});
const internal_value = defineModel({ type: Number, default: 0 });
</script>
<style lang="scss" scoped>
.input-number {
position: relative;
:deep(.el-input-number.is-controls-right .el-input__wrapper) {
.el-input__inner {
text-align: left;
}
}
&.has-icon {
:deep(.el-input-number.is-controls-right .el-input__wrapper) {
padding-left: 3rem;
}
.input-icon {
position: absolute;
z-index: 1;
top: 0.1rem;
left: 0.8rem;
}
}
}
</style>

View File

@ -0,0 +1,123 @@
<!-- vue 3 引入百度api -->
<template>
<!-- 百度地图 -->
<div class="container w">
<div id="map" class="map radius-md" :style="{ width: width, height: height }"></div>
</div>
</template>
<script>
export default defineComponent({
props: {
modelValue: {
type: String,
default: '上海市黄浦区上海中心大厦',
},
draggable: {
type: Boolean,
default: true,
},
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '216px',
},
},
emits: ['point'],
setup(props, context) {
const map = ref(null);
const lng = ref(121.478);
const lat = ref(31.223);
watch(
() => props.modelValue,
(val) => {
if (!val) return;
map_event(val);
}
);
onMounted(() => {
load_map_script(); // 加载地图资源
});
const load_map_script = () => {
// 此处在所需页面引入资源就是不用再public/index.html中引入
var script = document.createElement('script');
script.type = 'text/javascript';
script.className = 'loadmap'; // 给script一个类名
script.src = 'https://api.map.baidu.com/getscript?v=3.0&ak=XSdiGjfg3wOHiKjpYEMG6CYA';
// 此处需要注意申请ak时一定要应用类别一定要选浏览器端不能选服务端不然地图会报ak无效
script.onload = () => {
// 使用script.onload待资源加载完成再初始化地图
init();
};
let loadmap = document.getElementsByClassName('loadmap');
if (loadmap) {
// 每次append script之前判断一下避免重复添加script资源标签
for (var i = 0; i < loadmap.length; i++) {
document.body.removeChild(loadmap[i]);
}
}
document.body.appendChild(script);
};
// 初始化地图
const init = () => {
const BMap = window.BMap;
map.value = new BMap.Map('map', {
enableMapClick: false,
});
let point = new BMap.Point(lng.value, lat.value);
map.value.centerAndZoom(point, 10); // 初始化地图,设置中心点坐标和地图级别
// 添加控件
let navigationControl = new BMap.NavigationControl({
// 靠左上角位置
anchor: window.BMAP_ANCHOR_TOP_LEFT,
// LARGE类型
type: window.BMAP_NAVIGATION_CONTROL_LARGE,
});
map.value.addControl(navigationControl);
let marker = new BMap.Marker(point);
map.value.addOverlay(marker);
if (props.draggable) {
// 修正marker的初始化
marker.enableDragging();
marker.addEventListener('dragend', function (e) {
map.value.panTo(e.point);
lat.value = e.point.lat;
lng.value = e.point.lng;
context.emit('point', lng, lat);
});
// 设置标注提示信息
let cr = new BMap.CopyrightControl({ anchor: window.BMAP_ANCHOR_BOTTOM_RIGHT });
map.value.addControl(cr); //添加版权控件
let bs = map.value.getBounds(); //返回地图可视区域
cr.addCopyright({ id: 1, content: '<div class="map-dragging-tips"><span>' + '拖动红色图标直接定位' + '</span></div>', bounds: bs });
}
};
const map_event = (value) => {
// 创建地址解析器实例
let geo = new window.BMap.Geocoder();
// 将地址解析结果显示在地图上,并调整地图视野
geo.getPoint(
value,
function (point) {
if (point) {
lng.value = point.lng;
lat.value = point.lat;
context.emit('point', lng.value, lat.value);
init();
} else {
ElMessage.info(point?.getMsg() || '您选择地址没有解析到结果!');
}
},
'全国'
);
};
},
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,116 @@
<template>
<!-- 天地图 -->
<div class="container w">
<div id="map" class="map radius-md" :style="{ width: width, height: height }"></div>
</div>
</template>
<script>
export default defineComponent({
props: {
modelValue: {
type: String,
default: '上海市黄浦区上海中心大厦',
},
// 是否可拖拽
draggable: {
type: Boolean,
default: true,
},
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '216px',
},
},
emits: ['point'],
setup(props, context) {
const map = ref(null);
const lng = ref(121.47894);
const lat = ref(31.223);
watch(
() => props.modelValue,
(val) => {
if (!val) return;
map_event(val);
}
);
onMounted(() => {
load_map_script(); // 加载地图资源
});
const load_map_script = () => {
// 此处在所需页面引入资源就是不用再public/index.html中引入
var script = document.createElement('script');
script.type = 'text/javascript';
script.className = 'loadmap'; // 给script一个类名
script.src = 'https://webapi.amap.com/maps?v=2.0&key=3e92c6bfdd5ddb4aac39ed5e4d0db663';
// 此处需要注意申请ak时一定要应用类别一定要选浏览器端不能选服务端不然地图会报ak无效
script.onload = () => {
// 使用script.onload待资源加载完成再初始化地图
init();
};
let loadmap = document.getElementsByClassName('loadmap');
if (loadmap) {
// 每次append script之前判断一下避免重复添加script资源标签
for (var i = 0; i < loadmap.length; i++) {
document.body.removeChild(loadmap[i]);
}
}
document.body.appendChild(script);
};
const init = () => {
map.value = new AMap.Map('map', {
zoomEnable: true,
resizeEnable: false,
scrollWheel: false,
zoom: 10, // 初始化地图级别
center: [lng.value, lat.value], // 初始化地图中心点位置
});
AMap.plugin(['AMap.ToolBar'], function () {
// 在图面添加工具条控件, 工具条控件只有缩放功能
map.value.addControl(new AMap.ToolBar());
});
// 创建标注
var marker_config = {
position: map.value.getCenter(),
// offset: new AMap.Pixel(-13, -30),
draggable: props.draggable,
};
let marker4 = new AMap.Marker(marker_config);
marker4.setMap(map);
// 标注可拖拽回调
if (props.draggable) {
marker4.on('dragend', (e) => {
map.value.panTo(e.lnglat);
lng.value = e.lnglat.lng;
lat.value = e.lnglat.lat;
context.emit('point', lng.value, lat.value);
});
}
map.value.add(marker4);
};
const map_event = (value) => {
AMap.plugin('AMap.Geocoder', () => {
var geo = new AMap.Geocoder();
geo.getLocation(value, (status, result) => {
if (status === 'complete' && result.geocodes.length) {
var lnglat = result.geocodes[0].location;
lng.value = lnglat.lng;
lat.value = lnglat.lat;
init();
context.emit('point', lng.value, lat.value);
} else {
ElMessage.info('您选择地址没有解析到结果!');
}
});
});
};
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,291 @@
<template>
<!-- 地图 -->
<div class="container w">
<div id="map" class="map radius-md" :style="{ width: width, height: height }"></div>
</div>
</template>
<script>
export default defineComponent({
props: {
modelValue: {
type: String,
default: '上海市黄浦区上海中心大厦',
},
// 是否可拖拽
draggable: {
type: Boolean,
default: true,
},
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '350px',
},
type: {
type: String,
default: '1', // 地图类型 默认1.天地图/2.百度地图/3.腾讯地图/4.高德地图
},
},
emits: ['point'],
setup(props, context) {
const map = ref(null);
const lng = ref(121.47894);
const lat = ref(31.223);
watch(
() => props.modelValue,
(val) => {
if (!val) return;
map_event(val);
}
);
onMounted(() => {
load_map_script(); // 加载地图资源
});
const load_map_script = () => {
// 此处在所需页面引入资源就是不用再public/index.html中引入
let script = document.createElement('script');
script.type = 'text/javascript';
script.className = 'loadmap'; // 给script一个类名
if (props.type === '1') {
// 天地图
script.src = 'https://api.tianditu.gov.cn/api?v=4.0&tk=bf0676d6b99ee6f7f917640a54af0415';
} else if (props.type === '2') {
// 百度地图
script.src = 'https://api.map.baidu.com/getscript?v=3.0&ak=XSdiGjfg3wOHiKjpYEMG6CYA';
} else if (props.type === '3') {
// 腾讯地图
script.src = 'https://map.qq.com/api/js?v=2.exp&key=IMYBZ-QJ6C3-QPZ3Y-OUKL6-IVU5S-ZYBKA&callback=init';
} else if (props.type === '4') {
// 高德地图
script.src = 'https://webapi.amap.com/maps?v=2.0&key=3e92c6bfdd5ddb4aac39ed5e4d0db663';
}
// 使用script.onload待资源加载完成再初始化地图
if (props.type === '3') {
window.init = () => {
init();
};
load_tx_map();
} else {
script.onload = () => {
init();
};
}
let loadmap = document.getElementsByClassName('loadmap');
if (loadmap) {
// 每次append script之前判断一下避免重复添加script资源标签
for (var i = 0; i < loadmap.length; i++) {
document.body.removeChild(loadmap[i]);
}
}
if (props.type === '4') {
window._AMapSecurityConfig = {
securityJsCode: '6d68c17c7b2a96a0616b1b8c371f391f',
};
}
document.body.appendChild(script);
};
const load_tx_map = () => {
// 此处在所需页面引入资源就是不用再public/index.html中引入
let script = document.createElement('script');
script.type = 'text/javascript';
script.className = 'loadmap2'; // 给script一个类名
script.src = 'https://map.qq.com/api/gljs?v=1.exp&key=IMYBZ-QJ6C3-QPZ3Y-OUKL6-IVU5S-ZYBKA&libraries=service';
let loadmap2 = document.getElementsByClassName('loadmap2');
if (loadmap2) {
// 每次append script之前判断一下避免重复添加script资源标签
for (var i = 0; i < loadmap2.length; i++) {
document.body.removeChild(loadmap2[i]);
}
}
document.body.appendChild(script);
};
// 初始化地图
const init = () => {
switch (props.type) {
case '1':
const T = window.T;
// 坐标
map.value = new T.Map('map');
let point = new T.LngLat(lng.value, lat.value);
map.value.centerAndZoom(point, 10);
// 禁止鼠标滚动缩小放大
map.value.disableScrollWheelZoom();
// 添加控件
//创建缩放平移控件对象
let control = new T.Control.Zoom();
// control.setPosition(T_ANCHOR_TOP_RIGHT);
//添加缩放平移控件
map.value.addControl(control);
map.value.clearOverLays();
let marker = new T.Marker(point);
map.value.addOverLay(marker);
if (props.draggable) {
marker.enableDragging();
marker.addEventListener('dragend', function (e) {
map.value.panTo(new T.LngLat(e.lnglat.lng, e.lnglat.lat));
lat.value = e.lnglat.lat;
lng.value = e.lnglat.lng;
context.emit('point', lng, lat);
});
}
break;
case '2':
const BMap = window.BMap;
map.value = new BMap.Map('map', {
enableMapClick: false,
});
let point2 = new BMap.Point(lng.value, lat.value);
map.value.centerAndZoom(point2, 10); // 初始化地图,设置中心点坐标和地图级别
// 添加控件
let navigationControl = new BMap.NavigationControl({
// 靠左上角位置
anchor: window.BMAP_ANCHOR_TOP_LEFT,
// LARGE类型
type: window.BMAP_NAVIGATION_CONTROL_LARGE,
});
map.value.addControl(navigationControl);
let marker2 = new BMap.Marker(point2);
map.value.addOverlay(marker2);
if (props.draggable) {
// 修正marker的初始化
marker2.enableDragging();
marker2.addEventListener('dragend', function (e) {
map.value.panTo(e.point);
lat.value = e.point.lat;
lng.value = e.point.lng;
context.emit('point', lng, lat);
});
// 设置标注提示信息
let cr = new BMap.CopyrightControl({ anchor: window.BMAP_ANCHOR_BOTTOM_RIGHT });
map.value.addControl(cr); //添加版权控件
let bs = map.value.getBounds(); //返回地图可视区域
cr.addCopyright({ id: 1, content: '<div class="map-dragging-tips"><span>' + '拖动红色图标直接定位' + '</span></div>', bounds: bs });
}
break;
case '3':
const qq_maps = window.qq.maps;
let point3 = new qq_maps.LatLng(lat.value, lng.value);
map.value = new qq_maps.Map('map', {
center: point3,
zoom: 10,
});
let marker3 = new qq_maps.Marker({
map: map.value,
position: point3,
draggable: props.draggable,
});
qq_maps.event.addListener(marker3, 'dragend', function (e) {
lat.value = e.latLng.lat;
lng.value = e.latLng.lng;
map.value.panTo(e.latLng);
context.emit('point', lng, lat);
});
break;
case '4':
const AMap = window.AMap;
map.value = new AMap.Map('map', {
zoomEnable: true,
resizeEnable: false,
scrollWheel: false,
zoom: 10, // 初始化地图级别
center: [lng.value, lat.value], // 初始化地图中心点位置
});
AMap.plugin(['AMap.ToolBar'], function () {
// 在图面添加工具条控件, 工具条控件只有缩放功能
map.value.addControl(new AMap.ToolBar());
});
// 创建标注
var marker_config = {
position: map.value.getCenter(),
// offset: new AMap.Pixel(-13, -30),
draggable: props.draggable,
};
let marker4 = new AMap.Marker(marker_config);
marker4.setMap(map);
// 标注可拖拽回调
if (props.draggable) {
marker4.on('dragend', (e) => {
map.value.panTo(e.lnglat);
lng.value = e.lnglat.lng;
lat.value = e.lnglat.lat;
context.emit('point', lng.value, lat.value);
});
}
map.value.add(marker4);
break;
}
};
const map_event = (value) => {
switch (props.type) {
case '1':
let geo = new T.Geocoder();
geo.getPoint(value, function (result) {
let point = result.getLocationPoint();
if (result.getStatus() == 0) {
lng.value = point.lng;
lat.value = point.lat;
init();
map.value.panTo(new T.LngLat(lng.value, lat.value));
context.emit('point', lng.value, lat.value);
} else {
ElMessage.info(point?.getMsg() || '您选择地址没有解析到结果!');
}
});
break;
case '2':
// 创建地址解析器实例
let geo2 = new window.BMap.Geocoder();
// 将地址解析结果显示在地图上,并调整地图视野
geo2.getPoint(
value,
function (point) {
if (point) {
lng.value = point.lng;
lat.value = point.lat;
context.emit('point', lng.value, lat.value);
init();
} else {
ElMessage.info(point?.getMsg() || '您选择地址没有解析到结果!');
}
},
'全国'
);
break;
case '3':
let geo3 = new TMap.service.Geocoder();
geo3.getLocation({ address: value }).then((result) => {
let lnglat = result.result.location;
lng.value = lnglat.lng;
lat.value = lnglat.lat;
init();
context.emit('point', lng.value, lat.value);
});
break;
case '4':
AMap.plugin('AMap.Geocoder', () => {
new AMap.Geocoder().getLocation(value, (status, result) => {
if (status === 'complete' && result.geocodes.length) {
var lnglat = result.geocodes[0].location;
lng.value = lnglat.lng;
lat.value = lnglat.lat;
init();
context.emit('point', lng.value, lat.value);
} else {
ElMessage.info('您选择地址没有解析到结果!');
}
});
});
break;
}
};
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,108 @@
<template>
<!-- 天地图 -->
<div class="container w">
<div id="map" class="map radius-md" :style="{ width: width, height: height }"></div>
</div>
</template>
<script>
export default defineComponent({
props: {
modelValue: {
type: String,
default: '上海市黄浦区上海中心大厦',
},
// 是否可拖拽
draggable: {
type: Boolean,
default: true,
},
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '216px',
},
},
emits: ['point'],
setup(props, context) {
const map = ref(null);
const lng = ref(121.47894);
const lat = ref(31.223);
watch(
() => props.modelValue,
(val) => {
if (!val) return;
map_event(val);
}
);
onMounted(() => {
load_map_script(); // 加载地图资源
});
const load_map_script = () => {
// 此处在所需页面引入资源就是不用再public/index.html中引入
var script = document.createElement('script');
script.type = 'text/javascript';
script.className = 'loadmap'; // 给script一个类名
script.src = 'https://api.tianditu.gov.cn/api?v=4.0&tk=bf0676d6b99ee6f7f917640a54af0415';
// 此处需要注意申请ak时一定要应用类别一定要选浏览器端不能选服务端不然地图会报ak无效
script.onload = () => {
// 使用script.onload待资源加载完成再初始化地图
init();
};
let loadmap = document.getElementsByClassName('loadmap');
if (loadmap) {
// 每次append script之前判断一下避免重复添加script资源标签
for (var i = 0; i < loadmap.length; i++) {
document.body.removeChild(loadmap[i]);
}
}
document.body.appendChild(script);
};
// 初始化地图
const init = () => {
const T = window.T;
// 坐标
map.value = new T.Map('map');
let point = new T.LngLat(lng.value, lat.value);
map.value.centerAndZoom(point, 10);
// 禁止鼠标滚动缩小放大
map.value.disableScrollWheelZoom();
// 添加控件
//创建缩放平移控件对象
let control = new T.Control.Zoom();
// control.setPosition(T_ANCHOR_TOP_RIGHT);
//添加缩放平移控件
map.value.addControl(control);
map.value.clearOverLays();
let marker = new T.Marker(point);
map.value.addOverLay(marker);
if (props.draggable) {
marker.enableDragging();
marker.addEventListener('dragend', function (e) {
map.value.panTo(new T.LngLat(e.lnglat.lng, e.lnglat.lat));
});
}
};
const map_event = (value) => {
let geo = new T.Geocoder();
geo.getPoint(value, function (result) {
let point = result.getLocationPoint();
if (result.getStatus() == 0) {
lng.value = point.lng;
lat.value = point.lat;
init();
map.value.panTo(new T.LngLat(lng.value, lat.value));
context.emit('point', lng.value, lat.value);
} else {
ElMessage.info(point?.getMsg() || '您选择地址没有解析到结果!');
}
});
};
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,107 @@
<template>
<!-- 地图 -->
<div class="container w">
<div id="map" class="map radius-md" :style="{ width: width, height: height }"></div>
</div>
</template>
<script>
export default defineComponent({
props: {
modelValue: {
type: String,
default: '上海市黄浦区上海中心大厦',
},
// 是否可拖拽
draggable: {
type: Boolean,
default: true,
},
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '216px',
},
},
emits: ['point'],
setup(props, context) {
const map = ref(null);
const lng = ref(121.47894);
const lat = ref(31.223);
watch(
() => props.modelValue,
(val) => {
if (!val) return;
map_event(val);
}
);
onMounted(() => {
load_map_script1(); // 加载地图资源
load_map_script2(); // 加载地图资源
});
window.init = () => {
init();
};
const load_map_script1 = () => {
// 此处在所需页面引入资源就是不用再public/index.html中引入
let script = document.createElement('script');
script.type = 'text/javascript';
script.className = 'loadmap'; // 给script一个类名
script.src = 'https://map.qq.com/api/js?v=2.exp&key=IMYBZ-QJ6C3-QPZ3Y-OUKL6-IVU5S-ZYBKA&callback=init';
let loadmap = document.getElementsByClassName('loadmap');
if (loadmap) {
// 每次append script之前判断一下避免重复添加script资源标签
for (var i = 0; i < loadmap.length; i++) {
document.body.removeChild(loadmap[i]);
}
}
document.body.appendChild(script);
};
const load_map_script2 = () => {
let script = document.createElement('script');
script.type = 'text/javascript';
script.className = 'loadmap2'; // 给script一个类名
script.src = 'https://map.qq.com/api/gljs?v=1.exp&key=IMYBZ-QJ6C3-QPZ3Y-OUKL6-IVU5S-ZYBKA&libraries=service';
let loadmap2 = document.getElementsByClassName('loadmap2');
// 每次append script之前判断一下避免重复添加script资源标签
for (var i = 0; i < loadmap2.length; i++) {
document.body.removeChild(loadmap2[i]);
}
document.body.appendChild(script);
};
// 初始化地图
const init = () => {
const qq_maps = window.qq.maps;
let point = new qq_maps.LatLng(lat.value, lng.value);
map.value = new qq_maps.Map('map', {
center: point,
zoom: 10,
});
let marker3 = new qq_maps.Marker({
map: map.value,
position: point,
draggable: props.draggable,
});
qq_maps.event.addListener(marker3, 'dragend', function (e) {
lat.value = e.latLng.lat;
lng.value = e.latLng.lng;
map.value.panTo(e.latLng);
context.emit('point', lng, lat);
});
};
const map_event = (value) => {
let geo3 = new TMap.service.Geocoder();
geo3.getLocation({ address: value }).then((result) => {
let lnglat = result.result.location;
lng.value = lnglat.lng;
lat.value = lnglat.lat;
context.emit('point', lng.value, lat.value);
init();
});
};
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,64 @@
<template>
<div class="flex-col gap-10 w">
<slider v-model="form.padding" @update:model-value="padding_event"></slider>
<div class="flex-row flex-wrap gap-x-20 mt-10">
<div class="flex-width-half pr-10">
<input-number v-model="form.padding_top" icon-name="enter-t" @update:model-value="pt_event"></input-number>
</div>
<div class="flex-width-half pl-10">
<input-number v-model="form.padding_bottom" icon-name="enter-b" @update:model-value="pb_event"></input-number>
</div>
<div class="flex-width-half pr-10">
<input-number v-model="form.padding_left" icon-name="enter-l" @update:model-value="pl_event"></input-number>
</div>
<div class="flex-width-half pl-10">
<input-number v-model="form.padding_right" icon-name="enter-r" @update:model-value="pr_event"></input-number>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
value: {
type: Object,
default: () => {},
},
});
// 用于页面判断显示
const state = reactive({
form: props.value || {},
});
// 如果需要解构确保使用toRefs
const { form } = toRefs(state);
const emit = defineEmits(['update:value']);
const padding_event = (val: number | undefined) => {
form.value.padding = Number(val);
form.value.padding_top = Number(val);
form.value.padding_bottom = Number(val);
form.value.padding_left = Number(val);
form.value.padding_right = Number(val);
emit('update:value', form);
};
const pt_event = (val: number | undefined) => {
form.value.padding_top = Number(val);
form.value.padding = 0;
emit('update:value', form);
};
const pb_event = (val: number | undefined) => {
form.value.padding_bottom = Number(val);
form.value.padding = 0;
emit('update:value', form);
};
const pl_event = (val: number | undefined) => {
form.value.padding_left = Number(val);
form.value.padding = 0;
emit('update:value', form);
};
const pr_event = (val: number | undefined) => {
form.value.padding_right = Number(val);
form.value.padding = 0;
emit('update:value', form);
};
</script>

View File

@ -0,0 +1,99 @@
<!-- 二维码解析 -->
<template>
<div class="qrcode flex-col gap-5">
<div class="re flex-row qrcode-img">
<div v-if="isMask" class="mask">
{{ mask }}
</div>
<el-image :src="qrCodeUrl" fit="contain" class="w">
<template #error>
<div class="bg-f5 img flex-row jc-c align-c radius h w">
<icon name="error-img" size="42"></icon>
</div>
</template>
</el-image>
</div>
<div class="flex-row align-c gap-10 size-12">
{{ src }}
<div class="copy" @click="clipboard_event">复制</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, onMounted } from 'vue';
import QRCode from 'qrcode';
const props = defineProps({
src: {
type: String,
default: '',
},
isMask: {
type: Boolean,
default: false,
},
mask: {
type: String,
default: () => '请先选择分组',
},
});
const qrCodeUrl = ref('');
const generateQRCode = async (text: string, margin: number) => {
try {
const dataUrl = await QRCode.toDataURL(text, { margin });
qrCodeUrl.value = dataUrl;
} catch (error) {
console.error('Error generating QR code:', error);
}
};
const clipboard_event = async () => {
try {
await navigator.clipboard.writeText(props.src);
} catch (error) {
console.error('复制失败', error);
}
};
// 在组件挂载后自动调用生成二维码方法
onMounted(() => {
generateQRCode(props.src.trim(), 2);
});
</script>
<style lang="scss" scoped>
.qrcode {
.mask {
position: absolute;
inset: 0;
z-index: 1;
color: #fff;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(0.2rem);
}
&-img {
width: 12rem;
height: 12rem;
border: 0.1rem solid #eeeeee;
}
.copy {
width: 3.2rem;
height: 2rem;
line-height: 2rem;
border-radius: 0.2rem;
border: 0.1rem solid #eeeeee;
font-size: 1.1rem;
color: #666666;
text-align: center;
cursor: pointer;
&:hover {
color: $cr-primary;
border-color: $cr-primary-light;
}
}
}
</style>

View File

@ -0,0 +1,80 @@
<!-- 通用样式 -->
<template>
<div class="flex-col gap-10 w">
<slider v-model="form.radius" @update:model-value="radius_event"></slider>
<div class="flex-row flex-wrap gap-x-20 mt-10">
<div class="flex-width-half pr-10">
<input-number v-model="form.radius_top_left" icon-name="radius-l-t" @update:model-value="rtl_event"></input-number>
</div>
<div class="flex-width-half pl-10">
<input-number v-model="form.radius_top_right" icon-name="radius-r-t" @update:model-value="rtr_event"></input-number>
</div>
<div class="flex-width-half pr-10">
<input-number v-model="form.radius_bottom_left" icon-name="radius-l-b" @update:model-value="rbl_event"></input-number>
</div>
<div class="flex-width-half pl-10">
<input-number v-model="form.radius_bottom_right" icon-name="radius-r-b" @update:model-value="rbr_event"></input-number>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// interface common_radius {
// radius: number;
// radius_top_left: number;
// radius_top_right: number;
// radius_bottom_left: number;
// radius_bottom_right: number;
// }
// interface Props {
// value: common_radius;
// }
// const props = defineProps<Props>();
const props = defineProps({
value: {
type: Object,
default: () => {},
},
});
// 用于页面判断显示
const state = reactive({
form: props.value || {},
});
// 如果需要解构确保使用toRefs
const { form } = toRefs(state);
const emit = defineEmits(['update:value']);
const radius_event = (val: number | undefined) => {
form.value.radius = Number(val);
form.value.radius_top_left = Number(val);
form.value.radius_top_right = Number(val);
form.value.radius_bottom_left = Number(val);
form.value.radius_bottom_right = Number(val);
emit('update:value', form.value);
};
const rtl_event = (val: number | undefined) => {
form.value.radius_top_left = Number(val);
form.value.radius = 0;
emit('update:value', form.value);
};
const rtr_event = (val: number | undefined) => {
form.value.radius_top_right = Number(val);
form.value.radius = 0;
emit('update:value', form.value);
};
const rbl_event = (val: number | undefined) => {
form.value.radius_bottom_left = Number(val);
form.value.radius = 0;
emit('update:value', form.value);
};
const rbr_event = (val: number | undefined) => {
form.value.radius_bottom_right = Number(val);
form.value.radius = 0;
emit('update:value', form.value);
};
</script>
<style lang="scss" scoped>
.common-styles {
width: 100%;
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<div class="slider w">
<el-slider v-model="internal_value" :min="min" :max="max" :step="step" />
<input-number v-model="internal_value" class="slider-input" :min="min" :max="max"></input-number>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 100,
},
step: {
type: Number,
default: 1,
},
});
const internal_value = defineModel({ type: Number, default: 0 });
</script>
<style lang="scss" scoped>
.slider {
padding-left: 1.2rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
.slider-input {
:deep(.el-input-number) {
width: 9rem;
}
}
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<div class="mb-12">指示器设置</div>
<el-form-item label="是否显示">
<el-switch v-model="form.is_show" size="large" />
</el-form-item>
<el-form-item label="是否滚动">
<el-switch v-model="form.is_roll" size="large" />
</el-form-item>
<el-form-item label="间隔时间">
<slider v-model="form.interval_time" :max="100"></slider>
</el-form-item>
<template v-if="form.is_show">
<el-form-item label="指示器样式">
<el-radio-group v-model="form.indicator_style" is-button>
<el-tooltip content="点" placement="top" effect="light">
<el-radio-button value="dot"><icon name="iconfont icon-round-dot"></icon></el-radio-button>
</el-tooltip>
<el-tooltip content="线" placement="top" effect="light">
<el-radio-button value="elliptic"><icon name="iconfont icon-elliptic"></icon></el-radio-button>
</el-tooltip>
<el-tooltip content="数字" placement="top" effect="light">
<el-radio-button value="num">1/5</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="指示器位置">
<el-radio-group v-model="form.indicator_location" is-button>
<el-tooltip content="左对齐" placement="top" effect="light">
<el-radio-button value="flex-start"><icon name="iconfont icon-left"></icon></el-radio-button>
</el-tooltip>
<el-tooltip content="居中" placement="top" effect="light">
<el-radio-button value="center"><icon name="iconfont icon-center"></icon></el-radio-button>
</el-tooltip>
<el-tooltip content="右对齐" placement="top" effect="light">
<el-radio-button value="flex-end"><icon name="iconfont icon-right"></icon></el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="指示器大小">
<slider v-model="form.indicator_size" :max="100"></slider>
</el-form-item>
<el-form-item label="指示器色值">
<div class="flex-col gap-20">
<color-picker v-model="form.actived_color" default-color="#2A94FF" @update:value="color_picker_change($event, 'actived_color')"></color-picker>
<color-picker v-model="form.color" default-color="#DDDDDD" @update:value="color_picker_change($event, 'color')"></color-picker>
</div>
</el-form-item>
<el-form-item v-if="form.indicator_style != 'num'" label="指示器圆角">
<radius :value="form.indicator_radius" @update:value="indicator_radius_change"></radius>
</el-form-item>
</template>
</template>
<script setup lang="ts">
import { pick } from 'lodash';
// interface indicator {
// is_show: boolean;
// is_roll: boolean;
// interval_time: number;
// indicator_style: string;
// indicator_location: string;
// indicator_size: number;
// indicator_radius: radiusStyle;
// actived_color: string;
// color: string;
// }
// interface Props {
// value: indicator;
// }
// const props = defineProps<Props>();
const props = defineProps({
value: {
type: Object,
default: () => {}
}
})
const state = reactive({
form: props.value
});
// 如果需要解构确保使用toRefs
const { form } = toRefs(state);
const color_picker_change = (color: string, type: string) => {
if (type === 'actived_color') {
form.value.actived_color = color;
} else {
form.value.color = color;
}
};
// 指示器圆角
const indicator_radius_change = (radius: radiusStyle) => {
form.value.indicator_radius = Object.assign(form.value.indicator_radius, pick(radius, [
'radius',
'radius_top_left',
'radius_top_right',
'radius_bottom_left',
'radius_bottom_right',
]));
}
</script>

View File

@ -0,0 +1,174 @@
<!-- 通用样式 -->
<template>
<card-container>
<div class="common-styles">
<el-form :model="form" label-width="70">
<div class="mb-12">通用</div>
<el-form-item label="底部背景">
<div class="flex-col gap-10 w">
<div class="size-12">背景色</div>
<mult-color-picker :value="form.color_list" :type="form.direction" @update:value="mult_color_picker_event"></mult-color-picker>
<div class="flex-row jc-sb align-c">
<div class="size-12">背景图</div>
<el-radio-group v-model="form.background_img_style" is-button @change="background_img_style_change">
<el-tooltip content="单张" placement="top" effect="light">
<el-radio-button value="0"><icon name="single-sheet"></icon></el-radio-button>
</el-tooltip>
<el-tooltip content="平铺" placement="top" effect="light">
<el-radio-button value="1"><icon name="tile"></icon></el-radio-button>
</el-tooltip>
<el-tooltip content="铺满" placement="top" effect="light">
<el-radio-button value="2"><icon name="spread-over"></icon></el-radio-button>
</el-tooltip>
</el-radio-group>
</div>
<upload v-model="form.background_img_url" :limit="1" @update:model-value="background_img_url_change"></upload>
</div>
</el-form-item>
<el-form-item label="内边距">
<padding :value="form" @update:value="padding_change"></padding>
</el-form-item>
<el-form-item label="外边距">
<div class="flex-col gap-10 w">
<slider v-model="form.margin" @update:model-value="margin_event"></slider>
<div class="flex-row flex-wrap gap-x-20 mt-10">
<div class="flex-width-half pr-10">
<input-number v-model="form.margin_top" icon-name="out-t" @update:model-value="mt_event"></input-number>
</div>
<div class="flex-width-half pl-10">
<input-number v-model="form.margin_bottom" icon-name="out-b" @update:model-value="mb_event"></input-number>
</div>
<div class="flex-width-half pr-10">
<input-number v-model="form.margin_left" icon-name="out-l" @update:model-value="ml_event"></input-number>
</div>
<div class="flex-width-half pl-10">
<input-number v-model="form.margin_right" icon-name="out-r" @update:model-value="mr_event"></input-number>
</div>
</div>
</div>
</el-form-item>
<el-form-item label="圆角">
<radius :value="form" @update:value="radius_change"></radius>
</el-form-item>
<el-form-item label="阴影设置">
<div class="flex-col gap-10 w">
<el-form-item label="颜色" label-width="45">
<color-picker v-model="form.box_shadow_color" @update:value="box_shadow_color_event"></color-picker>
</el-form-item>
<el-form-item label="X轴" label-width="45">
<slider v-model="form.box_shadow_x" :min="-20" :max="20"></slider>
</el-form-item>
<el-form-item label="Y轴" label-width="45">
<slider v-model="form.box_shadow_y" :min="-20" :max="20"></slider>
</el-form-item>
<el-form-item label="模糊" label-width="45">
<slider v-model="form.box_shadow_blur"></slider>
</el-form-item>
<el-form-item label="扩展" label-width="45">
<slider v-model="form.box_shadow_spread"></slider>
</el-form-item>
</div>
</el-form-item>
</el-form>
</div>
</card-container>
</template>
<script setup lang="ts">
import { pick } from 'lodash';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
});
// 初始化表单数据
const init_form = reactive({
direction: '0deg',
background_img_url: [] as uploadList[],
color_list: [''],
background_img_style: 2,
padding: 0,
padding_top: 0,
padding_bottom: 0,
padding_left: 0,
padding_right: 0,
margin: 0,
margin_top: 0,
margin_bottom: 0,
margin_left: 0,
margin_right: 0,
radius: 0,
radius_top_left: 0,
radius_top_right: 0,
radius_bottom_left: 0,
radius_bottom_right: 0,
box_shadow_color: '',
box_shadow_x: 0,
box_shadow_y: 0,
box_shadow_blur: 0,
box_shadow_spread: 0,
});
// value 和初始化数据合并数据
let form = reactive(Object.assign({}, init_form, props.value));
const emit = defineEmits(['update:value']);
const mult_color_picker_event = (arry: string[], type: number) => {
form.color_list = arry;
form.direction = type.toString();
emit('update:value', form);
};
const background_img_style_change = (style: any) => {
form.background_img_style = style;
emit('update:value', form);
};
const background_img_url_change = (arry: uploadList[]) => {
form.background_img_url = arry;
emit('update:value', form);
};
const margin_event = (val: number | undefined) => {
form.margin = Number(val);
form.margin_top = Number(val);
form.margin_bottom = Number(val);
form.margin_left = Number(val);
form.margin_right = Number(val);
emit('update:value', form);
};
const mt_event = (val: number | undefined) => {
form.margin_top = Number(val);
form.margin = 0;
emit('update:value', form);
};
const mb_event = (val: number | undefined) => {
form.margin_bottom = Number(val);
form.margin = 0;
emit('update:value', form);
};
const ml_event = (val: number | undefined) => {
form.margin_left = Number(val);
form.margin = 0;
emit('update:value', form);
};
const mr_event = (val: number | undefined) => {
form.margin_right = Number(val);
form.margin = 0;
emit('update:value', form);
};
const radius_change = (radius: any) => {
form = Object.assign(form, pick(radius, ['radius', 'radius_top_left', 'radius_top_right', 'radius_bottom_left', 'radius_bottom_right']));
emit('update:value', form);
};
const padding_change = (padding: any) => {
form = Object.assign(form, pick(padding, ['padding', 'padding_top', 'padding_bottom', 'padding_left', 'padding_right']));
emit('update:value', form);
};
const box_shadow_color_event = (val: string) => {
form.box_shadow_color = val;
emit('update:value', form);
};
</script>
<style lang="scss" scoped>
.common-styles {
width: 100%;
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<div class="img-outer re w h" :style="border_style">
<image-empty v-model="img_src" :style="image_style"></image-empty>
</div>
</template>
<script setup lang="ts">
import { percentage_count, radius_computer } from '@/utils';
import { isEmpty } from 'lodash';
const props = defineProps({
value: {
type: Object,
default: () => {
return {};
},
required: true
},
isPercentage: {
type: Boolean,
default: false
}
});
// 用于页面判断显示
const form = reactive(props.value);
const img_src = computed(() => {
if (!isEmpty(form.img_src[0])) {
return form.img_src[0];
} else {
return {
url: form.data_source_list.url
}
}
});
const image_style = computed(() => {
return `${ set_count() };transform: rotate(${form.img_rotate}deg); ${ radius_computer(form.img_radius) };`;
});
const border_style = computed(() => {
let style = ``;
if (form.border_show) {
style += `border: ${form.border_size}px ${form.border_style} ${form.border_color}; ${ radius_computer(form.border_radius) };`
}
return style;
});
const set_count = () => {
if (props.isPercentage) {
return `width: ${ percentage_count(form.img_width, form.com_width) }; height: ${ percentage_count(form.img_height, form.com_height) };`;
} else {
return `width: ${form.img_width}px; height: ${form.img_height}px;`;
}
};
</script>
<style lang="scss" scoped>
.img-outer {
overflow: hidden;
}
:deep(.el-image) {
height: 100%;
width: 100%;
.el-image__inner {
object-fit: cover;
}
.image-slot img {
width: 3rem;
}
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<div class="w h bg-f">
<el-form :model="form" label-width="60">
<card-container>
<div class="mb-12">图片设置</div>
<el-form-item label="上传图片">
<div class="flex-column w">
<upload v-model="form.img_src" :limit="1" size="50"></upload>
<div class="flex-row align-c gap-10 mt-12">
<el-select v-model="form.data_source_id" value-key="id" placeholder="请选择图片数据字段" size="default" class="flex-1">
<el-option v-for="item in options" :key="item.id" :label="item.label" :value="item" />
</el-select>
<el-popover placement="top-start" :width="200" trigger="hover" content="this is content, this is content, this is content">
<template #reference>
<icon name="tips" size="24"></icon>
</template>
</el-popover>
</div>
</div>
</el-form-item>
<el-form-item label="链接">
<url-value v-model="form.link"></url-value>
</el-form-item>
<el-form-item label="图片圆角">
<radius :value="form.img_radius" @update:value="img_radius_change"></radius>
</el-form-item>
<el-form-item label="图片宽度">
<slider v-model="form.img_width" :max="1000"></slider>
</el-form-item>
<el-form-item label="图片高度">
<slider v-model="form.img_height" :max="1000"></slider>
</el-form-item>
<el-form-item label="旋转角度">
<slider v-model="form.img_rotate" :max="1000"></slider>
</el-form-item>
<el-form-item label="是否置底">
<el-switch v-model="form.bottom_up" />
</el-form-item>
</card-container>
<div class="bg-f5 partition-line" />
<card-container class="h">
<div class="mb-12">边框设置</div>
<el-form-item label="边框显示">
<el-radio-group v-model="form.border_show" class="ml-4">
<el-radio :value="true">显示</el-radio>
<el-radio :value="false">隐藏</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="form.border_show">
<el-form-item label="边框颜色">
<color-picker v-model="form.border_color" default-color="#FF3F3F"></color-picker>
</el-form-item>
<el-form-item label="边框样式">
<el-radio-group v-model="form.border_style" class="ml-4">
<el-radio value="dashed"><div class="border-style-item" style="border: 1px dashed #979797"></div></el-radio>
<el-radio value="solid"><div class="border-style-item" style="border: 1px solid #979797"></div></el-radio>
<el-radio value="dotted"><div class="border-style-item" style="border: 1px dotted #979797"></div></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="边框圆角">
<radius :value="form.border_radius" @update:value="border_radius_change"></radius>
</el-form-item>
<el-form-item label="边框粗细">
<slider v-model="form.border_size" :max="1000"></slider>
</el-form-item>
</template>
</card-container>
</el-form>
</div>
</template>
<script setup lang="ts">
import { location_compute } from '@/utils';
import { pick } from 'lodash';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
options: {
type: Array<any>,
default: () => [],
},
});
// 默认值
const state = reactive({
diy_data: props.value,
});
// 如果需要解构确保使用toRefs
const { diy_data } = toRefs(state);
const form = ref(diy_data.value.com_data);
const center_height = defineModel('height', { type: Number, default: 0 });
const img_radius_change = (radius: any) => {
form.value.img_radius = Object.assign(form.value.img_radius, pick(radius, ['radius', 'radius_top_left', 'radius_top_right', 'radius_bottom_left', 'radius_bottom_right']));
};
const border_radius_change = (radius: any) => {
form.value.border_radius = Object.assign(form.value.border_radius, pick(radius, ['radius', 'radius_top_left', 'radius_top_right', 'radius_bottom_left', 'radius_bottom_right']));
};
watch(
diy_data,
(val) => {
let width = form.value.img_width;
let height = form.value.img_height;
if (form.value.border_show) {
width += form.value.border_size * 2;
height += form.value.border_size * 2;
}
diy_data.value.location.x = location_compute(width, val.location.x, 390);
diy_data.value.location.y = location_compute(height, val.location.y, center_height.value);
diy_data.value.location.staging_y = diy_data.value.location.y;
form.value.com_width = width;
form.value.com_height = height;
form.value.staging_height = height;
},
{ immediate: true, deep: true }
);
</script>
<style lang="scss" scoped>
.card.mb-8 {
.el-form-item:last-child {
margin-bottom: 0;
}
}
.border-style-item {
width: 3rem;
height: 2rem;
}
.partition-line {
height: 0.8rem;
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<div :style="border_style"></div>
</template>
<script setup lang="ts">
const props = defineProps({
value: {
type: Object,
default: () => {
return {};
},
required: true
},
isPercentage: {
type: Boolean,
default: false
}
});
// 用于页面判断显示
const form = reactive(props.value);
const border_style = computed(() => {
if (form.line_settings === 'horizontal') {
return `${ set_count() } margin: 5px 0;border-bottom: ${form.line_size }px ${form.line_style} ${form.line_color};`;
} else {
return `${ set_count() } margin: 0 5px;border-right: ${form.line_size }px ${form.line_style} ${form.line_color};`;
}
});
const set_count = () => {
if (props.isPercentage) {
return '';
} else {
if (form.line_settings === 'horizontal') {
return `width: ${ form.com_width }px;`;
} else {
return `height: ${ form.com_height }px;`;
}
}
};
</script>
<style lang="scss" scoped>
:deep(.el-image) {
height: 100%;
width: 100%;
.el-image__inner {
object-fit: cover;
}
.image-slot img {
width: 3rem;
}
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<div class="w h bg-f">
<el-form :model="form" label-width="80">
<card-container class="">
<div class="mb-12">线条设置</div>
<el-form-item label="竖线横线">
<el-radio-group v-model="form.line_settings">
<el-radio value="horizontal">横线</el-radio>
<el-radio value="vertical">竖线</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="线条样式">
<el-radio-group v-model="form.line_style">
<el-radio value="dashed">
<icon name="line-point" class="re top-2" size="32"></icon>
</el-radio>
<el-radio value="solid">
<icon name="line" class="re top-2" size="32"></icon>
</el-radio>
<el-radio value="dotted">
<icon name="line-point" class="re top-2" size="32"></icon>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="form.line_settings === 'horizontal' ? '线条宽度' : '线条高度'">
<slider v-model="form.line_width" :max="1000"></slider>
</el-form-item>
<el-form-item label="线条粗细">
<slider v-model="form.line_size" :min="1" :max="100"></slider>
</el-form-item>
<el-form-item label="线条颜色">
<color-picker v-model="form.line_color" default-color="#FF3F3F"></color-picker>
</el-form-item>
<el-form-item label="是否置底">
<el-switch v-model="form.bottom_up" />
</el-form-item>
</card-container>
</el-form>
</div>
</template>
<script setup lang="ts">
import { location_compute } from '@/utils';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
}
});
// 默认值
const state = reactive({
diy_data: props.value
});
// 如果需要解构确保使用toRefs
const { diy_data } = toRefs(state);
const form = ref(diy_data.value.com_data);
const center_height = defineModel("height", { type: Number, default: 0 })
watch(diy_data, (val) => {
let width = 0;
let height = 0;
if (form.value.line_settings === 'horizontal') {
width = form.value.line_width;
height = form.value.line_size + 10;
} else {
width = form.value.line_size + 10;
height = form.value.line_width;
}
diy_data.value.location.x = location_compute(width, val.location.x, 390);
diy_data.value.location.y = location_compute(height, val.location.y, center_height.value);
diy_data.value.location.staging_y = diy_data.value.location.y;
form.value.com_width = width;
form.value.com_height = height;
form.value.staging_height = height;
}, {immediate: true, deep: true});
</script>
<style lang="scss" scoped>
.card.mb-8 {
.el-form-item:last-child {
margin-bottom: 0;
}
}
.border-style-item {
width: 3rem;
height: 2rem;
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<div class="img-outer re" :style="com_style">
<div :style="text_style">{{ form.text_title }}</div>
</div>
</template>
<script setup lang="ts">
import { radius_computer, padding_computer } from '@/utils';
const props = defineProps({
value: {
type: Object,
default: () => {
return {};
},
required: true
},
isPercentage: {
type: Boolean,
default: false
}
});
// 用于页面判断显示
const form = reactive(props.value);
const text_style = computed(() => {
let style = `font-size: ${ form.text_size }px;color: ${ form.text_color }; text-align: ${ form.text_location }; transform: rotate(${form.text_rotate}deg);text-decoration: ${ form.text_option };${ padding_computer(form.text_padding) };`;
if (form.text_weight == 'italic') {
style += `font-style: italic`;
} else if (form.text_weight == '500') {
style += `font-weight: 500`;
}
return style;
});
const com_style = computed(() => {
let style = `${ set_count() } background-color: ${ form.com_bg };`;
if (form.border_show) {
style += `border: ${form.border_size}px ${form.border_style} ${form.border_color}; ${ radius_computer(form.border_radius) };`;
}
return style;
});
const set_count = () => {
if (props.isPercentage) {
return '';
} else {
return `width: ${ form.com_width }px; height: ${ form.com_height }px;`;
}
};
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,165 @@
<template>
<div class="w h bg-f">
<el-form :model="form" label-width="80">
<card-container>
<div class="mb-12">文本设置</div>
<el-form-item label="文本内容">
<div class="flex-column w">
<el-input v-model="form.text_title" placeholder="请输入链接" type="textarea" :rows="3"></el-input>
<div class="flex-row align-c gap-10 mt-12">
<el-select v-model="form.data_source_id" value-key="id" placeholder="请选择图片数据字段" size="default" class="flex-1">
<el-option v-for="item in options" :key="item.id" :label="item.label" :value="item" />
</el-select>
<el-popover placement="top-start" :width="200" trigger="hover" content="this is content, this is content, this is content">
<template #reference>
<icon name="tips" size="24"></icon>
</template>
</el-popover>
</div>
</div>
</el-form-item>
<el-form-item label="链接">
<url-value v-model="form.text_link"></url-value>
</el-form-item>
<el-form-item label="文字颜色">
<color-picker v-model="form.text_color" default-color="#FF3F3F"></color-picker>
</el-form-item>
<el-form-item label="文字大小">
<el-radio-group v-model="form.text_weight" class="ml-4">
<el-radio value="500">加粗</el-radio>
<el-radio value="normal">正常</el-radio>
<el-radio value="italic">倾斜</el-radio>
</el-radio-group>
<el-form-item label="字号" label-width="40" class="mb-0 w">
<slider v-model="form.text_size" :max="100"></slider>
</el-form-item>
</el-form-item>
<el-form-item label="字符选项">
<el-radio-group v-model="form.text_option" class="ml-4">
<el-radio value="none"><span style="text-decoration: none">Aa</span></el-radio>
<el-radio value="underline"><span style="text-decoration: underline">Aa</span></el-radio>
<el-radio value="line-through"><span style="text-decoration: line-through">Aa</span></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="文字位置">
<el-radio-group v-model="form.text_location" is-button>
<el-tooltip content="左对齐" placement="top" effect="light">
<el-radio-button value="left"><icon name="iconfont icon-left"></icon></el-radio-button>
</el-tooltip>
<el-tooltip content="居中" placement="top" effect="light">
<el-radio-button value="center"><icon name="iconfont icon-center"></icon></el-radio-button>
</el-tooltip>
<el-tooltip content="右对齐" placement="top" effect="light">
<el-radio-button value="right"><icon name="iconfont icon-right"></icon></el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="内边距">
<padding :value="form.text_padding" @update:value="padding_change"></padding>
</el-form-item>
<el-form-item label="旋转角度">
<slider v-model="form.text_rotate" :max="1000"></slider>
</el-form-item>
<el-form-item label="是否置底">
<el-switch v-model="form.bottom_up" />
</el-form-item>
</card-container>
<div class="bg-f5 partition-line" />
<card-container>
<div class="mb-12">容器设置</div>
<el-form-item label="容器宽度">
<slider v-model="form.com_width" :max="1000"></slider>
</el-form-item>
<el-form-item label="容器高度">
<slider v-model="form.com_height" :max="1000"></slider>
</el-form-item>
<el-form-item label="背景颜色">
<color-picker v-model="form.com_bg" default-color="#FF3F3F"></color-picker>
</el-form-item>
</card-container>
<div class="bg-f5 partition-line" />
<card-container>
<div class="mb-12">边框设置</div>
<el-form-item label="边框显示">
<el-radio-group v-model="form.border_show" class="ml-4">
<el-radio :value="true">显示</el-radio>
<el-radio :value="false">隐藏</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="form.border_show">
<el-form-item label="边框颜色">
<color-picker v-model="form.border_color" default-color="#FF3F3F"></color-picker>
</el-form-item>
<el-form-item label="边框样式">
<el-radio-group v-model="form.border_style" class="ml-4">
<el-radio value="dashed"><div class="border-style-item" style="border: 1px dashed #979797"></div></el-radio>
<el-radio value="solid"><div class="border-style-item" style="border: 1px solid #979797"></div></el-radio>
<el-radio value="dotted"><div class="border-style-item" style="border: 1px dotted #979797"></div></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="边框圆角">
<radius :value="form.border_radius" @update:value="border_radius_change"></radius>
</el-form-item>
<el-form-item label="边框粗细">
<slider v-model="form.border_size" :max="1000"></slider>
</el-form-item>
</template>
</card-container>
</el-form>
</div>
</template>
<script setup lang="ts">
import { location_compute } from '@/utils';
import { pick } from 'lodash';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
options: {
type: Array<any>,
default: () => [],
},
});
// 默认值
const state = reactive({
diy_data: props.value,
});
// 如果需要解构确保使用toRefs
const { diy_data } = toRefs(state);
const form = ref(diy_data.value.com_data);
const center_height = defineModel('height', { type: Number, default: 0 });
const padding_change = (padding: any) => {
form.value.text_padding = Object.assign(form.value.text_padding, pick(padding, ['padding', 'padding_top', 'padding_bottom', 'padding_left', 'padding_right']));
};
const border_radius_change = (radius: any) => {
form.value.border_radius = Object.assign(form.value.border_radius, pick(radius, ['radius', 'radius_top_left', 'radius_top_right', 'radius_bottom_left', 'radius_bottom_right']));
};
watch(
diy_data,
(val) => {
diy_data.value.location.x = location_compute(form.value.com_width, val.location.x, 390);
diy_data.value.location.y = location_compute(form.value.com_height, val.location.y, center_height.value);
diy_data.value.location.staging_y = diy_data.value.location.y;
form.value.staging_height = form.value.com_height;
},
{ immediate: true, deep: true }
);
</script>
<style lang="scss" scoped>
.card.mb-8 {
.el-form-item:last-child {
margin-bottom: 0;
}
}
.border-style-item {
width: 3rem;
height: 2rem;
}
.partition-line {
height: 0.8rem;
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<div v-for="(item, index) in list" :key="index" class="flex-row align-s gap-12">
<div class="flex-col">
<el-color-picker v-model="item.color" show-alpha :predefine="predefine_colors" />
<div v-if="index == 0" class="connect-line"></div>
</div>
<icon name="reset" color="primary" size="16" class="c-pointer" @click="reset_event(index)"></icon>
</div>
</template>
<script setup lang="ts">
const predefine_colors = ref(['#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585', 'rgba(255, 69, 0, 0.68)', 'rgb(255, 120, 0)', 'hsv(51, 100, 98)', 'hsva(120, 40, 94, 0.5)', 'hsl(181, 100%, 37%)', 'hsla(209, 100%, 56%, 0.73)', '#c7158577']);
interface list_page {
color: string;
}
interface Props {
colorList: list_page[];
defaultColor: string;
}
const props = withDefaults(defineProps<Props>(), {
defaultColor: '',
});
const list = ref(props.colorList);
// 默认值
const reset_event = (index: number) => {
list.value[index].color = props.defaultColor;
};
</script>
<style lang="scss" scoped>
.connect-line {
width: 0.1rem;
height: 1.6rem;
background: #d8d8d8;
position: relative;
left: 1rem;
// 合并before和after重复代码
&::before,
&::after {
position: absolute;
left: -0.2rem;
content: '';
position: absolute;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: #ddd;
}
&::before {
top: -0.25rem;
}
&::after {
bottom: -0.25rem;
}
}
</style>

View File

@ -0,0 +1,379 @@
<!-- 上传组件 -->
<template>
<el-dialog v-model="dialog_visible" append-to-body fullscreen @close="close_event">
<template #header>
<div class="title re">
<div class="tc size-16 fw">编辑热区</div>
</div>
</template>
<el-scrollbar class="content-scrollbar">
<div class="pa-40 flex-row gap-40">
<div class="left-content flex-1 pa-20">
<el-scrollbar class="img-scrollbar">
<div class="img-container">
<div ref="imgBoxRef" @mousedown.prevent="start_drag" @mousemove.prevent="move_drag" @mouseup.prevent="end_drag">
<el-image :src="hot_list.img" class="w img" @selectstart.prevent @contextmenu.prevent @dragstart.prevent></el-image>
<div ref="areaRef" class="area" :style="init_drag_style"></div>
<div v-for="(item, index) in hot_list.hot" :key="index" class="area-box" :style="rect_style(item.drag_start, item.drag_end)" @mousedown.prevent="start_drag_area_box(index, $event)" @dblclick="dbl_drag_event(item, index)">
<div class="del-btn" @click.stop="del_area_event(index)"><icon name="close"></icon></div>
<div class="drag-btn" :data-index="index" @mousedown.prevent="start_drag_btn(index, $event)"></div>
<div class="text">
<div class="name">{{ item.name }}</div>
<div class="status" :class="!is_obj_empty(item.link) ? 'cr-primary' : 'cr-error'">{{ !is_obj_empty(item.link) ? '已设置' : '未设置' }}</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
</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.hot" :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>
</div>
</div>
</div>
</el-scrollbar>
<template #footer>
<span class="dialog-footer">
<el-button class="plr-28 ptb-10" type="primary" @click="confirm_event">完成</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="hot_dialog_visible" width="560" append-to-body @close="hot_close_event">
<template #header>
<div class="title re">
<div class="tc size-16 fw">设置热区</div>
</div>
</template>
<div class="content">
<el-form ref="formRef" :model="form" label-width="85px" class="pa-20 mt-16">
<el-form-item label="名称">
<el-input v-model="form.name" placeholder="请输入名称"></el-input>
</el-form-item>
<el-form-item label="热区跳转链接">
<url-value v-model="form.link"></url-value>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button class="plr-28 ptb-10" @click="hot_close_event">取消</el-button>
<el-button class="plr-28 ptb-10" type="primary" @click="hot_confirm_event">确定</el-button>
</span>
</template>
</el-dialog>
<el-button class="w" @click="open_hot_event"><icon name="add">编辑热区</icon></el-button>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash';
import { is_obj_empty } from '@/utils';
const app = getCurrentInstance();
/**
* @description: 热区
* @param modelValue{Object} 默认值
* @param dialog_visible {Boolean} 弹窗显示
* @return {*} update:modelValue
*/
const props = defineProps({});
const modelValue = defineModel({ type: Object as PropType<hotData>, default: {} });
const dialog_visible = defineModel('visibleDialog', { type: Boolean, default: false });
const hot_list = ref<hotData>({
img: '',
hot: [],
});
const hot_list_index = ref(0);
watch(
() => modelValue.value,
(val) => {
hot_list.value = cloneDeep(val);
},
{ immediate: true, deep: true }
);
//#region 左侧画布-----------------------------------------------start
const imgBoxRef = ref<HTMLElement | null>(null);
const rect_start = ref<rectCoords>({ x: 0, y: 0, width: 0, height: 0 });
const rect_end = ref<rectCoords>({ x: 0, y: 0, width: 0, height: 0 });
const areaRef = ref<HTMLElement | null>(null);
const init_drag_style = ref('');
const drag_bool = ref(false);
const drag_box_bool = ref(false);
const drag_box_scale_bool = ref(false);
const start_drag = (event: MouseEvent) => {
drag_bool.value = true;
if (!imgBoxRef.value) return;
rect_start.value.x = event.clientX - imgBoxRef.value.getBoundingClientRect().left;
rect_start.value.y = event.clientY - imgBoxRef.value.getBoundingClientRect().top;
rect_start.value.width = 0;
rect_start.value.height = 0;
};
const move_drag = (event: MouseEvent) => {
if (drag_bool.value) {
if (!imgBoxRef.value) return;
rect_end.value.x = event.clientX - imgBoxRef.value.getBoundingClientRect().left;
rect_end.value.y = event.clientY - imgBoxRef.value.getBoundingClientRect().top;
rect_end.value.width = rect_end.value.x - rect_start.value.x > 0 ? rect_end.value.x - rect_start.value.x : 0;
rect_end.value.height = rect_end.value.y - rect_start.value.y > 0 ? rect_end.value.y - rect_start.value.y : 0;
init_drag_style.value = `left: ${rect_start.value.x}px;top: ${rect_start.value.y}px;width: ${Math.max(rect_end.value.width, 1)}px;height: ${Math.max(rect_end.value.height, 1)}px;display: flex;`;
}
};
const end_drag = (event: MouseEvent) => {
drag_bool.value = false;
if (areaRef.value) areaRef.value.style.display = 'none';
if (!imgBoxRef.value) return;
init_drag_style.value = ``;
if (rect_end.value.width > 16 && rect_end.value.height > 16) {
hot_list.value.hot.push({
name: '热区' + (hot_list.value.hot.length + 1),
link: {},
drag_start: cloneDeep(rect_start.value),
drag_end: cloneDeep(rect_end.value),
});
}
rect_end.value = { x: 0, y: 0, width: 0, height: 0 };
};
const area_box_point = ref({ x: 0, y: 0 });
// area-box
const dbl_drag_event = (item: hotListData, index: number) => {
hot_dialog_visible.value = true;
form.value.link = item.link;
form.value.name = item.name;
hot_list_index.value = index;
};
const start_drag_area_box = (index: number, event: MouseEvent) => {
hot_list_index.value = index;
event.stopPropagation();
drag_box_bool.value = true;
let clone_drag_start = cloneDeep(hot_list.value.hot[hot_list_index.value].drag_start);
let clone_drag_end = cloneDeep(hot_list.value.hot[hot_list_index.value].drag_end);
// 记录原始位置
area_box_point.value = {
x: clone_drag_start.x - event.clientX,
y: clone_drag_start.y - event.clientY,
};
// 当子元素拖拽方法触发后夫元素方法不触发
document.onmousemove = (areaBoxEvent) => {
areaBoxEvent.stopPropagation();
if (drag_box_bool.value) {
if (!imgBoxRef.value) return;
const new_coordinate = {
x: areaBoxEvent.clientX + area_box_point.value.x,
y: areaBoxEvent.clientY + area_box_point.value.y,
};
// 左上边界判断
if (new_coordinate.x < 0) {
new_coordinate.x = 0;
}
if (new_coordinate.y < 0) {
new_coordinate.y = 0;
}
// 右下边界判断
if (new_coordinate.x + Math.max(clone_drag_end.width, 1) > imgBoxRef.value.getBoundingClientRect().width) {
new_coordinate.x = imgBoxRef.value.getBoundingClientRect().width - Math.max(clone_drag_end.width, 1) - 4;
}
if (new_coordinate.y + Math.max(clone_drag_end.height, 1) > imgBoxRef.value.getBoundingClientRect().height) {
new_coordinate.y = imgBoxRef.value.getBoundingClientRect().height - Math.max(clone_drag_end.height, 1) - 7;
}
hot_list.value.hot[hot_list_index.value].drag_start.x = new_coordinate.x;
hot_list.value.hot[hot_list_index.value].drag_start.y = new_coordinate.y;
}
};
document.onmouseup = (areaBoxEvent) => {
areaBoxEvent.stopPropagation();
drag_box_bool.value = false;
};
};
// drag-btn
const start_drag_btn = (index: number, event: MouseEvent) => {
hot_list_index.value = index;
event.stopPropagation();
drag_box_scale_bool.value = true;
let clone_drag_start = hot_list.value.hot[hot_list_index.value].drag_start;
let clone_drag_end = hot_list.value.hot[hot_list_index.value].drag_end;
document.onmousemove = (dragBtnEvent) => {
dragBtnEvent.stopPropagation();
//用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
if (drag_box_scale_bool.value) {
if (!imgBoxRef.value) return;
clone_drag_end.x = dragBtnEvent.clientX - imgBoxRef.value.getBoundingClientRect().left;
clone_drag_end.y = dragBtnEvent.clientY - imgBoxRef.value.getBoundingClientRect().top;
hot_list.value.hot[hot_list_index.value].drag_end = {
x: clone_drag_end.x,
y: clone_drag_end.y,
width: clone_drag_end.x - clone_drag_start.x > 0 ? clone_drag_end.x - clone_drag_start.x : 0,
height: clone_drag_end.y - clone_drag_start.y > 0 ? clone_drag_end.y - clone_drag_start.y : 0,
};
}
};
document.onmouseup = (dragBtnEvent2) => {
dragBtnEvent2.stopPropagation();
drag_box_scale_bool.value = false;
};
};
const del_area_event = (index: number) => {
hot_list.value.hot.splice(index, 1);
};
const rect_style = computed(() => {
return (start: rectCoords, end: rectCoords) => {
return `left: ${start.x}px;top: ${start.y}px;width: ${Math.max(end.width, 1)}px;height: ${Math.max(end.height, 1)}px;display: flex;`;
};
});
//#endregion 左侧画布-----------------------------------------------end
//#region 右侧热区编辑-----------------------------------------------start
const del_event = (index: number) => {
hot_list.value.hot.splice(index, 1);
};
//#endregion 右侧热区编辑-----------------------------------------------end
//#region 设置热区弹窗-----------------------------------------------start
const hot_dialog_visible = ref(false);
const form = ref({
link: {},
name: '',
});
const hot_close_event = () => {
hot_dialog_visible.value = false;
};
const hot_confirm_event = () => {
hot_list.value.hot[hot_list_index.value].link = form.value.link;
hot_list.value.hot[hot_list_index.value].name = form.value.name;
hot_close_event();
};
//#endregion 设置热区弹窗-----------------------------------------------end
//#region 热区开启关闭确认取消回调 -----------------------------------------------start
// 打开热区弹窗
const open_hot_event = () => {
if (modelValue.value.img.length > 0) {
dialog_visible.value = true;
} else {
ElMessage({
type: 'warning',
message: '请先选择图片',
});
}
};
// 取消回调
const close_event = () => {
dialog_visible.value = false;
};
// 确认回调
const confirm_event = () => {
modelValue.value = hot_list.value;
close_event();
};
//#endregion 热区开启关闭确认取消回调 -----------------------------------------------end
</script>
<style lang="scss" scoped>
.content-scrollbar {
height: calc(100vh - 13.8rem);
margin: 0 -1.6rem;
.left-content {
.img-scrollbar {
display: flex;
justify-content: center;
.img-container {
max-width: 60rem;
min-width: 30rem;
height: calc(100vh - 25.8rem);
position: relative;
.img {
user-select: none;
cursor: crosshair;
padding: 0 0.4rem 0.4rem 0;
}
.area {
position: absolute;
background: rgba(41, 128, 185, 0.3);
border: 1px dashed #34495e;
width: 0px;
height: 0px;
left: 0px;
top: 0px;
display: none;
}
.area-box {
position: absolute;
background: rgba(42, 148, 255, 0.25);
border: 1px dashed #8ec6ff;
display: flex;
justify-content: center;
align-items: center;
color: #1989fa;
font-size: 1.2rem;
cursor: move;
transition: transform 0.1s;
.del-btn {
display: flex;
justify-content: center;
align-items: center;
background: #1890ff;
color: #fff;
text-align: center;
border-radius: 0 0 0 0.3rem;
position: absolute;
right: 0.7rem;
top: 0.7rem;
transform: translate3d(50%, -50%, 0);
cursor: default;
width: 1.6rem;
height: 1.6rem;
line-height: 1.6rem;
z-index: 1;
i {
font-size: 0.9rem;
}
}
.drag-btn {
position: absolute;
width: 7px;
height: 7px;
background: #f0f0f0;
border: 1px solid #333;
right: -0.4rem;
bottom: -0.4rem;
cursor: nwse-resize;
z-index: 1;
}
.text {
overflow: hidden;
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 100%;
max-height: 100%;
text-align: center;
align-items: center;
color: #fff;
font-size: 1.2rem;
.name {
color: #fff;
margin: 0 0.2rem;
}
.status {
margin: 0 0.2rem;
}
}
}
}
}
}
.right-content {
.item {
max-width: 47.8rem;
.name {
width: 9.8rem;
}
}
}
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<div class="mult-color-picker">
<el-radio-group v-model="direction_type" @change="direction_type_change">
<el-radio value="0deg">横向</el-radio>
<el-radio value="90deg">纵向</el-radio>
<el-radio value="315deg">左斜</el-radio>
<el-radio value="45deg">右斜</el-radio>
</el-radio-group>
<div class="flex-col">
<div v-for="(item, index) in color_list" :key="index" class="flex-row align-s gap-12">
<div class="flex-col">
<el-color-picker v-model="item.color" show-alpha :predefine="predefine_colors" @change="change_color(index, $event)" />
<div v-if="index + 1 !== color_list.length" class="connect-line"></div>
</div>
<template v-if="index == 0">
<icon name="reset" color="primary" size="16" class="c-pointer" @click="reset_event"></icon>
</template>
<template v-else>
<icon name="close" color="c" size="12" class="c-pointer" @click="del_event(index)" />
</template>
</div>
<div class="add-color mt-15" @click="add_event">
<icon name="add"></icon>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
type: {
type: String,
default: '0deg',
},
// 颜色数组 ['','']
value: {
type: Array,
default: () => {
[''];
},
},
});
const predefine_colors = ref(['#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585', 'rgba(255, 69, 0, 0.68)', 'rgb(255, 120, 0)', 'hsv(51, 100, 98)', 'hsva(120, 40, 94, 0.5)', 'hsl(181, 100%, 37%)', 'hsla(209, 100%, 56%, 0.73)', '#c7158577']);
const direction_type = ref(props.type);
let color_list = reactive(
//将数组['#fff']改为对象数组
props.value.map((item: any) => {
return {
color: item,
};
})
);
const emit = defineEmits(['update:value']);
const direction_type_change = (type: any) => {
direction_type.value = type.toString();
update_value();
};
const reset_event = () => {
color_list = [{ color: '' }];
update_value();
};
const del_event = (index: number) => {
color_list.splice(index, 1);
update_value();
};
const add_event = () => {
color_list.push({ color: '' });
update_value();
};
const change_color = (index: number, color: string | null) => {
color_list[index].color = color;
update_value();
};
const update_value = () => {
let new_color_list = color_list.map((item) => item.color) || [];
emit('update:value', new_color_list, direction_type.value);
};
</script>
<style lang="scss" scoped>
.mult-color-picker {
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
.connect-line {
width: 0.1rem;
height: 1.6rem;
background: #d8d8d8;
position: relative;
left: 1rem;
// 合并before和after重复代码
&::before,
&::after {
position: absolute;
left: -0.2rem;
content: '';
position: absolute;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: #ddd;
}
&::before {
top: -0.25rem;
}
&::after {
bottom: -0.25rem;
}
}
.add-color {
width: 3.2rem;
height: 3.2rem;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid #dcdfe6;
border-radius: 0.4rem;
color: #dcdfe6;
cursor: pointer;
&:hover {
color: #d4d7de;
border-color: #d4d7de;
}
}
.icon-close:hover {
color: $cr-error;
}
.icon-reset:hover {
color: $cr-primary-dark;
}
}
</style>

View File

@ -0,0 +1,300 @@
<template>
<div>
<div class="decorate-cube">
<ul v-for="(n, index) in densityNum" :key="index" class="cube-col">
<li v-for="(i, index1) in densityNum" :key="index1" class="cube-item" :style="{ width: cubeCellWidth + 'px', height: cubeCellHeight + 'px' }" :data-x="n" :data-y="i" @click="onClickCubeItem($event)" @mouseenter="onEnterCubeItem($event)">
<div :class="['w h item', { 'item-selecting': isSelecting(n, i), 'item-selected': isSelected(n, i) }]">
<icon name="add" color="9" :style="{ 'line-height': cubeCellHeight + 'px' }"></icon>
</div>
</li>
</ul>
<div v-for="(item, index) in selectedList" :key="index" :class="['cube-selected', {'cube-selected_active': selected_active == index }]" :style="selected_style(item)" @click="selected_click(index)">
<div v-if="selected_active == index && props.flag" class="cube-del" @click.stop="on_selected_del(index)">
<icon name="close" color="f" size="8"></icon>
</div>
<template v-if="!isEmpty(item.img[0])">
<image-empty v-model="item.img[0]"></image-empty>
</template>
<template v-else>
<div class="cube-selected-text">
{{ Math.round((750 / densityNum) * (item.end.y - item.start.y + 1)) }}
x
{{ Math.round((750 / densityNum) * (item.end.x - item.start.x + 1)) }}
像素
</div>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { isEmpty } from 'lodash';
interface CubeItem {
start: {
x: number;
y: number;
};
end: {
x: number;
y: number;
};
img: uploadList[];
}
interface Props {
flag: boolean;
list: CubeItem[];
cubeWidth: number;
cubeHeight: number;
}
const props = withDefaults(defineProps<Props>(), {
list: () => [],
flag: false,
cubeWidth: 390,
cubeHeight: 390,
});
const selected_active = ref(0);
//#region 容器大小变更
const density = ref('4');
//#endregion
const selectingItem = reactive<any>({
tempStart: null,
tempEnd: null,
start: null,
end: null,
});
const selectedList = ref(props.list);
//单元魔方宽度。
const cubeCellWidth = computed(() => props.cubeWidth / parseInt(density.value));
//密度值。
const densityNum = computed(() => parseInt(density.value));
//单元魔方高度。
const cubeCellHeight = computed(() => props.cubeHeight / parseInt(density.value));
const emits = defineEmits(['selected_click']);
// 判断选择的内容的长度是否发生变化
watch(() => selectedList.value.length, (val) => {
if (val > 1) {
selected_active.value = val - 1;
} else {
selected_active.value = 0;
}
emits('selected_click', selected_active.value);
}, {deep: true});
const updateSelecting = () => {
//获取开始和结束之间的所有魔方单元。
const tempStart = selectingItem.tempStart;
const tempEnd = selectingItem.tempEnd;
selectingItem.start = {
x: Math.min(tempStart.x, tempEnd.x),
y: Math.min(tempStart.y, tempEnd.y),
};
selectingItem.end = {
x: Math.max(tempStart.x, tempEnd.x),
y: Math.max(tempStart.y, tempEnd.y),
};
};
//清除正在选择的。
const clearSelecting = () => {
selectingItem.tempStart = null;
selectingItem.tempEnd = null;
selectingItem.start = null;
selectingItem.end = null;
};
const coordFromCubeEvent = (event: any) => {
var el = event.currentTarget;
var x = el.getAttribute('data-x');
var y = el.getAttribute('data-y');
return { x: x, y: y };
};
const isContain = (x: number, y: number, item: CubeItem) => {
return item.start.x <= x && x <= item.end.x && item.start.y <= y && y <= item.end.y;
};
//魔方点击事件。
const onClickCubeItem = (event: any) => {
let domclass = event.currentTarget.getAttribute('class');
if (-1 !== domclass.indexOf('item-selected')) {
return;
}
let coord = coordFromCubeEvent(event);
if (null == selectingItem.tempStart) {
selectingItem.tempStart = coord;
selectingItem.tempEnd = coord;
selectingItem.start = coord;
selectingItem.end = coord;
return;
}
selectingItem.tempEnd = coord;
updateSelecting();
//加入选中的。
let selectedItem = {
start: selectingItem.start,
end: selectingItem.end,
img: [],
};
selectedList.value.push(selectedItem);
clearSelecting();
};
const onEnterCubeItem = (event: any) => {
if (selectingItem.tempStart) {
var coord = coordFromCubeEvent(event);
selectingItem.tempEnd = coord;
updateSelecting();
}
};
// 删除当前选中的内容
const on_selected_del = (index: number) => {
clearSelecting();
// splice() 会先从原数组中添加/删除项目 然后返回被删除的项目。
selectedList.value.splice(index, 1);
};
//判断是否正在选择
const isSelecting = (x: number, y: number) => {
const item = selectingItem;
if (item.tempStart) {
return isContain(x, y, item);
}
return false;
};
//判断是否已经选择。
const isSelected = (x: number, y: number) => {
for (var i = 0; i < selectedList.value.length; i++) {
if (isContain(x, y, selectedList.value[i])) {
return true;
}
}
return false;
};
//计算选中层的宽度。
const getSelectedWidth = (item: CubeItem) => {
return (item.end.x - item.start.x + 1) * cubeCellWidth.value;
};
//计算选中层的高度。
const getSelectedHeight = (item: CubeItem) => {
return (item.end.y - item.start.y + 1) * cubeCellHeight.value;
};
//计算选中层的右边距离。
const getSelectedTop = (item: CubeItem) => {
return (item.start.y - 1) * cubeCellHeight.value;
};
//计算选中层的左边距离。
const getSelectedLeft = (item: CubeItem) => {
return (item.start.x - 1) * cubeCellWidth.value;
};
// 生成的样式
const selected_style = (item: CubeItem) => {
return `width: ${ getSelectedWidth(item) }px; height: ${ getSelectedHeight(item) }px; top: ${ getSelectedTop(item) }px; left: ${ getSelectedLeft(item) }px;`
}
// 选中的点击事件
const selected_click = (index: number) => {
selected_active.value = index;
emits('selected_click', index);
};
</script>
<style lang="scss" scoped>
.decorate-cube {
position: relative;
.cube-col {
float: left;
list-style: none;
padding: 0;
margin: 0;
}
.cube-item {
background: #f5f5f5;
cursor: pointer;
text-align: center;
box-sizing: border-box;
position: relative;
.item {
border: 1px solid #fff;
border-top: 0;
}
.item-selecting {
background: #e0edff;
position: absolute;
z-index: 99999;
}
.item-selected {
background: #e0edff;
}
}
.cube-item:first-child {
.item {
border-top: 0;
}
}
.cube-item:last-child {
.item {
border-bottom: 0;
}
}
.cube-col:first-child {
.cube-item .item {
border-left: 0;
}
}
.cube-col:last-of-type {
.cube-item .item {
border-right: 0;
}
}
.cube-selected-text {
font-size: 12px;
width: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
.cube-selected {
position: absolute;
background-color: #eee;
border: 1px solid #fff;
text-align: center;
color: $cr-primary;
cursor: pointer;
box-sizing: border-box;
z-index: 2;
}
.cube-selected_active {
border: 1px solid $cr-primary;
}
.cube-del {
background: $cr-primary;
position: absolute;
top: 0;
right: 0;
height: 1.6rem;
width: 1.6rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
:deep(.el-image) {
height: 100%;
width: 100%;
background-color: #fff;
.el-image__inner {
object-fit: cover;
}
.image-slot img {
width: 6rem;
}
}
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<card-container class="card-container-br">
<div class="mb-12">显示内容</div>
<el-form-item label="是否显示">
<el-checkbox-group v-model="form.is_show">
<el-checkbox v-for="item in base_list.list_show_list" :key="item.value" :value="item.value">{{ item.name }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</card-container>
<card-container>
<div class="mb-12">购物车设置</div>
<el-form-item label="是否显示">
<el-switch v-model="form.is_shop_show"></el-switch>
</el-form-item>
<el-form-item label="按钮样式" class="align-c">
<div class="flex-row align-c jc-s gap-20 shopping_button_all">
<div v-for="item in base_list.shopping_button_list" :key="item.value" :class="['pa-10 re', { 'br-c br-primary radius-sm': shop_type(item) }]" @click="shopping_button_click(item)">
<template v-if="item.value == '0'">
<div class="pl-13 pr-13 round size-12 bg-primary cr-f shopping_button">{{ item.name }}</div>
</template>
<template v-else-if="item.value == '1'">
<div class="pl-11 pr-11 round size-12 bg-primary cr-f shopping_button">{{ item.name }}</div>
</template>
<template v-else-if="item.value == '2'">
<icon class="shopping_button round pl-6 pr-6 bg-primary " name="add" color="f" size="16"></icon>
</template>
<template v-else>
<icon class="shopping_button round pl-6 pr-6 bg-primary" name="cart" color="f" size="16"></icon>
</template>
<div v-if="shop_type(item)" class="button-checked">
<icon name="true" color="f" size="8"></icon>
</div>
</div>
</div>
</el-form-item>
<el-form-item label="是否显示" label-width="140">
<el-radio-group v-model="form.shop_button_size">
<el-radio v-for="item in base_list.shopping_button_size" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否显示">
<el-radio-group v-model="form.shop_button_effect">
<el-radio v-for="item in base_list.shopping_cart_list" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
</card-container>
</template>
<script setup lang="ts">
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
});
const state = reactive({
form: props.value,
});
// 如果需要解构确保使用toRefs
const { form } = toRefs(state);
const base_list = {
list_show_list: [
{ name: '商品名称', value: '0' },
{ name: '商品标签', value: '1' },
{ name: '商品售价', value: '2' },
{ name: '商品销量', value: '3' },
{ name: '商品评分', value: '4' },
{ name: '商品原价', value: '5' },
],
shopping_button_list: [
{ name: '购买', value: '0' },
{ name: '立即购买', value: '1' },
{ name: '添加', value: '2' },
{ name: '购物车', value: '3' },
],
shopping_cart_list: [
{ name: '进入商品详情页', value: '0' },
{ name: '商品加购', value: '1' }
],
shopping_button_size: [
{ name: '大', value: '0' },
{ name: '中', value: '1' },
{ name: '小', value: '2' },
]
};
const shop_type = computed(() => {
return (item: { value: string; }) => {
return item.value == form.value.shop_type;
};
});
const shopping_button_click = (item: { value: string; }) => {
form.value.shop_type = item.value;
};
</script>
<style lang="scss">
.card-container-br {
border-bottom: 0.8rem solid #f0f2f5;
}
.shopping_button {
height: 2.7rem;
line-height: 2.7rem;
}
.shopping_button_all {
height: 4.7rem;
box-sizing: border-box;
}
.button-checked {
width: 0;
height: 0;
position: absolute;
right: 0;
bottom: 0;
border: 1.1rem solid #000;
border-color: transparent $cr-primary $cr-primary transparent;
.iconfont {
width: 1rem;
height: 2.1rem;
display: flex;
align-items: flex-end;
justify-content: center;
font-weight: 700;
}
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<div class="tabs flex-row oh">
<template v-for="(item, index) in tabs.content.tabs_list" :key="index">
<div class="item nowrap flex-col jc-c gap-4" :class="tabs_style + (index == 0 ? ' active' : '')">
<img class="img" src="@/assets/images/components/model-user-info/avatar.png" />
<div class="title" :style="index == 0 ? tabs_style_style.tabs_title_checked : tabs_style_style.tabs_title">{{ item.title }}</div>
<div class="desc">{{ item.desc }}</div>
<icon name="checked-1" class="icon"></icon>
<div class="bottom_line" :style="tabs_style_style.tabs_check"></div>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { gradient_computer } from '@/utils';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
});
const tabs = ref(props.value);
const tabs_style = computed(() => {
let tabs_style = '';
if (tabs.value.content.tabs_style == '1') {
tabs_style = 'tabs-style-2';
} else if (tabs.value.content.tabs_style == '2') {
tabs_style = 'tabs-style-3';
} else if (tabs.value.content.tabs_style == '3') {
tabs_style = 'tabs-style-4';
} else if (tabs.value.content.tabs_style == '4') {
tabs_style = 'tabs-style-5';
} else {
tabs_style = 'tabs-style-1';
}
return tabs_style;
});
const tabs_style_style = computed(() => {
const new_gradient_params = {
color_list: tabs.value.style.tabs_checked,
direction: tabs.value.style.tabs_direction,
};
const new_style = {
tabs_check: gradient_computer(new_gradient_params),
tabs_title_checked: 'font-weight:' + tabs.value.style.tabs_weight_checked + ';' + 'font-size:' + tabs.value.style.tabs_size_checked + 'px;' + 'color:' + tabs.value.style.tabs_color_checked,
tabs_title: 'font-weight:' + tabs.value.style.tabs_weight + ';' + 'font-size:' + tabs.value.style.tabs_size + 'px;' + 'color:' + tabs.value.style.tabs_color,
};
return new_style;
});
</script>
<style lang="scss" scoped>
.tabs {
max-width: 39rem;
.item {
padding: 0.5rem 0;
margin: 0 1rem;
position: relative;
&:first-of-type {
margin-left: 0;
}
&:last-of-type {
margin-right: 0;
}
.title {
font-size: 1.4rem;
text-align: center;
}
.desc {
font-size: 1.1rem;
color: #999;
text-align: center;
display: none;
}
.bottom_line {
width: 100%;
height: 0.3rem;
border-radius: 1rem;
background-color: red;
position: absolute;
left: 0;
right: 0;
bottom: 0;
display: none;
}
.icon {
position: absolute;
left: 0;
right: 0;
bottom: 0;
text-align: center;
font-size: 2rem;
display: none;
}
.img {
width: 3.9rem;
height: 3.9rem;
border-radius: 100%;
border: 0.1rem solid transparent;
display: none;
}
&.tabs-style-1 {
&.active {
.bottom_line {
display: block;
}
}
}
&.tabs-style-2 {
&.active {
.desc {
background: red;
color: #fff;
}
}
.desc {
border-radius: 2rem;
padding: 0.2rem 0.6rem;
display: inline-block;
}
}
&.tabs-style-3 {
&.active {
.title {
background: red;
border-radius: 2rem;
padding: 0.2rem 1.2rem;
color: #fff;
}
}
}
&.tabs-style-4 {
padding-bottom: 1.8rem;
&.active {
.title {
color: red;
}
.icon {
color: red;
display: block;
}
}
}
&.tabs-style-5 {
align-items: center;
&.active {
.title {
font-size: 1.1rem;
background: red;
border-radius: 2rem;
padding: 0.2rem 0.7rem;
color: #fff;
}
.img {
border-color: red;
}
}
.img {
display: block;
}
}
}
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<el-radio-group v-model="typeface" class="ml-4">
<el-radio v-for="item in font_weight" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
<el-form-item label="字号" label-width="40" class="mb-0 w">
<slider v-model="size" :max="100"></slider>
</el-form-item>
</template>
<script setup lang="ts">
const typeface = defineModel('typeface', {
type: String,
default: 'normal'
});
const size = defineModel('size', {
type: Number,
default: 15
});
const font_weight = [
{ name: '加粗', value: '500' },
{ name: '正常', value: 'normal' },
];
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,157 @@
.upload-content {
height: 57.4rem;
gap: 4.5rem;
.left-content {
width: 22.5rem;
.el-tree {
--el-tree-node-content-height: 40px;
}
}
.right-content {
position: relative;
.el-button--primary.is-plain {
--el-button-bg-color: transparent;
}
.right-classify {
width: 21.9rem;
}
.right-search {
width: 22.5rem;
}
.img-content {
width: calc(100% + 2rem);
margin: 0 -1rem;
.item {
width: calc(100% / 7 - 1.286rem);
.badge {
:deep(.el-badge__content.is-fixed) {
min-width: 1.8rem;
height: 1.8rem;
line-height: 1.8rem;
text-align: center;
padding: 0;
}
.item-content {
width: 100%;
height: 10rem;
.check-icon {
background: rgba(0, 0, 0, 0.5);
border-radius: 0.4rem;
opacity: 0;
transition: all 0.3s linear;
&.active {
opacity: 1;
}
}
.oprate {
position: absolute;
bottom: 0rem;
left: 0;
right: 0;
z-index: 1;
height: 2.4rem;
overflow: hidden;
.oprate-content {
opacity: 0;
transition: all 0.5s linear;
position: absolute;
bottom: -2.4rem;
left: 0;
right: 0;
z-index: 1;
background-color: #ccc;
border-bottom-left-radius: 0.4rem;
border-bottom-right-radius: 0.4rem;
height: 2.4rem;
}
.oprate-icon {
position: relative;
&::before {
content: '';
width: 0.1rem;
height: 40%;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
background-color: #bbb;
}
&::after {
content: '';
width: 0.1rem;
height: 40%;
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
background-color: #bbb;
}
}
}
}
}
.name {
font-size: 1.2rem;
height: 2.8rem;
line-height: 2.8rem;
}
&:hover {
.oprate-content {
opacity: 1 !important;
bottom: 0 !important;
}
}
}
}
}
}
.upload-btn {
cursor: pointer;
&-style-0 {
position: relative;
background: #fafcff;
border-radius: 0.2rem;
border: 0.1rem dashed #d7eeff;
display: flex;
justify-content: center;
align-items: center;
}
&-style-1 {
position: relative;
background: transparent;
border: 1px solid #dddddd;
display: flex;
justify-content: center;
align-items: center;
.upload-btn-bottom-text {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1.8rem;
font-size: 1.2rem;
color: #fff;
background: rgba(0, 0, 0, 0.59);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
}
&-style-2 {
position: relative;
background: transparent;
border-radius: 0.2rem;
border: 1px solid #dddddd;
display: flex;
justify-content: center;
align-items: center;
}
.upload-del-icon {
position: absolute;
top: -6px;
right: -6px;
z-index: 1;
line-height: normal;
}
}

View File

@ -0,0 +1,542 @@
<!-- 上传组件 -->
<template>
<el-dialog v-model="dialogVisible" class="radius-lg" width="1168" append-to-body>
<template #header>
<div class="title re">
<el-radio-group v-model="upload_type" is-button @change="upload_type_change">
<el-radio-button value="img" :disabled="!(upload_type == 'img') && isCheckConfirm">图片</el-radio-button>
<el-radio-button value="video" :disabled="!(upload_type == 'video') && isCheckConfirm">视频</el-radio-button>
<el-radio-button value="file" :disabled="!(upload_type == 'file') && isCheckConfirm">文件</el-radio-button>
</el-radio-group>
<div class="middle size-16 fw">附件管理</div>
</div>
</template>
<div class="upload-content pa-20 flex-row">
<div class="left-content">
<el-input v-model="search_filter" class="mb-10" placeholder="请输入分类名称">
<template #suffix>
<icon name="search" size="18"></icon>
</template>
</el-input>
<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" />
</el-scrollbar>
</div>
<div class="right-content flex-1 flex-width">
<div class="flex-row jc-sb align-c mb-15">
<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>
</div>
<div class="right-search">
<el-input v-model="search_name" :placeholder="'请输入' + upload_type_name + '名称'" @input="get_list">
<template #suffix>
<icon name="search" size="18"></icon>
</template>
</el-input>
</div>
</div>
<div class="img-content pr">
<!-- 574px -->
<el-scrollbar height="440px">
<div class="flex-row flex-wrap align-c gap-y-15 gap-x-10 pa-10">
<div v-for="(item, index) in upload_list" :key="index" class="item" @click="check_img_event(item)">
<el-badge :value="view_list_value.findIndex((i) => i.id === item.id) == -1 ? '' : view_list_value.findIndex((i) => i.id === item.id) + 1" class="badge flex-col gap-5 w" :hidden="view_list_value.findIndex((i) => i.id === item.id) == -1">
<div class="item-content re br-f5 radius">
<template v-if="upload_type == 'video'">
<video :src="item.url" class="w h" @error="handle_error(index)"></video>
<div v-if="item.error == true" class="bg-f5 img flex-row jc-c align-c radius h w abs top-0">
<icon name="video" size="42" color="9"></icon>
</div>
</template>
<template v-else-if="upload_type == 'file'">
<div class="bg-f5 img flex-row jc-c align-c radius h w">
<icon :name="ext_file_name_list_map.filter((ext) => ext.type == item.type).length > 0 && ext_file_name_list_map.filter((ext) => ext.type == item.type)[0].type == item.type ? ext_file_name_list_map.filter((ext) => ext.type == item.type)[0].icon : 'file'" size="42" color="9"></icon>
</div>
</template>
<template v-else>
<el-image :src="item.url" fit="contain" class="w h">
<template #error>
<div class="bg-f5 img flex-row jc-c align-c radius h w">
<icon name="error-img" size="42" color="9"></icon>
</div>
</template>
</el-image>
</template>
<div class="check-icon fill flex-row jc-c align-c" :class="view_list_value.findIndex((i) => i.id === item.id) !== -1 ? 'active' : ''">
<icon name="true-o" color="f" size="26"></icon>
</div>
<div class="oprate">
<div class="oprate-content flex-row jc-sa align-c">
<div class="flex-1 tc c-pointer" @click.stop="edit_event(item, index)">
<icon name="edit" class="flex-1" size="14" color="f"></icon>
</div>
<div v-if="upload_type !== 'file'" class="oprate-icon flex-1 tc c-pointer" @click.stop="preview_event(item, index)">
<icon name="eye" size="14" color="f"></icon>
</div>
<div class="flex-1 tc c-pointer" @click.stop="del_event(item)">
<icon name="del" size="14" color="f"></icon>
</div>
</div>
</div>
</div>
<div class="text-line-1 name">
<template v-if="edit_index !== -1 && edit_index === index">
<el-input v-model="item.original" type="text" placeholder="请输入内容" size="small" @change="edit_input_change" @blur="edit_input_blur" />
</template>
<template v-else>
<div class="ptb-1 plr-7">
{{ item.original }}
</div>
</template>
</div>
</el-badge>
</div>
</div>
</el-scrollbar>
<div v-if="preview_switch_video && upload_type == 'video'">
<div class="middle clickable-area" :class="preview_url ? '' : 'hide'">
<!-- 视频预览 -->
<!-- 自动播放 -->
<video ref="videoPlayer" width="320" height="240" controls autoplay :src="preview_url"></video>
</div>
</div>
<div class="mt-10 flex-row jc-e">
<el-pagination :current-page="page" :page-size="21" :pager-count="5" layout="prev, pager, next" :total="data_total" @current-change="get_list" />
</div>
</div>
</div>
</div>
<template v-if="isCheckConfirm" #footer>
<span class="dialog-footer">
<el-button class="plr-28 ptb-10" @click="dialogVisible = false">取消</el-button>
<el-button class="plr-28 ptb-10" type="primary" @click="confirm_event">确定</el-button>
</span>
</template>
</el-dialog>
<template v-if="!isCustomDialog">
<div class="flex-col h">
<div class="flex-row flex-wrap gap-10 h">
<div v-for="(item, index) in modelValue" :key="item.id" :class="'upload-btn upload-btn-style-' + styles + ' ' + (styles == 2 ? 'br-none' : '')" :style="'height:' + upload_size + ';width:' + upload_size + ';'" @click="replace_file_event(index)">
<div class="upload-del-icon" @click.stop="del_upload_event(index)">
<icon name="close-o" color="c" size="14"></icon>
</div>
<template v-if="type == 'video'">
<video :src="item.url" class="w h"></video>
<div v-if="item.error == true" class="bg-f5 img flex-row jc-c align-c radius h w abs top-0">
<icon name="video" :size="Number(size) / 2 + ''" color="9"></icon>
</div>
</template>
<template v-else-if="type == 'file'">
<div class="bg-f5 img flex-row jc-c align-c radius h w">
<icon :name="ext_file_name_list_map.filter((ext) => ext.type == item.type).length > 0 && ext_file_name_list_map.filter((ext) => ext.type == item.type)[0].type == item.type ? ext_file_name_list_map.filter((ext) => ext.type == item.type)[0].icon : 'file'" :size="Number(size) / 2 + ''" color="9"></icon>
</div>
</template>
<template v-else>
<el-image :src="item.url" fit="contain" class="w h">
<template #error>
<div class="bg-f5 img flex-row jc-c align-c radius h w">
<icon name="error-img" :size="Number(size) / 2 + ''" color="9"></icon>
</div>
</template>
</el-image>
</template>
<template v-if="styles == 1">
<div class="upload-btn-bottom-text">替换</div>
</template>
</div>
<div v-if="limit !== modelValue.length" :class="'upload-btn upload-btn-style-' + styles" :style="'height:' + upload_size + ';width:' + upload_size + ';'" @click="dialogVisible = true">
<icon name="add" :size="Number(size) / 2 + ''" color="c"></icon>
</div>
</div>
<div v-if="isTips" class="size-12 cr-9">{{ tipsText }}</div>
</div>
</template>
<!-- 图片预览 -->
<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>
</template>
<script lang="ts" setup>
const app = getCurrentInstance();
/**
* @description: 图片上传
* @param modelValue{uploadList[]} 默认值
* @param visibleDialog{Boolean} 弹窗开启关闭
* @param type{String} 上传类型 默认图片 1.图片(img) 2.视频(video) 3.文件(file)
* @param isCustomDialog{Boolean} 是否自定义弹窗, 配置true后将不会显示上传按钮改为传v-model:visible-dialog=""来开启关闭弹窗,通过@update:v-model=""来获取最新数据
* @param isCheckConfirm{Boolean} 弹窗是否需要操作提交取消按钮
* @param limit{Number} 上传数量限制
* @param isTips{Boolean} 是否显示提示文字
* @param tipsText{String} 提示文字
* @param size{Number|String} 上传图片大小
* @param style{Number} 样式 0.默认样式 1.自定义样式1 2.自定义样式2
* @return {*} update:modelValue
*/
const props = defineProps({
type: {
type: String,
default: 'img', // img/video/file
},
isCustomDialog: {
type: Boolean,
default: false,
},
isCheckConfirm: {
type: Boolean,
default: true,
},
limit: {
type: Number,
default: 10,
},
isTips: {
type: Boolean,
default: false,
},
tipsText: {
type: String,
default: '建议尺寸690*240px',
},
size: {
type: [Number, String],
default: 72,
},
styles: {
type: [Number, String],
default: 0,
},
});
const modelValue = defineModel({ type: Array as PropType<uploadList[]>, default: [] });
const view_list_value = ref<uploadList[]>([]);
// 弹窗显示
// const dialogVisible = ref(props.visibleDialog);
const dialogVisible = defineModel('visibleDialog', { type: Boolean, default: false });
// 文件后缀分类
const ext_img_name_list = ref(['.png', '.jpg', '.jpeg', '.bmp', '.webp', '.gif']);
const ext_video_name_list = ref(['.flv', '.swf', '.mkv', '.avi', '.rm', '.rmvb', '.mpeg', '.mpg', '.ogg', '.ogv', '.mov', '.wmv', '.mp4', '.webm']);
const ext_file_name_list = ref(['.png', 'jpg', 'jpeg', 'bmp', 'webp', 'gif', '.flv', '.swf', '.mkv', '.avi', '.rm', '.rmvb', '.mpeg', '.mpg', '.ogg', '.ogv', '.mov', '.wmv', '.mp4', '.webm', '.mp3', '.csv', '.wav', '.mid', '.cab', '.iso', '.ofd', '.xml', '.rar', '.zip', '.tar', '.gz', '.7z', '.bz2', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf', '.txt', '.md', '.vsd']);
const ext_file_name_list_map = ref([
{ type: '.png', icon: 'error-img' },
{ type: 'jpg', icon: 'error-img' },
{ type: 'jpeg', icon: 'error-img' },
{ type: 'bmp', icon: 'error-img' },
{ type: 'webp', icon: 'error-img' },
{ type: 'gif', icon: 'error-img' },
{ type: '.flv', icon: 'video' },
{ type: '.swf', icon: 'video' },
{ type: '.mkv', icon: 'video' },
{ type: '.avi', icon: 'video' },
{ type: '.rm', icon: 'video' },
{ type: '.rmvb', icon: 'video' },
{ type: '.mpeg', icon: 'video' },
{ type: '.mpg', icon: 'video' },
{ type: '.ogg', icon: 'video' },
{ type: '.ogv', icon: 'video' },
{ type: '.mov', icon: 'video' },
{ type: '.wmv', icon: 'video' },
{ type: '.mp4', icon: 'video' },
{ type: '.webm', icon: 'video' },
{ type: '.mp3', icon: 'vf' },
{ type: '.csv', icon: 'file' },
{ type: '.wav', icon: 'file' },
{ type: '.mid', icon: 'file' },
{ type: '.cab', icon: 'file' },
{ type: '.iso', icon: 'file' },
{ type: '.ofd', icon: 'file' },
{ type: '.xml', icon: 'file' },
{ type: '.rar', icon: 'zip' },
{ type: '.zip', icon: 'zip' },
{ type: '.tar', icon: 'zip' },
{ type: '.gz', icon: 'zip' },
{ type: '.7z', icon: 'zip' },
{ type: '.bz2', icon: 'bz2' },
{ type: '.doc', icon: 'word' },
{ type: '.docx', icon: 'word' },
{ type: '.xls', icon: 'excel' },
{ type: '.xlsx', icon: 'excel' },
{ type: '.ppt', icon: 'ppt' },
{ type: '.pptx', icon: 'ppt' },
{ type: '.pdf', icon: 'pdf' },
{ type: '.txt', icon: 'txt' },
{ type: '.md', icon: 'txt' },
{ type: '.vsd', icon: 'vsd' },
]);
// 弹窗上传显示
const upload_model_visible = ref(false);
// 上传类型
const upload_type = ref(props.type);
const upload_size = computed(() => {
const size = props.size.toString();
return size.includes('%') ? size : size + 'px';
});
// 上传类型转换成name
const upload_type_name = computed(() => {
return upload_type.value === 'img' ? '图片' : upload_type.value === 'video' ? '视频' : '文件';
});
// 切换图片/视频/文件
const upload_type_change = (type: any) => {
view_list_value.value = [];
};
const treeRef = ref();
const defaultProps = {
children: 'children',
label: 'label',
};
// 分类查询
const search_filter = ref('');
watch(search_filter, (val) => {
treeRef.value!.filter(val);
});
// 名称查询
const search_name = ref('');
// 总页数
// const page_total = ref(0);
// 当前页
const page = ref(1);
// 总数量
const data_total = ref(0);
interface Tree {
id: number;
label: string;
children?: Tree[];
}
const filter_node = (value: string, data: any): boolean => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
const type_data: Tree[] = [
{
id: 0,
label: '全部图片',
},
{
id: 1,
label: '金刚区',
children: [
{
id: 2,
label: '金刚区 1-1',
children: [{ id: 3, label: '金刚区 1-1-1' }],
},
],
},
];
// 图片/视频/文件移动至
const category_list = [
{
value: 'component',
label: 'Component',
children: [
{
value: 'basic',
label: 'Basic',
children: [
{
value: 'layout',
label: 'Layout',
},
],
},
],
},
];
// 已上传数据的列表
const upload_list = ref<uploadList[]>([
{ id: 1, url: '/src/assets/images/layout/main/phone.png', original: '头像1', title: '头像1', ext: '.png', type: 'img' },
{ id: 2, url: '/src/assets/images/components/model-user-info/avatar.png', original: '头像2', ext: '.jpeg', type: 'img' },
{ id: 3, url: '/src/assets/images/components/model-hot/test-1.png', original: '头像3', title: '头像3', ext: '.png', type: 'img' },
{ id: 4, url: '/src/assets/images/components/model-hot/test-2.png', original: '头像4', ext: '.jpeg', type: 'img' },
{ 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_event = (item: any) => {
const item_id = item.id;
const index = view_list_value.value.findIndex((item: any) => item.id === item_id);
if (index !== -1) {
view_list_value.value.splice(index, 1);
} else {
if (is_replace.value) {
view_list_value.value = [item];
} else {
if (props.limit == 1) {
view_list_value.value = [item];
} else {
view_list_value.value.push(item);
}
}
}
};
// 预览开关
const preview_switch_img = ref(false);
const preview_switch_video = ref(false);
// 视频预览的路径
const preview_url = ref('');
const edit_index = ref(-1);
// 监听点击事件
onMounted(() => {
document.addEventListener('click', video_show);
});
// 移除监听事件
onUnmounted(() => {
document.removeEventListener('click', video_show);
});
// 预览视频
const video_show = (event: any) => {
if (!preview_switch_video.value) return;
if (!event.target.closest('.clickable-area')) {
preview_switch_video.value = false;
preview_url.value = '';
}
};
// 编辑图片/视频/文件名称
const edit_event = (item: any, index: number) => {
edit_index.value = index;
};
// 输入框 输入完成
const edit_input_change = (val: string) => {
edit_index.value = -1;
};
// 输入框失去焦点
const edit_input_blur = () => {
edit_index.value = -1;
};
// 预览图片/视频
const preview_event = (item: any, index: number) => {
preview_url.value = item.url;
if (upload_type.value == 'img') {
preview_switch_img.value = true;
} else if (upload_type.value == 'video') {
preview_switch_video.value = true;
}
};
// 预览关闭
const preview_close = () => {
preview_switch_img.value = false;
};
// 删除图片/视频/文件
const del_event = (item: uploadList) => {
app?.appContext.config.globalProperties.$common.message_box('删除后不可恢复,确定继续吗?', 'warning').then(() => {
ElMessage({
type: 'success',
message: '删除成功!',
});
// 调用删除接口,然后,更新数据
});
};
// 打开上传弹窗
const upload_model_open = () => {
upload_model_visible.value = true;
};
// 批量删除
const mult_del_event = () => {
app?.appContext.config.globalProperties.$common.message_box('删除后不可恢复,确定继续吗?', 'warning').then(() => {
ElMessage({
type: 'success',
message: '删除成功!',
});
// console.log('选中的数据 = ', view_list_value.value);
// 调用删除接口,然后,更新数据
});
};
// 查询文件
const search_data = ref({
page: page.value,
type: '',
name: search_name.value,
});
// 查询文件
const get_list = () => {
console.log('查询接口', search_data);
};
// 左侧分类树结构节点点击事件
const tree_node_event = (data: any) => {
search_filter.value = data.id;
get_list();
};
// 确认
const confirm_event = () => {
dialogVisible.value = false;
if (props.limit == 1) {
modelValue.value = view_list_value.value;
} else {
if (is_replace.value) {
// 替换modelValue的replace下标下的文件
modelValue.value.splice(replace_index.value, 1, view_list_value.value[0]);
} else {
if (props.limit >= view_list_value.value.length + modelValue.value.length) {
// 数组合并
modelValue.value = modelValue.value.concat(view_list_value.value);
// view_list_value.value.forEach((item: uploadList) => {
// modelValue.value.push(item);
// });
} else {
app?.appContext.config.globalProperties.$common.alert(`最多上传 ${props.limit} 个文件!`, 'warning');
}
}
}
view_list_value.value = [];
search_filter.value = '';
is_replace.value = false;
replace_index.value = -1;
};
// 替换标识
const is_replace = ref(false);
// 替换的文件的下标
const replace_index = ref(-1);
// 上传回显替换文件事件
const replace_file_event = (index: number) => {
dialogVisible.value = true;
is_replace.value = true;
replace_index.value = index;
};
// 上传回显删除事件
const del_upload_event = (index: number) => {
const new_model_val = JSON.parse(JSON.stringify(modelValue.value));
new_model_val.splice(index, 1);
modelValue.value = new_model_val;
};
const handle_error = (index: number) => {
// 当视频加载失败时触发
upload_list.value[index].error = true;
};
//#region 上传组件回调 -----------------------------------------------start
// 关闭上传弹窗回调
const close_upload_model = (data: any) => {
if (props.isCheckConfirm) {
dialogVisible.value = false;
if (data.web_image.length > 0) {
const new_web_file = {
url: data.web_image,
};
if (props.limit == 1) {
modelValue.value = [new_web_file];
} else {
if (is_replace.value) {
// 替换modelValue的replace下标下的文件
modelValue.value.splice(replace_index.value, 1, new_web_file);
} else {
if (props.limit >= view_list_value.value.length + modelValue.value.length) {
// 数组合并
modelValue.value.push(new_web_file);
} else {
app?.appContext.config.globalProperties.$common.alert(`最多上传 ${props.limit} 个文件!`, 'warning');
}
}
}
}
}
};
//#endregion 上传组件回调 -----------------------------------------------end
</script>
<style lang="scss" scoped>
@import 'index.scss';
</style>

View File

@ -0,0 +1,573 @@
<!-- 上传组件 -->
<template>
<el-dialog v-model="dialogVisible" class="radius-lg" width="1168" append-to-body>
<template #header>
<div class="title center re">
<div class="tc size-16 fw">{{ upload_type_name }}上传</div>
</div>
</template>
<div class="upload-content pa-20" @paste="handle_paste">
<el-form :model="form" label-width="75">
<el-form-item label="上传方式">
<el-radio-group v-model="form.type" @change="upload_type_change">
<el-radio value="loc">本地上传</el-radio>
<el-radio value="scan">扫码上传</el-radio>
<el-radio v-if="upload_type !== 'file'" value="web">网络上传</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="上传至分组" prop="group">
<div class="form-item-width">
<el-cascader v-model="form.group" class="w" :options="classify" placeholder="请选择" :show-all-levels="false" @change="group_change"></el-cascader>
</div>
</el-form-item>
<template v-if="form.type == 'loc'">
<div class="flex-row jc-sb align-c mt-30">
<div class="flex-row">
<el-upload ref="fileUpload1" v-model:file-list="file_list" multiple action="#" :accept="exts_text" :auto-upload="false" :show-file-list="false" :on-change="upload_change" :before-upload="before_upload" :limit="limit" :on-exceed="handle_exceed">
<template #trigger>
<el-button @click="folder_mode(false)"> 上传{{ upload_type_name }} </el-button>
<el-button @click="folder_mode(true)"> 上传文件夹 </el-button>
</template>
</el-upload>
</div>
<el-button @click="clear_list_event">清空列表</el-button>
</div>
<div class="table mt-10">
<div class="table-header">
<div class="table-row">
<div class="table-cell">文件名</div>
<div class="table-cell">文件大小</div>
<div class="table-cell">上传状态</div>
<div class="table-cell-oprate">操作</div>
</div>
</div>
<div id="dropzone" @dragover.prevent="handle_drag_in" @dragenter="handle_drag_in" @dragleave="handle_drag_leave" @drop.prevent="handle_drop">
<el-scrollbar v-if="!is_dragging && form.file.length > 0" height="341px">
<div class="table-body">
<div v-for="(item, index) in form.file" :key="item.file.name + item.file.size" class="table-row">
<div class="table-cell">
<el-image :src="file_to_base64(item.file)" class="preview-img radius-sm" fit="contain">
<template #error>
<div class="bg-f5 img flex-row jc-c align-c radius h w">
<icon name="error-img" size="12"></icon>
</div>
</template>
</el-image>
<div class="desc">{{ item.file.name }}</div>
</div>
<div class="table-cell">{{ annex_size_to_unit(item.file.size) }}</div>
<div class="table-cell">{{ item.status }}</div>
<div class="table-cell-oprate" @click="del_upload(index)">移除</div>
</div>
</div>
</el-scrollbar>
<div v-show="is_dragging || form.file.length < 1" class="folder-upload mt-20" :class="is_dragging ? 'active' : ''">
<el-upload ref="fileUpload2" v-model:file-list="file_list" :accept="exts_text" multiple action="#" :auto-upload="false" :show-file-list="false" :on-change="upload_change" :before-upload="before_upload" :limit="limit" :on-exceed="handle_exceed">
<div class="flex-col jc-c align-c">
<icon name="add" size="60" color="#dbeef6"></icon>
<p class="size-18 cr-c fw">请将需要上传的文件/文件夹拖到此处或粘贴</p>
</div>
</el-upload>
</div>
</div>
</div>
</template>
<template v-else-if="form.type == 'scan'">
<el-form-item label="二维码" class="mb-10">
<qrcode :src="form.qrcode" :is-mask="is_mask"></qrcode>
</el-form-item>
<div class="table">
<div class="table-header">
<div class="table-row">
<div class="table-cell">文件名</div>
<div class="table-cell">文件大小</div>
<div class="table-cell-oprate">操作</div>
</div>
</div>
<el-scrollbar height="224px">
<div class="table-body">
<div v-for="(item, index) in scan_file_list" :key="item.name + item.size" class="table-row">
<div class="table-cell">
<el-image :src="item.url" class="preview-img radius-sm" fit="contain">
<template #error>
<div class="bg-f5 img flex-row jc-c align-c radius h w">
<icon name="error-img" size="12"></icon>
</div>
</template>
</el-image>
<div class="desc">{{ item.name }}</div>
</div>
<div class="table-cell">{{ annex_size_to_unit(item.size) }}</div>
<div class="table-cell-oprate" @click="del_already_upload(index)">删除</div>
</div>
</div>
</el-scrollbar>
</div>
</template>
<template v-else-if="form.type == 'web'">
<el-form-item label="网络图片">
<div class="flex-row align-c gap-10">
<el-input v-model="form.web_image" class="form-item-width" placeholder="请输入网络图片地址" />
<div class="c-pointer cr-primary size-12" @click="extract_images">提取图片</div>
</div>
</el-form-item>
</template>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button class="plr-28 ptb-10" @click="close_dialog">取消</el-button>
<el-button v-if="form.type == 'loc'" class="plr-28 ptb-10" type="primary" @click="submit">上传</el-button>
<el-button v-else-if="form.type == 'scan'" class="plr-28 ptb-10" type="primary" @click="close_dialog">返回图库</el-button>
<el-button v-else-if="form.type == 'web'" class="plr-28 ptb-10" type="primary" @click="close_all_dialog">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import type { UploadFile, UploadFiles, UploadUserFile } from 'element-plus';
import { annex_size_to_unit, ext_name, get_math } from '@/utils';
/**
* @description: 图片执行上传弹窗
* @param close{String} 默认值
* @return {*} close
*/
const props = defineProps({
type: {
type: String,
default: 'img', // img/video/file
},
limit: {
type: Number,
default: 10000,
},
exts: {
type: Array,
default: () => ['.png', '.jpg', '.jpeg', '.bmp', '.webp', '.gif'],
},
fileSize: {
type: Number,
default: 1024 * 1024,
},
});
const dialogVisible = defineModel({ type: Boolean, default: false });
// const v_model = defineModel({ type: Array, default: [] });
// const dialogVisible = ref(false);
// 上传类型
const upload_type = ref(props.type);
// 上传类型转换成name
const upload_type_name = computed(() => {
return upload_type.value === 'img' ? '图片' : upload_type.value === 'video' ? '视频' : '文件';
});
// 格式限制
const exts_text = ref(props.exts.join(','));
const file_list = ref<UploadUserFile[]>([]);
interface fileData {
file: File;
status: string;
}
interface formData {
type: string;
group: string;
file: fileData[];
qrcode: string;
web_image: string;
}
const form = ref<formData>({
type: 'loc',
group: '',
file: [],
qrcode: '11223344',
web_image: '',
});
// 是否给二维码加模糊效果
const is_mask = ref(true);
const timer = ref<number | null>(null);
// 上传方式
const upload_type_change = (type: any) => {
// 清除之前的定时器(如果存在)
if (timer.value) {
// 直接检查 timer.value 是否存在(不是 null 或 undefined
clearTimeout(timer.value);
timer.value = null; // 清除引用,防止内存泄漏
}
// 如果需要设置新的定时器
if (type === 'scan') {
timer.value = setInterval(() => {
console.log('timer-----定时调用');
// 此处写定时调用接口,获取文件列表
}, 3000);
}
};
// 选择分组
const group_change = (val: any) => {
if (val && val.length > 0) {
is_mask.value = false;
}
};
// 图片/视频/文件移动至
const classify = [
{
value: 'resource',
label: 'Resource',
},
{
value: 'resource',
label: 'Resource',
children: [
{
value: 'axure',
label: 'Axure Components',
},
],
},
{
value: 'guide',
label: 'Guide',
children: [
{
value: 'disciplines',
label: 'Disciplines',
children: [
{
value: 'consistency',
label: 'Consistency',
},
],
},
{
value: 'navigation',
label: 'Navigation',
children: [
{
value: 'side nav',
label: 'Side Navigation',
},
],
},
],
},
];
//#region 本地上传 -----------------------------------------------start
// 选择文件夹
const state = reactive({
uploadEle: null as HTMLInputElement | null,
uploadList: [],
});
const folder_mode = (type: boolean) => {
if (!state.uploadEle) {
state.uploadEle = document.querySelector('.el-upload__input') as HTMLInputElement;
}
nextTick(() => {
(state.uploadEle as HTMLInputElement).webkitdirectory = type;
// console.log(state.uploadEle);
});
};
// 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用
const upload_change = async (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
// console.log('文件状态改变时的钩子', uploadFile, uploadFiles);
// // 过滤已上传的文件和重复的文件
const results = uploadFiles.flat(Infinity).filter((f: any) => validExt(f.name) && validSize(f.size));
const new_upload_files = results.filter((item: UploadFile) => {
return !form.value.file.find((item2: fileData) => {
return item2.file.name === item.name && item2.file.size === item.size;
});
});
// 将过滤后的数组和form.file数组合并
new_upload_files.forEach((item: UploadFile) => {
// item.status = 'ready';
const new_file_obj = {
status: '等待上传',
file: item.raw as File,
};
form.value.file.push(new_file_obj);
});
};
// 上传文件之前的钩子,参数为上传的文件, 若返回false或者返回 Promise 且被 reject则停止上传。
const validExt = (name: string) => props.exts.includes(ext_name(name));
const validSize = (size: number) => size <= props.fileSize;
// 上传前的钩子
const before_upload = (file: any) => {
// 检查文件是否为图片
if (validExt(file.name) && validSize(file.size)) {
console.log('允许上传');
return true; // 允许上传
} else {
console.log('不允许上传');
return false; // 不允许上传
}
};
// 超出限制时的钩子
const handle_exceed = (files: any, fileList: any) => {
ElMessageBox.alert(`最多上传 ${props.limit} 个文件!`);
};
// 清空列表
const clear_list_event = () => {
form.value.file = [];
file_list.value = [];
};
const is_dragging = ref(false);
// 引用dropzone DOM元素以便在dragLeave中检查
const dropzone = ref<HTMLElement | null>(null);
onMounted(() => {
dropzone.value = document.getElementById('dropzone') as HTMLElement;
});
// 拖拽事件 和 列表拖动文件进入触发事件
const handle_drag_in = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
is_dragging.value = true;
};
// 列表拖动文件离开触发事件
const handle_drag_leave = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
// 使用一个变量来跟踪鼠标是否离开了dropzone
const leaveTimer = setTimeout(() => {
// 确保鼠标不在dropzone或其子元素中
if (!document.getElementById('dropzone')?.contains(event.relatedTarget as Node)) {
is_dragging.value = false;
}
}, 50); // 延迟50毫秒
// 当鼠标重新进入dropzone时清除定时器
event.currentTarget?.addEventListener(
'dragenter',
() => {
clearTimeout(leaveTimer);
},
{ once: true }
);
};
// 松开拖拽的文件 获取文件信息
const handle_drop = async (event: any) => {
event.preventDefault();
event.stopPropagation();
is_dragging.value = false;
let results = await Promise.all([...event.dataTransfer.items].map((item) => handle_entry(item.webkitGetAsEntry())));
// 过滤符合条件的数据 文件后缀名,文件大小过滤
results = results.flat(Infinity).filter((f: any) => validExt(f.name) && validSize(f.size));
// 遍历过滤后的数据,过滤重复数据后并添加到上传列表中
if (results.length + form.value.file.length <= props.limit) {
results.forEach((file: any) => {
if (!form.value.file.some((item: any) => item.file.name === file.name && item.file.size === file.size)) {
const new_file_obj = {
status: '等待上传',
file: file,
};
const new_file_obj_upload = {
name: file.name,
url: 'xxx',
file: file,
};
form.value.file.push(new_file_obj);
file_list.value.push(new_file_obj_upload);
}
});
} else {
ElMessageBox.alert(`最多上传 ${props.limit} 个文件!`);
}
};
/**
* 处理文件/目录并返回一个Promise。
* 此函数旨在处理文件/目录无论是文件还是目录。如果是文件则直接以文件形式解析并解决Promise。如果是目录则递归处理目录中的所有文件。
* @param {any} entry 文件/目录,可以是文件或目录。
* @returns {Promise<any[]>} 返回一个Promise解析为一个数组包含所有条目的文件对象。
*/
const handle_entry = (entry: any) => {
return new Promise((resolve) => {
// 如果entry是一个文件
if (entry.isFile) {
// 直接以文件形式解析Promise
entry.file(resolve);
return;
}
// 如果entry是一个目录
const dirReader = entry.createReader();
dirReader.readEntries(async (entries: any) => {
// 递归处理目录中的所有条目并通过Promise.all等待所有处理完成
resolve(await Promise.all(entries.map(handle_entry)));
});
});
};
// 移除文件
const del_upload = (index: number) => {
// 根据下标删除文件
form.value.file.splice(index, 1);
file_list.value.splice(index, 1);
};
// 粘贴上传
const handle_paste = (event: any) => {
console.log(event);
// 获取粘贴板中的文件列表
const files = event.clipboardData.files;
// 过滤符合条件的数据 文件后缀名,文件大小过滤
const results = [...files].filter((f: any) => validExt(f.name) && validSize(f.size));
// 遍历过滤后的数据,过滤重复数据后并添加到上传列表中
if (results.length + form.value.file.length <= props.limit) {
console.log(results);
results.forEach((file: any) => {
// 判断是否重复
// 如果上传列表中没有与当前文件相同的文件,则添加到上传列表中
if (!form.value.file.some((item: any) => item.file.name === file.name && item.file.size === file.size)) {
const new_file_obj = {
status: '等待上传',
file: file,
};
const new_file_obj_upload = {
name: file.name,
url: 'xxx',
file: file,
};
form.value.file.push(new_file_obj);
file_list.value.push(new_file_obj_upload);
} else {
ElMessageBox.alert(`文件 ${file.name} 已存在!`);
}
});
} else {
ElMessageBox.alert(`最多上传 ${props.limit} 个文件!`);
}
};
// 执行上传
const submit = () => {};
//#endregion 本地上传 -----------------------------------------------end
//#region 扫码上传 -----------------------------------------------start
interface scanFile {
name: string;
url: string;
size: number;
}
const scan_file_list = ref<scanFile[]>([
{ name: '1', url: '1', size: 0 },
{ name: '2', url: '2', size: 0 },
{ name: '3', url: '3', size: 0 },
{ name: '4', url: '4', size: 0 },
{ name: '5', url: '5', size: 0 },
]);
// 删除已上传的文件
const del_already_upload = (index: number) => {
// 根据下标删除文件
scan_file_list.value.splice(index, 1);
// 调接口真实删除
};
//#endregion 扫码上传 -----------------------------------------------end
//#region 网络上传 -----------------------------------------------start
// 提取图片
const extract_images = () => {
// 此处调用接口获取提取后的图片更新输入框的地址,改为在线地址
ElMessage({
type: 'success',
message: '提取成功!',
});
};
const emit = defineEmits(['close']);
// 关闭所有弹窗
const close_all_dialog = () => {
const new_form = JSON.parse(JSON.stringify(form.value));
emit('close', new_form);
close_dialog();
};
//#endregion 网络上传 -----------------------------------------------end
// file转换成base64
const file_to_base64 = (file: any) => {
return URL.createObjectURL(file);
};
// 关闭弹窗
const close_dialog = () => {
dialogVisible.value = false;
form.value = {
type: 'loc',
group: '',
file: [],
qrcode: '',
web_image: '',
};
scan_file_list.value = [];
// 直接检查 timer.value 是否存在(不是 null 或 undefined
if (timer.value !== null) {
clearTimeout(timer.value);
}
timer.value = null; // 清除引用,防止内存泄漏
};
</script>
<style lang="scss" scoped>
.upload-content {
height: 57.4rem;
gap: 4.5rem;
.table {
width: 100%;
height: 30rem;
.table-header,
.table-body {
.table-row {
display: flex;
width: 100%;
border-bottom: 0.1rem solid #eee;
color: #999;
font-size: 1.4rem;
.table-cell {
flex: 1;
padding: 1rem;
color: #666;
display: flex;
align-items: center;
word-break: break-all;
gap: 1rem;
.preview-img {
width: 2.8rem;
height: 2.8rem;
}
.desc {
flex: 1;
width: 0;
}
}
.table-cell-oprate {
padding: 1rem;
width: 5rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 1rem;
word-break: break-all;
}
}
}
.table-body {
.table-cell,
.table-cell-oprate {
padding: 1.5rem 1rem !important;
}
.table-cell-oprate {
color: $cr-primary;
}
}
.folder-upload {
background: #fafcff;
border-radius: 0.2rem;
border: 0.1rem dashed #afdafa;
height: 32rem;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
.active {
background-color: #eef5ff;
border-color: #35a9ff;
}
}
}
.form-item-width {
width: 33.5rem !important;
}
}
</style>

View File

@ -0,0 +1,31 @@
.url-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;
}
}
.url-value-content {
height: 57.3rem;
gap: 6rem;
.left-content {
width: 22.5rem;
.el-menu-item {
height: 4rem;
line-height: 4rem;
&.is-active {
background: var(--el-menu-hover-bg-color);
color: #333;
}
}
}
.right-content {
}
}

View File

@ -0,0 +1,230 @@
<!-- 上传组件 -->
<template>
<el-dialog v-model="dialogVisible" class="radius-lg" width="1168" append-to-body @close="close_event">
<template #header>
<div class="title center re">
<div class="tc size-16 fw">选择链接</div>
</div>
</template>
<div class="url-value-content pa-20 flex-row">
<div class="left-content">
<el-menu :default-active="link_select" class="w br-none" @select="handle_select">
<el-menu-item v-for="item in base_data" :key="item.type" :index="item.type" :disabled="!(custom_link_type.length == 0 || custom_link_type.includes(item.type))">
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
</div>
<div class="right-content flex-1">
<template v-if="link_select == 'shop'">
<link-list v-model="link_value" :reset="reset_compontent"></link-list>
</template>
<template v-else-if="link_select == 'goods-category'">
<link-goods-category v-model="link_value" :reset="reset_compontent"></link-goods-category>
</template>
<template v-else-if="link_select == 'goods-search'">
<link-goods-search :reset="reset_compontent" :status="component_status" @update:link="goods_category_link" @type="goods_category_type_change" @required="required_tips"></link-goods-search>
</template>
<template v-else-if="link_select == 'goods'">
<link-goods v-model="link_value" :reset="reset_compontent"></link-goods>
</template>
<template v-else-if="link_select == 'articles'">
<link-articles v-model="link_value" :reset="reset_compontent"></link-articles>
</template>
<template v-else-if="link_select == 'diy'">
<link-table v-model="link_value" :reset="reset_compontent"></link-table>
</template>
<template v-else-if="link_select == 'design'">
<link-table v-model="link_value" :reset="reset_compontent"></link-table>
</template>
<template v-else-if="link_select == 'custom-view'">
<link-table v-model="link_value" :reset="reset_compontent"></link-table>
</template>
<template v-else-if="link_select == 'custom'">
<link-custom :reset="reset_compontent" :status="component_status" @update:link="custom_link" @required="required_tips"></link-custom>
</template>
<template v-else-if="link_select == 'plugins'">
<link-list v-model="link_value" :reset="reset_compontent"></link-list>
</template>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button class="plr-28 ptb-10" @click="close_event">取消</el-button>
<el-button class="plr-28 ptb-10" type="primary" @click="confirm_event">确定</el-button>
</span>
</template>
</el-dialog>
<div class="flex-row align-c gap-10 br-d radius-sm plr-11 url-value-input" @click="dialogVisible = true">
<div class="flex-1 flex-width size-12 text-line-1">
<text v-if="!is_obj_empty(modelValue)">{{ modelValue.name }}</text>
<text v-else class="cr-9">{{ placeholder }}</text>
</div>
<div class="value-input-icon">
<template v-if="is_obj_empty(modelValue)">
<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>
<script lang="ts" setup>
import { MenuItemClicked } from 'element-plus/es/components/menu/src/types';
import { is_obj_empty } from '@/utils';
import { PropType } from 'vue';
const app = getCurrentInstance();
/**
* @description: 页面链接
* @param modelValue{Object} 默认值
* @param dialogVisible {Boolean} 弹窗显示
* @param type{String} 链接类型为空数组则表示无限制,全部可用,传过来则表示传的值可用
* @param placeholder{String} 提示文字
* @return {*} update:modelValue
*/
const props = defineProps({
type: {
type: Array as PropType<string[]>,
default: () => [],
},
placeholder: {
type: String,
default: '请选择链接',
},
});
const modelValue = defineModel({ type: Object, default: {} });
const dialogVisible = defineModel('visibleDialog', { type: Boolean, default: false });
const link_value = ref({});
const reset_compontent = ref(false);
const custom_link_type = ref(props.type);
const base_data = reactive([
{
name: '商城页面',
type: 'shop',
data: [{ name: '基础链接', data: [{ name: '', page: '' }] }],
},
{
name: '商品分类',
type: 'goods-category',
data: null,
},
{
name: '商品搜索',
type: 'goods-search',
data: null,
},
{
name: '商品页面',
type: 'goods',
data: null,
},
{
name: '文章页面',
type: 'articles',
data: null,
},
{
name: 'DIY页面',
type: 'diy',
data: null,
},
{
name: '页面设计',
type: 'design',
data: null,
},
{
name: '自定义页面',
type: 'custom-view',
data: null,
},
{
name: '自定义链接',
type: 'custom',
data: null,
},
{
name: '插件',
type: 'plugins',
data: [{ name: '多商户', data: [{ name: '1', page: '2' }] }],
},
]);
// 弹窗显示
//#region 链接回调 -----------------------------------------------start
// 菜单选中回调
const link_select = ref(props.type.length == 0 ? 'shop' : props.type[0]);
const handle_select = (index: string, indexPath: string[], item: MenuItemClicked, routeResult: any) => {
// console.log(index, indexPath, item, routeResult);
link_select.value = index;
};
//#endregion 链接回调 -----------------------------------------------end
//@region 子组件回调 -----------------------------------------------start
const component_status = ref(false);
// 商品分类选中回调
const goods_category_type = ref(0);
const goods_category_type_change = (type: number) => {
goods_category_type.value = type;
};
const goods_category_link = (data: object, type: number) => {
if (type == 2) {
modelValue.value = data;
close_event();
} else {
link_value.value = data;
}
};
// 自定义地址回调
const custom_link = (data: object) => {
modelValue.value = data;
close_event();
};
// 错误回调
const required_tips = () => {
ElMessage({
type: 'warning',
message: '必填项不能为空',
});
};
//#endregion 子组件回调 -----------------------------------------------end
//#region 链接确认回调 -----------------------------------------------start
// 取消回调
const close_event = () => {
link_select.value = props.type.length == 0 ? 'shop' : props.type[0];
dialogVisible.value = false;
link_value.value = {};
reset_compontent.value = !reset_compontent.value;
};
// 确认回调
const confirm_event = () => {
// 判断是否是自定义页面和商品查询
if (link_select.value == 'custom' || (link_select.value == 'goods-search' && goods_category_type.value == 2)) {
component_status.value = !component_status.value;
} else {
if (is_obj_empty(link_value.value)) {
ElMessage({
type: 'warning',
message: '请先选择链接',
});
} else {
modelValue.value = link_value.value;
close_event();
}
}
};
//#endregion 链接确认回调 -----------------------------------------------end
//#endregion 链接清空-------------------------------------------------start
const clear_model_value = () => {
modelValue.value = {};
};
//#endregion 链接清空-------------------------------------------------end
</script>
<style lang="scss" scoped>
@import 'index.scss';
</style>

View File

@ -0,0 +1,140 @@
<template>
<!-- 商品分类 -->
<div class="container">
<div class="flex-row jc-e gap-20 mb-20">
<el-select v-model="type" class="search-w" placeholder="请选择">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-input v-model="search_value" placeholder="请输入搜索内容" class="search-w" @change="handle_search">
<template #suffix>
<icon name="search" size="16" color="9"></icon>
</template>
</el-input>
</div>
<div class="content">
<el-table :data="tableData" class="w" :header-cell-style="{ background: '#f7f7f7' }" row-key="id" height="438" fixed @row-click="row_click">
<el-table-column label="#" width="120" type="">
<template #default="scope">
<el-radio v-model="template_selection" :label="scope.$index + ''">&nbsp;</el-radio>
</template>
</el-table-column>
<el-table-column prop="id" label="ID" width="180" type="" />
<el-table-column prop="icon" label="文章图片" width="130">
<template #default="scope">
<el-image :src="scope.row.icon" class="img">
<template #error>
<div class="bg-f5 flex-row jc-c align-c radius h w">
<icon name="error-img" size="14" color="9"></icon>
</div>
</template>
</el-image>
</template>
</el-table-column>
<el-table-column prop="name" label="文章名称" />
</el-table>
<div class="mt-10 flex-row jc-e">
<el-pagination :current-page="page" :page-size="21" :pager-count="5" layout="prev, pager, next" :total="data_total" @current-change="get_list" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
// 重置
reset: {
type: Boolean,
default: () => false,
},
});
watch(
() => props.reset,
() => {
template_selection.value = '';
}
);
const modelValue = defineModel({ type: Object, default: {} });
interface User {
id: number;
name: string;
icon: string;
link: string;
}
const tableData: User[] = [
{
id: 1,
name: '一级分类一级分类一级分类一级分类一级分类一级分类一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'a',
},
{
id: 3,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'c',
},
{
id: 4,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'd',
},
{
id: 5,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'e',
},
];
const search_value = ref('');
const handle_search = () => {
console.log(search_value.value);
};
const type = ref('');
const options = ref([
{ value: '1', label: '一级分类' },
{ value: '2', label: '二级分类' },
{ value: '3', label: '三级分类' },
]);
const template_selection = ref('');
const row_click = (row: any) => {
const new_table_data = JSON.parse(JSON.stringify(tableData));
template_selection.value = new_table_data.findIndex((item: User) => item.id == row.id).toString();
modelValue.value = row;
};
//#region 分页 -----------------------------------------------start
// 总页数
// const page_total = ref(0);
// 当前页
const page = ref(1);
// 总数量
const data_total = ref(0);
// 查询文件
const search_data = ref({
page: page.value,
type: '',
name: search_value.value,
});
// 查询文件
const get_list = () => {
console.log('查询接口', search_data);
};
//#region 分页 -----------------------------------------------end
</script>
<style lang="scss" scoped>
.container {
.search-w {
width: 22.5rem;
}
.content {
:deep(.el-table__inner-wrapper:before) {
background-color: transparent;
}
.img {
width: 3.6rem;
height: 3.6rem;
}
}
}
</style>

View File

@ -0,0 +1,211 @@
<template>
<!-- 商城 -->
<div class="container">
<div class="tabs flex-row gap-10 mb-30">
<div v-for="item in custom_type" :key="item.id" class="item bg-f5 radius-sm" :class="custom_type_active == item.id ? 'active' : ''" @click="custom_type_event(item)">{{ item.name }}</div>
</div>
<div class="content">
<el-scrollbar height="470px">
<el-form ref="ruleFormRef" :model="form" label-width="85px" status-icon>
<template v-if="custom_type_active == 0">
<el-form-item label="跳转路径" prop="link" :rules="link">
<el-input v-model="form.link" class="link-input" placeholder="请输入跳转路径" />
</el-form-item>
</template>
<template v-if="custom_type_active == 1">
<el-form-item label="APPID" prop="app_id" :rules="app_id">
<el-input v-model="form.app_id" class="link-input" placeholder="请输入小程序APPID" />
</el-form-item>
<el-form-item label="小程序路径" prop="app_link" :rules="app_link">
<el-input v-model="form.app_link" class="link-input" placeholder="请输入小程序路径" />
</el-form-item>
</template>
<template v-if="custom_type_active == 2">
<el-form-item label="电话号码" prop="phone" :rules="phone">
<el-input v-model="form.phone" class="link-input" placeholder="请输入电话号码" />
</el-form-item>
</template>
<template v-if="custom_type_active == 3">
<el-form-item label="名称" prop="name" :rules="name">
<el-input v-model="form.name" class="link-input" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="详细地址" prop="address" :rules="address">
<el-input v-model="form.address" class="link-input" placeholder="请输入地址" type="1" @change="address_change" />
</el-form-item>
<el-form-item label="经纬度">
<maps v-model="map_address" type="4" @point="map_point"></maps>
<!-- <t-map v-model="map_address" @point="map_point"></t-map> -->
<!-- <bd-map v-model="map_address" @point="map_point"></bd-map> -->
<!-- <gd-map v-model="map_address" @point="map_point"></gd-map> -->
<!-- <tx-map v-model="map_address" @point="map_point"></tx-map> -->
</el-form-item>
</template>
<el-button type="primary" class="hide" @click="on_submit">Create</el-button>
</el-form>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus';
const props = defineProps({
status: {
type: Boolean,
default: false,
},
// 重置
reset: {
type: Boolean,
default: () => false,
},
});
watch(
() => props.status,
(val) => {
on_submit();
}
);
watch(
() => props.reset,
() => {
reset_data();
custom_type_active.value = 0;
}
);
interface customType {
id: number;
name: string;
}
const custom_type = ref<customType[]>([
{ id: 0, name: '普通链接' },
{ id: 1, name: '跳转小程序' },
{ id: 2, name: '拨打电话' },
{ id: 3, name: '跳转地图' },
]);
const custom_type_active = ref(0);
const custom_type_event = (item: any) => {
custom_type_active.value = item.id;
};
const map_address = ref('');
const address_change = (val: string) => {
map_address.value = val;
};
//#region 天地图 -----------------------------------------------start
const map_point = (lng: number, lat: number) => {
form.lng = lng;
form.lat = lat;
};
//#endregion 天地图 -----------------------------------------------end
const form = reactive({
link: '',
app_id: '',
app_link: '',
phone: '',
name: '',
address: '',
lng: 121.47894,
lat: 31.223,
});
const link = computed(() => {
return { trigger: 'change', message: '跳转路径不能为空', required: custom_type_active.value == 0 };
});
const app_id = computed(() => {
return { trigger: 'change', message: '跳转小程序APPID不能为空', required: custom_type_active.value == 1 };
});
const app_link = computed(() => {
return { trigger: 'change', message: '跳转小程序路径不能为空', required: custom_type_active.value == 1 };
});
const phone = computed(() => {
return { trigger: 'change', message: '电话号码不能为空', required: custom_type_active.value == 2 };
});
const name = computed(() => {
return { trigger: 'change', message: '名称不能为空', required: custom_type_active.value == 3 };
});
const address = computed(() => {
return { trigger: 'change', message: '详细地址不能为空', required: custom_type_active.value == 3 };
});
const ruleFormRef = ref<FormInstance>();
const emit = defineEmits(['update:link', 'required']);
interface formType {
id?: number;
name: string;
link: string;
lng?: number;
lat?: number;
}
const on_submit = () => {
if (!ruleFormRef.value) return;
ruleFormRef.value.validate((valid: boolean) => {
if (valid) {
let new_value: formType = {
name: '',
link: '',
};
if (custom_type_active.value == 1) {
new_value = {
name: form.app_id,
link: form.app_link,
};
} else if (custom_type_active.value == 2) {
new_value = {
name: form.phone,
link: form.phone,
};
} else if (custom_type_active.value == 3) {
new_value = {
name: form.name,
link: form.address,
lng: form.lng,
lat: form.lat,
};
} else {
new_value = {
name: form.link,
link: form.link,
};
}
emit('update:link', new_value);
} else {
emit('required');
}
});
};
const reset_data = () => {
form.link = '';
form.app_id = '';
form.app_link = '';
form.phone = '';
form.name = '';
form.address = '';
form.lng = 121.47894;
form.lat = 31.223;
};
</script>
<style lang="scss" scoped>
.container {
.tabs {
.item {
width: 8rem;
height: 3rem;
line-height: 3rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease-in-out;
&:hover {
background-color: #edf4ff;
color: $cr-primary;
}
&.active {
background: #edf4ff;
color: $cr-primary;
}
}
}
.link-input {
width: 33.2rem;
}
}
</style>

View File

@ -0,0 +1,159 @@
<template>
<!-- 商品分类 -->
<div class="container">
<div class="flex-row jc-e gap-20 mb-20">
<el-select v-model="type" class="search-w" placeholder="请选择">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="brand" class="search-w" placeholder="品牌">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-input v-model="search_value" placeholder="请输入搜索内容" class="search-w" @change="handle_search">
<template #suffix>
<icon name="search" size="16" color="9"></icon>
</template>
</el-input>
</div>
<div class="content">
<el-table :data="tableData" class="w" :header-cell-style="{ background: '#f7f7f7' }" row-key="id" height="438" fixed @row-click="row_click">
<el-table-column label="#" width="120" type="">
<template #default="scope">
<el-radio v-model="template_selection" :label="scope.$index + ''">&nbsp;</el-radio>
</template>
</el-table-column>
<el-table-column prop="id" label="ID" width="180" type="" />
<el-table-column prop="name" label="分类名称" />
<el-table-column prop="icon" label="分类图标">
<template #default="scope">
<el-image :src="scope.row.icon" class="img" />
</template>
</el-table-column>
</el-table>
<div class="mt-10 flex-row jc-e">
<el-pagination :current-page="page" :page-size="21" :pager-count="5" layout="prev, pager, next" :total="data_total" @current-change="get_list" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
// 重置
reset: {
type: Boolean,
default: () => false,
},
});
watch(
() => props.reset,
() => {
template_selection.value = '';
}
);
const modelValue = defineModel({ type: Object, default: {} });
interface User {
id: number;
name: string;
icon: string;
link: string;
hasChildren?: boolean;
children?: User[];
}
const tableData: User[] = [
{
id: 1,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'a',
children: [
{
id: 2,
name: '二级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'b',
},
],
},
{
id: 3,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'c',
},
{
id: 4,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'd',
},
{
id: 5,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'e',
},
];
const search_value = ref('');
const handle_search = () => {
console.log(search_value.value);
};
const type = ref('');
const brand = ref('');
const options = ref([
{ value: '1', label: '一级分类' },
{ value: '2', label: '二级分类' },
{ value: '3', label: '三级分类' },
]);
const template_selection = ref('');
const row_click = (row: any) => {
let new_data: User[] = [];
// 多级数组转换一级数组
const array_conversion = (item: User[]) => {
item.forEach((item) => {
new_data.push(item);
if (item.children) {
array_conversion(item.children);
}
});
return new_data;
};
const new_table_data = array_conversion(JSON.parse(JSON.stringify(tableData)));
template_selection.value = new_table_data.findIndex((item: User) => item.id == row.id).toString();
modelValue.value = row;
};
//#region 分页 -----------------------------------------------start
// 总页数
// const page_total = ref(0);
// 当前页
const page = ref(1);
// 总数量
const data_total = ref(0);
// 查询文件
const search_data = ref({
page: page.value,
type: '',
name: search_value.value,
});
// 查询文件
const get_list = () => {
console.log('查询接口', search_data);
};
//#region 分页 -----------------------------------------------end
</script>
<style lang="scss" scoped>
.container {
.search-w {
width: 22.5rem;
}
.content {
:deep(.el-table__inner-wrapper:before) {
background-color: transparent;
}
.img {
width: 3.6rem;
height: 3.6rem;
}
}
}
</style>

View File

@ -0,0 +1,275 @@
<template>
<!-- 商城 -->
<div class="container">
<div class="tabs flex-row gap-10 mb-20">
<div v-for="item in custom_type" :key="item.id" class="item bg-f5 radius-sm" :class="custom_type_active === item.id ? 'active' : ''" @click="custom_type_event(item)">{{ item.name }}</div>
</div>
<div class="content">
<template v-if="custom_type_active === 0">
<div class="goods-tips ptb-10 plr-14 mb-20 size-12">
<text class="cr-666">您当前选择的产品类别是</text>
<text>{{ one_item_text }} {{ two_item_text ? '>' : '' }}</text>
<text class="pl-3">{{ two_item_text }} {{ three_item_text ? '>' : '' }}</text>
<text class="pl-3">{{ three_item_text }}</text>
</div>
<div class="goods-category flex-row gap-30">
<div v-if="goods_category_data.length > 0" class="goods-category-wdith br-d radius-xs">
<el-scrollbar height="420px">
<div v-for="(item, index) in goods_category_data" :key="item.id" class="item flex-row jc-c align-c gap-10 pa-10" :class="one_item_index === index + 1 ? 'active' : ''" @click="goods_item_click(item, 1, index)">
<text class="flex-1 flex-width text-line-1">{{ item.name }}</text>
<icon v-if="item?.children" name="arrow-right"></icon>
</div>
</el-scrollbar>
</div>
<div v-if="two_item_data.length > 0" class="goods-category-wdith br-d radius-xs">
<el-scrollbar height="420px">
<div v-for="(item, index) in two_item_data" :key="item.id" class="item flex-row jc-c align-c gap-10 pa-10" :class="two_item_index === index + 1 ? 'active' : ''" @click="goods_item_click(item, 2, index)">
<text class="flex-1 flex-width text-line-1">{{ item.name }}</text>
<icon v-if="item?.children" name="arrow-right"></icon>
</div>
</el-scrollbar>
</div>
<div v-if="three_item_data.length > 0" class="goods-category-wdith br-d radius-xs">
<el-scrollbar height="420px">
<div v-for="(item, index) in three_item_data" :key="item.id" class="item flex-row jc-c align-c gap-10 pa-10" :class="three_item_index === index + 1 ? 'active' : ''" @click="goods_item_click(item, 3, index)">
<text class="flex-1 flex-width text-line-1">{{ item.name }}</text>
</div>
</el-scrollbar>
</div>
</div>
</template>
<template v-if="custom_type_active === 1">
<div class="brand">
<div class="flex-1 br-d radius-xs">
<el-scrollbar height="480px">
<div v-for="(item, index) in brand_data" :key="item.id" class="item flex-row jc-c align-c gap-10 pa-10" :class="brand_item_index === index + 1 ? 'active' : ''" @click="brand_item_click(item, index)">
<text class="flex-1 flex-width text-line-1">{{ item.name }}</text>
</div>
</el-scrollbar>
</div>
</div>
</template>
<template v-if="custom_type_active === 2">
<el-form ref="ruleFormRef" :model="form" :rules="rules" label-width="85px" status-icon>
<el-form-item label="关键字" prop="key">
<el-input v-model="form.key" class="link-input" placeholder="请输入关键字" />
</el-form-item>
<el-button type="primary" class="hide" @click="on_submit">Create</el-button>
</el-form>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus';
const props = defineProps({
status: {
type: Boolean,
default: false,
},
// 重置
reset: {
type: Boolean,
default: () => false,
},
});
watch(
() => props.status,
(val) => {
on_submit();
}
);
watch(
() => props.reset,
() => {
reset_data();
custom_type_active.value = 0;
}
);
const emit = defineEmits(['update:link', 'required', 'type']);
interface customType {
id: number;
name: string;
}
const custom_type = ref<customType[]>([
{ id: 0, name: '商品分类' },
{ id: 1, name: '品牌' },
{ id: 2, name: '关键字' },
]);
interface baseData {
id: number;
name: string;
children?: baseData[];
}
const goods_category_data = reactive<baseData[]>([
{
id: 0,
name: '服饰鞋包',
children: [
{
id: 1,
name: '服饰',
children: [
{ id: 1, name: '上衣' },
{ id: 2, name: '裤子' },
],
},
{
id: 2,
name: '鞋包',
children: [
{ id: 1, name: '休闲鞋' },
{ id: 2, name: '商务休闲鞋' },
],
},
],
},
{ id: 1, name: '家居生活', children: [{ id: 1, name: '生活用品' }] },
{ id: 2, name: '母婴' },
]);
const brand_data = reactive<baseData[]>([
{ id: 1, name: '品牌1' },
{ id: 2, name: '品牌2' },
]);
const custom_type_active = ref(0);
const custom_type_event = (item: any) => {
custom_type_active.value = item.id;
emit('type', item.id);
};
//#region 商品分类 -----------------------------------------------start
const check_data = ref({});
const two_item_data = ref<baseData[]>([]);
const three_item_data = ref<baseData[]>([]);
const one_item_index = ref(0);
const two_item_index = ref(0);
const three_item_index = ref(0);
const one_item_text = ref('');
const two_item_text = ref('');
const three_item_text = ref('');
// 商品项点击事件
const goods_item_click = (item: baseData, level: number, index: number) => {
if (level === 1) {
one_item_index.value = index + 1;
one_item_text.value = item.name;
two_item_index.value = 0;
two_item_text.value = '';
three_item_index.value = 0;
three_item_text.value = '';
two_item_data.value = [];
three_item_data.value = [];
} else if (level === 2) {
two_item_index.value = index + 1;
two_item_text.value = item.name;
three_item_index.value = 0;
three_item_text.value = '';
three_item_data.value = [];
} else {
three_item_index.value = index + 1;
three_item_text.value = item.name;
}
if (item.children && item.children.length > 0) {
if (level === 1) {
two_item_data.value = item.children;
} else if (level === 2) {
three_item_data.value = item.children;
}
} else {
check_data.value = item;
emit('update:link', item, 0);
}
};
//#endregion 商品分类 -----------------------------------------------end
//#region 品牌 -----------------------------------------------start
const brand_item_index = ref(0);
const brand_item_click = (item: any, index: number) => {
brand_item_index.value = index + 1;
check_data.value = item;
emit('update:link', item, 1);
};
//#endregion 品牌 -----------------------------------------------end
//#region 关键字 -----------------------------------------------start
const form = reactive({
key: '',
});
const rules = ref<FormRules>({
key: [{ required: true, trigger: 'change', message: '关键词不能为空' }],
});
const ruleFormRef = ref<FormInstance>();
interface formType {
id?: number;
name: string;
link: string;
}
const on_submit = () => {
if (!ruleFormRef.value) return;
ruleFormRef.value.validate((valid: boolean) => {
if (valid) {
let new_value: formType = {
name: form.key,
link: form.key,
};
emit('update:link', new_value, 2);
} else {
emit('required');
}
});
};
//#endregion 关键字 -----------------------------------------------end
const reset_data = () => {
one_item_index.value = 0;
one_item_text.value = '';
two_item_index.value = 0;
two_item_text.value = '';
three_item_index.value = 0;
three_item_text.value = '';
two_item_data.value = [];
brand_item_index.value = 0;
form.key = '';
};
</script>
<style lang="scss" scoped>
.container {
.tabs {
.item {
width: 8rem;
height: 3rem;
line-height: 3rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease-in-out;
&:hover {
background-color: #edf4ff;
color: $cr-primary;
}
&.active {
background: #edf4ff;
color: $cr-primary;
}
}
}
.content {
.goods-tips {
background-color: #f3f2fc;
}
.goods-category,
.brand {
.item {
transition: all 0.3s ease-in-out;
&:hover,
&.active {
background: #edf4ff;
color: $cr-primary;
}
}
}
.goods-category-wdith {
width: calc((100% - 6rem) / 3);
}
}
.link-input {
width: 33.2rem;
}
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<!-- 商品 -->
<div class="container">
<div class="flex-row jc-e gap-20 mb-20">
<el-select v-model="type" class="search-w" placeholder="请选择">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="brand" class="search-w" placeholder="品牌">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-input v-model="search_value" placeholder="请输入搜索内容" class="search-w" @change="handle_search">
<template #suffix>
<icon name="search" size="16" color="9"></icon>
</template>
</el-input>
</div>
<div class="content">
<el-table :data="tableData" class="w" :header-cell-style="{ background: '#f7f7f7' }" row-key="id" height="438" fixed @row-click="row_click">
<el-table-column label="#" width="120" type="">
<template #default="scope">
<el-radio v-model="template_selection" :label="scope.$index + ''">&nbsp;</el-radio>
</template>
</el-table-column>
<el-table-column prop="id" label="ID" width="180" type="" />
<el-table-column prop="icon" label="商品图片">
<template #default="scope">
<el-image :src="scope.row.icon" class="img" />
</template>
</el-table-column>
<el-table-column prop="name" label="分类名称" />
</el-table>
<div class="mt-10 flex-row jc-e">
<el-pagination :current-page="page" :page-size="21" :pager-count="5" layout="prev, pager, next" :total="data_total" @current-change="get_list" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
// 重置
reset: {
type: Boolean,
default: () => false,
},
});
watch(
() => props.reset,
() => {
template_selection.value = '';
}
);
const modelValue = defineModel({ type: Object, default: {} });
interface User {
id: number;
name: string;
icon: string;
link: string;
}
const tableData: User[] = [
{
id: 1,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'a',
},
{
id: 3,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'c',
},
{
id: 4,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'd',
},
{
id: 5,
name: '一级分类',
icon: 'https://img.yzcdn.cn/vant/cat.jpeg',
link: 'e',
},
];
const search_value = ref('');
const handle_search = () => {
console.log(search_value.value);
};
const type = ref('');
const brand = ref('');
const options = ref([
{ value: '1', label: '一级分类' },
{ value: '2', label: '二级分类' },
{ value: '3', label: '三级分类' },
]);
const emit = defineEmits(['update:link']);
const template_selection = ref('');
const row_click = (row: any) => {
const new_table_data = JSON.parse(JSON.stringify(tableData));
template_selection.value = new_table_data.findIndex((item: User) => item.id == row.id).toString();
modelValue.value = row;
};
//#region 分页 -----------------------------------------------start
// 总页数
// const page_total = ref(0);
// 当前页
const page = ref(1);
// 总数量
const data_total = ref(0);
// 查询文件
const search_data = ref({
page: page.value,
type: '',
name: search_value.value,
});
// 查询文件
const get_list = () => {
console.log('查询接口', search_data);
};
//#region 分页 -----------------------------------------------end
</script>
<style lang="scss" scoped>
.container {
.search-w {
width: 22.5rem;
}
.content {
:deep(.el-table__inner-wrapper:before) {
background-color: transparent;
}
.img {
width: 3.6rem;
height: 3.6rem;
}
}
}
</style>

View File

@ -0,0 +1,142 @@
<template>
<!-- 商城 -->
<div class="container">
<div class="flex-row jc-e mb-20">
<div class="search">
<el-input v-model="search_value" placeholder="请输入搜索内容" class="" @change="handle_search">
<template #suffix>
<icon name="search" size="16" color="9"></icon>
</template>
</el-input>
</div>
</div>
<div class="content">
<el-scrollbar height="480px">
<div class="flex-col gap-30">
<div v-for="item in base_data" :key="item.id">
<div class="fw mb-15">{{ item.name }}</div>
<div class="flex-row flex-wrap gap-15">
<div v-for="child in item.data" :key="child.id" class="item" :class="menu_active == item.id + '-' + child.id ? 'active' : ''" @click="menu_link_event(child, item.id)">{{ child.name }}</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
// 重置
reset: {
type: Boolean,
default: () => false,
},
});
watch(
() => props.reset,
() => {
menu_active.value = '';
}
);
const modelValue = defineModel({ type: Object, default: {} });
const search_value = ref('');
interface Data {
id: number;
name: string;
link?: String;
data?: Data[];
}
const base_data = ref<Data[]>([
{
id: 0,
name: '基础链接',
data: [
{ id: 0, name: '首页', link: '首页' },
{ id: 1, name: '商城分类', link: '商城分类' },
{ id: 2, name: '购物车', link: '购物车' },
{ id: 3, name: '分类商品列表', link: '分类商品列表' },
{ id: 4, name: '退款列表', link: '退款列表' },
{ id: 5, name: '我的订单', link: '我的订单' },
{ id: 6, name: '文章列表', link: '文章列表' },
{ id: 7, name: '供应商入驻', link: '供应商入驻' },
],
},
{
id: 1,
name: '个人中心',
data: [
{ id: 0, name: '付费会员', link: '付费会员' },
{ id: 1, name: '收银页面', link: '收银页面' },
{ id: 2, name: '我的订单', link: '我的订单' },
{ id: 3, name: '我的收藏', link: '我的收藏' },
{ id: 4, name: '我的地址', link: '我的地址' },
{ id: 5, name: '我的优惠券', link: '我的优惠券' },
{ id: 6, name: '我的消息', link: '我的消息' },
{ id: 7, name: '我的资料', link: '我的资料' },
{ id: 8, name: '我的积分', link: '我的积分' },
{ id: 9, name: '我的余额', link: '我的余额' },
{ id: 10, name: '我的红包', link: '我的红包' },
],
},
{
id: 2,
name: '分销',
data: [
{ id: 0, name: '分销中心', link: '分销中心' },
{ id: 1, name: '分销订单', link: '分销订单' },
{ id: 2, name: '分销商品', link: '分销商品' },
{ id: 3, name: '分销提现', link: '分销提现' },
{ id: 4, name: '分销佣金', link: '分销佣金' },
{ id: 5, name: '分销设置', link: '分销设置' },
{ id: 6, name: '分销关系', link: '分销关系' },
{ id: 7, name: '分销商列表', link: '分销商列表' },
{ id: 8, name: '分销商等级', link: '分销商等级' },
{ id: 9, name: '分销商统计', link: '分销商统计' },
{ id: 10, name: '分销商提现', link: '分销商提现' },
],
},
]);
const handle_search = () => {
console.log(search_value.value);
};
const menu_active = ref('');
const emit = defineEmits(['update:link']);
const menu_link_event = (item: Data, parent_id: number) => {
if (`${parent_id}-${item.id}` == menu_active.value) {
menu_active.value = '';
modelValue.value = {};
} else {
menu_active.value = `${parent_id}-${item.id}`;
modelValue.value = item;
}
};
</script>
<style lang="scss" scoped>
.container {
search {
width: 22.5rem;
}
.content {
.item {
width: 10.3rem;
padding: 0 0.5rem;
height: 3.6rem;
line-height: 3.6rem;
background-color: #f9f9f9;
border-radius: 0.2rem;
font-size: 1.2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s linear;
&:hover {
background-color: #edf4ff;
color: $cr-primary;
}
&.active {
background: #edf4ff;
color: $cr-primary;
}
}
}
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<!-- 商品 -->
<div class="container">
<div class="flex-row jc-e gap-20 mb-20">
<el-input v-model="search_value" placeholder="请输入搜索内容" class="search-w" @change="handle_search">
<template #suffix>
<icon name="search" size="16" color="9"></icon>
</template>
</el-input>
</div>
<div class="content">
<el-table :data="tableData" class="w" :header-cell-style="{ background: '#f7f7f7' }" row-key="id" height="438" fixed @row-click="row_click">
<el-table-column label="#" width="120" type="">
<template #default="scope">
<el-radio v-model="template_selection" :label="scope.$index + ''">&nbsp;</el-radio>
</template>
</el-table-column>
<el-table-column prop="id" label="ID" width="180" type="" />
<el-table-column prop="name" label="页面名称" />
<el-table-column prop="link" label="页面链接" />
</el-table>
<div class="mt-10 flex-row jc-e">
<el-pagination :current-page="page" :page-size="21" :pager-count="5" layout="prev, pager, next" :total="data_total" @current-change="get_list" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
// 重置
reset: {
type: Boolean,
default: () => false,
},
});
watch(
() => props.reset,
() => {
template_selection.value = '';
}
);
const modelValue = defineModel({ type: Object, default: {} });
interface User {
id: number;
name: string;
link: string;
}
const tableData: User[] = [
{
id: 1,
name: '一级分类',
link: 'a',
},
{
id: 3,
name: '一级分类',
link: 'c',
},
{
id: 4,
name: '一级分类',
link: 'd',
},
{
id: 5,
name: '一级分类',
link: 'e',
},
];
const search_value = ref('');
const handle_search = () => {
console.log(search_value.value);
};
const emit = defineEmits(['update:link']);
const template_selection = ref('');
const row_click = (row: any) => {
const new_table_data = JSON.parse(JSON.stringify(tableData));
template_selection.value = new_table_data.findIndex((item: User) => item.id == row.id).toString();
modelValue.value = row;
};
//#region 分页 -----------------------------------------------start
// 总页数
// const page_total = ref(0);
// 当前页
const page = ref(1);
// 总数量
const data_total = ref(0);
// 查询文件
const search_data = ref({
page: page.value,
type: '',
name: search_value.value,
});
// 查询文件
const get_list = () => {
console.log('查询接口', search_data);
};
//#region 分页 -----------------------------------------------end
</script>
<style lang="scss" scoped>
.container {
.search-w {
width: 22.5rem;
}
.content {
:deep(.el-table__inner-wrapper:before) {
background-color: transparent;
}
.img {
width: 3.6rem;
height: 3.6rem;
}
}
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<div class="container">
<el-form :model="form" label-width="70">
<card-container class="mb-8">
<div class="mb-12">展示设置</div>
<el-form-item label="导航样式">
<el-radio-group v-model="form.nav_style" is-button @change="nav_style_change">
<el-radio value="0">图片加文字</el-radio>
<el-radio value="1">图片</el-radio>
<el-radio value="2">文字</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="导航类型">
<el-radio-group v-model="form.nav_type" is-button @change="nav_type_change">
<el-radio value="0">底部固定</el-radio>
<el-radio value="1">底部悬浮</el-radio>
</el-radio-group>
</el-form-item>
</card-container>
<card-container class="footer-nav-height">
<div class="mb-12">导航内容</div>
<div class="size-12 cr-c mb-20">图片建议宽高80*80鼠标拖拽左侧圆点可调整导航顺序</div>
<div class="nav-list">
<drag :data="form.nav_content" type="card" :space-col="20" @remove="nav_content_remove">
<template #default="{ row }">
<div class="w">
<el-form-item label="图标" label-width="45">
<div class="flex-col jc-c align-c mr-12">
<upload v-model="row.src" :limit="1" :size="44" :styles="1"></upload>
<text class="cr-9 size-12">选中</text>
</div>
<div class="flex-col jc-c align-c">
<upload v-model="row.src_checked" :limit="1" :size="44" :styles="1"></upload>
<text class="cr-9 size-12">未选中</text>
</div>
</el-form-item>
<el-form-item label="名称" label-width="45">
<el-input v-model="row.name" placeholder="请输入名称" clearable />
</el-form-item>
<el-form-item label="链接" label-width="45">
<url-value v-model="row.href"></url-value>
</el-form-item>
</div>
</template>
</drag>
<el-button class="mtb-20 w" @click="add">+添加</el-button>
</div>
</card-container>
</el-form>
</div>
</template>
<script setup lang="ts">
import { get_math } from '@/utils';
const props = defineProps({
value: {
type: Object,
default: () => ({
nav_style: '0',
nav_type: '0',
nav_content: [
{ id: '1', name: '首页', src: [{ id: 1, url: '/src/assets/images/layout/main/phone.png', original: '头像1', title: '头像1', ext: '.png', type: 'img' }], src_checked: 'tabs', href: {} },
{ id: '2', name: '分类', src: [{ id: 1, url: '/src/assets/images/layout/main/phone.png', original: '头像1', title: '头像1', ext: '.png', type: 'img' }], src_checked: 'tabs', href: {} },
{ id: '3', name: '购物车', src: [{ id: 1, url: '/src/assets/images/layout/main/phone.png', original: '头像1', title: '头像1', ext: '.png', type: 'img' }], src_checked: 'tabs', href: {} },
{ id: '4', name: '我的', src: [{ id: 1, url: '/src/assets/images/layout/main/phone.png', original: '头像1', title: '头像1', ext: '.png', type: 'img' }], src_checked: 'tabs', href: {} },
],
}),
},
});
const form = reactive(props.value);
const emit = defineEmits(['update:value']);
const nav_style_change = (style: any) => {
form.nav_style = style;
emit('update:value', form);
};
const nav_type_change = (type: any) => {
form.nav_type = type;
emit('update:value', form);
};
const nav_content_remove = (index: number) => {
form.nav_content.splice(index, 1);
emit('update:value', form);
};
const add = () => {
form.nav_content.push({
id: get_math(),
name: '',
src: [],
src_checked: [],
href: {},
});
};
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
.footer-nav-height {
min-height: calc(100vh - 36.8rem);
}
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div class="container">
<template v-if="type == '1'">
<footer-nav-content :value="form.content" @update:value="content_update"></footer-nav-content>
</template>
<template v-else-if="type == '2'">
<footer-nav-styles :value="form.style" @update:value="style_update"></footer-nav-styles>
</template>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
type: {
type: String,
default: '1',
},
value: {
type: Object,
default: () => ({}),
},
});
const form = reactive(props.value);
const emit = defineEmits(['update:value']);
const content_update = (value: any) => {
form.content = value;
let new_data = form.style.common_style;
const new_arry = ['margin', 'margin_left', 'margin_right', 'margin_top', 'margin_bottom', 'padding', 'padding_left', 'padding_right', 'padding_top', 'padding_bottom', 'radius', 'radius_top_left', 'radius_top_right', 'radius_bottom_left', 'radius_bottom_right'];
if (value.nav_type == '1') {
new_arry.forEach((item) => {
if (item.indexOf('radius') !== -1) {
new_data[item] = 100;
} else {
new_data[item] = 10;
}
});
} else {
new_arry.forEach((item) => {
new_data[item] = 0;
});
}
};
const style_update = (value: any) => {
form.style = value;
};
</script>

View File

@ -0,0 +1,59 @@
<template>
<div class="container">
<el-form :model="form" label-width="70">
<card-container class="mb-8">
<div class="mb-12">颜色设置</div>
<el-form-item label="选中文本">
<color-picker v-model="form.text_color_checked" default-color="rgba(204, 204, 204, 1)" @update:value="text_color_checked_event" />
</el-form-item>
<el-form-item label="默认文本">
<color-picker v-model="form.default_text_color" default-color="rgba(0, 0, 0, 1)" @update:value="default_text_color_event" />
</el-form-item>
</card-container>
</el-form>
<common-styles :value="form.common_style" @update:value="common_styles_update" />
</div>
</template>
<script setup lang="ts">
import { omit } from 'lodash';
const props = defineProps({
value: {
type: Object,
default: () => {
return {
text_color_checked: 'rgba(204, 204, 204, 1)',
default_text_color: 'rgba(0, 0, 0, 1)',
};
},
},
});
const emit = defineEmits(['update:value']);
const state = reactive({
form: props.value,
});
// 如果需要解构确保使用toRefs
const { form } = toRefs(state);
const text_color_checked_event = (val: string | null) => {
form.value.text_color_checked = val;
call_back_update(form);
};
const default_text_color_event = (val: string | null) => {
form.value.default_text_color = val;
call_back_update(form);
};
const common_styles_update = (val: Object) => {
form.value.common_style = val;
call_back_update(form.value);
};
const call_back_update = (val: Object) => {
emit('update:value', val);
};
</script>
<style lang="scss" scoped>
.container {
width: 100%;
.container-height {
min-height: calc(100vh - 36.8rem);
}
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<div class="footer-nav flex-row jc-c align-c" :class="showFooter ? 'br-2 br-primary' : ''" @click="footer_nav_event">
<div class="footer-nav-content flex-row jc-c align-c w" :style="style_container">
<ul class="flex-row jc-sa align-c w">
<li v-for="(item, index) in footerData.content.nav_content" :key="index" class="flex-1 flex-col jc-c align-c gap-5" @mouseenter="is_hover = index + 1" @mouseleave="is_hover = 0">
<div v-if="footerData.content.nav_style !== '2'" class="img re">
<img class="img-item abs radius-xs animate-linear w" :class="is_hover != index + 1 ? 'active' : ''" :src="item.src[0]?.url" width="22" height="22" />
<img class="img-item abs radius-xs animate-linear w" :class="is_hover == index + 1 ? 'active' : ''" :src="item.src_checked[0]?.url" width="22" height="22" />
</div>
<span v-if="footerData.content.nav_style !== '1'" class="animate-linear size-12" :style="is_hover == index + 1 ? text_color_checked : default_text_color">{{ item.name }}</span>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { common_styles_computer } from '@/utils';
import { footerNavCounterStore } from '@/store/modules/footer-nav-content';
const footer_nav_counter_store = footerNavCounterStore();
const props = defineProps({
showFooter: {
type: Boolean,
default: false,
},
footerData: {
type: Object,
default: () => ({}),
},
});
const style_container = ref('');
const default_text_color = ref('');
const text_color_checked = ref('');
let is_hover = ref(0);
watch(
props.footerData,
(newVal, oldValue) => {
const new_content = newVal?.content || {};
const new_style = newVal?.style || {};
default_text_color.value = 'color:' + new_style.default_text_color || 'rgba(0, 0, 0, 1)';
text_color_checked.value = 'color:' + new_style.text_color_checked || 'rgba(204, 204, 204, 1)';
style_container.value = common_styles_computer(new_style.common_style);
let footer_height = new_style.common_style.padding_top + new_style.common_style.padding_bottom + new_style.common_style.margin_top + new_style.common_style.margin_bottom + 50;
if (footer_height >= 70) {
footer_height = footer_height;
} else {
footer_height = 70;
}
footer_nav_counter_store.padding_footer_computer(footer_height);
},
{ immediate: true, deep: true }
);
const emits = defineEmits(['footer-nav']);
const footer_nav_event = () => {
emits('footer-nav');
};
</script>
<style lang="scss" scoped>
.footer-nav {
width: 39rem;
margin: 0 auto;
padding: 0rem;
cursor: pointer;
background-color: transparent;
.footer-nav-content {
min-height: 7rem;
.img {
width: 2rem;
height: 2rem;
.img-item {
opacity: 0;
&.active {
opacity: 1;
}
}
}
}
}
.br-2 {
border: 0.2rem solid $cr-main;
}
</style>

View File

@ -0,0 +1,98 @@
<template>
<div :style="style_container">
<div class="re" :style="style">
<div class="flex-warp gap-10" :class="article_type == '1' ? 'style1 flex-row' : article_type == '0' ? 'style2 flex-col' : 'style3 flex-col'">
<card-container class="item gap-10" padding="10px" :class="article_type == '0' ? 'flex-row' : 'flex-col'" :style="content_radius">
<img v-if="is_img_show" src="@/assets/images/components/model-video/video.png" />
<div class="flex-col jc-sb gap-8">
<div class="title text-line-2" :style="article_name">华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修</div>
<div class="flex-row jc-sb gap-8">
<div :style="article_date">{{ is_show.includes('0') ? '2020-06-05 15:20' : '' }}</div>
<icon v-show="is_show.includes('1')" name="eye" :style="article_page_view">16</icon>
</div>
</div>
</card-container>
<card-container class="item gap-10" padding="10px" :class="article_type == '0' ? 'flex-row' : 'flex-col'" :style="content_radius">
<img v-if="is_img_show" src="@/assets/images/components/model-video/video.png" />
<div class="flex-col jc-sb gap-8">
<div class="title text-line-2" :style="article_name">华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修华为荣耀畅想平板换屏服务屏幕换外屏幕主板维修</div>
<div class="flex-row jc-sb gap-8">
<div :style="article_date">{{ is_show.includes('0') ? '2020-06-05 15:20' : '' }}</div>
<icon v-show="is_show.includes('1')" name="eye" :style="article_page_view">16</icon>
</div>
</div>
</card-container>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { common_styles_computer, radius_computer } from '@/utils';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
isCommonStyle: {
type: Boolean,
default: true,
},
});
const style = ref('');
const style_container = ref('');
// 风格
const article_type = ref('0');
// 是否显示
const is_show = ref(['0', '1']);
// 是否显示封面图片
const is_img_show = ref(true);
// 文章
const article_name = ref('');
// 日期
const article_date = ref('');
// 浏览量
const article_page_view = ref('');
// 内容圆角
const content_radius = ref('');
watch(
props.value,
(newVal, oldValue) => {
const new_content = newVal?.content;
const new_style = newVal?.style;
// 内容
article_type.value = new_content.article_style;
is_show.value = new_content.is_show;
is_img_show.value = new_content.is_img_show;
// 样式
article_name.value = 'font-size:' + new_style.name_size + 'px;' + 'font-weight:' + new_style.name_weight + ';' + 'color:' + new_style.name_color + ';';
article_date.value = 'font-size:' + new_style.time_size + 'px;' + 'font-weight:' + new_style.time_weight + ';' + 'color:' + new_style.time_color + ';';
article_page_view.value = 'font-size:' + new_style.page_view_size + 'px;' + 'font-weight:' + new_style.page_view_weight + ';' + 'color:' + new_style.page_view_color + ';';
content_radius.value = radius_computer(new_style);
if (new_style.common_style && props.isCommonStyle) {
style_container.value = common_styles_computer(new_style.common_style);
}
},
{ immediate: true, deep: true }
);
</script>
<style lang="scss" scoped>
.style1 {
.item {
width: calc(50% - 0.5rem);
}
}
.style2 {
.item {
width: 100%;
img {
width: 11rem;
height: 8.3rem;
}
}
}
.style3 {
.item {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<div class="content">
<el-form :model="form" label-width="70" class="m-h">
<card-container class="mb-8">
<div class="mb-12">展示设置</div>
<el-form-item label="选择风格">
<el-radio-group v-model="form.article_style">
<el-radio v-for="item in base_list.article_style_list" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
</card-container>
<div class="content-height bg-f">
<card-container class="card-container-br">
<div class="mb-12">文章设置</div>
<el-form-item label="读取方式">
<el-radio-group v-model="form.article_check">
<el-radio v-for="item in base_list.article_list" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="form.article_check === '0'">
<div class="nav-list">
<drag :data="form.article_list" :space-col="20" @remove="article_list_remove" @on-sort="article_list_sort">
<template #default="{ row }">
<upload v-model="row.new_url" :limit="1" size="40" styles="2"></upload>
<el-image :src="row.url" fit="contain" class="img">
<template #error>
<div class="bg-f5 flex-row jc-c align-c radius h w">
<icon name="error-img" size="16" color="9"></icon>
</div>
</template>
</el-image>
<div class="flex-1 flex-width">
<url-value v-model="row.link"></url-value>
</div>
</template>
</drag>
<el-button class="mtb-20 w" @click="add">+添加</el-button>
</div>
</template>
<template v-else>
<el-form-item label="文章分类">
<el-select v-model="form.article_type" multiple collapse-tags placeholder="请选择文章分类">
<el-option v-for="item in base_list.article_type_list" :key="item.value" :label="item.name" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="显示数量">
<el-input v-model="form.number" type="number" placeholder="请输入显示数量" clearable />
</el-form-item>
<el-form-item label="排序类型">
<el-radio-group v-model="form.sort">
<el-radio v-for="item in base_list.sort_list" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序规则">
<el-radio-group v-model="form.sort_rules">
<el-radio v-for="item in base_list.sort_rules_list" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="封面图片">
<el-switch v-model="form.is_img_show" />
</el-form-item>
</template>
</card-container>
<card-container>
<div class="mb-12">列表设置</div>
<el-form-item label="是否显示">
<el-checkbox-group v-model="form.is_show">
<el-checkbox v-for="item in base_list.list_show_list" :key="item.value" :value="item.value">{{ item.name }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</card-container>
</div>
</el-form>
</div>
</template>
<script setup lang="ts">
import { get_math } from '@/utils';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
});
const form = reactive(props.value);
const base_list = reactive({
article_style_list: [
{ name: '单列展示', value: '0' },
{ name: '两列展示(纵向)', value: '1' },
{ name: '大图展示', value: '2' },
{ name: '左右滑动展示', value: '3' },
],
article_list: [
{ name: '选择文章', value: '0' },
{ name: '筛选文章', value: '1' },
],
article_type_list: [
{ name: '样式一', value: '0' },
{ name: '样式二', value: '1' },
{ name: '样式三', value: '2' },
],
sort_list: [
{ name: '综合', value: '0' },
{ name: '时间', value: '1' },
{ name: '浏览量', value: '2' },
],
sort_rules_list: [
{ name: '降序desc', value: '0' },
{ name: '升序asc', value: '1' },
],
list_show_list: [
{ name: '日期时间', value: '0' },
{ name: '浏览量', value: '1' },
],
});
const article_list_remove = (index: number) => {
form.article_list.splice(index, 1);
};
const article_list_sort = (item: any) => {
form.article_list = item;
};
const add = () => {
form.article_list.push({
id: get_math(),
src: '',
new_url: [],
href: {},
});
};
</script>
<style lang="scss" scoped>
.content {
width: 100%;
.content-height {
min-height: calc(100vh - 31.8rem);
.card-container-br {
border-bottom: 0.8rem solid #f0f2f5;
}
}
}
.img {
width: 4rem;
height: 4rem;
}
</style>

View File

@ -0,0 +1,22 @@
<template>
<div class="setting-content">
<template v-if="type == '1'">
<model-article-list-content :value="value.content"></model-article-list-content>
</template>
<template v-else-if="type == '2'">
<model-article-list-styles :value="value.style"></model-article-list-styles>
</template>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
type: {
type: String,
default: '1',
},
value: {
type: Object,
default: () => ({}),
},
});
</script>

View File

@ -0,0 +1,69 @@
<template>
<div class="styles">
<el-form :model="form" label-width="70">
<card-container class="mb-8">
<div class="mb-12">列表样式</div>
<el-form-item label="文章名称">
<el-radio-group v-model="form.name_weight">
<el-radio v-for="item in font_weight" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="字号">
<slider v-model="form.name_size"></slider>
</el-form-item>
<el-form-item label="名称色值">
<color-picker v-model="form.name_color"></color-picker>
</el-form-item>
<el-form-item label="日期时间">
<el-radio-group v-model="form.time_weight">
<el-radio v-for="item in font_weight" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="字号">
<slider v-model="form.time_size"></slider>
</el-form-item>
<el-form-item label="日期颜色">
<color-picker v-model="form.time_color"></color-picker>
</el-form-item>
<el-form-item label="浏览量">
<el-radio-group v-model="form.page_view_weight">
<el-radio v-for="item in font_weight" :key="item.value" :value="item.value">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="字号">
<slider v-model="form.page_view_size"></slider>
</el-form-item>
<el-form-item label="浏览色值">
<color-picker v-model="form.page_view_color"></color-picker>
</el-form-item>
<el-form-item label="内容圆角">
<radius :value="form"></radius>
</el-form-item>
</card-container>
</el-form>
<common-styles :value="form.common_style" @update:value="common_style_update" />
</div>
</template>
<script setup lang="ts">
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['update:value']);
const font_weight = reactive([
{ name: '加粗', value: '500' },
{ name: '正常', value: '400' },
]);
// 默认值
const form = ref(props.value);
const common_style_update = (value: any) => {
form.value.common_style = value;
};
</script>
<style lang="scss" scoped>
.styles {
width: 100%;
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div :style="style_container">
<tabs-view ref="tabs" :value="article_tabs"></tabs-view>
<div class="pt-10">
<model-article-list :value="article_tabs" :is-common-style="false"></model-article-list>
</div>
</div>
</template>
<script setup lang="ts">
import { common_styles_computer, get_math } from '@/utils';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
});
const style_container = ref('');
const article_tabs = ref(props.value);
watch(
article_tabs.value,
(newVal, oldValue) => {
const new_style = newVal?.style;
let new_data = newVal;
new_data.content.article_check = new_data.content.tabs_list[0].article_check;
new_data.content.article_type = new_data.content.tabs_list[0].article_type;
new_data.content.number = new_data.content.tabs_list[0].number;
new_data.content.sort = new_data.content.tabs_list[0].sort;
new_data.content.sort_rules = new_data.content.tabs_list[0].sort_rules;
new_data.content.is_img_show = new_data.content.tabs_list[0].is_img_show;
new_data.content.article_list = new_data.content.tabs_list[0].article_list;
article_tabs.value = new_data;
style_container.value += common_styles_computer(new_style.common_style);
},
{ immediate: true, deep: true }
);
</script>
<style lang="scss" scoped>
.video {
height: 22rem;
}
</style>

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