基本功能实现

This commit is contained in:
hxuanyu 2025-06-25 23:53:40 +08:00
parent 9e4aa2c725
commit d70b962ef5
21 changed files with 2318 additions and 54 deletions

View File

@ -1,5 +1,62 @@
# Vue 3 + TypeScript + Vite # OBS Overlay Widget
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. A collection of highly customizable widgets for OBS Studio streaming and recording, built with Vue 3, TypeScript, and Vite.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup). ## Features
- **Time and Date Display**: Customizable formats for showing current time and date
- **Text Display**: Show fixed text or API-returned content with custom styling
- **Image Display**: Display local or remote images with customization options
- **Split View Interface**: Configuration panel on the left, real-time preview on the right
- **Transparent Background**: All widgets have transparent backgrounds suitable for OBS overlay
- **URL Generation**: Automatically generates sharable URLs with encoded configuration
- **Pure Preview Mode**: Open generated URLs to display only the widget with transparent background
## Widget Types
1. **Clock Widget**: Display current time with customizable format, style, and effects
2. **Date Widget**: Show current date with customizable format, style, and effects
3. **Text Widget**: Display text with customizable styles including gradients, shadows, and fonts
4. **Image Widget**: Show images with customizable size, effects, and positioning
## Usage
1. Select a widget type from the dropdown
2. Configure the widget using the control panel on the left
3. See real-time preview on the right
4. Copy the generated URL to use in OBS Studio as a Browser Source
## Development
```bash
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
```
## Troubleshooting
If you encounter TypeScript errors related to undefined properties, make sure that:
1. All widget components handle possible undefined configuration properties
2. Default values are provided for all configuration options
3. Use proper null checking (e.g., `props.config.property || defaultValue`)
## Integration with OBS Studio
1. Run this application on a web server or locally
2. Configure your widget using the configuration interface
3. Copy the generated URL
4. In OBS Studio:
- Add a "Browser Source" to your scene
- Paste the URL into the Browser Source URL field
- Set width and height according to your needs
- Check "Shutdown source when not visible" for better performance

View File

@ -4,7 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title> <meta name="description" content="OBS Overlay Widget - Customizable widgets for OBS streaming and recording" />
<title>OBS Overlay Widget</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

269
package-lock.json generated
View File

@ -8,7 +8,11 @@
"name": "obs-overlay-widget", "name": "obs-overlay-widget",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"vue": "^3.5.17" "@element-plus/icons-vue": "^2.3.1",
"dayjs": "^1.11.13",
"element-plus": "^2.10.2",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",
@ -64,6 +68,24 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
"integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5", "version": "0.25.5",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
@ -489,12 +511,48 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.7.1",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.1.tgz",
"integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.1",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.1.tgz",
"integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.1",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",
"resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.19", "version": "1.0.0-beta.19",
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz",
@ -789,6 +847,27 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": {
"version": "4.17.18",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.18.tgz",
"integrity": "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz",
@ -896,6 +975,12 @@
"he": "^1.2.0" "he": "^1.2.0"
} }
}, },
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/language-core": { "node_modules/@vue/language-core": {
"version": "2.2.10", "version": "2.2.10",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.10.tgz", "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.10.tgz",
@ -990,6 +1075,94 @@
} }
} }
}, },
"node_modules/@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"license": "MIT",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/alien-signals": { "node_modules/alien-signals": {
"version": "1.0.13", "version": "1.0.13",
"resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz",
@ -997,6 +1170,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1020,6 +1199,12 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/de-indent": { "node_modules/de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
@ -1027,6 +1212,32 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/element-plus": {
"version": "2.10.2",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.10.2.tgz",
"integrity": "sha512-p2KiAa0jEGXrzdlTAfpiS7HQFAhla4gvx6H7RuDf+OO0uC3DGpolxvdHjFR8gt7+vaWyxQNcHa1sAdBkmjqlgA==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.1",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.14.182",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^9.1.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.13",
"escape-html": "^1.0.3",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/entities": { "node_modules/entities": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
@ -1080,6 +1291,12 @@
"@esbuild/win32-x64": "0.25.5" "@esbuild/win32-x64": "0.25.5"
} }
}, },
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
@ -1126,6 +1343,29 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"license": "MIT",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz",
@ -1135,6 +1375,12 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
@ -1176,6 +1422,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause"
},
"node_modules/path-browserify": { "node_modules/path-browserify": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
@ -1413,6 +1665,21 @@
} }
} }
}, },
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vue-tsc": { "node_modules/vue-tsc": {
"version": "2.2.10", "version": "2.2.10",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.10.tgz", "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.10.tgz",

View File

@ -9,7 +9,11 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"vue": "^3.5.17" "@element-plus/icons-vue": "^2.3.1",
"dayjs": "^1.11.13",
"element-plus": "^2.10.2",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",

View File

@ -1,30 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue' // App.vue is the main component that uses the router
</script> </script>
<template> <template>
<div> <router-view />
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</template> </template>
<style scoped> <style>
.logo { body {
height: 6em; margin: 0;
padding: 1.5em; padding: 0;
will-change: filter; font-family: Arial, sans-serif;
transition: filter 300ms; overflow: hidden;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
} }
</style> </style>

View File

