diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..9575e9d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/package.json b/package.json index 98d443f..900e1fc 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "jsqr": "^1.3.1", + "jszip": "^3.7.1", "medium-zoom": "^1.0.5", "prop-types": "^15.7.2", "react": "^16.13.1", diff --git a/src/actions/index.js b/src/actions/index.js index d092a62..f90064b 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -1,10 +1,19 @@ import {actionTypes} from "../constant/ActionTypes"; import {handleStyle} from "../utils/gaHelper"; -export const genQRInfo = text => ({ - type: actionTypes.GENERATE_QR_INFO, - text -}) +export const genQRInfo = text => { + if (Array.isArray(text)) { + return { + type: actionTypes.GENERATE_QR_INFO, + text: text[0], + textArray: text + } + } + return { + type: actionTypes.GENERATE_QR_INFO, + text + } +} export const changeStyle = (rendererIndex, value) => { handleStyle(value); diff --git a/src/components/Qrcode.css b/src/components/Qrcode.css index a0adebc..f7879db 100644 --- a/src/components/Qrcode.css +++ b/src/components/Qrcode.css @@ -318,7 +318,7 @@ td { .Qr-input-upload { display: flex; max-width: calc(100vw - 46px); - width: 20em; + width: 22em; } .Qr-input-upload-div { @@ -360,7 +360,7 @@ td { height: calc(2em + 6px); font-size: calc(10px + 2vmin); border: var(--border-color) solid 2px; - margin-left: 10px; + margin: 0 10px; flex: 1; } @@ -372,6 +372,7 @@ td { .Qr-upload { color: var(--upload-color); + fill: var(--upload-color); font-size: calc(10px + 2vmin); border: var(--border-color) solid 2px; width: calc(2em + 6px)!important; @@ -409,6 +410,7 @@ td { .Qr-upload:hover { border-color: var(--border-color-focus); color: var(--border-color-focus); + fill: var(--border-color-focus); } .Qr-upload:active { @@ -417,6 +419,7 @@ td { -moz-transition-duration: 0s; border-color: #3BBC9F; color: #3BBC9F; + fill: #3BBC9F; } .Qr-upload-svg { diff --git a/src/components/app/PartDownload.js b/src/components/app/PartDownload.js index f881e4e..1584182 100644 --- a/src/components/app/PartDownload.js +++ b/src/components/app/PartDownload.js @@ -4,6 +4,12 @@ import PropTypes from 'prop-types'; import {isWeiXin} from "../../utils/navigatorUtils"; import PresetModalViewer from "../../containers/preset/PresetModalViewer"; +const CountComponent = ({ value }) => { + if (isNaN(value)) return null; + if (value >= 10000) value = (value / 10000).toFixed(1) + "万"; + return {value} +} + const WxMessage = () => { if (isWeiXin()) { return ( @@ -13,7 +19,7 @@ const WxMessage = () => { ) } - return null + return null; } const ImgBox = ({ imgData }) => { @@ -29,7 +35,7 @@ const ImgBox = ({ imgData }) => { return null } -const PartDownload = ({ value, downloadCount, onSvgDownload, onImgDownload, savePreset }) => { +const PartDownload = ({ value, batchMode, downloadCount, onSvgDownload, onImgDownload, savePreset }) => { const [imgData, setImgData] = useState(''); const [visible, setVisible] = useState(false); @@ -38,8 +44,10 @@ const PartDownload = ({ value, downloadCount, onSvgDownload, onImgDownload, save
Downloads

- 下载二维码 — {value} - {downloadCount} + 下载二维码 + ({batchMode ? "批量模式" : "单文件模式"}) + — {value} +

diff --git a/src/components/app/PartMore.js b/src/components/app/PartMore.js index e25f8b1..aae1891 100644 --- a/src/components/app/PartMore.js +++ b/src/components/app/PartMore.js @@ -51,6 +51,7 @@ const PartMore = () => {

最新消息

+

2021.10.5
新增批量生成二维码。

2020.9.1
新增 C3 样式、图标插入、 PNG 下载。

2020.6.29
新的反馈渠道!我们开始征集好玩的二维码设计啦,可以是推送尾图、海报等等,快来上传吧。 ( key={"input_" + rendererIndex + "_" + paramIndex} id="image_upload" hidden={true} - accept=".jpg, .jpeg, .png" + accept="image/*" onChange={onChange} /> diff --git a/src/components/svg/FileImport.js b/src/components/svg/FileImport.js new file mode 100644 index 0000000..6991079 --- /dev/null +++ b/src/components/svg/FileImport.js @@ -0,0 +1,15 @@ +import * as React from "react" + +function FileImport(props) { + return ( + + + + + + + ); +} + +export default FileImport; \ No newline at end of file diff --git a/src/containers/app/InputText.js b/src/containers/app/InputText.js index 2c98a53..1a19047 100644 --- a/src/containers/app/InputText.js +++ b/src/containers/app/InputText.js @@ -4,6 +4,7 @@ import React, {useRef} from "react"; import {isPicture} from "../../utils/imageUtils"; import {decodeData} from "../../utils/qrcodeHandler"; import { handleUpload, handleInputUrl } from "../../utils/gaHelper"; +import FileImport from "../../components/svg/FileImport"; const InputText = ({dispatch}) => { const textRef = useRef(); @@ -41,7 +42,6 @@ const InputText = ({dispatch}) => { decodeData(file).then((res) => { if (res) { textRef.current.value = res.data; - console.log(res.data) dispatch(genQRInfo(res.data)) } }).catch(console.err); @@ -65,9 +65,39 @@ const InputText = ({dispatch}) => { } }} /> + + e.target.value = null} + onChange={(e) => { + if (e.target.files.length > 0) { + const file = e.target.files[0]; + if (file.type === "text/plain") { + const fileReader = new FileReader(); + fileReader.onload = function(event) { + const lines = event.target.result + .split(/[\r\n]+/g) + .map(line => line.trim()) + .filter(line => line.length > 0); + console.log(lines) + if (lines.length > 0) { + textRef.current.value = lines[0]; + dispatch(genQRInfo(lines)) + } + } + fileReader.readAsText(file, "UTF-8"); + } + } + }} + />

- 上传普通二维码或输入网址 + 上传普通二维码、输入网址或上传 txt 文本批量生成
); diff --git a/src/containers/app/PartDownloadViewer.js b/src/containers/app/PartDownloadViewer.js index 260a4f2..0bf9161 100644 --- a/src/containers/app/PartDownloadViewer.js +++ b/src/containers/app/PartDownloadViewer.js @@ -1,11 +1,15 @@ import {connect} from 'react-redux'; import PartDownload from "../../components/app/PartDownload"; -import {saveImg, saveSvg} from "../../utils/downloader"; +import {saveImg, saveSvg, startTask} from "../../utils/downloader"; import {getDownloadCount, increaseDownloadData, recordDownloadDetail} from "../../api/TcbHandler"; -import {getParamDetailedValue, outerHtml} from "../../utils/util"; +import {fillEmptyWith, getParamDetailedValue, outerHtml} from "../../utils/util"; import {handleDownloadImg, handleDownloadSvg} from "../../utils/gaHelper"; import {appendPreset} from "../../utils/storageUtils"; import {svgToBase64} from "../../utils/imageUtils"; +import React from "react"; +import ReactDOMServer from "react-dom/server"; +import JSZip from "jszip"; +import {encodeData} from "../../utils/qrcodeHandler"; function saveDB(state, type, updateDownloadData) { return new Promise(resolve => { @@ -42,20 +46,66 @@ function saveDB(state, type, updateDownloadData) { const mapStateToProps = (state, ownProps) => ({ value: state.value, downloadCount: state.downloadData[state.value], - onSvgDownload: () => { - saveSvg(state.value, outerHtml(state.selectedIndex)); - saveDB(state, 'svg', ownProps.updateDownloadData).catch(console.error); - handleDownloadSvg(state.value); - }, - onImgDownload: (type) => { - return new Promise(resolve => { - saveImg(state.value, outerHtml(state.selectedIndex), 1500, 1500, type).then((res) => { - saveDB(state, type, ownProps.updateDownloadData).then(() => { - handleDownloadImg(state.value, type); - resolve(res) + batchMode: state.textUrlArray.length > 0, + onSvgDownload: async () => { + let data; + if (state.textUrlArray.length === 0) { + data = saveSvg(state.value, outerHtml(state.selectedIndex)); + const filename = "QRcode_" + state.value + ".svg"; + startTask(URL.createObjectURL(data), filename); + } else { + const zip = new JSZip(); + for (let i = 0; i < state.textUrlArray.length; i++) { + const qrcode = encodeData({ text: state.textUrlArray[i], correctLevel: state.correctLevel }); + const el = React.createElement(state.rendererType, { + qrcode: qrcode, + params: fillEmptyWith(state.paramValue[state.selectedIndex].slice(), 0), + title: state.title, + icon: state.icon, + setParamInfo: () => {} }); - }); - }); + data = saveSvg(state.value, ReactDOMServer.renderToString(el)); + console.log(data) + const filename = "QRcode_" + state.value + "_" + i + ".svg"; + zip.file(filename, data); + } + const href = URL.createObjectURL(await zip.generateAsync({ type: "blob" })); + startTask(href, "QRcode_" + state.value + ".zip"); + } + + await saveDB(state, 'svg', ownProps.updateDownloadData); + handleDownloadSvg(state.value); + return data; + }, + onImgDownload: async (type) => { + let data; + if (state.textUrlArray.length === 0) { + data = await saveImg(state.value, outerHtml(state.selectedIndex), 1500, 1500, type); + const filename = "QRcode_" + state.value + "." + type; + startTask(data, filename); + } else { + const zip = new JSZip(); + for (let i = 0; i < state.textUrlArray.length; i++) { + const qrcode = encodeData({ text: state.textUrlArray[i], correctLevel: state.correctLevel }); + const el = React.createElement(state.rendererType, { + qrcode: qrcode, + params: fillEmptyWith(state.paramValue[state.selectedIndex].slice(), 0), + title: state.title, + icon: state.icon, + setParamInfo: () => {} + }); + data = await saveImg(state.value, ReactDOMServer.renderToString(el), 1500, 1500, type); + const startIdx = data.indexOf("base64,") + "base64,".length; + const filename = "QRcode_" + state.value + "_" + i + "." + type; + zip.file(filename, data.substring(startIdx), {base64: true}); + } + const href = URL.createObjectURL(await zip.generateAsync({ type: "blob" })); + startTask(href, "QRcode_" + state.value + ".zip"); + } + + await saveDB(state, type, ownProps.updateDownloadData) + handleDownloadImg(state.value, type); + return data; }, savePreset: () => { let preset = { diff --git a/src/reducers/index.js b/src/reducers/index.js index cb7c468..fe990a0 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -8,6 +8,7 @@ const initialState = { value: 'A1', correctLevel: 0, textUrl: QRBTF_URL, + textUrlArray: [], history: [], downloadData: [], qrcode: encodeData({text: QRBTF_URL, correctLevel: 0}), @@ -24,7 +25,8 @@ export default function appReducer(state = initialState, action) { if (!text || text.length === 0) text = QRBTF_URL; return Object.assign({}, state, { textUrl: text, - qrcode: encodeData({text: text, correctLevel: state.correctLevel}) + qrcode: encodeData({text: text, correctLevel: state.correctLevel}), + textUrlArray: action.textArray || [] }); } case actionTypes.CHANGE_STYLE: { diff --git a/src/utils/downloader.js b/src/utils/downloader.js index cbbd01c..e690082 100644 --- a/src/utils/downloader.js +++ b/src/utils/downloader.js @@ -3,23 +3,25 @@ const svgHead = "\n " + const MIME = { "jpg": "image/jpeg", "png": "image/png" }; +export function startTask(data, filename) { + let a = document.createElement('a'); + a.setAttribute('href', data) + a.setAttribute('target', 'download') + a.setAttribute('download', filename); + a.setAttribute('hidden', true); + a.click(); +} + export function saveSvg(value, content) { let htmlContent = [svgHead + content] let bl = new Blob(htmlContent, {type: "image/svg+xml"}) - let a = document.createElement("a") - let filename = "QRcode_" + value + ".svg" - - a.href = URL.createObjectURL(bl) - a.download = filename - a.hidden = true - a.click() + return bl; } export function saveImg(value, content, width, height, type) { if (!MIME[type]) throw "Error image type"; // Finish creating downloadable data - let filename = "QRcode_" + value + "." + type; const wrap = document.createElement('div'); wrap.innerHTML = content; @@ -51,12 +53,8 @@ export function saveImg(value, content, width, height, type) { // Will result in a download popup for chrome and the // image opening in a new tab for others. - let a = document.createElement('a'); let data = canvas.toDataURL(MIME[type], 0.8); - a.setAttribute('href', data) - a.setAttribute('target', 'download') - a.setAttribute('download', filename); - a.click(); + // startTask(data, filename); resolve(data) }; diff --git a/src/utils/imageUtils.js b/src/utils/imageUtils.js index 665d777..7377327 100644 --- a/src/utils/imageUtils.js +++ b/src/utils/imageUtils.js @@ -39,7 +39,6 @@ export function toBase64(file, aspectRatio) { height = img.height; width = height * aspectRatio; } - console.log(width + ' ' + height) canvas.setAttribute('width', width); canvas.setAttribute('height', height); diff --git a/yarn.lock b/yarn.lock index dc61e95..8094262 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5279,6 +5279,11 @@ ignore@^4.0.6: resolved "https://registry.npm.taobao.org/ignore/download/ignore-4.0.6.tgz?cache=0&sync_timestamp=1565775199290&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fignore%2Fdownload%2Fignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha1-dQ49tYYgh7RzfrrIIH/9HvJ7Jfw= +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.nlark.com/immediate/download/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + immer@1.10.0: version "1.10.0" resolved "https://registry.npm.taobao.org/immer/download/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" @@ -6397,6 +6402,16 @@ jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3: array-includes "^3.0.3" object.assign "^4.1.0" +jszip@^3.7.1: + version "3.7.1" + resolved "https://registry.nlark.com/jszip/download/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9" + integrity sha1-vWNAEiHBViWhIoxVbKimjab9o9k= + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -6488,6 +6503,13 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.nlark.com/lie/download/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha1-3Pgt7lRfRgdNryAMfBxaCOD0D2o= + dependencies: + immediate "~3.0.5" + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.npm.taobao.org/lines-and-columns/download/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -7483,7 +7505,7 @@ p-try@^2.0.0: resolved "https://registry.npm.taobao.org/p-try/download/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha1-yyhoVA4xPWHeWPr741zpAE1VQOY= -pako@~1.0.5: +pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.npm.taobao.org/pako/download/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha1-bJWZ00DVTf05RjgCUqNXBaa5kr8= @@ -9503,6 +9525,11 @@ set-blocking@^2.0.0: resolved "https://registry.npm.taobao.org/set-blocking/download/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-immediate-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.nlark.com/set-immediate-shim/download/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.npm.taobao.org/set-value/download/set-value-2.0.1.tgz?cache=0&sync_timestamp=1585775409029&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fset-value%2Fdownload%2Fset-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"