基本功能实现
This commit is contained in:
parent
9e4aa2c725
commit
d70b962ef5
63
README.md
63
README.md
@ -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
|
||||
|
@ -4,7 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<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>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
269
package-lock.json
generated
269
package-lock.json
generated
@ -8,7 +8,11 @@
|
||||
"name": "obs-overlay-widget",
|
||||
"version": "0.0.0",
|
||||
"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": {
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
@ -64,6 +68,24 @@
|
||||
"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": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
@ -489,12 +511,48 @@
|
||||
"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": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"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": {
|
||||
"version": "1.0.0-beta.19",
|
||||
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz",
|
||||
@ -789,6 +847,27 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz",
|
||||
@ -896,6 +975,12 @@
|
||||
"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": {
|
||||
"version": "2.2.10",
|
||||
"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": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz",
|
||||
@ -997,6 +1170,12 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -1020,6 +1199,12 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
|
||||
@ -1027,6 +1212,32 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||
@ -1080,6 +1291,12 @@
|
||||
"@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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
@ -1126,6 +1343,29 @@
|
||||
"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": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz",
|
||||
@ -1135,6 +1375,12 @@
|
||||
"@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": {
|
||||
"version": "9.0.5",
|
||||
"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_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": {
|
||||
"version": "1.0.1",
|
||||
"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": {
|
||||
"version": "2.2.10",
|
||||
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.10.tgz",
|
||||
|
@ -9,7 +9,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"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": {
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
|
30
src/App.vue
30
src/App.vue
@ -1,30 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
// App.vue is the main component that uses the router
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<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" />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
224
src/components/config/ClockConfig.vue
Normal file
224
src/components/config/ClockConfig.vue
Normal 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>
|
225
src/components/config/DateConfig.vue
Normal file
225
src/components/config/DateConfig.vue
Normal 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>
|
175
src/components/config/ImageConfig.vue
Normal file
175
src/components/config/ImageConfig.vue
Normal 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>
|
208
src/components/config/TextConfig.vue
Normal file
208
src/components/config/TextConfig.vue
Normal 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>
|
@ -1,5 +1,12 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import router from './router'
|
||||
import './style.css'
|
||||
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
24
src/router/index.ts
Normal 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;
|
@ -5,7 +5,7 @@
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
background-color: transparent;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
@ -13,38 +13,24 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
/* Preview mode specific styles */
|
||||
.preview-view {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
|
28
src/utils/configUtils.ts
Normal file
28
src/utils/configUtils.ts
Normal 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
248
src/views/ConfigView.vue
Normal 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
227
src/views/HomeView.vue
Normal 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 © 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
60
src/views/PreviewView.vue
Normal 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>
|
120
src/widgets/ClockWidget-new.vue
Normal file
120
src/widgets/ClockWidget-new.vue
Normal 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
120
src/widgets/ClockWidget.vue
Normal 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
130
src/widgets/DateWidget.vue
Normal 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>
|
90
src/widgets/ImageWidget.vue
Normal file
90
src/widgets/ImageWidget.vue
Normal 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>
|
77
src/widgets/TextWidget.vue
Normal file
77
src/widgets/TextWidget.vue
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user