@ -0,0 +1,224 @@
<template>
<div class="clock-config">
<h2>Clock Widget Settings</h2>
<el-form label-position="top">
<el-form-item label="Time Format">
<el-select v-model="localConfig.format" placeholder="Select format">
<el-option label="HH:mm:ss (24-hour)" value="HH:mm:ss" />
<el-option label="HH:mm (24-hour)" value="HH:mm" />
<el-option label="hh:mm:ss A (12-hour)" value="hh:mm:ss A" />
<el-option label="hh:mm A (12-hour)" value="hh:mm A" />
</el-select>
</el-form-item>
<el-form-item label="Show Seconds">
<el-switch v-model="localConfig.showSeconds" />
</el-form-item>
<el-divider>Text Style</el-divider>
<el-form-item label="Font Size">
<el-slider v-model="localConfig.fontSize" :min="12" :max="120" show-input />
</el-form-item>
<el-form-item label="Font Family">
<el-select v-model="localConfig.fontFamily">
<el-option label="Arial" value="Arial" />
<el-option label="Helvetica" value="Helvetica" />
<el-option label="Times New Roman" value="'Times New Roman'" />
<el-option label="Courier New" value="'Courier New'" />
<el-option label="Georgia" value="Georgia" />
<el-option label="Verdana" value="Verdana" />
<el-option label="Impact" value="Impact" />
</el-select>
</el-form-item>
<el-form-item label="Font Weight">
<el-select v-model="localConfig.fontWeight">
<el-option label="Normal" value="normal" />
<el-option label="Bold" value="bold" />
<el-option label="Light" value="lighter" />
</el-select>
</el-form-item>
<el-divider>Color Settings</el-divider>
<el-form-item label="Use Gradient Colors">
<el-switch v-model="localConfig.useGradient" />
</el-form-item>
<template v-if="!localConfig.useGradient">
<el-form-item label="Text Color">
<el-color-picker v-model="localConfig.color" show-alpha />
</el-form-item>
</template>
<template v-else>
<el-form-item label="Gradient Start Color">
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
</el-form-item>
<el-form-item label="Gradient End Color">
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
</el-form-item>
</template>
<el-divider>Effects</el-divider>
<el-form-item label="Text Shadow">
<el-switch v-model="localConfig.textShadow" />
</el-form-item>
<template v-if="localConfig.textShadow">
<el-form-item label="Shadow Color">
<el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item>
<el-form-item label="Shadow Blur">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
</el-form-item>
</template>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="applyPreset('modern')">Modern</el-button>
<el-button type="success" @click="applyPreset('neon')">Neon</el-button>
<el-button type="warning" @click="applyPreset('elegant')">Elegant</el-button>
<el-button type="danger" @click="applyPreset('minimal')">Minimal</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// Define props interface for config
interface ClockConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showSeconds: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ClockConfig>;
}>(), {
config: () => ({
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
})
});
// Define emit
const emit = defineEmits<{
(event: 'update:config', config: ClockConfig): void;
}>();
// Local config for two-way binding
const localConfig = ref<ClockConfig>({
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
});
// Sync with parent config on mount
onMounted(() => {
// Merge default config with provided config
localConfig.value = { ...localConfig.value, ...props.config };
});
// Preset styles
const presets = {
modern: {
format: 'HH:mm',
fontSize: 72,
fontFamily: 'Arial',
fontWeight: 'bold',
useGradient: true,
gradientColors: ['#3498db', '#9b59b6'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 10,
showSeconds: false
},
neon: {
format: 'HH:mm:ss',
fontSize: 60,
fontFamily: 'Impact',
fontWeight: 'normal',
color: '#39ff14',
useGradient: false,
textShadow: true,
shadowColor: 'rgba(57, 255, 20, 0.8)',
shadowBlur: 15,
showSeconds: true
},
elegant: {
format: 'hh:mm A',
fontSize: 64,
fontFamily: 'Georgia',
fontWeight: 'normal',
useGradient: true,
gradientColors: ['#d4af37', '#f1c40f'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowBlur: 5,
showSeconds: false
},
minimal: {
format: 'HH:mm',
fontSize: 56,
fontFamily: 'Helvetica',
fontWeight: 'lighter',
color: '#ffffff',
useGradient: false,
textShadow: false,
showSeconds: false
}
};
// Apply preset
const applyPreset = (presetName: keyof typeof presets) => {
const preset = presets[presetName];
localConfig.value = { ...localConfig.value, ...preset };
};
// Watch for local changes and emit to parent
watch(localConfig, (newConfig) => {
emit('update:config', { ...newConfig });
}, { deep: true });
</script>
<style scoped>
.clock-config {
padding: 10px;
}
</style>

View File

@ -0,0 +1,225 @@
<template>
<div class="date-config">
<h2>Date Widget Settings</h2>
<el-form label-position="top">
<el-form-item label="Date Format">
<el-select v-model="localConfig.format" placeholder="Select format">
<el-option label="YYYY-MM-DD" value="YYYY-MM-DD" />
<el-option label="MM/DD/YYYY" value="MM/DD/YYYY" />
<el-option label="DD/MM/YYYY" value="DD/MM/YYYY" />
<el-option label="MMMM D, YYYY" value="MMMM D, YYYY" />
<el-option label="D MMMM YYYY" value="D MMMM YYYY" />
</el-select>
</el-form-item>
<el-form-item label="Show Weekday">
<el-switch v-model="localConfig.showWeekday" />
</el-form-item>
<el-divider>Text Style</el-divider>
<el-form-item label="Font Size">
<el-slider v-model="localConfig.fontSize" :min="12" :max="80" show-input />
</el-form-item>
<el-form-item label="Font Family">
<el-select v-model="localConfig.fontFamily">
<el-option label="Arial" value="Arial" />
<el-option label="Helvetica" value="Helvetica" />
<el-option label="Times New Roman" value="'Times New Roman'" />
<el-option label="Courier New" value="'Courier New'" />
<el-option label="Georgia" value="Georgia" />
<el-option label="Verdana" value="Verdana" />
<el-option label="Impact" value="Impact" />
</el-select>
</el-form-item>
<el-form-item label="Font Weight">
<el-select v-model="localConfig.fontWeight">
<el-option label="Normal" value="normal" />
<el-option label="Bold" value="bold" />
<el-option label="Light" value="lighter" />
</el-select>
</el-form-item>
<el-divider>Color Settings</el-divider>
<el-form-item label="Use Gradient Colors">
<el-switch v-model="localConfig.useGradient" />
</el-form-item>
<template v-if="!localConfig.useGradient">
<el-form-item label="Text Color">
<el-color-picker v-model="localConfig.color" show-alpha />
</el-form-item>
</template>
<template v-else>
<el-form-item label="Gradient Start Color">
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
</el-form-item>
<el-form-item label="Gradient End Color">
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
</el-form-item>
</template>
<el-divider>Effects</el-divider>
<el-form-item label="Text Shadow">
<el-switch v-model="localConfig.textShadow" />
</el-form-item>
<template v-if="localConfig.textShadow">
<el-form-item label="Shadow Color">
<el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item>
<el-form-item label="Shadow Blur">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
</el-form-item>
</template>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="applyPreset('modern')">Modern</el-button>
<el-button type="success" @click="applyPreset('elegant')">Elegant</el-button>
<el-button type="warning" @click="applyPreset('casual')">Casual</el-button>
<el-button type="danger" @click="applyPreset('minimal')">Minimal</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// Define props interface for config
interface DateConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showWeekday: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<DateConfig>;
}>(), {
config: () => ({
format: 'YYYY-MM-DD',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showWeekday: true
})
});
// Define emit
const emit = defineEmits<{
(event: 'update:config', config: DateConfig): void;
}>();
// Local config for two-way binding
const localConfig = ref<DateConfig>({
format: 'YYYY-MM-DD',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showWeekday: true
});
// Sync with parent config on mount
onMounted(() => {
// Merge default config with provided config
localConfig.value = { ...localConfig.value, ...props.config };
});
// Preset styles
const presets = {
modern: {
format: 'YYYY-MM-DD',
fontSize: 42,
fontFamily: 'Arial',
fontWeight: 'bold',
useGradient: true,
gradientColors: ['#3498db', '#9b59b6'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 10,
showWeekday: true
},
elegant: {
format: 'MMMM D, YYYY',
fontSize: 36,
fontFamily: 'Georgia',
fontWeight: 'normal',
useGradient: true,
gradientColors: ['#d4af37', '#f1c40f'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowBlur: 5,
showWeekday: true
},
casual: {
format: 'D MMMM YYYY',
fontSize: 32,
fontFamily: 'Verdana',
fontWeight: 'normal',
color: '#2ecc71',
useGradient: false,
textShadow: true,
shadowColor: 'rgba(46, 204, 113, 0.5)',
shadowBlur: 8,
showWeekday: true
},
minimal: {
format: 'MM/DD/YYYY',
fontSize: 28,
fontFamily: 'Helvetica',
fontWeight: 'lighter',
color: '#ffffff',
useGradient: false,
textShadow: false,
showWeekday: false
}
};
// Apply preset
const applyPreset = (presetName: keyof typeof presets) => {
const preset = presets[presetName];
localConfig.value = { ...localConfig.value, ...preset };
};
// Watch for local changes and emit to parent
watch(localConfig, (newConfig) => {
emit('update:config', { ...newConfig });
}, { deep: true });
</script>
<style scoped>
.date-config {
padding: 10px;
}
</style>

View File

@ -0,0 +1,175 @@
<template>
<div class="image-config">
<h2>Image Widget Settings</h2>
<el-form label-position="top">
<el-form-item label="Image URL">
<el-input v-model="localConfig.imageUrl" placeholder="Enter image URL" />
</el-form-item>
<div class="preview-image" v-if="localConfig.imageUrl">
<img :src="localConfig.imageUrl" alt="Preview" style="max-width: 100%; max-height: 150px;" />
</div>
<el-divider>Size & Appearance</el-divider>
<el-form-item label="Width (px)">
<el-slider v-model="localConfig.width" :min="50" :max="800" show-input />
</el-form-item>
<el-form-item label="Height (px)">
<el-slider v-model="localConfig.height" :min="50" :max="800" show-input />
</el-form-item>
<el-form-item label="Opacity">
<el-slider v-model="localConfig.opacity" :min="0" :max="1" :step="0.01" show-input />
</el-form-item>
<el-form-item label="Border Radius (px)">
<el-slider v-model="localConfig.borderRadius" :min="0" :max="100" show-input />
</el-form-item>
<el-divider>Effects</el-divider>
<el-form-item label="Shadow">
<el-switch v-model="localConfig.shadow" />
</el-form-item>
<template v-if="localConfig.shadow">
<el-form-item label="Shadow Color">
<el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item>
<el-form-item label="Shadow Blur">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="50" show-input />
</el-form-item>
</template>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="applyPreset('normal')">Normal</el-button>
<el-button type="success" @click="applyPreset('rounded')">Rounded</el-button>
<el-button type="warning" @click="applyPreset('shadow')">Shadow</el-button>
<el-button type="danger" @click="applyPreset('circular')">Circular</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// Define props interface for config
interface ImageConfig {
imageUrl: string;
width: number;
height: number;
opacity: number;
borderRadius: number;
shadow: boolean;
shadowColor: string;
shadowBlur: number;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ImageConfig>;
}>(), {
config: () => ({
imageUrl: '',
width: 200,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 10
})
});
// Define emit
const emit = defineEmits<{
(event: 'update:config', config: ImageConfig): void;
}>();
// Local config for two-way binding
const localConfig = ref<ImageConfig>({
imageUrl: '',
width: 200,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 10
});
// Sync with parent config on mount
onMounted(() => {
// Merge default config with provided config
localConfig.value = { ...localConfig.value, ...props.config };
});
// Preset styles
const presets = {
normal: {
width: 300,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false
},
rounded: {
width: 300,
height: 200,
opacity: 1,
borderRadius: 12,
shadow: false
},
shadow: {
width: 300,
height: 200,
opacity: 1,
borderRadius: 8,
shadow: true,
shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowBlur: 20
},
circular: {
width: 200,
height: 200,
opacity: 1,
borderRadius: 100,
shadow: true,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 15
}
};
// Apply preset
const applyPreset = (presetName: keyof typeof presets) => {
const preset = presets[presetName];
localConfig.value = { ...localConfig.value, ...preset };
};
// Watch for local changes and emit to parent
watch(localConfig, (newConfig) => {
emit('update:config', { ...newConfig });
}, { deep: true });
</script>
<style scoped>
.image-config {
padding: 10px;
}
.preview-image {
display: flex;
justify-content: center;
margin: 10px 0;
padding: 10px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,208 @@
<template>
<div class="text-config">
<h2>Text Widget Settings</h2>
<el-form label-position="top">
<el-form-item label="Text Content">
<el-input v-model="localConfig.text" type="textarea" :rows="3" placeholder="Enter text to display" />
</el-form-item>
<el-divider>Text Style</el-divider>
<el-form-item label="Font Size">
<el-slider v-model="localConfig.fontSize" :min="12" :max="100" show-input />
</el-form-item>
<el-form-item label="Font Family">
<el-select v-model="localConfig.fontFamily">
<el-option label="Arial" value="Arial" />
<el-option label="Helvetica" value="Helvetica" />
<el-option label="Times New Roman" value="'Times New Roman'" />
<el-option label="Courier New" value="'Courier New'" />
<el-option label="Georgia" value="Georgia" />
<el-option label="Verdana" value="Verdana" />
<el-option label="Impact" value="Impact" />
</el-select>
</el-form-item>
<el-form-item label="Font Weight">
<el-select v-model="localConfig.fontWeight">
<el-option label="Normal" value="normal" />
<el-option label="Bold" value="bold" />
<el-option label="Light" value="lighter" />
</el-select>
</el-form-item>
<el-divider>Color Settings</el-divider>
<el-form-item label="Use Gradient Colors">
<el-switch v-model="localConfig.useGradient" />
</el-form-item>
<template v-if="!localConfig.useGradient">
<el-form-item label="Text Color">
<el-color-picker v-model="localConfig.color" show-alpha />
</el-form-item>
</template>
<template v-else>
<el-form-item label="Gradient Start Color">
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
</el-form-item>
<el-form-item label="Gradient End Color">
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
</el-form-item>
</template>
<el-divider>Effects</el-divider>
<el-form-item label="Text Shadow">
<el-switch v-model="localConfig.textShadow" />
</el-form-item>
<template v-if="localConfig.textShadow">
<el-form-item label="Shadow Color">
<el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item>
<el-form-item label="Shadow Blur">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
</el-form-item>
</template>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="applyPreset('modern')">Modern</el-button>
<el-button type="success" @click="applyPreset('neon')">Neon</el-button>
<el-button type="warning" @click="applyPreset('retro')">Retro</el-button>
<el-button type="danger" @click="applyPreset('minimal')">Minimal</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// Define props interface for config
interface TextConfig {
text: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<TextConfig>;
}>(), {
config: () => ({
text: 'Sample Text',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff']
})
});
// Define emit
const emit = defineEmits<{
(event: 'update:config', config: TextConfig): void;
}>();
// Local config for two-way binding
const localConfig = ref<TextConfig>({
text: 'Sample Text',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff']
});
// Sync with parent config on mount
onMounted(() => {
// Merge default config with provided config
localConfig.value = { ...localConfig.value, ...props.config };
});
// Preset styles
const presets = {
modern: {
text: localConfig.value.text,
fontSize: 48,
fontFamily: 'Arial',
fontWeight: 'bold',
useGradient: true,
gradientColors: ['#3498db', '#9b59b6'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 10
},
neon: {
text: localConfig.value.text,
fontSize: 54,
fontFamily: 'Impact',
fontWeight: 'normal',
color: '#39ff14',
useGradient: false,
textShadow: true,
shadowColor: 'rgba(57, 255, 20, 0.8)',
shadowBlur: 15
},
retro: {
text: localConfig.value.text,
fontSize: 42,
fontFamily: 'Courier New',
fontWeight: 'bold',
useGradient: true,
gradientColors: ['#f39c12', '#e74c3c'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.6)',
shadowBlur: 6
},
minimal: {
text: localConfig.value.text,
fontSize: 36,
fontFamily: 'Helvetica',
fontWeight: 'lighter',
color: '#ffffff',
useGradient: false,
textShadow: false
}
};
// Apply preset
const applyPreset = (presetName: keyof typeof presets) => {
const preset = presets[presetName];
localConfig.value = { ...localConfig.value, ...preset };
};
// Watch for local changes and emit to parent
watch(localConfig, (newConfig) => {
emit('update:config', { ...newConfig });
}, { deep: true });
</script>
<style scoped>
.text-config {
padding: 10px;
}
</style>

View File

@ -1,5 +1,12 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'
import './style.css' import './style.css'
import App from './App.vue' import App from './App.vue'
createApp(App).mount('#app') const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.mount('#app')

24
src/router/index.ts Normal file
View File

@ -0,0 +1,24 @@
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'HomeView',
component: () => import('../views/HomeView.vue')
},
{
path: '/config',
name: 'ConfigView',
component: () => import('../views/ConfigView.vue')
},
{
path: '/preview',
name: 'PreviewView',
component: () => import('../views/PreviewView.vue')
}
]
});
export default router;

