style preset

This commit is contained in:
CPunisher 2021-01-16 21:33:22 +08:00
parent f06f2c9b2a
commit 6fed952a22
17 changed files with 304 additions and 15 deletions

View File

@ -16,6 +16,7 @@
"react-github-btn": "^1.2.0", "react-github-btn": "^1.2.0",
"react-indiana-drag-scroll": "^1.6.1", "react-indiana-drag-scroll": "^1.6.1",
"react-lazy-load": "^3.0.13", "react-lazy-load": "^3.0.13",
"react-modal": "^3.12.1",
"react-redux": "^7.2.0", "react-redux": "^7.2.0",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"redux": "^4.0.5", "redux": "^4.0.5",

View File

@ -6,11 +6,11 @@ export const genQRInfo = text => ({
text text
}) })
export const changeStyle = (rendererIndex, rendererType, value) => { export const changeStyle = (rendererIndex, value) => {
handleStyle(value); handleStyle(value);
return { return {
type: actionTypes.CHANGE_STYLE, type: actionTypes.CHANGE_STYLE,
rendererIndex, rendererType, value rendererIndex, value
} }
} }

View File

@ -2,6 +2,7 @@ import React, {useState} from 'react';
import './App.css'; import './App.css';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {isWeiXin} from "../../utils/navigatorUtils"; import {isWeiXin} from "../../utils/navigatorUtils";
import PresetModalViewer from "../../containers/preset/PresetModalViewer";
const WxMessage = () => { const WxMessage = () => {
if (isWeiXin()) { if (isWeiXin()) {
@ -28,8 +29,9 @@ const ImgBox = ({ imgData }) => {
return null return null
} }
const PartDownload = ({ value, downloadCount, onSvgDownload, onImgDownload }) => { const PartDownload = ({ value, downloadCount, onSvgDownload, onImgDownload, savePreset }) => {
const [imgData, setImgData] = useState(''); const [imgData, setImgData] = useState('');
const [visible, setVisible] = useState(false);
return ( return (
<div className="Qr-titled"> <div className="Qr-titled">
@ -41,11 +43,15 @@ const PartDownload = ({ value, downloadCount, onSvgDownload, onImgDownload }) =>
</p> </p>
</div> </div>
<div className="Qr-Centered"> <div className="Qr-Centered">
<PresetModalViewer visible={visible} onClose={() => setVisible(false)} />
<div className="btn-row"> <div className="btn-row">
<div className="div-btn img-dl-btn"> <div className="div-btn img-dl-btn">
<button className="dl-btn" onClick={() => {onImgDownload("jpg").then(res => setImgData(res));}}>JPG</button> <button className="dl-btn" onClick={() => {onImgDownload("jpg").then(res => setImgData(res));}}>JPG</button>
<button className="dl-btn" onClick={() => {onImgDownload("png").then(res => setImgData(res));}}>PNG</button> <button className="dl-btn" onClick={() => {onImgDownload("png").then(res => setImgData(res));}}>PNG</button>
<button className="dl-btn" onClick={onSvgDownload}>SVG</button> <button className="dl-btn" onClick={onSvgDownload}>SVG</button>
<button className="dl-btn" onClick={() => setVisible(true)}>管理预设</button>
<button className="dl-btn" onClick={savePreset}>保存预设</button>
</div> </div>
</div> </div>
<div id="wx-message"> <div id="wx-message">

View File

@ -38,7 +38,7 @@ const ParamIcon = ({icon, onBlur, onKeyPress}) => (
<FrameworkParam paramName={"图标"}> <FrameworkParam paramName={"图标"}>
<select <select
className="Qr-select" className="Qr-select"
defaultValue={icon.enabled} value={icon.enabled}
onChange={(e) => onBlur({...icon, enabled: e.target.value})}> onChange={(e) => onBlur({...icon, enabled: e.target.value})}>
<option value={0}></option> <option value={0}></option>
<option value={1}>自定义</option> <option value={1}>自定义</option>

View File

@ -0,0 +1,29 @@
.ReactModal__Overlay {
opacity: 0;
transition: opacity 100ms ease-in-out;
}
.ReactModal__Overlay--after-open{
opacity: 1;
}
.ReactModal__Overlay--before-close{
opacity: 0;
}
.Qr-preset-container {
width: 100%;
height: 100%;
display: flex;
}
.Qr-preset-side {
width: 70%;
height: 100%;
padding-top: 10px;
}
.Qr-preset-detail {
width: 40%;
height: 100%;
}

View File

@ -0,0 +1,26 @@
import React from "react";
import PropTypes from 'prop-types';
const PresetCard = ({ preset }) => (
<div className="Qr-Centered">
<div className="Qr-item-image">
<div className="Qr-item-image-inner">
<img id="dl-image-inner-jpg" src={preset.preview}/>
</div>
</div>
<div className="Qr-item-detail">
{preset.name}
</div>
</div>
);
PresetCard.propTypes = {
preset: PropTypes.shape({
name: PropTypes.string.isRequired,
styleName: PropTypes.string.isRequired,
preview: PropTypes.string.isRequired,
params: PropTypes.array.isRequired,
})
}
export default PresetCard;

View File

@ -0,0 +1,49 @@
import React from "react";
import PropTypes from 'prop-types';
const ParamLabel = ({ params, label }) => (
params.map((param, index) => {
if (param.value.length > 30) return null;
return (
<table key={label + '_' + index} className="Qr-table">
<tbody>
<tr>
<td>{param.name}</td>
<td>{param.value}</td>
</tr>
</tbody>
</table>
)
})
)
const PresetDetail = ({ preset }) => (
<div className="Qr-Centered">
<div id="dl-image">
<div id="dl-image-inner">
<img id="dl-image-inner-jpg" src={preset.preview}/>
</div>
</div>
<div>
<table className="Qr-table">
<tbody><tr>
<td>样式名</td>
<td>{preset.styleName}</td>
</tr></tbody>
</table>
<ParamLabel params={preset.params} label="preset_param"/>
</div>
</div>
);
PresetDetail.propTypes = {
preset: PropTypes.shape({
name: PropTypes.string.isRequired,
styleName: PropTypes.string.isRequired,
preview: PropTypes.string.isRequired,
globalParams: PropTypes.array.isRequired,
params: PropTypes.array.isRequired,
})
}
export default PresetDetail;

View File

@ -1,6 +1,80 @@
import React from "react"; import React, {useState} from "react";
import Modal from "react-modal";
import './Preset.css';
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import PresetCard from "./PresetCard";
import PresetDetail from "./PresetDetail";
import ScrollContainer from "react-indiana-drag-scroll";
import {getPresets, removePreset} from "../../utils/storageUtils";
const PresetModal({ visible, onClose, }) => { function calClassName(selected) {
if (selected === true) return 'Qr-item Qr-item-selected';
return 'Qr-item';
} }
const customStyles = {
content: {
inset: '40px 60px 40px 60px',
}
}
const PresetModal = ({ visible, onClose, loadPreset }) => {
const storedPresets = getPresets();
const [selected, setSelected] = useState(0);
const [presets, setPresets] = useState(storedPresets);
if (presets.length !== storedPresets.length) setPresets(storedPresets);
return (
<Modal
appElement={document.getElementById("root")}
closeTimeoutMS={100}
isOpen={visible}
onRequestClose={onClose}
style={customStyles}>
<div className="Qr-preset-container">
<ScrollContainer
className="Qr-div-table Qr-preset-side"
vertical={true}
horizontal={false}
hideScrollbars={false}>
{
presets.map((preset, index) => (
<div
key={'preset_' + index}
className={calClassName(selected === index)}
onClick={() => setSelected(index)}>
<PresetCard preset={preset}/>
</div>
))
}
</ScrollContainer>
<div className="Qr-preset-detail">
{presets[selected] ? <PresetDetail preset={presets[selected]}/> : null}
<div>
<button className="dl-btn" onClick={() => {
if (presets[selected]) {
loadPreset(presets[selected]);
onClose();
}
}}>加载预设</button>
<button className="dl-btn" onClick={() => {
if (presets[selected]) {
removePreset(selected);
setPresets(getPresets())
}
}}>删除预设</button>
<button className="dl-btn" onClick={onClose}>取消</button>
</div>
</div>
</div>
</Modal>
);
};
PresetModal.propTypes = {
visible: PropTypes.bool,
onClose: PropTypes.func.isRequired,
presetArray: PropTypes.array.isRequired,
loadPreset: PropTypes.func.isRequired,
}
export default PresetModal;

View File

@ -7,7 +7,6 @@ import {getExactValue, getIdNum} from "../../utils/util";
function listPoints({ qrcode, params, icon }) { function listPoints({ qrcode, params, icon }) {
if (!qrcode) return [] if (!qrcode) return []
console.log(icon)
const nCount = qrcode.getModuleCount(); const nCount = qrcode.getModuleCount();
const typeTable = getTypeTable(qrcode); const typeTable = getTypeTable(qrcode);
const pointList = new Array(nCount); const pointList = new Array(nCount);

View File

@ -36,7 +36,7 @@ let defaultDrawIcon = function ({ qrcode, params, title, icon }) {
const randomIdDefs = getIdNum(); const randomIdDefs = getIdNum();
const randomIdClips = getIdNum(); const randomIdClips = getIdNum();
pointList.push(<path d={sq25} stroke="#FFF" strokeWidth={100/iconSize * 1} fill="#FFF" transform={`translate(${iconXY}, ${iconXY}) scale(${iconSize / 100}, ${iconSize / 100})`} />); pointList.push(<path key={id++} d={sq25} stroke="#FFF" strokeWidth={100/iconSize * 1} fill="#FFF" transform={`translate(${iconXY}, ${iconXY}) scale(${iconSize / 100}, ${iconSize / 100})`} />);
pointList.push( pointList.push(
<g key={id++}> <g key={id++}>
<defs> <defs>
@ -117,7 +117,7 @@ let builtinDrawIcon = function ({ qrcode, params, title, icon }) {
if (icon && iconMode) { if (icon && iconMode) {
const randomIdDefs = getIdNum(); const randomIdDefs = getIdNum();
const randomIdClips = getIdNum(); const randomIdClips = getIdNum();
pointList.push(<path d={sq25} stroke="#FFF" strokeWidth={100/iconSize * 1} fill="#FFF" transform={`translate(${iconXY}, ${iconXY}) scale(${iconSize / 100}, ${iconSize / 100})`} />); pointList.push(<path key={id++} d={sq25} stroke="#FFF" strokeWidth={100/iconSize * 1} fill="#FFF" transform={`translate(${iconXY}, ${iconXY}) scale(${iconSize / 100}, ${iconSize / 100})`} />);
pointList.push( pointList.push(
<g key={id++}> <g key={id++}>
<defs> <defs>

View File

@ -4,6 +4,8 @@ import {saveImg, saveSvg} from "../../utils/downloader";
import {getDownloadCount, increaseDownloadData, recordDownloadDetail} from "../../api/TcbHandler"; import {getDownloadCount, increaseDownloadData, recordDownloadDetail} from "../../api/TcbHandler";
import {getParamDetailedValue, outerHtml} from "../../utils/util"; import {getParamDetailedValue, outerHtml} from "../../utils/util";
import {handleDownloadImg, handleDownloadSvg} from "../../utils/gaHelper"; import {handleDownloadImg, handleDownloadSvg} from "../../utils/gaHelper";
import {appendPreset} from "../../utils/storageUtils";
import {svgToBase64} from "../../utils/imageUtils";
function saveDB(state, type, updateDownloadData) { function saveDB(state, type, updateDownloadData) {
return new Promise(resolve => { return new Promise(resolve => {
@ -42,7 +44,7 @@ const mapStateToProps = (state, ownProps) => ({
downloadCount: state.downloadData[state.value], downloadCount: state.downloadData[state.value],
onSvgDownload: () => { onSvgDownload: () => {
saveSvg(state.value, outerHtml(state.selectedIndex)); saveSvg(state.value, outerHtml(state.selectedIndex));
saveDB(state, 'svg', ownProps.updateDownloadData); saveDB(state, 'svg', ownProps.updateDownloadData).catch(console.error);
handleDownloadSvg(state.value); handleDownloadSvg(state.value);
}, },
onImgDownload: (type) => { onImgDownload: (type) => {
@ -54,6 +56,26 @@ const mapStateToProps = (state, ownProps) => ({
}); });
}); });
}); });
},
savePreset: () => {
let preset = {
name: '测试预设',
selectedIndex: state.selectedIndex,
preview: svgToBase64(outerHtml(state.selectedIndex), 1500, 1500),
styleName: state.value,
params: state.paramInfo[state.selectedIndex].map((paramInfo, index) => {
return {
name: paramInfo.key,
value: state.paramValue[state.selectedIndex][index],
}
}),
globalParams: new Array(3),
};
preset.globalParams[0] = {name: '容错率', value: state.correctLevel};
preset.globalParams[1] = {name: '图标', value: state.icon};
preset.globalParams[2] = {name: '文字', value: state.title};
appendPreset(preset);
alert('saved');
} }
}) })

View File

@ -0,0 +1,21 @@
import { connect } from 'react-redux';
import PresetModal from "../../components/preset/PresetModal";
import {getPresets} from "../../utils/storageUtils";
import {changeCorrectLevel, changeIcon, changeParam, changeStyle, changeTitle} from "../../actions";
const mapStateToProps = (state, ownProps) => ({
visible: ownProps.visible,
});
const mapDispatchToProps = (dispatch, ownProps) => ({
onClose: ownProps.onClose,
loadPreset: (preset) => {
dispatch(changeStyle(preset.selectedIndex, preset.styleName));
dispatch(changeCorrectLevel(preset.globalParams[0].value));
dispatch(changeIcon(preset.globalParams[1].value));
dispatch(changeTitle(preset.globalParams[2].value));
preset.params.forEach((param, index) => dispatch(changeParam(preset.selectedIndex, index, param.value)));
}
});
export default connect(mapStateToProps, mapDispatchToProps)(PresetModal);

View File

@ -52,7 +52,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onSelected: rendererIndex => { onSelected: rendererIndex => {
dispatch(changeStyle(rendererIndex, styles[rendererIndex].renderer, styles[rendererIndex].value)) dispatch(changeStyle(rendererIndex, styles[rendererIndex].value))
} }
}) })

View File

@ -2,12 +2,10 @@ import {encodeData} from "../utils/qrcodeHandler";
import {actionTypes} from "../constant/ActionTypes"; import {actionTypes} from "../constant/ActionTypes";
import {QRBTF_URL} from "../constant/References"; import {QRBTF_URL} from "../constant/References";
import {getExactValue} from "../utils/util"; import {getExactValue} from "../utils/util";
import {RendererRect} from "../components/renderer/RendererBase";
const initialState = { const initialState = {
selectedIndex: 0, selectedIndex: 0,
value: 'A1', value: 'A1',
rendererType: RendererRect,
correctLevel: 0, correctLevel: 0,
textUrl: QRBTF_URL, textUrl: QRBTF_URL,
history: [], history: [],
@ -32,7 +30,6 @@ export default function appReducer(state = initialState, action) {
case actionTypes.CHANGE_STYLE: { case actionTypes.CHANGE_STYLE: {
return Object.assign({}, state, { return Object.assign({}, state, {
value: action.value, value: action.value,
rendererType: action.rendererType,
selectedIndex: action.rendererIndex, selectedIndex: action.rendererIndex,
history: state.history.slice().concat(action.value) history: state.history.slice().concat(action.value)
}); });

View File

@ -8,6 +8,20 @@ export function isPicture(file) {
return fileTypes.includes(file.type); return fileTypes.includes(file.type);
} }
export function svgToBase64(content, width, height) {
const wrap = document.createElement('div');
wrap.innerHTML = content;
const $svg = wrap.firstChild
const $clone = $svg.cloneNode(true);
$clone.setAttribute('width', width);
$clone.setAttribute('height', height);
const svgData = new XMLSerializer().serializeToString($clone);
return 'data:image/svg+xml;base64,' + btoa(svgData);
}
export function toBase64(file, aspectRatio) { export function toBase64(file, aspectRatio) {
let canvas = document.createElement('canvas'); let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d'); let ctx = canvas.getContext('2d');

24
src/utils/storageUtils.js Normal file
View File

@ -0,0 +1,24 @@
const STORAGE_KEYS = {
PRESETS: 'presets'
};
export function getPresets() {
let presetArray = [];
const presetsValue = localStorage.getItem(STORAGE_KEYS.PRESETS);
if (presetsValue) {
presetArray = JSON.parse(presetsValue);
}
return presetArray;
}
export function appendPreset(preset) {
const presets = getPresets();
presets.push(preset);
localStorage.setItem(STORAGE_KEYS.PRESETS, JSON.stringify(presets));
}
export function removePreset(index) {
const presets = getPresets();
presets.splice(index, 1);
localStorage.setItem(STORAGE_KEYS.PRESETS, JSON.stringify(presets));
}

View File

@ -4311,6 +4311,11 @@ execa@^1.0.0:
signal-exit "^3.0.0" signal-exit "^3.0.0"
strip-eof "^1.0.0" strip-eof "^1.0.0"
exenv@^1.2.0:
version "1.2.2"
resolved "https://registry.npm.taobao.org/exenv/download/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
exit@^0.1.2: exit@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.npm.taobao.org/exit/download/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" resolved "https://registry.npm.taobao.org/exit/download/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@ -8774,6 +8779,21 @@ react-lazy-load@^3.0.13:
lodash.throttle "^4.0.0" lodash.throttle "^4.0.0"
prop-types "^15.5.8" prop-types "^15.5.8"
react-lifecycles-compat@^3.0.0:
version "3.0.4"
resolved "https://registry.npm.taobao.org/react-lifecycles-compat/download/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha1-TxonOv38jzSIqMUWv9p4+HI1I2I=
react-modal@^3.12.1:
version "3.12.1"
resolved "https://registry.npm.taobao.org/react-modal/download/react-modal-3.12.1.tgz?cache=0&sync_timestamp=1606096428365&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-modal%2Fdownload%2Freact-modal-3.12.1.tgz#38c33f70d81c33d02ff1ed115530443a3dc2afd3"
integrity sha1-OMM/cNgcM9Av8e0RVTBEOj3Cr9M=
dependencies:
exenv "^1.2.0"
prop-types "^15.5.10"
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-redux@^7.2.0: react-redux@^7.2.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.npm.taobao.org/react-redux/download/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" resolved "https://registry.npm.taobao.org/react-redux/download/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
@ -10642,6 +10662,13 @@ walker@^1.0.7, walker@~1.0.5:
dependencies: dependencies:
makeerror "1.0.x" makeerror "1.0.x"
warning@^4.0.3:
version "4.0.3"
resolved "https://registry.npm.taobao.org/warning/download/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha1-Fungd+uKhtavfWSqHgX9hbRnjKM=
dependencies:
loose-envify "^1.0.0"
watchpack-chokidar2@^2.0.0: watchpack-chokidar2@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npm.taobao.org/watchpack-chokidar2/download/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" resolved "https://registry.npm.taobao.org/watchpack-chokidar2/download/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"