View File

@ -5,7 +5,7 @@
color-scheme: light dark; color-scheme: light dark;
color: rgba(255, 255, 255, 0.87); color: rgba(255, 255, 255, 0.87);
background-color: #242424; background-color: transparent;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
@ -13,38 +13,24 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body { body {
margin: 0; margin: 0;
display: flex; padding: 0;
place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
background-color: transparent;
} }
h1 { /* Preview mode specific styles */
font-size: 3.2em; .preview-view {
line-height: 1.1; background-color: transparent !important;
} }
button { @media (prefers-color-scheme: light) {
border-radius: 8px; :root {
border: 1px solid transparent; color: #213547;
padding: 0.6em 1.2em; background-color: transparent;
font-size: 1em; }
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
} }
button:hover { button:hover {
border-color: #646cff; border-color: #646cff;

28
src/utils/configUtils.ts Normal file
View File

@ -0,0 +1,28 @@
/**
* Utility functions for encoding and decoding widget configurations in URL
*/
/**
* Encodes widget configuration for URL sharing
* @param config The configuration object to encode
* @returns Encoded string for URL
*/
export const encodeConfig = (config: any): string => {
const jsonString = JSON.stringify(config);
return btoa(encodeURIComponent(jsonString));
};
/**
* Decodes widget configuration from URL parameter
* @param encodedString The encoded configuration string from URL
* @returns Decoded configuration object
*/
export const decodeConfig = (encodedString: string): any => {
try {
const jsonString = decodeURIComponent(atob(encodedString));
return JSON.parse(jsonString);
} catch (e) {
console.error('Failed to decode configuration', e);
throw new Error('Invalid configuration format');
}
};

248
src/views/ConfigView.vue Normal file
View File

@ -0,0 +1,248 @@
<template>
<div class="config-view">
<div class="left-panel">
<div class="widget-selector">
<el-select v-model="selectedWidget" placeholder="Select Widget" @change="handleWidgetChange">
<el-option v-for="widget in widgets" :key="widget.value" :label="widget.label" :value="widget.value" />
</el-select>
</div>
<div class="config-panel">
<component :is="currentConfigComponent" v-if="currentConfigComponent" @update:config="updateWidgetConfig" :config="currentWidgetConfig" />
</div>
<div class="url-generator">
<el-input v-model="generatedUrl" readonly>
<template #append>
<el-button @click="copyUrl">
<el-icon><CopyDocument /></el-icon> Copy
</el-button>
</template>
</el-input>
</div>
</div>
<div class="right-panel">
<div class="preview-container">
<div class="preview-wrapper">
<component :is="currentWidgetComponent" v-if="currentWidgetComponent" :config="currentWidgetConfig" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { CopyDocument } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { encodeConfig, decodeConfig } from '../utils/configUtils';
// Widget components and their configs
import ClockWidget from '../widgets/ClockWidget.vue';
import DateWidget from '../widgets/DateWidget.vue';
import TextWidget from '../widgets/TextWidget.vue';
import ImageWidget from '../widgets/ImageWidget.vue';
// Config components
import ClockConfig from '../components/config/ClockConfig.vue';
import DateConfig from '../components/config/DateConfig.vue';
import TextConfig from '../components/config/TextConfig.vue';
import ImageConfig from '../components/config/ImageConfig.vue';
const widgets = [
{ label: 'Clock Widget', value: 'clock', component: ClockWidget, configComponent: ClockConfig },
{ label: 'Date Widget', value: 'date', component: DateWidget, configComponent: DateConfig },
{ label: 'Text Widget', value: 'text', component: TextWidget, configComponent: TextConfig },
{ label: 'Image Widget', value: 'image', component: ImageWidget, configComponent: ImageConfig },
];
const selectedWidget = ref('clock');
const currentWidgetConfig = ref({});
const generatedUrl = ref('');
// Get widget component based on selection
const currentWidgetComponent = computed(() => {
const widget = widgets.find(w => w.value === selectedWidget.value);
return widget?.component;
});
// Get config component based on selection
const currentConfigComponent = computed(() => {
const widget = widgets.find(w => w.value === selectedWidget.value);
return widget?.configComponent;
});
// Set default config for each widget type
const getDefaultConfig = (widgetType: string) => {
switch (widgetType) {
case 'clock':
return {
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
};
case 'date':
return {
format: 'YYYY-MM-DD',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showWeekday: true
};
case 'text':
return {
text: 'Sample Text',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
};
case 'image':
return {
imageUrl: '',
width: 200,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 10
};
default:
return {};
}
};
// Handle widget type change
const handleWidgetChange = () => {
currentWidgetConfig.value = getDefaultConfig(selectedWidget.value);
updateGeneratedUrl();
};
// Update widget configuration
const updateWidgetConfig = (newConfig: any) => {
currentWidgetConfig.value = newConfig;
updateGeneratedUrl();
};
// Generate preview URL
const updateGeneratedUrl = () => {
const baseUrl = window.location.origin;
const configStr = encodeConfig({
type: selectedWidget.value,
config: currentWidgetConfig.value
});
generatedUrl.value = `${baseUrl}/preview?data=${configStr}`;
};
// Copy URL to clipboard
const copyUrl = () => {
navigator.clipboard.writeText(generatedUrl.value).then(() => {
ElMessage.success('URL copied to clipboard!');
}).catch(() => {
ElMessage.error('Failed to copy URL');
});
};
// Check for query params on load (for direct linking)
onMounted(() => {
const queryParams = new URLSearchParams(window.location.search);
const data = queryParams.get('data');
if (data) {
try {
const decodedData = decodeConfig(data);
selectedWidget.value = decodedData.type;
currentWidgetConfig.value = decodedData.config;
} catch (e) {
ElMessage.error('Invalid configuration in URL');
handleWidgetChange(); // Load default config
}
} else {
handleWidgetChange(); // Load default config
}
});
// Update URL when configuration changes
watch([selectedWidget, currentWidgetConfig], () => {
updateGeneratedUrl();
}, { deep: true });
</script>
<style scoped>
.config-view {
display: flex;
height: 100vh;
overflow: hidden;
}
.left-panel {
width: 380px;
padding: 20px;
background-color: #f5f7fa;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.right-panel {
flex: 1;
background-color: transparent;
background-image: linear-gradient(45deg, #ddd 25%, transparent 25%),
linear-gradient(-45deg, #ddd 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ddd 75%),
linear-gradient(-45deg, transparent 75%, #ddd 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.widget-selector {
width: 100%;
}
.config-panel {
flex-grow: 1;
}
.url-generator {
margin-top: 20px;
}
.preview-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.preview-wrapper {
padding: 20px;
border-radius: 8px;
}
</style>

227
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,227 @@
<template>
<div class="home-view">
<div class="container">
<div class="header">
<h1>OBS Overlay Widget</h1>
<p>Create customizable widgets for OBS Studio streaming and recording</p>
</div>
<div class="cards">
<div class="card" @click="goToConfig">
<div class="card-icon">
<el-icon><Setting /></el-icon>
</div>
<div class="card-title">Configure Widgets</div>
<div class="card-description">
Design and customize widgets for your OBS streams with an interactive interface
</div>
</div>
<div class="card" @click="goToDoc">
<div class="card-icon">
<el-icon><Document /></el-icon>
</div>
<div class="card-title">Documentation</div>
<div class="card-description">
Learn how to use and integrate OBS Overlay Widgets into your streams
</div>
</div>
</div>
<div class="features">
<h2>Available Widgets</h2>
<div class="widget-list">
<div class="widget-item">
<div class="widget-icon"></div>
<div class="widget-info">
<h3>Clock Widget</h3>
<p>Display current time with customizable format, style, and effects</p>
</div>
</div>
<div class="widget-item">
<div class="widget-icon">📅</div>
<div class="widget-info">
<h3>Date Widget</h3>
<p>Show current date with customizable format, style, and effects</p>
</div>
</div>
<div class="widget-item">
<div class="widget-icon">📝</div>
<div class="widget-info">
<h3>Text Widget</h3>
<p>Display text with customizable styles including gradients, shadows, and fonts</p>
</div>
</div>
<div class="widget-item">
<div class="widget-icon">🖼</div>
<div class="widget-info">
<h3>Image Widget</h3>
<p>Show images with customizable size, effects, and positioning</p>
</div>
</div>
</div>
</div>
<div class="footer">
<p>OBS Overlay Widget &copy; 2025</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { Setting, Document } from '@element-plus/icons-vue';
const router = useRouter();
const goToConfig = () => {
router.push('/config');
};
const goToDoc = () => {
// This would go to documentation in a real app
window.open('https://github.com/yourusername/obs-overlay-widget', '_blank');
};
</script>
<style scoped>
.home-view {
min-height: 100vh;
background-color: #f5f7fa;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 50px;
padding: 30px 0;
}
.header h1 {
font-size: 48px;
margin-bottom: 10px;
background: linear-gradient(45deg, #3498db, #9b59b6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
}
.header p {
font-size: 18px;
color: #666;
}
.cards {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 50px;
}
.card {
background-color: white;
border-radius: 10px;
padding: 30px;
width: 300px;
text-align: center;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
}
.card-icon {
font-size: 48px;
margin-bottom: 20px;
color: #3498db;
}
.card-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 15px;
}
.card-description {
color: #666;
line-height: 1.6;
}
.features {
margin-bottom: 50px;
}
.features h2 {
text-align: center;
margin-bottom: 30px;
font-size: 32px;
color: #333;
}
.widget-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 20px;
}
.widget-item {
display: flex;
background-color: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
}
.widget-icon {
font-size: 36px;
margin-right: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.widget-info h3 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
.widget-info p {
color: #666;
margin: 0;
line-height: 1.5;
}
.footer {
text-align: center;
padding: 20px 0;
color: #666;
border-top: 1px solid #eee;
}
@media (max-width: 768px) {
.cards {
flex-direction: column;
align-items: center;
}
.widget-list {
grid-template-columns: 1fr;
}
}
</style>

60
src/views/PreviewView.vue Normal file
View File

@ -0,0 +1,60 @@
<template>
<div class="preview-view">
<component
:is="widgetComponent"
v-if="widgetComponent"
:config="widgetConfig"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { decodeConfig } from '../utils/configUtils';
// Import widget components
import ClockWidget from '../widgets/ClockWidget.vue';
import DateWidget from '../widgets/DateWidget.vue';
import TextWidget from '../widgets/TextWidget.vue';
import ImageWidget from '../widgets/ImageWidget.vue';
// Widget registry
const widgetRegistry = {
'clock': ClockWidget,
'date': DateWidget,
'text': TextWidget,
'image': ImageWidget
};
const widgetType = ref('');
const widgetConfig = ref({});
const widgetComponent = computed(() => {
return widgetRegistry[widgetType.value as keyof typeof widgetRegistry];
});
onMounted(() => {
const queryParams = new URLSearchParams(window.location.search);
const data = queryParams.get('data');
if (data) {
try {
const decodedData = decodeConfig(data);
widgetType.value = decodedData.type;
widgetConfig.value = decodedData.config;
} catch (e) {
console.error('Invalid configuration in URL', e);
}
}
});
</script>
<style scoped>
.preview-view {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<div class="clock-widget" :style="clockStyle">
{{ currentTime }}
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import dayjs from 'dayjs';
// Define props interface
interface ClockConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showSeconds: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ClockConfig>;
}>(), {
config: () => ({
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
})
});
// State for current time
const currentTime = ref('');
// Update time string based on format
const updateTime = () => {
const currentFormat = props.config.format || 'HH:mm:ss';
const format = props.config.showSeconds ? currentFormat : currentFormat.replace(':ss', '');
currentTime.value = dayjs().format(format);
};
// Interval for updating time
let timeInterval: number | null = null;
// Start the clock
onMounted(() => {
updateTime();
// Set interval based on whether seconds are shown
const intervalTime = props.config.showSeconds ? 1000 : 60000;
timeInterval = window.setInterval(updateTime, intervalTime);
});
// Clean up interval on component unmount
onUnmounted(() => {
if (timeInterval !== null) {
window.clearInterval(timeInterval);
}
});
// Update interval if showSeconds changes
watch(() => props.config.showSeconds, (newValue) => {
if (timeInterval !== null) {
window.clearInterval(timeInterval);
}
const intervalTime = newValue ? 1000 : 60000;
timeInterval = window.setInterval(updateTime, intervalTime);
updateTime();
});
// Computed styles for the clock
const clockStyle = computed(() => {
const style: Record<string, string> = {
fontSize: `${props.config.fontSize || 48}px`,
fontFamily: props.config.fontFamily || 'Arial',
fontWeight: props.config.fontWeight || 'normal'
};
// Apply gradient or solid color
if (props.config.useGradient && (props.config.gradientColors || []).length >= 2) {
const colors = props.config.gradientColors || ['#ff0000', '#0000ff'];
style.backgroundImage = `linear-gradient(to right, ${colors.join(', ')})`;
style.webkitBackgroundClip = 'text';
style.backgroundClip = 'text';
style.color = 'transparent';
} else {
style.color = props.config.color || '#ffffff';
}
// Apply text shadow if enabled
if (props.config.textShadow) {
style.textShadow = `0 0 ${props.config.shadowBlur || 4}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'}`;
}
return style;
});
</script>
<style scoped>
.clock-widget {
display: inline-block;
padding: 10px;
font-family: Arial, sans-serif;
user-select: none;
}
</style>

120
src/widgets/ClockWidget.vue Normal file
View File

@ -0,0 +1,120 @@
<template>
<div class="clock-widget" :style="clockStyle">
{{ currentTime }}
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import dayjs from 'dayjs';
// Define props interface
interface ClockConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showSeconds: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ClockConfig>;
}>(), {
config: () => ({
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
})
});
// State for current time
const currentTime = ref('');
// Update time string based on format
const updateTime = () => {
const currentFormat = props.config.format || 'HH:mm:ss';
const format = props.config.showSeconds ? currentFormat : currentFormat.replace(':ss', '');
currentTime.value = dayjs().format(format);
};
// Interval for updating time
let timeInterval: number | null = null;
// Start the clock
onMounted(() => {
updateTime();
// Set interval based on whether seconds are shown
const intervalTime = props.config.showSeconds ? 1000 : 60000;
timeInterval = window.setInterval(updateTime, intervalTime);
});
// Clean up interval on component unmount
onUnmounted(() => {
if (timeInterval !== null) {
window.clearInterval(timeInterval);
}
});
// Update interval if showSeconds changes
watch(() => props.config.showSeconds, (newValue) => {
if (timeInterval !== null) {
window.clearInterval(timeInterval);
}
const intervalTime = newValue ? 1000 : 60000;
timeInterval = window.setInterval(updateTime, intervalTime);
updateTime();
});
// Computed styles for the clock
const clockStyle = computed(() => {
const style: Record<string, string> = {
fontSize: `${props.config.fontSize || 48}px`,
fontFamily: props.config.fontFamily || 'Arial',
fontWeight: props.config.fontWeight || 'normal'
};
// Apply gradient or solid color
if (props.config.useGradient && (props.config.gradientColors || []).length >= 2) {
const colors = props.config.gradientColors || ['#ff0000', '#0000ff'];
style.backgroundImage = `linear-gradient(to right, ${colors.join(', ')})`;
style.webkitBackgroundClip = 'text';
style.backgroundClip = 'text';
style.color = 'transparent';
} else {
style.color = props.config.color || '#ffffff';
}
// Apply text shadow if enabled
if (props.config.textShadow) {
style.textShadow = `0 0 ${props.config.shadowBlur || 4}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'}`;
}
return style;
});
</script>
<style scoped>
.clock-widget {
display: inline-block;
padding: 10px;
font-family: Arial, sans-serif;
user-select: none;
}
</style>

130
src/widgets/DateWidget.vue Normal file
View File

@ -0,0 +1,130 @@
<template>
<div class="date-widget" :style="dateStyle">
{{ currentDate }}
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import dayjs from 'dayjs';
// Define props interface
interface DateConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showWeekday: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<DateConfig>;
}>(), {
config: () => ({
format: 'YYYY-MM-DD',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showWeekday: true
})
});
// State for current date
const currentDate = ref('');
// Update date string based on format
const updateDate = () => {
let dateStr = dayjs().format(props.config.format || 'YYYY-MM-DD');
// Add weekday if enabled
if (props.config.showWeekday) {
const weekday = dayjs().format('dddd');
dateStr = `${weekday}, ${dateStr}`;
}
currentDate.value = dateStr;
};
// Interval for updating date (at midnight)
let dateInterval: number | null = null;
// Start the date display and set interval to update at midnight
onMounted(() => {
updateDate();
// Calculate time until next midnight
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
const timeUntilMidnight = tomorrow.getTime() - now.getTime();
// Set timeout to update at midnight, then set daily interval
setTimeout(() => {
updateDate();
// Update daily
dateInterval = window.setInterval(updateDate, 24 * 60 * 60 * 1000);
}, timeUntilMidnight);
});
// Clean up interval on component unmount
onUnmounted(() => {
if (dateInterval !== null) {
window.clearInterval(dateInterval);
}
});
// Watch for config changes
watch(() => props.config, () => {
updateDate();
}, { deep: true });
// Computed styles for the date
const dateStyle = computed(() => {
const style: Record<string, string> = {
fontSize: `${props.config.fontSize || 32}px`,
fontFamily: props.config.fontFamily || 'Arial',
fontWeight: props.config.fontWeight || 'normal'
};
// Apply gradient or solid color
if (props.config.useGradient && (props.config.gradientColors || []).length >= 2) {
const colors = props.config.gradientColors || ['#ff0000', '#0000ff'];
style.backgroundImage = `linear-gradient(to right, ${colors.join(', ')})`;
style.webkitBackgroundClip = 'text';
style.backgroundClip = 'text';
style.color = 'transparent';
} else {
style.color = props.config.color || '#ffffff';
}
// Apply text shadow if enabled
if (props.config.textShadow) {
style.textShadow = `0 0 ${props.config.shadowBlur || 4}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'}`;
}
return style;
});
</script>
<style scoped>
.date-widget {
display: inline-block;
padding: 10px;
font-family: Arial, sans-serif;
user-select: none;
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<div class="image-widget">
<img
v-if="config.imageUrl"
:src="config.imageUrl"
:style="imageStyle"
alt="Widget Image"
/>
<div v-else class="placeholder" :style="placeholderStyle">
Please set an image URL
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
// Define props interface
interface ImageConfig {
imageUrl: string;
width: number;
height: number;
opacity: number;
borderRadius: number;
shadow: boolean;
shadowColor: string;
shadowBlur: number;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ImageConfig>;
}>(), {
config: () => ({
imageUrl: '',
width: 200,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 10
})
});
// Computed styles for the image
const imageStyle = computed(() => {
const style: Record<string, string> = {
width: `${props.config.width || 200}px`,
height: `${props.config.height || 200}px`,
opacity: `${props.config.opacity || 1}`,
borderRadius: `${props.config.borderRadius || 0}px`,
objectFit: 'cover'
};
// Apply shadow if enabled
if (props.config.shadow) {
style.filter = `drop-shadow(0 0 ${props.config.shadowBlur || 10}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'})`;
}
return style;
});
// Style for placeholder when no image is set
const placeholderStyle = computed(() => {
return {
width: `${props.config.width || 200}px`,
height: `${props.config.height || 200}px`,
borderRadius: `${props.config.borderRadius || 0}px`,
opacity: `${props.config.opacity || 1}`,
};
});
</script>
<style scoped>
.image-widget {
display: inline-block;
}
.placeholder {
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.1);
border: 2px dashed rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.7);
font-family: Arial, sans-serif;
font-size: 16px;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<div class="text-widget" :style="textStyle">
{{ config.text }}
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
// Define props interface
interface TextConfig {
text: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<TextConfig>;
}>(), {
config: () => ({
text: 'Sample Text',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff']
})
});
// Computed styles for the text
const textStyle = computed(() => {
const style: Record<string, string> = {
fontSize: `${props.config.fontSize || 32}px`,
fontFamily: props.config.fontFamily || 'Arial',
fontWeight: props.config.fontWeight || 'normal'
};
// Apply gradient or solid color
if (props.config.useGradient && (props.config.gradientColors || []).length >= 2) {
const colors = props.config.gradientColors || ['#ff0000', '#0000ff'];
style.backgroundImage = `linear-gradient(to right, ${colors.join(', ')})`;
style.webkitBackgroundClip = 'text';
style.backgroundClip = 'text';
style.color = 'transparent';
} else {
style.color = props.config.color || '#ffffff';
}
// Apply text shadow if enabled
if (props.config.textShadow) {
style.textShadow = `0 0 ${props.config.shadowBlur || 4}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'}`;
}
return style;
});
</script>
<style scoped>
.text-widget {
display: inline-block;
padding: 10px;
font-family: Arial, sans-serif;
user-select: none;
}
</style>