分享海报

This commit is contained in:
DESKTOP-RQ919RC\Pc 2025-04-14 19:17:58 +08:00
parent b90d67fcd5
commit 3f8653b337
64 changed files with 1021 additions and 5578 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@
/.git
*.log
project.private.config.json
project.config.json
project.config.json
node_modules

View File

@ -3,6 +3,6 @@
"*.wxss": "css",
"*.tpl": "html",
"*.vue": "vue",
"*.wxml": "html"
"*.wxml": "wxml"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,779 +0,0 @@
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else {
var a = factory();
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
}
})(window, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
const hex = (color) => {
let result = null
if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
return color
// eslint-disable-next-line no-cond-assign
} else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
return '#' + result[2].split(',').map((part, index) => {
part = part.trim()
part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
part = part.toString(16)
if (part.length === 1) {
part = '0' + part
}
return part
}).join('')
} else {
return '#00000000'
}
}
const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
if (index === 0) {
return part
}
return part[0].toUpperCase() + part.slice(1)
}).join('')
const compareVersion = (v1, v2) => {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
module.exports = {
hex,
splitLineToCamelCase,
compareVersion
}
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
const xmlParse = __webpack_require__(2)
const {Widget} = __webpack_require__(3)
const {Draw} = __webpack_require__(5)
const {compareVersion} = __webpack_require__(0)
const canvasId = 'weui-canvas'
Component({
properties: {
width: {
type: Number,
value: 400
},
height: {
type: Number,
value: 300
}
},
data: {
use2dCanvas: false, // 2.9.2 后可用canvas 2d 接口
},
lifetimes: {
attached() {
const {SDKVersion, pixelRatio: dpr} = wx.getSystemInfoSync()
const use2dCanvas = compareVersion(SDKVersion, '2.9.2') >= 0
this.dpr = dpr
this.setData({use2dCanvas}, () => {
if (use2dCanvas) {
const query = this.createSelectorQuery()
query.select(`#${canvasId}`)
.fields({node: true, size: true})
.exec(res => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
this.ctx = ctx
this.canvas = canvas
})
} else {
this.ctx = wx.createCanvasContext(canvasId, this)
}
})
}
},
methods: {
async renderToCanvas(args) {
const {wxml, style} = args
const ctx = this.ctx
const canvas = this.canvas
const use2dCanvas = this.data.use2dCanvas
if (use2dCanvas && !canvas) {
return Promise.reject(new Error('renderToCanvas: fail canvas has not been created'))
}
ctx.clearRect(0, 0, this.data.width, this.data.height)
const {root: xom} = xmlParse(wxml)
const widget = new Widget(xom, style)
const container = widget.init()
this.boundary = {
top: container.layoutBox.top,
left: container.layoutBox.left,
width: container.computedStyle.width,
height: container.computedStyle.height,
}
const draw = new Draw(ctx, canvas, use2dCanvas)
await draw.drawNode(container)
if (!use2dCanvas) {
await this.canvasDraw(ctx)
}
return Promise.resolve(container)
},
canvasDraw(ctx, reserve) {
return new Promise(resolve => {
ctx.draw(reserve, () => {
resolve()
})
})
},
canvasToTempFilePath(args = {}) {
const use2dCanvas = this.data.use2dCanvas
return new Promise((resolve, reject) => {
const {
top, left, width, height
} = this.boundary
const copyArgs = {
x: left,
y: top,
width,
height,
destWidth: width * this.dpr,
destHeight: height * this.dpr,
canvasId,
fileType: args.fileType || 'png',
quality: args.quality || 1,
success: resolve,
fail: reject
}
if (use2dCanvas) {
delete copyArgs.canvasId
copyArgs.canvas = this.canvas
}
wx.canvasToTempFilePath(copyArgs, this)
})
}
}
})
/***/ }),
/* 2 */
/***/ (function(module, exports) {
/**
* Module dependencies.
*/
/**
* Expose `parse`.
*/
/**
* Parse the given string of `xml`.
*
* @param {String} xml
* @return {Object}
* @api public
*/
function parse(xml) {
xml = xml.trim()
// strip comments
xml = xml.replace(/<!--[\s\S]*?-->/g, '')
return document()
/**
* XML document.
*/
function document() {
return {
declaration: declaration(),
root: tag()
}
}
/**
* Declaration.
*/
function declaration() {
const m = match(/^<\?xml\s*/)
if (!m) return
// tag
const node = {
attributes: {}
}
// attributes
while (!(eos() || is('?>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
match(/\?>\s*/)
return node
}
/**
* Tag.
*/
function tag() {
const m = match(/^<([\w-:.]+)\s*/)
if (!m) return
// name
const node = {
name: m[1],
attributes: {},
children: []
}
// attributes
while (!(eos() || is('>') || is('?>') || is('/>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
// self closing tag
if (match(/^\s*\/>\s*/)) {
return node
}
match(/\??>\s*/)
// content
node.content = content()
// children
let child
while (child = tag()) {
node.children.push(child)
}
// closing
match(/^<\/[\w-:.]+>\s*/)
return node
}
/**
* Text content.
*/
function content() {
const m = match(/^([^<]*)/)
if (m) return m[1]
return ''
}
/**
* Attribute.
*/
function attribute() {
const m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/)
if (!m) return
return {name: m[1], value: strip(m[2])}
}
/**
* Strip quotes from `val`.
*/
function strip(val) {
return val.replace(/^['"]|['"]$/g, '')
}
/**
* Match `re` and advance the string.
*/
function match(re) {
const m = xml.match(re)
if (!m) return
xml = xml.slice(m[0].length)
return m
}
/**
* End-of-source.
*/
function eos() {
return xml.length == 0
}
/**
* Check for `prefix`.
*/
function is(prefix) {
return xml.indexOf(prefix) == 0
}
}
module.exports = parse
/***/ }),
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
const Block = __webpack_require__(4)
const {splitLineToCamelCase} = __webpack_require__(0)
class Element extends Block {
constructor(prop) {
super(prop.style)
this.name = prop.name
this.attributes = prop.attributes
}
}
class Widget {
constructor(xom, style) {
this.xom = xom
this.style = style
this.inheritProps = ['fontSize', 'lineHeight', 'textAlign', 'verticalAlign', 'color']
}
init() {
this.container = this.create(this.xom)
this.container.layout()
this.inheritStyle(this.container)
return this.container
}
// 继承父节点的样式
inheritStyle(node) {
const parent = node.parent || null
const children = node.children || {}
const computedStyle = node.computedStyle
if (parent) {
this.inheritProps.forEach(prop => {
computedStyle[prop] = computedStyle[prop] || parent.computedStyle[prop]
})
}
Object.values(children).forEach(child => {
this.inheritStyle(child)
})
}
create(node) {
let classNames = (node.attributes.class || '').split(' ')
classNames = classNames.map(item => splitLineToCamelCase(item.trim()))
const style = {}
classNames.forEach(item => {
Object.assign(style, this.style[item] || {})
})
const args = {name: node.name, style}
const attrs = Object.keys(node.attributes)
const attributes = {}
for (const attr of attrs) {
const value = node.attributes[attr]
const CamelAttr = splitLineToCamelCase(attr)
if (value === '' || value === 'true') {
attributes[CamelAttr] = true
} else if (value === 'false') {
attributes[CamelAttr] = false
} else {
attributes[CamelAttr] = value
}
}
attributes.text = node.content
args.attributes = attributes
const element = new Element(args)
node.children.forEach(childNode => {
const childElement = this.create(childNode)
element.add(childElement)
})
return element
}
}
module.exports = {Widget}
/***/ }),
/* 4 */
/***/ (function(module, exports) {
module.exports = require("widget-ui");
/***/ }),
/* 5 */
/***/ (function(module, exports) {
class Draw {
constructor(context, canvas, use2dCanvas = false) {
this.ctx = context
this.canvas = canvas || null
this.use2dCanvas = use2dCanvas
}
roundRect(x, y, w, h, r, fill = true, stroke = false) {
if (r < 0) return
const ctx = this.ctx
ctx.beginPath()
ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)
ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)
ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)
ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)
ctx.lineTo(x, y + r)
if (stroke) ctx.stroke()
if (fill) ctx.fill()
}
drawView(box, style) {
const ctx = this.ctx
const {
left: x, top: y, width: w, height: h
} = box
const {
borderRadius = 0,
borderWidth = 0,
borderColor,
color = '#000',
backgroundColor = 'transparent',
} = style
ctx.save()
// 外环
if (borderWidth > 0) {
ctx.fillStyle = borderColor || color
this.roundRect(x, y, w, h, borderRadius)
}
// 内环
ctx.fillStyle = backgroundColor
const innerWidth = w - 2 * borderWidth
const innerHeight = h - 2 * borderWidth
const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0
this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)
ctx.restore()
}
async drawImage(img, box, style) {
await new Promise((resolve, reject) => {
const ctx = this.ctx
const canvas = this.canvas
const {
borderRadius = 0
} = style
const {
left: x, top: y, width: w, height: h
} = box
ctx.save()
this.roundRect(x, y, w, h, borderRadius, false, false)
ctx.clip()
const _drawImage = (img) => {
if (this.use2dCanvas) {
const Image = canvas.createImage()
Image.onload = () => {
ctx.drawImage(Image, x, y, w, h)
ctx.restore()
resolve()
}
Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }
Image.src = img
} else {
ctx.drawImage(img, x, y, w, h)
ctx.restore()
resolve()
}
}
const isTempFile = /^wxfile:\/\//.test(img)
const isNetworkFile = /^https?:\/\//.test(img)
if (isTempFile) {
_drawImage(img)
} else if (isNetworkFile) {
wx.downloadFile({
url: img,
success(res) {
if (res.statusCode === 200) {
_drawImage(res.tempFilePath)
} else {
reject(new Error(`downloadFile:fail ${img}`))
}
},
fail() {
reject(new Error(`downloadFile:fail ${img}`))
}
})
} else {
reject(new Error(`image format error: ${img}`))
}
})
}
// eslint-disable-next-line complexity
drawText(text, box, style) {
const ctx = this.ctx
let {
left: x, top: y, width: w, height: h
} = box
let {
color = '#000',
lineHeight = '1.4em',
fontSize = 14,
textAlign = 'left',
verticalAlign = 'top',
backgroundColor = 'transparent'
} = style
if (typeof lineHeight === 'string') { // 2em
lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
}
if (!text || (lineHeight > h)) return
ctx.save()
ctx.textBaseline = 'top'
ctx.font = `${fontSize}px sans-serif`
ctx.textAlign = textAlign
// 背景色
ctx.fillStyle = backgroundColor
this.roundRect(x, y, w, h, 0)
// 文字颜色
ctx.fillStyle = color
// 水平布局
switch (textAlign) {
case 'left':
break
case 'center':
x += 0.5 * w
break
case 'right':
x += w
break
default: break
}
const textWidth = ctx.measureText(text).width
const actualHeight = Math.ceil(textWidth / w) * lineHeight
let paddingTop = Math.ceil((h - actualHeight) / 2)
if (paddingTop < 0) paddingTop = 0
// 垂直布局
switch (verticalAlign) {
case 'top':
break
case 'middle':
y += paddingTop
break
case 'bottom':
y += 2 * paddingTop
break
default: break
}
const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
// 不超过一行
if (textWidth <= w) {
ctx.fillText(text, x, y + inlinePaddingTop)
return
}
// 多行文本
const chars = text.split('')
const _y = y
// 逐行绘制
let line = ''
for (const ch of chars) {
const testLine = line + ch
const testWidth = ctx.measureText(testLine).width
if (testWidth > w) {
ctx.fillText(line, x, y + inlinePaddingTop)
y += lineHeight
line = ch
if ((y + lineHeight) > (_y + h)) break
} else {
line = testLine
}
}
// 避免溢出
if ((y + lineHeight) <= (_y + h)) {
ctx.fillText(line, x, y + inlinePaddingTop)
}
ctx.restore()
}
async drawNode(element) {
const {layoutBox, computedStyle, name} = element
const {src, text} = element.attributes
if (name === 'view') {
this.drawView(layoutBox, computedStyle)
} else if (name === 'image') {
await this.drawImage(src, layoutBox, computedStyle)
} else if (name === 'text') {
this.drawText(text, layoutBox, computedStyle)
}
const childs = Object.values(element.children)
for (const child of childs) {
await this.drawNode(child)
}
}
}
module.exports = {
Draw
}
/***/ })
/******/ ]);
});

View File

@ -1,4 +0,0 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -1,2 +0,0 @@
<canvas wx:if="{{use2dCanvas}}" id="weui-canvas" type="2d" style="width: {{width}}px; height: {{height}}px;"></canvas>
<canvas wx:else canvas-id="weui-canvas" style="width: {{width}}px; height: {{height}}px;"></canvas>

View File

@ -1,57 +0,0 @@
const hex = (color) => {
let result = null
if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
return color
// eslint-disable-next-line no-cond-assign
} else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
return '#' + result[2].split(',').map((part, index) => {
part = part.trim()
part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
part = part.toString(16)
if (part.length === 1) {
part = '0' + part
}
return part
}).join('')
} else {
return '#00000000'
}
}
const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
if (index === 0) {
return part
}
return part[0].toUpperCase() + part.slice(1)
}).join('')
const compareVersion = (v1, v2) => {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
module.exports = {
hex,
splitLineToCamelCase,
compareVersion
}

View File

@ -23,8 +23,5 @@
"typescript": "^3.6.4",
"webpack": "^4.41.1",
"webpack-cli": "^3.3.9"
},
"__npminstall_done": true,
"_from": "widget-ui@1.0.2",
"_resolved": "https://registry.npmmirror.com/widget-ui/-/widget-ui-1.0.2.tgz"
}
}

View File

@ -23,8 +23,5 @@
"typescript": "^3.6.4",
"webpack": "^4.41.1",
"webpack-cli": "^3.3.9"
},
"__npminstall_done": true,
"_from": "widget-ui@1.0.2",
"_resolved": "https://registry.npmmirror.com/widget-ui/-/widget-ui-1.0.2.tgz"
}
}

View File

@ -23,8 +23,5 @@
"typescript": "^3.6.4",
"webpack": "^4.41.1",
"webpack-cli": "^3.3.9"
},
"__npminstall_done": true,
"_from": "widget-ui@1.0.2",
"_resolved": "https://registry.npmmirror.com/widget-ui/-/widget-ui-1.0.2.tgz"
}
}

View File

@ -59,8 +59,5 @@
},
"dependencies": {
"widget-ui": "^1.0.2"
},
"__npminstall_done": true,
"_from": "wxml-to-canvas@1.1.1",
"_resolved": "https://registry.npmmirror.com/wxml-to-canvas/-/wxml-to-canvas-1.1.1.tgz"
}
}

View File

@ -1,9 +0,0 @@
module.exports = {
presets: [
["@babel/preset-env", {
targets: {
node: "current"
}
}]
]
};

View File

@ -1,40 +0,0 @@
declare type LayoutData = {
left: number;
top: number;
width: number;
height: number;
};
declare type LayoutNode = {
id: number;
style: Object;
children: LayoutNode[];
layout?: LayoutData;
};
declare class Element {
static uuid(): number;
parent: Element | null;
id: number;
style: {
[key: string]: any;
};
computedStyle: {
[key: string]: any;
};
lastComputedStyle: {
[key: string]: any;
};
children: {
[key: string]: Element;
};
layoutBox: LayoutData;
constructor(style?: {
[key: string]: any;
});
getAbsolutePosition(element: Element): any;
add(element: Element): void;
remove(element?: Element): void;
getNodeTree(): LayoutNode;
applyLayout(layoutNode: LayoutNode): void;
layout(): void;
}
export default Element;

View File

@ -1,5 +0,0 @@
export default class EventEmitter {
emit(event: string, data?: any): void;
on(event: string, callback: any): void;
off(event: string, callback: any): void;
}

File diff suppressed because one or more lines are too long

View File

@ -1,36 +0,0 @@
declare const textStyles: string[];
declare const scalableStyles: string[];
declare const layoutAffectedStyles: string[];
declare const getDefaultStyle: () => {
left: undefined;
top: undefined;
right: undefined;
bottom: undefined;
width: undefined;
height: undefined;
maxWidth: undefined;
maxHeight: undefined;
minWidth: undefined;
minHeight: undefined;
margin: undefined;
marginLeft: undefined;
marginRight: undefined;
marginTop: undefined;
marginBottom: undefined;
padding: undefined;
paddingLeft: undefined;
paddingRight: undefined;
paddingTop: undefined;
paddingBottom: undefined;
borderWidth: undefined;
flexDirection: undefined;
justifyContent: undefined;
alignItems: undefined;
alignSelf: undefined;
flex: undefined;
flexWrap: undefined;
position: undefined;
hidden: boolean;
scale: number;
};
export { getDefaultStyle, scalableStyles, textStyles, layoutAffectedStyles };

View File

@ -1,6 +0,0 @@
module.exports = {
transform: {
"^.+\\.js$": "babel-jest",
"^.+\\.ts$": "ts-jest"
}
};

30
node_modules/widget-ui/package.json generated vendored
View File

@ -1,30 +0,0 @@
{
"name": "widget-ui",
"version": "1.0.2",
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "jest",
"build": "webpack"
},
"author": "",
"license": "ISC",
"dependencies": {
"eventemitter3": "^4.0.0"
},
"devDependencies": {
"@babel/preset-env": "^7.6.3",
"@babel/preset-typescript": "^7.6.0",
"@types/jest": "^24.0.18",
"babel-jest": "^24.9.0",
"jest": "^24.9.0",
"ts-jest": "^24.1.0",
"ts-loader": "^6.2.0",
"typescript": "^3.6.4",
"webpack": "^4.41.1",
"webpack-cli": "^3.3.9"
},
"__npminstall_done": true,
"_from": "widget-ui@1.0.2",
"_resolved": "https://registry.npmmirror.com/widget-ui/-/widget-ui-1.0.2.tgz"
}

File diff suppressed because it is too large Load Diff

172
node_modules/widget-ui/src/element.ts generated vendored
View File

@ -1,172 +0,0 @@
import computeLayout from "./css-layout";
import { getDefaultStyle, scalableStyles, layoutAffectedStyles } from "./style";
type LayoutData = {
left: number,
top: number,
width: number,
height: number
};
type LayoutNode = {
id: number,
style: Object,
children: LayoutNode[],
layout?: LayoutData
};
let uuid = 0;
class Element {
public static uuid(): number {
return uuid++;
}
public parent: Element | null = null;
public id: number = Element.uuid();
public style: { [key: string]: any } = {};
public computedStyle: { [key: string]: any } = {};
public lastComputedStyle: { [key: string]: any } = {};
public children: { [key: string]: Element } = {};
public layoutBox: LayoutData = { left: 0, top: 0, width: 0, height: 0 };
constructor(style: { [key: string]: any } = {}) {
// 拷贝一份,防止被外部逻辑修改
style = Object.assign(getDefaultStyle(), style);
this.computedStyle = Object.assign(getDefaultStyle(), style);
this.lastComputedStyle = Object.assign(getDefaultStyle(), style);
Object.keys(style).forEach(key => {
Object.defineProperty(this.style, key, {
configurable: true,
enumerable: true,
get: () => style[key],
set: (value: any) => {
if (value === style[key] || value === undefined) {
return;
}
this.lastComputedStyle = this.computedStyle[key]
style[key] = value
this.computedStyle[key] = value
// 如果设置的是一个可缩放的属性, 计算自己
if (scalableStyles.includes(key) && this.style.scale) {
this.computedStyle[key] = value * this.style.scale
}
// 如果设置的是 scale, 则把所有可缩放的属性计算
if (key === "scale") {
scalableStyles.forEach(prop => {
if (style[prop]) {
this.computedStyle[prop] = style[prop] * value
}
})
}
if (key === "hidden") {
if (value) {
layoutAffectedStyles.forEach((key: string) => {
this.computedStyle[key] = 0;
});
} else {
layoutAffectedStyles.forEach((key: string) => {
this.computedStyle[key] = this.lastComputedStyle[key];
});
}
}
}
})
})
if (this.style.scale) {
scalableStyles.forEach((key: string) => {
if (this.style[key]) {
const computedValue = this.style[key] * this.style.scale;
this.computedStyle[key] = computedValue;
}
});
}
if (style.hidden) {
layoutAffectedStyles.forEach((key: string) => {
this.computedStyle[key] = 0;
});
}
}
getAbsolutePosition(element: Element) {
if (!element) {
return this.getAbsolutePosition(this)
}
if (!element.parent) {
return {
left: 0,
top: 0
}
}
const {left, top} = this.getAbsolutePosition(element.parent)
return {
left: left + element.layoutBox.left,
top: top + element.layoutBox.top
}
}
public add(element: Element) {
element.parent = this;
this.children[element.id] = element;
}
public remove(element?: Element) {
// 删除自己
if (!element) {
Object.keys(this.children).forEach(id => {
const child = this.children[id]
child.remove()
delete this.children[id]
})
} else if (this.children[element.id]) {
// 是自己的子节点才删除
element.remove()
delete this.children[element.id];
}
}
public getNodeTree(): LayoutNode {
return {
id: this.id,
style: this.computedStyle,
children: Object.keys(this.children).map((id: string) => {
const child = this.children[id];
return child.getNodeTree();
})
}
}
public applyLayout(layoutNode: LayoutNode) {
["left", "top", "width", "height"].forEach((key: string) => {
if (layoutNode.layout && typeof layoutNode.layout[key] === "number") {
this.layoutBox[key] = layoutNode.layout[key];
if (this.parent && (key === "left" || key === "top")) {
this.layoutBox[key] += this.parent.layoutBox[key];
}
}
});
layoutNode.children.forEach((child: LayoutNode) => {
this.children[child.id].applyLayout(child);
});
}
layout() {
const nodeTree = this.getNodeTree();
computeLayout(nodeTree);
this.applyLayout(nodeTree);
}
}
export default Element;

15
node_modules/widget-ui/src/event.ts generated vendored
View File

@ -1,15 +0,0 @@
import _EventEmitter from "eventemitter3";
const emitter = new _EventEmitter();
export default class EventEmitter {
public emit(event: string, data?: any) {
emitter.emit(event, data);
}
public on(event: string, callback) {
emitter.on(event, callback);
}
public off(event: string, callback) {
emitter.off(event, callback);
}
}

87
node_modules/widget-ui/src/style.ts generated vendored
View File

@ -1,87 +0,0 @@
const textStyles: string[] = ["color", "fontSize", "textAlign", "fontWeight", "lineHeight", "lineBreak"];
const scalableStyles: string[] = ["left", "top", "right", "bottom", "width", "height",
"margin", "marginLeft", "marginRight", "marginTop", "marginBottom",
"padding", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom",
"borderWidth", "borderLeftWidth", "borderRightWidth", "borderTopWidth", "borderBottomWidth"];
const layoutAffectedStyles: string[] = [
"margin", "marginTop", "marginBottom", "marginLeft", "marginRight",
"padding", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
"width", "height"];
type Style = {
left: number,
top: number,
right: number,
bottom: number,
width: number,
height: number,
maxWidth: number,
maxHeight: number,
minWidth: number,
minHeight: number,
margin: number,
marginLeft: number,
marginRight: number,
marginTop: number,
marginBottom: number,
padding: number,
paddingLeft: number,
paddingRight: number,
paddingTop: number,
paddingBottom: number,
borderWidth: number,
borderLeftWidth: number,
borderRightWidth: number,
borderTopWidth: number,
borderBottomWidth: number,
flexDirection: "column" | "row",
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around",
alignItems: "flex-start" | "center" | "flex-end" | "stretch",
alignSelf: "flex-start" | "center" | "flex-end" | "stretch",
flex: number,
flexWrap: "wrap" | "nowrap",
position: "relative" | "absolute",
hidden: boolean,
scale: number
}
const getDefaultStyle = () => ({
left: undefined,
top: undefined,
right: undefined,
bottom: undefined,
width: undefined,
height: undefined,
maxWidth: undefined,
maxHeight: undefined,
minWidth: undefined,
minHeight: undefined,
margin: undefined,
marginLeft: undefined,
marginRight: undefined,
marginTop: undefined,
marginBottom: undefined,
padding: undefined,
paddingLeft: undefined,
paddingRight: undefined,
paddingTop: undefined,
paddingBottom: undefined,
borderWidth: undefined,
flexDirection: undefined,
justifyContent: undefined,
alignItems: undefined,
alignSelf: undefined,
flex: undefined,
flexWrap: undefined,
position: undefined,
hidden: false,
scale: 1
})
export {
getDefaultStyle, scalableStyles, textStyles, layoutAffectedStyles
}

View File

@ -1,183 +0,0 @@
import Element from "../src/element";
test("layout", () => {
const container = new Element({
width: 100,
height: 100,
padding: 10,
borderWidth: 2
})
const div1 = new Element({
left: 5,
top: 5,
width: 14,
height: 14
})
container.add(div1);
container.layout();
// css-layout 是 border-box
expect(container.layoutBox.left).toBe(0);
expect(container.layoutBox.top).toBe(0);
expect(container.layoutBox.width).toBe(100);
expect(container.layoutBox.height).toBe(100);
expect(div1.layoutBox.left).toBe(10 + 2 + 5);
expect(div1.layoutBox.top).toBe(10 + 2 + 5);
expect(div1.layoutBox.width).toBe(14);
expect(div1.layoutBox.height).toBe(14);
});
test("overflow", () => {
const container = new Element({
width: 100,
height: 100,
padding: 10,
borderWidth: 2
})
const div1 = new Element({
width: 114,
height: 114,
})
container.add(div1);
container.layout();
// 写死尺寸的情况下子元素不收缩父元素不撑开
expect(container.layoutBox.width).toBe(100);
expect(container.layoutBox.height).toBe(100);
expect(div1.layoutBox.left).toBe(10 + 2);
expect(div1.layoutBox.top).toBe(10 + 2);
expect(div1.layoutBox.width).toBe(114);
expect(div1.layoutBox.height).toBe(114);
});
test("right bottom", () => {
const container = new Element({
width: 100,
height: 100,
padding: 10,
borderWidth: 2
})
const div1 = new Element({
width: 14,
height: 14,
right: 13,
bottom: 9,
position: "absolute"
})
container.add(div1);
container.layout();
// right bottom 只有在 position 为 absolute 的情况下才有用
expect(container.layoutBox.width).toBe(100);
expect(container.layoutBox.height).toBe(100);
// 但这时就是以整个父元素为边界,而不是 border + padding 后的边界
expect(div1.layoutBox.left).toBe(100 - 13 - 14);
expect(div1.layoutBox.top).toBe(100 - 9 - 14);
});
test("flex center", () => {
const container = new Element({
width: 100,
height: 100,
padding: 10,
borderWidth: 2,
flexDirection: "row",
justifyContent: "center",
alignItems: "center"
})
const div1 = new Element({
width: 14,
height: 14
})
container.add(div1);
container.layout();
// 使用 flex 水平垂直居中
expect(div1.layoutBox.left).toBe((100 - 14)/2);
expect(div1.layoutBox.top).toBe((100 - 14)/2);
})
test("flex top bottom", () => {
const container = new Element({
width: 100,
height: 100,
padding: 10,
borderWidth: 2,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "stretch"
})
// flex 实现一上一下两行水平填满
const div1 = new Element({
height: 10
})
const div2 = new Element({
height: 20
})
container.add(div1);
container.add(div2);
container.layout();
expect(div1.layoutBox.left).toBe(10 + 2);
expect(div1.layoutBox.top).toBe(10 + 2);
expect(div1.layoutBox.width).toBe(100 - 10*2 - 2*2);
expect(div2.layoutBox.left).toBe(10 + 2);
expect(div2.layoutBox.top).toBe(100 - 10 - 2 - 20);
expect(div2.layoutBox.width).toBe(100 - 10*2 - 2*2);
})
test("rewrite uuid", () => {
// 小程序为了保证 webview 和 service 侧的 coverview 不冲突,所以设置了不同的自增起点
// uuid 静态方法就是为了根据不同的需求去覆写
let uuid = 79648527;
Element.uuid = () => uuid++;
const container = new Element();
expect(container.id).toEqual(79648527);
const div = new Element();
expect(div.id).toEqual(79648528);
});
test("absolute left top", () => {
const container = new Element({
width: 300,
height: 200,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center'
})
const div1 = new Element({
width: 80,
height: 60
})
const div2 = new Element({
width: 40,
height: 30
})
div1.add(div2)
container.add(div1)
container.layout()
expect(div1.layoutBox.left).toBe(110)
expect(div1.layoutBox.top).toBe(70)
expect(div2.layoutBox.left).toBe(110)
expect(div2.layoutBox.top).toBe(70)
})

47
node_modules/widget-ui/tsconfig.json generated vendored
View File

@ -1,47 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src",
"resolveJsonModule": true,
"downlevelIteration": false,
"target": "es5",
"module": "commonjs",
"lib": [
"es5",
"es2015.promise",
"es2016",
"dom"
],
"outDir": "./dist",
"paths": {
"@/*": [
"*"
],
"*": [
"*"
]
},
"typeRoots": [
"./node_modules/@types"
],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"declaration": true,
"stripInternal": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"alwaysStrict": true,
"noFallthroughCasesInSwitch": true,
"removeComments": false,
"strictNullChecks": true,
"strictFunctionTypes": true,
"skipLibCheck": true,
"pretty": true,
"strictPropertyInitialization": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}

206
node_modules/widget-ui/tslint.json generated vendored
View File

@ -1,206 +0,0 @@
{
"defaultSeverity": "error",
"extends": [],
"rules": {
"adjacent-overload-signatures": true,
"align": {
"options": [
"parameters",
"statements"
]
},
"arrow-return-shorthand": true,
"ban-types": {
"options": [
[
"Object",
"Avoid using the `Object` type. Did you mean `object`?"
],
[
"Function",
"Avoid using the `Function` type. Prefer a specific function type, like `() => void`."
],
[
"Boolean",
"Avoid using the `Boolean` type. Did you mean `boolean`?"
],
[
"Number",
"Avoid using the `Number` type. Did you mean `number`?"
],
[
"String",
"Avoid using the `String` type. Did you mean `string`?"
],
[
"Symbol",
"Avoid using the `Symbol` type. Did you mean `symbol`?"
]
]
},
"comment-format": {
"options": [
"check-space"
]
},
"curly": {
"options": [
"ignore-same-line"
]
},
"cyclomatic-complexity": false,
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
},
"interface-over-type-literal": true,
"member-ordering": [
true,
{
"order": [
"public-static-field",
"public-instance-field",
"private-static-field",
"private-instance-field",
"public-constructor",
"private-constructor",
"public-instance-method",
"protected-instance-method",
"private-instance-method"
],
"alphabetize": false
}
],
"no-angle-bracket-type-assertion": true,
"no-arg": true,
"no-conditional-assignment": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-eval": true,
"no-internal-module": true,
"no-misused-new": true,
"no-reference-import": true,
"no-string-literal": true,
"no-string-throw": true,
"no-unnecessary-initializer": true,
"no-unsafe-finally": true,
"no-unused-expression": true,
"no-use-before-declare": false,
"no-var-keyword": true,
"no-var-requires": true,
"one-line": {
"options": [
"check-catch",
"check-else",
"check-finally",
"check-open-brace",
"check-whitespace"
]
},
"one-variable-per-declaration": {
"options": [
"ignore-for-loop"
]
},
"ordered-imports": {
"options": {
"import-sources-order": "case-insensitive",
"module-source-path": "full",
"named-imports-order": "case-insensitive"
}
},
"prefer-const": true,
"prefer-for-of": false,
"quotemark": {
"options": [
"double",
"avoid-escape"
]
},
"radix": true,
"semicolon": {
"options": [
"always"
]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"trailing-comma": {
"options": {
"esSpecCompliant": true,
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "always",
"typeLiterals": "always"
},
"singleline": "never"
}
},
"triple-equals": {
"options": [
"allow-null-check"
]
},
"typedef": false,
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"typeof-compare": false,
"unified-signatures": true,
"use-isnan": true,
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
}
},
"jsRules": {},
"rulesDirectory": [],
"no-var-requires": false,
"trailing-comma": [
true,
{
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "always",
"typeLiterals": "ignore"
},
"esSpecCompliant": true
}
],
"no-unused-expression": [
true,
"allow-fast-null-checks"
]
}

View File

@ -1,25 +0,0 @@
const path = require("path");
module.exports = {
mode: "production",
entry: path.resolve(__dirname, "src/element.ts"),
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader",
exclude: /node_modules/
}
]
},
resolve: {
extensions: [".js", ".ts"]
},
output: {
filename: "index.js",
path: path.resolve(__dirname, "dist"),
libraryTarget: "umd", // 采用通用模块定义
libraryExport: "default", // 兼容 ES6(ES2015) 的模块系统、CommonJS 和 AMD 模块规范
globalObject: "this" // 兼容node和浏览器运行避免window is not undefined情况
}
};

10
node_modules/wxml-to-canvas/.babelrc generated vendored
View File

@ -1,10 +0,0 @@
{
"plugins": [
["module-resolver", {
"root": ["./src"],
"alias": {}
}],
"@babel/transform-runtime"
],
"presets": ["@babel/preset-env"]
}

View File

@ -1,99 +0,0 @@
module.exports = {
'extends': [
'airbnb-base',
'plugin:promise/recommended'
],
'parserOptions': {
'ecmaVersion': 9,
'ecmaFeatures': {
'jsx': false
},
'sourceType': 'module'
},
'env': {
'es6': true,
'node': true,
'jest': true
},
'plugins': [
'import',
'node',
'promise'
],
'rules': {
'arrow-parens': 'off',
'comma-dangle': [
'error',
'only-multiline'
],
'complexity': ['error', 10],
'func-names': 'off',
'global-require': 'off',
'handle-callback-err': [
'error',
'^(err|error)$'
],
'import/no-unresolved': [
'error',
{
'caseSensitive': true,
'commonjs': true,
'ignore': ['^[^.]']
}
],
'import/prefer-default-export': 'off',
'linebreak-style': 'off',
'no-catch-shadow': 'error',
'no-continue': 'off',
'no-div-regex': 'warn',
'no-else-return': 'off',
'no-param-reassign': 'off',
'no-plusplus': 'off',
'no-shadow': 'off',
'no-multi-assign': 'off',
'no-underscore-dangle': 'off',
'node/no-deprecated-api': 'error',
'node/process-exit-as-throw': 'error',
'object-curly-spacing': [
'error',
'never'
],
'operator-linebreak': [
'error',
'after',
{
'overrides': {
':': 'before',
'?': 'before'
}
}
],
'prefer-arrow-callback': 'off',
'prefer-destructuring': 'off',
'prefer-template': 'off',
'quote-props': [
1,
'as-needed',
{
'unnecessary': true
}
],
'semi': [
'error',
'never'
],
'no-await-in-loop': 'off',
'no-restricted-syntax': 'off',
'promise/always-return': 'off',
},
'globals': {
'window': true,
'document': true,
'App': true,
'Page': true,
'Component': true,
'Behavior': true,
'wx': true,
'getCurrentPages': true,
}
}

21
node_modules/wxml-to-canvas/LICENSE generated vendored
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 wechat-miniprogram
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

187
node_modules/wxml-to-canvas/README.md generated vendored
View File

@ -1,187 +0,0 @@
# wxml-to-canvas
[![](https://img.shields.io/npm/v/wxml-to-canvas)](https://www.npmjs.com/package/wxml-to-canvas)
[![](https://img.shields.io/npm/l/wxml-to-canvas)](https://github.com/wechat-miniprogram/wxml-to-canvas)
小程序内通过静态模板和样式绘制 canvas ,导出图片,可用于生成分享图等场景。[代码片段](https://developers.weixin.qq.com/s/r6UBlEm17pc6)
## 使用方法
#### Step1. npm 安装,参考 [小程序 npm 支持](https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html)
```
npm install --save wxml-to-canvas
```
#### Step2. JSON 组件声明
```
{
"usingComponents": {
"wxml-to-canvas": "wxml-to-canvas",
}
}
```
#### Step3. wxml 引入组件
```
<video class="video" src="{{src}}">
<wxml-to-canvas class="widget"></wxml-to-canvas>
</video>
<image src="{{src}}" style="width: {{width}}px; height: {{height}}px"></image>
```
##### 属性列表
| 属性 | 类型 | 默认值 | 必填 | 说明 |
| --------------- | ------- | ------- | ---- | ---------------------- |
| width | Number | 400 | 否 | 画布宽度 |
| height | Number | 300 | 否 | 画布高度 |
#### Step4. js 获取实例
```
const {wxml, style} = require('./demo.js')
Page({
data: {
src: ''
},
onLoad() {
this.widget = this.selectComponent('.widget')
},
renderToCanvas() {
const p1 = this.widget.renderToCanvas({ wxml, style })
p1.then((res) => {
this.container = res
this.extraImage()
})
},
extraImage() {
const p2 = this.widget.canvasToTempFilePath()
p2.then(res => {
this.setData({
src: res.tempFilePath,
width: this.container.layoutBox.width,
height: this.container.layoutBox.height
})
})
}
})
```
## wxml 模板
支持 `view``text``image` 三种标签,通过 class 匹配 style 对象中的样式。
```
<view class="container" >
<view class="item-box red">
</view>
<view class="item-box green" >
<text class="text">yeah!</text>
</view>
<view class="item-box blue">
<image class="img" src="https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3582589792,4046843010&fm=26&gp=0.jpg"></image>
</view>
</view>
```
## 样式
对象属性值为对应 wxml 标签的 cass 驼峰形式。**需为每个元素指定 width 和 height 属性**,否则会导致布局错误。
存在多个 className 时,位置靠后的优先级更高,子元素会继承父级元素的可继承属性。
元素均为 flex 布局。left/top 等 仅在 absolute 定位下生效。
```
const style = {
container: {
width: 300,
height: 200,
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#ccc',
alignItems: 'center',
},
itemBox: {
width: 80,
height: 60,
},
red: {
backgroundColor: '#ff0000'
},
green: {
backgroundColor: '#00ff00'
},
blue: {
backgroundColor: '#0000ff'
},
text: {
width: 80,
height: 60,
textAlign: 'center',
verticalAlign: 'middle',
}
}
```
## 接口
#### f1. `renderToCanvas({wxml, style}): Promise`
渲染到 canvas传入 wxml 模板 和 style 对象,返回的容器对象包含布局和样式信息。
#### f2. `canvasToTempFilePath({fileType, quality}): Promise`
提取画布中容器所在区域内容生成相同大小的图片,返回临时文件地址。
`fileType` 支持 `jpg``png` 两种格式quality 为图片的质量,目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。
## 支持的 css 属性
### 布局相关
| 属性名 | 支持的值或类型 | 默认值 |
| --------------------- | --------------------------------------------------------- | ---------- |
| width | number | 0 |
| height | number | 0 |
| position | relative, absolute | relative |
| left | number | 0 |
| top | number | 0 |
| right | number | 0 |
| bottom | number | 0 |
| margin | number | 0 |
| padding | number | 0 |
| borderWidth | number | 0 |
| borderRadius | number | 0 |
| flexDirection | column, row | row |
| flexShrink | number | 1 |
| flexGrow | number | |
| flexWrap | wrap, nowrap | nowrap |
| justifyContent | flex-start, center, flex-end, space-between, space-around | flex-start |
| alignItems, alignSelf | flex-start, center, flex-end, stretch | flex-start |
支持 marginLeft、paddingLeft 等
### 文字
| 属性名 | 支持的值或类型 | 默认值 |
| --------------- | ------------------- | ----------- |
| fontSize | number | 14 |
| lineHeight | number / string | '1.4em' |
| textAlign | left, center, right | left |
| verticalAlign | top, middle, bottom | top |
| color | string | #000000 |
| backgroundColor | string | transparent |
lineHeight 可取带 em 单位的字符串或数字类型。
### 变形
| 属性名 | 支持的值或类型 | 默认值 |
| ------ | -------------- | ------ |
| scale | number | 1 |

View File

@ -1,26 +0,0 @@
const gulp = require('gulp')
const clean = require('gulp-clean')
const config = require('./tools/config')
const BuildTask = require('./tools/build')
const id = require('./package.json').name || 'miniprogram-custom-component'
// 构建任务实例
// eslint-disable-next-line no-new
new BuildTask(id, config.entry)
// 清空生成目录和文件
gulp.task('clean', gulp.series(() => gulp.src(config.distPath, {read: false, allowEmpty: true}).pipe(clean()), done => {
if (config.isDev) {
return gulp.src(config.demoDist, {read: false, allowEmpty: true})
.pipe(clean())
}
return done()
}))
// 监听文件变化并进行开发模式构建
gulp.task('watch', gulp.series(`${id}-watch`))
// 开发模式构建
gulp.task('dev', gulp.series(`${id}-dev`))
// 生产模式构建
gulp.task('default', gulp.series(`${id}-default`))

View File

@ -1,779 +0,0 @@
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else {
var a = factory();
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
}
})(window, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
const hex = (color) => {
let result = null
if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
return color
// eslint-disable-next-line no-cond-assign
} else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
return '#' + result[2].split(',').map((part, index) => {
part = part.trim()
part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
part = part.toString(16)
if (part.length === 1) {
part = '0' + part
}
return part
}).join('')
} else {
return '#00000000'
}
}
const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
if (index === 0) {
return part
}
return part[0].toUpperCase() + part.slice(1)
}).join('')
const compareVersion = (v1, v2) => {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
module.exports = {
hex,
splitLineToCamelCase,
compareVersion
}
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
const xmlParse = __webpack_require__(2)
const {Widget} = __webpack_require__(3)
const {Draw} = __webpack_require__(5)
const {compareVersion} = __webpack_require__(0)
const canvasId = 'weui-canvas'
Component({
properties: {
width: {
type: Number,
value: 400
},
height: {
type: Number,
value: 300
}
},
data: {
use2dCanvas: false, // 2.9.2 后可用canvas 2d 接口
},
lifetimes: {
attached() {
const {SDKVersion, pixelRatio: dpr} = wx.getSystemInfoSync()
const use2dCanvas = compareVersion(SDKVersion, '2.9.2') >= 0
this.dpr = dpr
this.setData({use2dCanvas}, () => {
if (use2dCanvas) {
const query = this.createSelectorQuery()
query.select(`#${canvasId}`)
.fields({node: true, size: true})
.exec(res => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
this.ctx = ctx
this.canvas = canvas
})
} else {
this.ctx = wx.createCanvasContext(canvasId, this)
}
})
}
},
methods: {
async renderToCanvas(args) {
const {wxml, style} = args
const ctx = this.ctx
const canvas = this.canvas
const use2dCanvas = this.data.use2dCanvas
if (use2dCanvas && !canvas) {
return Promise.reject(new Error('renderToCanvas: fail canvas has not been created'))
}
ctx.clearRect(0, 0, this.data.width, this.data.height)
const {root: xom} = xmlParse(wxml)
const widget = new Widget(xom, style)
const container = widget.init()
this.boundary = {
top: container.layoutBox.top,
left: container.layoutBox.left,
width: container.computedStyle.width,
height: container.computedStyle.height,
}
const draw = new Draw(ctx, canvas, use2dCanvas)
await draw.drawNode(container)
if (!use2dCanvas) {
await this.canvasDraw(ctx)
}
return Promise.resolve(container)
},
canvasDraw(ctx, reserve) {
return new Promise(resolve => {
ctx.draw(reserve, () => {
resolve()
})
})
},
canvasToTempFilePath(args = {}) {
const use2dCanvas = this.data.use2dCanvas
return new Promise((resolve, reject) => {
const {
top, left, width, height
} = this.boundary
const copyArgs = {
x: left,
y: top,
width,
height,
destWidth: width * this.dpr,
destHeight: height * this.dpr,
canvasId,
fileType: args.fileType || 'png',
quality: args.quality || 1,
success: resolve,
fail: reject
}
if (use2dCanvas) {
delete copyArgs.canvasId
copyArgs.canvas = this.canvas
}
wx.canvasToTempFilePath(copyArgs, this)
})
}
}
})
/***/ }),
/* 2 */
/***/ (function(module, exports) {
/**
* Module dependencies.
*/
/**
* Expose `parse`.
*/
/**
* Parse the given string of `xml`.
*
* @param {String} xml
* @return {Object}
* @api public
*/
function parse(xml) {
xml = xml.trim()
// strip comments
xml = xml.replace(/<!--[\s\S]*?-->/g, '')
return document()
/**
* XML document.
*/
function document() {
return {
declaration: declaration(),
root: tag()
}
}
/**
* Declaration.
*/
function declaration() {
const m = match(/^<\?xml\s*/)
if (!m) return
// tag
const node = {
attributes: {}
}
// attributes
while (!(eos() || is('?>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
match(/\?>\s*/)
return node
}
/**
* Tag.
*/
function tag() {
const m = match(/^<([\w-:.]+)\s*/)
if (!m) return
// name
const node = {
name: m[1],
attributes: {},
children: []
}
// attributes
while (!(eos() || is('>') || is('?>') || is('/>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
// self closing tag
if (match(/^\s*\/>\s*/)) {
return node
}
match(/\??>\s*/)
// content
node.content = content()
// children
let child
while (child = tag()) {
node.children.push(child)
}
// closing
match(/^<\/[\w-:.]+>\s*/)
return node
}
/**
* Text content.
*/
function content() {
const m = match(/^([^<]*)/)
if (m) return m[1]
return ''
}
/**
* Attribute.
*/
function attribute() {
const m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/)
if (!m) return
return {name: m[1], value: strip(m[2])}
}
/**
* Strip quotes from `val`.
*/
function strip(val) {
return val.replace(/^['"]|['"]$/g, '')
}
/**
* Match `re` and advance the string.
*/
function match(re) {
const m = xml.match(re)
if (!m) return
xml = xml.slice(m[0].length)
return m
}
/**
* End-of-source.
*/
function eos() {
return xml.length == 0
}
/**
* Check for `prefix`.
*/
function is(prefix) {
return xml.indexOf(prefix) == 0
}
}
module.exports = parse
/***/ }),
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
const Block = __webpack_require__(4)
const {splitLineToCamelCase} = __webpack_require__(0)
class Element extends Block {
constructor(prop) {
super(prop.style)
this.name = prop.name
this.attributes = prop.attributes
}
}
class Widget {
constructor(xom, style) {
this.xom = xom
this.style = style
this.inheritProps = ['fontSize', 'lineHeight', 'textAlign', 'verticalAlign', 'color']
}
init() {
this.container = this.create(this.xom)
this.container.layout()
this.inheritStyle(this.container)
return this.container
}
// 继承父节点的样式
inheritStyle(node) {
const parent = node.parent || null
const children = node.children || {}
const computedStyle = node.computedStyle
if (parent) {
this.inheritProps.forEach(prop => {
computedStyle[prop] = computedStyle[prop] || parent.computedStyle[prop]
})
}
Object.values(children).forEach(child => {
this.inheritStyle(child)
})
}
create(node) {
let classNames = (node.attributes.class || '').split(' ')
classNames = classNames.map(item => splitLineToCamelCase(item.trim()))
const style = {}
classNames.forEach(item => {
Object.assign(style, this.style[item] || {})
})
const args = {name: node.name, style}
const attrs = Object.keys(node.attributes)
const attributes = {}
for (const attr of attrs) {
const value = node.attributes[attr]
const CamelAttr = splitLineToCamelCase(attr)
if (value === '' || value === 'true') {
attributes[CamelAttr] = true
} else if (value === 'false') {
attributes[CamelAttr] = false
} else {
attributes[CamelAttr] = value
}
}
attributes.text = node.content
args.attributes = attributes
const element = new Element(args)
node.children.forEach(childNode => {
const childElement = this.create(childNode)
element.add(childElement)
})
return element
}
}
module.exports = {Widget}
/***/ }),
/* 4 */
/***/ (function(module, exports) {
module.exports = require("widget-ui");
/***/ }),
/* 5 */
/***/ (function(module, exports) {
class Draw {
constructor(context, canvas, use2dCanvas = false) {
this.ctx = context
this.canvas = canvas || null
this.use2dCanvas = use2dCanvas
}
roundRect(x, y, w, h, r, fill = true, stroke = false) {
if (r < 0) return
const ctx = this.ctx
ctx.beginPath()
ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)
ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)
ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)
ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)
ctx.lineTo(x, y + r)
if (stroke) ctx.stroke()
if (fill) ctx.fill()
}
drawView(box, style) {
const ctx = this.ctx
const {
left: x, top: y, width: w, height: h
} = box
const {
borderRadius = 0,
borderWidth = 0,
borderColor,
color = '#000',
backgroundColor = 'transparent',
} = style
ctx.save()
// 外环
if (borderWidth > 0) {
ctx.fillStyle = borderColor || color
this.roundRect(x, y, w, h, borderRadius)
}
// 内环
ctx.fillStyle = backgroundColor
const innerWidth = w - 2 * borderWidth
const innerHeight = h - 2 * borderWidth
const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0
this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)
ctx.restore()
}
async drawImage(img, box, style) {
await new Promise((resolve, reject) => {
const ctx = this.ctx
const canvas = this.canvas
const {
borderRadius = 0
} = style
const {
left: x, top: y, width: w, height: h
} = box
ctx.save()
this.roundRect(x, y, w, h, borderRadius, false, false)
ctx.clip()
const _drawImage = (img) => {
if (this.use2dCanvas) {
const Image = canvas.createImage()
Image.onload = () => {
ctx.drawImage(Image, x, y, w, h)
ctx.restore()
resolve()
}
Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }
Image.src = img
} else {
ctx.drawImage(img, x, y, w, h)
ctx.restore()
resolve()
}
}
const isTempFile = /^wxfile:\/\//.test(img)
const isNetworkFile = /^https?:\/\//.test(img)
if (isTempFile) {
_drawImage(img)
} else if (isNetworkFile) {
wx.downloadFile({
url: img,
success(res) {
if (res.statusCode === 200) {
_drawImage(res.tempFilePath)
} else {
reject(new Error(`downloadFile:fail ${img}`))
}
},
fail() {
reject(new Error(`downloadFile:fail ${img}`))
}
})
} else {
reject(new Error(`image format error: ${img}`))
}
})
}
// eslint-disable-next-line complexity
drawText(text, box, style) {
const ctx = this.ctx
let {
left: x, top: y, width: w, height: h
} = box
let {
color = '#000',
lineHeight = '1.4em',
fontSize = 14,
textAlign = 'left',
verticalAlign = 'top',
backgroundColor = 'transparent'
} = style
if (typeof lineHeight === 'string') { // 2em
lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
}
if (!text || (lineHeight > h)) return
ctx.save()
ctx.textBaseline = 'top'
ctx.font = `${fontSize}px sans-serif`
ctx.textAlign = textAlign
// 背景色
ctx.fillStyle = backgroundColor
this.roundRect(x, y, w, h, 0)
// 文字颜色
ctx.fillStyle = color
// 水平布局
switch (textAlign) {
case 'left':
break
case 'center':
x += 0.5 * w
break
case 'right':
x += w
break
default: break
}
const textWidth = ctx.measureText(text).width
const actualHeight = Math.ceil(textWidth / w) * lineHeight
let paddingTop = Math.ceil((h - actualHeight) / 2)
if (paddingTop < 0) paddingTop = 0
// 垂直布局
switch (verticalAlign) {
case 'top':
break
case 'middle':
y += paddingTop
break
case 'bottom':
y += 2 * paddingTop
break
default: break
}
const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
// 不超过一行
if (textWidth <= w) {
ctx.fillText(text, x, y + inlinePaddingTop)
return
}
// 多行文本
const chars = text.split('')
const _y = y
// 逐行绘制
let line = ''
for (const ch of chars) {
const testLine = line + ch
const testWidth = ctx.measureText(testLine).width
if (testWidth > w) {
ctx.fillText(line, x, y + inlinePaddingTop)
y += lineHeight
line = ch
if ((y + lineHeight) > (_y + h)) break
} else {
line = testLine
}
}
// 避免溢出
if ((y + lineHeight) <= (_y + h)) {
ctx.fillText(line, x, y + inlinePaddingTop)
}
ctx.restore()
}
async drawNode(element) {
const {layoutBox, computedStyle, name} = element
const {src, text} = element.attributes
if (name === 'view') {
this.drawView(layoutBox, computedStyle)
} else if (name === 'image') {
await this.drawImage(src, layoutBox, computedStyle)
} else if (name === 'text') {
this.drawText(text, layoutBox, computedStyle)
}
const childs = Object.values(element.children)
for (const child of childs) {
await this.drawNode(child)
}
}
}
module.exports = {
Draw
}
/***/ })
/******/ ]);
});

View File

@ -1,4 +0,0 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -1,2 +0,0 @@
<canvas wx:if="{{use2dCanvas}}" id="weui-canvas" type="2d" style="width: {{width}}px; height: {{height}}px;"></canvas>
<canvas wx:else canvas-id="weui-canvas" style="width: {{width}}px; height: {{height}}px;"></canvas>

View File

@ -1,57 +0,0 @@
const hex = (color) => {
let result = null
if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
return color
// eslint-disable-next-line no-cond-assign
} else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
return '#' + result[2].split(',').map((part, index) => {
part = part.trim()
part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
part = part.toString(16)
if (part.length === 1) {
part = '0' + part
}
return part
}).join('')
} else {
return '#00000000'
}
}
const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
if (index === 0) {
return part
}
return part[0].toUpperCase() + part.slice(1)
}).join('')
const compareVersion = (v1, v2) => {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
module.exports = {
hex,
splitLineToCamelCase,
compareVersion
}

View File

@ -1,66 +0,0 @@
{
"name": "wxml-to-canvas",
"version": "1.1.1",
"description": "",
"main": "miniprogram_dist/index.js",
"scripts": {
"dev": "gulp dev --develop",
"watch": "gulp watch --develop --watch",
"build": "gulp",
"dist": "npm run build",
"clean-dev": "gulp clean --develop",
"clean": "gulp clean",
"test": "jest --bail",
"test-debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --bail",
"coverage": "jest ./test/* --coverage --bail",
"lint": "eslint \"src/**/*.js\" --fix",
"lint-tools": "eslint \"tools/**/*.js\" --rule \"import/no-extraneous-dependencies: false\" --fix"
},
"miniprogram": "miniprogram_dist",
"jest": {
"testEnvironment": "jsdom",
"testURL": "https://jest.test",
"collectCoverageFrom": [
"src/**/*.js"
],
"moduleDirectories": [
"node_modules",
"src"
]
},
"repository": {
"type": "git",
"url": ""
},
"author": "sanfordsun",
"license": "MIT",
"devDependencies": {
"colors": "^1.3.1",
"eslint": "^5.14.1",
"eslint-config-airbnb-base": "13.1.0",
"eslint-loader": "^2.1.2",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^3.8.0",
"gulp": "^4.0.0",
"gulp-clean": "^0.4.0",
"gulp-if": "^2.0.2",
"gulp-install": "^1.1.0",
"gulp-less": "^4.0.1",
"gulp-rename": "^1.4.0",
"gulp-sourcemaps": "^2.6.5",
"jest": "^23.5.0",
"miniprogram-simulate": "^1.0.0",
"through2": "^2.0.3",
"vinyl": "^2.2.0",
"webpack": "^4.29.5",
"webpack-cli": "^3.3.10",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"widget-ui": "^1.0.2"
},
"__npminstall_done": true,
"_from": "wxml-to-canvas@1.1.1",
"_resolved": "https://registry.npmmirror.com/wxml-to-canvas/-/wxml-to-canvas-1.1.1.tgz"
}

View File

@ -1,225 +0,0 @@
class Draw {
constructor(context, canvas, use2dCanvas = false) {
this.ctx = context
this.canvas = canvas || null
this.use2dCanvas = use2dCanvas
}
roundRect(x, y, w, h, r, fill = true, stroke = false) {
if (r < 0) return
const ctx = this.ctx
ctx.beginPath()
ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)
ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)
ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)
ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)
ctx.lineTo(x, y + r)
if (stroke) ctx.stroke()
if (fill) ctx.fill()
}
drawView(box, style) {
const ctx = this.ctx
const {
left: x, top: y, width: w, height: h
} = box
const {
borderRadius = 0,
borderWidth = 0,
borderColor,
color = '#000',
backgroundColor = 'transparent',
} = style
ctx.save()
// 外环
if (borderWidth > 0) {
ctx.fillStyle = borderColor || color
this.roundRect(x, y, w, h, borderRadius)
}
// 内环
ctx.fillStyle = backgroundColor
const innerWidth = w - 2 * borderWidth
const innerHeight = h - 2 * borderWidth
const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0
this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)
ctx.restore()
}
async drawImage(img, box, style) {
await new Promise((resolve, reject) => {
const ctx = this.ctx
const canvas = this.canvas
const {
borderRadius = 0
} = style
const {
left: x, top: y, width: w, height: h
} = box
ctx.save()
this.roundRect(x, y, w, h, borderRadius, false, false)
ctx.clip()
const _drawImage = (img) => {
if (this.use2dCanvas) {
const Image = canvas.createImage()
Image.onload = () => {
ctx.drawImage(Image, x, y, w, h)
ctx.restore()
resolve()
}
Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }
Image.src = img
} else {
ctx.drawImage(img, x, y, w, h)
ctx.restore()
resolve()
}
}
const isTempFile = /^wxfile:\/\//.test(img)
const isNetworkFile = /^https?:\/\//.test(img)
if (isTempFile) {
_drawImage(img)
} else if (isNetworkFile) {
wx.downloadFile({
url: img,
success(res) {
if (res.statusCode === 200) {
_drawImage(res.tempFilePath)
} else {
reject(new Error(`downloadFile:fail ${img}`))
}
},
fail() {
reject(new Error(`downloadFile:fail ${img}`))
}
})
} else {
reject(new Error(`image format error: ${img}`))
}
})
}
// eslint-disable-next-line complexity
drawText(text, box, style) {
const ctx = this.ctx
let {
left: x, top: y, width: w, height: h
} = box
let {
color = '#000',
lineHeight = '1.4em',
fontSize = 14,
textAlign = 'left',
verticalAlign = 'top',
backgroundColor = 'transparent'
} = style
if (typeof lineHeight === 'string') { // 2em
lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
}
if (!text || (lineHeight > h)) return
ctx.save()
ctx.textBaseline = 'top'
ctx.font = `${fontSize}px sans-serif`
ctx.textAlign = textAlign
// 背景色
ctx.fillStyle = backgroundColor
this.roundRect(x, y, w, h, 0)
// 文字颜色
ctx.fillStyle = color
// 水平布局
switch (textAlign) {
case 'left':
break
case 'center':
x += 0.5 * w
break
case 'right':
x += w
break
default: break
}
const textWidth = ctx.measureText(text).width
const actualHeight = Math.ceil(textWidth / w) * lineHeight
let paddingTop = Math.ceil((h - actualHeight) / 2)
if (paddingTop < 0) paddingTop = 0
// 垂直布局
switch (verticalAlign) {
case 'top':
break
case 'middle':
y += paddingTop
break
case 'bottom':
y += 2 * paddingTop
break
default: break
}
const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
// 不超过一行
if (textWidth <= w) {
ctx.fillText(text, x, y + inlinePaddingTop)
return
}
// 多行文本
const chars = text.split('')
const _y = y
// 逐行绘制
let line = ''
for (const ch of chars) {
const testLine = line + ch
const testWidth = ctx.measureText(testLine).width
if (testWidth > w) {
ctx.fillText(line, x, y + inlinePaddingTop)
y += lineHeight
line = ch
if ((y + lineHeight) > (_y + h)) break
} else {
line = testLine
}
}
// 避免溢出
if ((y + lineHeight) <= (_y + h)) {
ctx.fillText(line, x, y + inlinePaddingTop)
}
ctx.restore()
}
async drawNode(element) {
const {layoutBox, computedStyle, name} = element
const {src, text} = element.attributes
if (name === 'view') {
this.drawView(layoutBox, computedStyle)
} else if (name === 'image') {
await this.drawImage(src, layoutBox, computedStyle)
} else if (name === 'text') {
this.drawText(text, layoutBox, computedStyle)
}
const childs = Object.values(element.children)
for (const child of childs) {
await this.drawNode(child)
}
}
}
module.exports = {
Draw
}

View File

@ -1,117 +0,0 @@
const xmlParse = require('./xml-parser')
const {Widget} = require('./widget')
const {Draw} = require('./draw')
const {compareVersion} = require('./utils')
const canvasId = 'weui-canvas'
Component({
properties: {
width: {
type: Number,
value: 400
},
height: {
type: Number,
value: 300
}
},
data: {
use2dCanvas: false, // 2.9.2 后可用canvas 2d 接口
},
lifetimes: {
attached() {
const {SDKVersion, pixelRatio: dpr} = wx.getSystemInfoSync()
const use2dCanvas = compareVersion(SDKVersion, '2.9.2') >= 0
this.dpr = dpr
this.setData({use2dCanvas}, () => {
if (use2dCanvas) {
const query = this.createSelectorQuery()
query.select(`#${canvasId}`)
.fields({node: true, size: true})
.exec(res => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
this.ctx = ctx
this.canvas = canvas
})
} else {
this.ctx = wx.createCanvasContext(canvasId, this)
}
})
}
},
methods: {
async renderToCanvas(args) {
const {wxml, style} = args
const ctx = this.ctx
const canvas = this.canvas
const use2dCanvas = this.data.use2dCanvas
if (use2dCanvas && !canvas) {
return Promise.reject(new Error('renderToCanvas: fail canvas has not been created'))
}
ctx.clearRect(0, 0, this.data.width, this.data.height)
const {root: xom} = xmlParse(wxml)
const widget = new Widget(xom, style)
const container = widget.init()
this.boundary = {
top: container.layoutBox.top,
left: container.layoutBox.left,
width: container.computedStyle.width,
height: container.computedStyle.height,
}
const draw = new Draw(ctx, canvas, use2dCanvas)
await draw.drawNode(container)
if (!use2dCanvas) {
await this.canvasDraw(ctx)
}
return Promise.resolve(container)
},
canvasDraw(ctx, reserve) {
return new Promise(resolve => {
ctx.draw(reserve, () => {
resolve()
})
})
},
canvasToTempFilePath(args = {}) {
const use2dCanvas = this.data.use2dCanvas
return new Promise((resolve, reject) => {
const {
top, left, width, height
} = this.boundary
const copyArgs = {
x: left,
y: top,
width,
height,
destWidth: width * this.dpr,
destHeight: height * this.dpr,
canvasId,
fileType: args.fileType || 'png',
quality: args.quality || 1,
success: resolve,
fail: reject
}
if (use2dCanvas) {
delete copyArgs.canvasId
copyArgs.canvas = this.canvas
}
wx.canvasToTempFilePath(copyArgs, this)
})
}
}
})

View File

@ -1,4 +0,0 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -1,2 +0,0 @@
<canvas wx:if="{{use2dCanvas}}" id="weui-canvas" type="2d" style="width: {{width}}px; height: {{height}}px;"></canvas>
<canvas wx:else canvas-id="weui-canvas" style="width: {{width}}px; height: {{height}}px;"></canvas>

View File

View File

@ -1,57 +0,0 @@
const hex = (color) => {
let result = null
if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
return color
// eslint-disable-next-line no-cond-assign
} else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
return '#' + result[2].split(',').map((part, index) => {
part = part.trim()
part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
part = part.toString(16)
if (part.length === 1) {
part = '0' + part
}
return part
}).join('')
} else {
return '#00000000'
}
}
const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
if (index === 0) {
return part
}
return part[0].toUpperCase() + part.slice(1)
}).join('')
const compareVersion = (v1, v2) => {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
module.exports = {
hex,
splitLineToCamelCase,
compareVersion
}

View File

@ -1,81 +0,0 @@
const Block = require('widget-ui')
const {splitLineToCamelCase} = require('./utils')
class Element extends Block {
constructor(prop) {
super(prop.style)
this.name = prop.name
this.attributes = prop.attributes
}
}
class Widget {
constructor(xom, style) {
this.xom = xom
this.style = style
this.inheritProps = ['fontSize', 'lineHeight', 'textAlign', 'verticalAlign', 'color']
}
init() {
this.container = this.create(this.xom)
this.container.layout()
this.inheritStyle(this.container)
return this.container
}
// 继承父节点的样式
inheritStyle(node) {
const parent = node.parent || null
const children = node.children || {}
const computedStyle = node.computedStyle
if (parent) {
this.inheritProps.forEach(prop => {
computedStyle[prop] = computedStyle[prop] || parent.computedStyle[prop]
})
}
Object.values(children).forEach(child => {
this.inheritStyle(child)
})
}
create(node) {
let classNames = (node.attributes.class || '').split(' ')
classNames = classNames.map(item => splitLineToCamelCase(item.trim()))
const style = {}
classNames.forEach(item => {
Object.assign(style, this.style[item] || {})
})
const args = {name: node.name, style}
const attrs = Object.keys(node.attributes)
const attributes = {}
for (const attr of attrs) {
const value = node.attributes[attr]
const CamelAttr = splitLineToCamelCase(attr)
if (value === '' || value === 'true') {
attributes[CamelAttr] = true
} else if (value === 'false') {
attributes[CamelAttr] = false
} else {
attributes[CamelAttr] = value
}
}
attributes.text = node.content
args.attributes = attributes
const element = new Element(args)
node.children.forEach(childNode => {
const childElement = this.create(childNode)
element.add(childElement)
})
return element
}
}
module.exports = {Widget}

View File

@ -1,164 +0,0 @@
/**
* Module dependencies.
*/
/**
* Expose `parse`.
*/
/**
* Parse the given string of `xml`.
*
* @param {String} xml
* @return {Object}
* @api public
*/
function parse(xml) {
xml = xml.trim()
// strip comments
xml = xml.replace(/<!--[\s\S]*?-->/g, '')
return document()
/**
* XML document.
*/
function document() {
return {
declaration: declaration(),
root: tag()
}
}
/**
* Declaration.
*/
function declaration() {
const m = match(/^<\?xml\s*/)
if (!m) return
// tag
const node = {
attributes: {}
}
// attributes
while (!(eos() || is('?>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
match(/\?>\s*/)
return node
}
/**
* Tag.
*/
function tag() {
const m = match(/^<([\w-:.]+)\s*/)
if (!m) return
// name
const node = {
name: m[1],
attributes: {},
children: []
}
// attributes
while (!(eos() || is('>') || is('?>') || is('/>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
// self closing tag
if (match(/^\s*\/>\s*/)) {
return node
}
match(/\??>\s*/)
// content
node.content = content()
// children
let child
while (child = tag()) {
node.children.push(child)
}
// closing
match(/^<\/[\w-:.]+>\s*/)
return node
}
/**
* Text content.
*/
function content() {
const m = match(/^([^<]*)/)
if (m) return m[1]
return ''
}
/**
* Attribute.
*/
function attribute() {
const m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/)
if (!m) return
return {name: m[1], value: strip(m[2])}
}
/**
* Strip quotes from `val`.
*/
function strip(val) {
return val.replace(/^['"]|['"]$/g, '')
}
/**
* Match `re` and advance the string.
*/
function match(re) {
const m = xml.match(re)
if (!m) return
xml = xml.slice(m[0].length)
return m
}
/**
* End-of-source.
*/
function eos() {
return xml.length == 0
}
/**
* Check for `prefix`.
*/
function is(prefix) {
return xml.indexOf(prefix) == 0
}
}
module.exports = parse

22
package-lock.json generated Normal file
View File

@ -0,0 +1,22 @@
{
"name": "小程序 - 租房",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/.store/eventemitter3@4.0.7/node_modules/eventemitter3": {
"version": "4.0.7",
"extraneous": true,
"license": "MIT",
"devDependencies": {
"assume": "^2.2.0",
"browserify": "^16.5.0",
"mocha": "^8.0.1",
"nyc": "^15.1.0",
"pre-commit": "^1.2.0",
"sauce-browsers": "^2.0.0",
"sauce-test": "^1.3.3",
"uglify-js": "^3.9.0"
}
}
}
}

View File

@ -1,6 +1 @@
{
"dependencies": {
"widget-ui": "^1.0.2",
"wxml-to-canvas": "^1.1.1"
}
}
{}

View File

@ -66,6 +66,8 @@ Page({
isShowVideo: true, // 是否显示 视频
shareImage: "", // 分享图片链接
},
timer: null,
headHeight: 0,
@ -174,13 +176,20 @@ Page({
// 来自页面内转发按钮
if (res.from === 'button') var types = res.from === 'button' ? 'share_btn' : 'show';
let that = this;
var title = that.data.data.title;
if (that.data.data.isquarantine) title = this.data.listTab.quarantineLists + '-' + that.data.data.title
// let that = this;
// var title = that.data.data.title;
// if (that.data.data.isquarantine) title = this.data.listTab.quarantineLists + '-' + that.data.data.title
const data = this.data.data
let title = "";
if (data.sharetitle) title = '香港租房 | ' + data.sharetitle
else title = '香港租房 | ' + data.title
return {
title,
imageUrl: that.data.data.thumbnail,
// imageUrl: that.data.data.thumbnail,
imageUrl: this.data.shareImage || '',
success: function (res) {
miucms.share(app, types)
},
@ -188,13 +197,16 @@ Page({
},
onShareTimeline() {
let that = this;
var title = that.data.data.title;
if (that.data.data.isquarantine) title = this.data.listTab.quarantineLists + '-' + that.data.data.title
const data = this.data.data
let title = "";
if (data.sharetitle) title = '香港租房 | ' + data.sharetitle
else title = '香港租房 | ' + data.title
// if (that.data.data.isquarantine) title = this.data.listTab.quarantineLists + '-' + that.data.data.title
return {
title,
imageUrl: that.data.data.thumbnail,
imageUrl: this.data.shareImage || '',
}
},
@ -294,16 +306,8 @@ Page({
isloding: false
})
this.widget = this.selectComponent('.widget')
console.log(this.widget);
setTimeout(() => {
this.setwidget()
}, 800)
if (data.withsameapartments > 0) this.getList()
// this.handleSwiperInit()
this.drawPoster()
}
}).catch(res => {
@ -321,302 +325,27 @@ Page({
})
},
setwidget() {
wx.getImageInfo({
src: "https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-T9pwkfHvqqsgFptxhXa6QWi2uePJ5Bg8cFLPIqoYV7MsLCmeW5lr_-kU8uRQ0NDI5",
success: res => {
console.log("res", res.path);
drawPoster() {
const user = app.globalData.user
const data = this.data.data
const roomList = this.data.roomList || []
let title = `${ roomList.length > 1 ? '多房型' : '' }`
let wxml = `<view class="placard">
<image class="placard-tag" src="https://app.gter.net/image/miniApp/HKRenting/high-quality-tag.png" mode="widthFix"></image>
<view class="placard-info">
<text class="text">肖荣豪 为你推荐</text>
</view>
const price = Math.min(...roomList.map(item => item.price));
title += ` HK$${ price }`
<view class="position">
<image class="position-icon" src="https://app.gter.net/image/miniApp/HKRenting/position-icon.svg" mode="widthFix"></image>
<view class="position-text">
香港 | 多房型 HK$5600起
</view>
<image class="position-arrow" src="https://app.gter.net/image/miniApp/HKRenting/arrow-round-yellow.svg"></image>
</view>
</view>`
let style = {
placard: {
width: "210",
height: "158",
position: "relative",
// paddingTop: "3",
backgroundColor: "azure"
},
placardBj: {
width: "210",
height: "155",
borderRadius: 10,
position: "absolute",
top: 3,
},
placardTag: {
position: "absolute",
top: 0,
right: 0,
width: 40,
height: 33,
},
placardInfo: {
height: 22,
// borderRadius: "0 50px 50px 0",
position: "absolute",
top: 15,
left: 0,
backgroundColor: "#f2f2f2",
width: 156,
color: '#fff',
// display: flex,
// alignItems: "center",
// fontSize: 10,
// padding-right: 10,
},
text: {
// width: 80,
height: 60,
color: "#000000",
fontSize: 14,
},
// position: {
// position: 'absolute',
// bottom: 0,
// left: 0,
// width: '210px',
// height: '26px',
// backgroundColor: 'rgba(0, 0, 0, 0.3)',
// color: '#fff',
// display: 'flex',
// alignItems: 'center',
// padding: '0 4px',
// fontSize: '11px',
// borderRadius: '0 0 10px 10px',
// },
// positionIcon: {
// width: '10px',
// height: '14px',
// marginRight: '4px',
// },
// positionText: {
// flex: 1,
// fontFamily: 'microsoft yahei',
// },
// positionArrow: {
// width: '12px',
// height: '12px',
// }
}
console.log(this.widget);
const p1 = this.widget.renderToCanvas({
wxml,
style
})
console.log("p1", p1);
p1.then((res) => {
console.log('container', res)
this.container = res
}).catch(err => {
console.log("err", err);
})
const p2 = this.widget.canvasToTempFilePath()
p2.then(res => {
console.log("res.tempFilePath", res.tempFilePath);
this.setData({
src: res.tempFilePath,
width: 100,
height: 100
})
})
}
})
},
async drawPoster() {
const bj = data.attachment?.[0] || data.videos?.[0]?.thumbnail || data.attachment?.[0]
return
const canvas = wx.createOffscreenCanvas({
type: '2d',
width: 210,
height: 168
});
const ctx = canvas.getContext('2d');
// 绘制圆角路径
const radius = 10;
const width = 210;
const height = 168;
ctx.beginPath();
ctx.moveTo(radius, 0);
ctx.lineTo(width - radius, 0);
ctx.quadraticCurveTo(width, 0, width, radius);
ctx.lineTo(width, height - radius);
ctx.quadraticCurveTo(width, height, width - radius, height);
ctx.lineTo(radius, height);
ctx.quadraticCurveTo(0, height, 0, height - radius);
ctx.lineTo(0, radius);
ctx.quadraticCurveTo(0, 0, radius, 0);
ctx.closePath();
ctx.clip();
// 绘制背景
let bgImg = canvas.createImage();
// 等待图片加载
const backImg = "https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-T9pwkfHvqqsgFptxhXa6QWi2uePJ5Bg8cFLPIqoYV7MsLCmeW5lr_-kU8uRQ0NDI5"
await new Promise(resolve => {
bgImg.onload = resolve;
bgImg.src = backImg; // 要加载的图片 url
})
console.log(11);
ctx.drawImage(bgImg, 0, 10, 210, 158);
// 绘制二维码
let mycode = canvas.createImage();
// 等待图片加载
const promoImg = "https://app.gter.net/image/miniApp/HKRenting/high-quality-tag.png"
await new Promise(resolve => {
mycode.onload = resolve;
mycode.src = promoImg; // 要加载的图片 url
})
console.log(12);
// 创建二维码圆形
ctx.save();
ctx.drawImage(mycode, 165, -0, 48, 40);
ctx.restore();
// ctx.arc(568, 524, 120, 0, 2 * Math.PI);
// ctx.clip();
// ctx.fillStyle = "#FFF";
// ctx.fill();
// ctx.drawImage(mycode, 458, 414, 220, 220);
// ctx.restore();
const imgData = canvas.toDataURL();
const time = new Date().getTime();
const filePath = wx.env.USER_DATA_PATH + "/poster" + time + "share" + ".png";
const fs = wx.getFileSystemManager();
console.log("imgData", imgData);
this.setData({
imgData
})
// wx.getImageInfo({
// src: imgData,
// success(res) {
// console.log(res.width)
// console.log(res.height)
// }
// })
// fs.writeFile({
// filePath,
// data: imgData.replace(/^data:image\/\w+;base64,/, ""),
// encoding: 'base64',
// success: res => {
// console.log("filePath", filePath);
// wx.showShareImageMenu({
// path: filePath,
// success: res => {
// console.log(res);
// }
// })
// },
// fail: err => {
// // 此处可能存在内存满了的情况
// // 需要根据具体需求处理
// console.log(err);
// }
// });
// fs.close();
},
generateShareImage() {
const screenWidth = this.data.screenWidth
console.log("screenWidth", screenWidth);
// / 创建离屏 2D canvas 实例
const canvas = wx.createOffscreenCanvas({
type: '2d',
width: 300,
height: 150
})
// 获取 context。注意这里必须要与创建时的 type 一致
const context = canvas.getContext('2d')
// 创建一个图片
const image = canvas.createImage()
// 等待图片加载
new Promise(resolve => {
image.onload = resolve
image.src = '/img/apartment-bottom.png' // 要加载的图片 url
})
// 把图片画到离屏 canvas 上
context.clearRect(0, 0, 300, 150)
context.drawImage(image, 0, 0, 300, 150)
// 获取画完后的数据
const imgData = context.getImageData(0, 0, 300, 150)
console.log("imgData", imgData);
// 获取视图层Canvas
const query = wx.createSelectorQuery();
query.select('#displayCanvas')
.fields({
node: true,
size: true
miucms.generatePoster({
bj,
title,
type: 1,
}).then(res => {
this.setData({
shareImage: res
})
.exec((res) => {
const displayCanvas = res[0].node;
const displayCtx = displayCanvas.getContext('2d');
// 绘制imgData到视图层Canvas
displayCtx.putImageData(imgData, 0, 0);
// 将视图层Canvas内容转换为临时文件路径
wx.canvasToTempFilePath({
canvas: displayCanvas,
success: (res) => {
const imagePath = res.tempFilePath;
// 更新页面数据,显示图片
this.setData({
imagePath: imagePath
});
},
fail: (err) => {
console.error('转换失败:', err);
}
});
});
// return
// wx.createSelectorQuery().select('#cvs1').node(res => {
// console.log('select canvas', res)
// const ctx1 = res.node.getContext('2d')
// res.node.width = screenWidth
// res.node.height = screenWidth
// setInterval(() => {
// ctx1.drawImage(video, 0, 0, w * dpr, h * dpr);
// }, 1000 / 24)
// }).exec()
console.log("shareImage", this.data.shareImage);
})
},
getList() {

View File

@ -6,7 +6,6 @@
"go-login": "../../template/goLogin/goLogin",
"report": "../../template/report/report",
"head-swiper": "../../template/headSwiper/headSwiper",
"nearby-school": "/template/nearbySchool/nearbySchool",
"wxml-to-canvas": "wxml-to-canvas"
"nearby-school": "/template/nearbySchool/nearbySchool"
}
}

View File

@ -1264,77 +1264,4 @@ map .clickmap {
.bottom-bar .bottom-bar-share::after {
border: none;
height: 0;
}
.placard {
width: 210px;
height: 158px;
position: relative;
padding-top: 3px;
.placard-bj {
width: 210px;
height: 155px;
border-radius: 10px;
}
.placard-tag {
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 33px;
}
.placard-info {
height: 22px;
border-radius: 0 50px 50px 0;
position: absolute;
top: 15px;
left: 0;
background-color: rgba(242, 242, 242, 1);
display: flex;
align-items: center;
font-size: 10px;
padding-right: 10px;
.avatar {
width: 20px;
height: 20px;
border-radius: 50%;
margin-left: 2px;
margin-right: 4px;
}
}
.position {
position: absolute;
bottom: 0;
left: 0;
width: 210px;
height: 26px;
background-color: rgba(0, 0, 0, 0.3);
color: #fff;
display: flex;
align-items: center;
padding: 0 4px;
font-size: 11px;
border-radius: 0 0 10px 10px;
.position-icon {
width: 10px;
height: 14px;
margin-right: 4px;
}
.position-text {
flex: 1;
font-family: 'microsoft yahei';
}
.position-arrow {
width: 12px;
height: 12px;
}
}
}

View File

@ -4,26 +4,6 @@
<header-nav bgcolor="{{ operationsTop ? '#fff' : 'transparent' }}">公寓详情</header-nav>
<view class="bj"></view>
<view class="content">
<wxml-to-canvas class="widget" width="{{ 210 }}" height="{{ 158 }}"></wxml-to-canvas>
<view class="placard">
<image class="placard-bj" src="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-T9pwkfHvqqsgFptxhXa6QWi2uePJ5Bg8cFLPIqoYV7MsLCmeW5lr_-kU8uRQ0NDI5" mode="aspectFill"></image>
<image class="placard-tag" src="https://app.gter.net/image/miniApp/HKRenting/high-quality-tag.png" mode="widthFix"></image>
<view class="placard-info">
<image class="avatar" src="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-d_Zwocn3qqsgFptxhcq_cQnrld6YjDgwVBq_D-81qNDQyOQ~~" mode="widthFix"></image>
肖荣豪 为你推荐
</view>
<view class="position">
<image class="position-icon" src="https://app.gter.net/image/miniApp/HKRenting/position-icon.svg" mode="widthFix"></image>
<view class="position-text">
香港 | 多房型 HK$5600起
</view>
<image class="position-arrow" src="https://app.gter.net/image/miniApp/HKRenting/arrow-round-yellow.svg"></image>
</view>
</view>
<image src="{{ src }}" mode="widthFix"></image>
<image class="arc" src="/img/arc-shadow.png"></image>
<view class="media-module">
@ -192,11 +172,11 @@
<!-- <report uniqid="{{ data.uniqid }}" bindtoReport="toReport" types="apartment"></report> -->
<cover-view class="around-school-alert" data-show="0" hidden="{{ !showSchool }}">
<cover-view class="around-school-alert" data-show="0" hidden="{{ showSchool }}">
<cover-view class="inner">
<cover-view class="title">
<cover-view class="text">房源距离学校</cover-view>
<cover-image src="/img/right-close.png" class="close" bindtap="showSchoolAlert" data-show="0"></cover-image>
<!-- <cover-image src="/img/.png" class="close" bindtap="showSchoolAlert" data-show="0"></cover-image> -->
</cover-view>
<cover-view class="list">
<cover-view class="item" wx:for="{{ data.pointData }}" wx:key="index">

View File

@ -2404,67 +2404,3 @@ map .clickmap {
border: none;
height: 0;
}
.placard {
width: 210px;
height: 158px;
position: relative;
padding-top: 3px;
}
.placard .placard-bj {
width: 210px;
height: 155px;
border-radius: 10px;
}
.placard .placard-tag {
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 33px;
}
.placard .placard-info {
height: 22px;
border-radius: 0 50px 50px 0;
position: absolute;
top: 15px;
left: 0;
background-color: #f2f2f2;
display: flex;
align-items: center;
font-size: 10px;
padding-right: 10px;
}
.placard .placard-info .avatar {
width: 20px;
height: 20px;
border-radius: 50%;
margin-left: 2px;
margin-right: 4px;
}
.placard .position {
position: absolute;
bottom: 0;
left: 0;
width: 210px;
height: 26px;
background-color: rgba(0, 0, 0, 0.3);
color: #fff;
display: flex;
align-items: center;
padding: 0 4px;
font-size: 11px;
border-radius: 0 0 10px 10px;
}
.placard .position .position-icon {
width: 10px;
height: 14px;
margin-right: 4px;
}
.placard .position .position-text {
flex: 1;
font-family: 'microsoft yahei';
}
.placard .position .position-arrow {
width: 12px;
height: 12px;
}

View File

@ -13,6 +13,8 @@ Page({
},
async drawPoster() {
return
// 修改画布创建方式
const canvas = wx.createOffscreenCanvas({
type: "2d",
@ -26,8 +28,7 @@ Page({
let bgImg = canvas.createImage();
await new Promise((resolve) => {
bgImg.onload = resolve;
bgImg.src = "https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-c-pstfHzqqsgFptxhT66QUmybYLYnAVBJQe2HpJNYt7VMACPX-Rzrt0ByvH4SjsUfxT00NDI5";
// bgImg.src = "https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-T9pwkfHvqqsgFptxhXa6QWi2uePJ5Bg8cFLPIqoYV7MsLCmeW5lr_-kU8uRQ0NDI5";
bgImg.src = "https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-T9pwkfHvqqsgFptxhXa6QWi2uePJ5Bg8cFLPIqoYV7MsLCmeW5lr_-kU8uRQ0NDI5";
});
// 计算aspectFill模式参数
const containerRatio = 280 / 219; // 容器宽高比 (280x219)
@ -67,38 +68,36 @@ Page({
// 修正缩放逻辑
if (imageRatio > containerRatio) {
// 图片更宽时,按容器高度缩放
drawHeight = 219;
drawWidth = drawHeight * imageRatio;
offsetX = (280 - drawWidth) / 2;
// 源图片裁剪:取中间宽度区域
srcWidth = bgImg.height * containerRatio;
srcX = (bgImg.width - srcWidth) / 2;
// 图片更宽时,按容器高度缩放
drawHeight = 219;
drawWidth = drawHeight * imageRatio;
offsetX = (280 - drawWidth) / 2;
// 源图片裁剪:取中间宽度区域
srcWidth = bgImg.height * containerRatio;
srcX = (bgImg.width - srcWidth) / 2;
} else {
// 图片更高时,按容器宽度缩放
drawWidth = 280;
drawHeight = drawWidth / imageRatio;
offsetY = (219 - drawHeight) / 2;
// 源图片裁剪:取中间高度区域
srcHeight = bgImg.width / containerRatio;
srcY = (bgImg.height - srcHeight) / 2;
// 图片更高时,按容器宽度缩放
drawWidth = 280;
drawHeight = drawWidth / imageRatio;
offsetY = (219 - drawHeight) / 2;
// 源图片裁剪:取中间高度区域
srcHeight = bgImg.width / containerRatio;
srcY = (bgImg.height - srcHeight) / 2;
}
console.log("srcX", srcX, "srcY", srcY);
// 修改后的drawImage调用
ctx.drawImage(
bgImg,
srcX,
srcY,
srcWidth, // 改为计算后的裁剪宽度
srcHeight, // 改为计算后的裁剪高度
srcY,
srcWidth, // 改为计算后的裁剪宽度
srcHeight, // 改为计算后的裁剪高度
offsetX,
4,
drawWidth,
drawHeight
4,
drawWidth,
drawHeight
);
ctx.restore();
@ -152,9 +151,6 @@ Page({
ctx.closePath();
ctx.fill();
// 绘制用户名位置需要同步调整
// ctx.fillText(displayText + " 为你推荐", 30, 43); // 保持原位置实际可根据infoWidth调整
// 绘制头像
let avatarImg = canvas.createImage();
await new Promise((resolve) => {
@ -172,7 +168,7 @@ Page({
ctx.fillStyle = "#000";
ctx.font = "14px PingFang SC";
// 修改最终文本绘制调用
console.log("displayText",displayText);
console.log("displayText", displayText);
ctx.fillText(displayText, 30, 43); // 使用处理后的文本
// 绘制底部信息栏
@ -288,4 +284,4 @@ Page({
imageUrl: this.data.filePath,
};
},
});
});

View File

@ -1,5 +1,4 @@
{
"usingComponents": {
"wxml-to-canvas": "wxml-to-canvas"
}
}

View File

@ -83,4 +83,72 @@
height: 16px;
}
}
}
.placard-no-pic {
width: 280px;
height: 224px;
position: relative;
.info {
display: flex;
align-items: center;
color: #555555;
font-size: 14px;
margin-bottom: 13px;
.avatar {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 5px;
}
}
.box {
position: relative;
.bj {
width: 280px;
height: 188px;
border-radius: 10px;
}
.text {
font-size: 16px;
color: #000000;
font-family: 'PingFangSC-Regular', 'PingFang SC', sans-serif;
line-height: 50px;
}
.position {
position: absolute;
top: 0;
left: 44px;
}
.type {
position: absolute;
top: 50px;
left: 44px;
}
.price {
position: absolute;
top: 100px;
left: 44px;
display: flex;
align-items: center;
.sum {
font-family: 'PingFangSC-Semibold', 'PingFang SC Semibold', 'PingFang SC', sans-serif;
font-weight: 650;
font-size: 20px;
color: #FA6B11;
margin-right: 4px;
}
}
}
}

View File

@ -1,21 +1,44 @@
<!--pages/dome/dome.wxml-->
<view class="container">
<view class="placard-no-pic">
<view class="info">
<image class="avatar"
src="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-d_Zwocn3qqsgFptxhcq_cQnrld6YjDgwVBq_D-81qNDQyOQ~~"
mode="widthFix"></image>
肖荣豪 为你推荐
</view>
<view class="box">
<image class="bj" src="https://app.gter.net/image/miniApp/HKRenting/share-default-bj.png" mode="widthFix">
</image>
<view class="position text">香港 > 新界 > 大围/沙田</view>
<view class="type text">合租 · 房间</view>
<view class="price text"><view class="sum">5300</view> HK$/月</view>
</view>
</view>
<image class="image" src="{{ src }}" mode="widthFix"></image>
23
<view class="placard">
<image class="placard-bj" src="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-T9pwkfHvqqsgFptxhXa6QWi2uePJ5Bg8cFLPIqoYV7MsLCmeW5lr_-kU8uRQ0NDI5" mode="aspectFill"></image>
<image class="placard-tag" src="https://app.gter.net/image/miniApp/HKRenting/high-quality-tag.png" mode="widthFix"></image>
<image class="placard-bj"
src="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-T9pwkfHvqqsgFptxhXa6QWi2uePJ5Bg8cFLPIqoYV7MsLCmeW5lr_-kU8uRQ0NDI5"
mode="aspectFill"></image>
<image class="placard-tag" src="https://app.gter.net/image/miniApp/HKRenting/high-quality-tag.png"
mode="widthFix"></image>
<view class="placard-info">
<image class="avatar" src="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-d_Zwocn3qqsgFptxhcq_cQnrld6YjDgwVBq_D-81qNDQyOQ~~" mode="widthFix"></image>
<image class="avatar"
src="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-d_Zwocn3qqsgFptxhcq_cQnrld6YjDgwVBq_D-81qNDQyOQ~~"
mode="widthFix"></image>
肖荣豪 为你推荐
</view>
<view class="position">
<image class="position-icon" src="https://app.gter.net/image/miniApp/HKRenting/position-icon.svg" mode="widthFix"></image>
<image class="position-icon" src="https://app.gter.net/image/miniApp/HKRenting/position-icon.svg"
mode="widthFix"></image>
<view class="position-text">
香港 | 多房型 HK$5600起
</view>
<image class="position-arrow" src="https://app.gter.net/image/miniApp/HKRenting/arrow-round-yellow.svg"></image>
<image class="position-arrow" src="https://app.gter.net/image/miniApp/HKRenting/arrow-round-yellow.svg">
</image>
</view>
</view>
</view>

View File

@ -71,3 +71,59 @@
width: 16px;
height: 16px;
}
.placard-no-pic {
width: 280px;
height: 224px;
position: relative;
}
.placard-no-pic .info {
display: flex;
align-items: center;
color: #555555;
font-size: 14px;
margin-bottom: 13px;
}
.placard-no-pic .info .avatar {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 5px;
}
.placard-no-pic .box {
position: relative;
}
.placard-no-pic .box .bj {
width: 280px;
height: 188px;
border-radius: 10px;
}
.placard-no-pic .box .text {
font-size: 16px;
color: #000000;
font-family: 'PingFangSC-Regular', 'PingFang SC', sans-serif;
line-height: 50px;
}
.placard-no-pic .box .position {
position: absolute;
top: 0;
left: 44px;
}
.placard-no-pic .box .type {
position: absolute;
top: 50px;
left: 44px;
}
.placard-no-pic .box .price {
position: absolute;
top: 100px;
left: 44px;
display: flex;
align-items: center;
}
.placard-no-pic .box .price .sum {
font-family: 'PingFangSC-Semibold', 'PingFang SC Semibold', 'PingFang SC', sans-serif;
font-weight: 650;
font-size: 20px;
color: #FA6B11;
margin-right: 4px;
}

View File

@ -231,7 +231,7 @@ Page({
// 来自页面内转发按钮
if (res.from === 'button') var types = res.from === 'button' ? 'share_btn' : 'show';
let that = this;
console.log("res", that.data.info.share_img);
// console.log("res", that.data.info.share_img);
const info = this.data.info
// let title = `中国香港 > ${ info.locationList[0].head } > ${ info.locationList[0].end }`
@ -243,7 +243,7 @@ Page({
else imageUrl = that.data.info.share_img
return {
title,
imageUrl,
imageUrl: this.data.shareImage,
success: function (res) {
miucms.share(app, types)
},
@ -259,7 +259,7 @@ Page({
return {
// title: this.data.info.subject,
title,
imageUrl,
imageUrl: this.data.shareImage,
}
},
get_content: function () {
@ -362,6 +362,8 @@ Page({
this.handleDetailData()
if (data.info.intermediary != 6) this.drawPoster()
// 判断是否需要获取附近房源
if (data.info.latitude && data.info.verified == 0 && data.info.intermediary != 6 && data.isintermediary != 1) this.getNearbListings()
else this.nearbListingsState = true // 阻止上拉底部加载的
@ -380,6 +382,51 @@ Page({
},
drawPoster() {
console.log("444");
let obj = {}
const info = this.data.info
console.log("intermediary", info.intermediary);
const image = info.picturegroup || []
const isintermediary = this.data.isintermediary || 0 // 是否是认证中介
console.log("isintermediary", isintermediary);
let title = `${ info.gptype }·${ info.type }·HK$${ info.rent }`
obj['bj'] = image?.[0]?.thumbnail || ''
obj['title'] = title
// console.log("image", image);
if (isintermediary == 1) { // 认证中介
obj['type'] = 2
} else if (info.intermediary == 1) { // 普通中介
obj['type'] = 3
} else if (info.intermediary == 3 && info.verified == 0) { // 普通个人房源
obj['type'] = 4
} else if (info.intermediary == 3 && info.verified == 1) { // 认证个人房源
obj['type'] = 5
} else if (info.intermediary == 4) { // 招室友
obj['type'] = 6
} else if (info.intermediary == 5) { // 其他
obj['type'] = 7
}
console.log("obj", obj);
let res = null
if (obj['bj']) res = miucms.generatePoster(obj)
else {
console.log(5555);
obj['position'] = `香港 > ${ info.locationList[0].head } > ${ info.locationList[0].end }`
obj['typeText'] = `${ info.gptype } · ${ info.type }`
obj['price'] = info.rent
res = miucms.generatePosterNoImage(obj)
}
console.log("res", res);
res.then(res => {
this.setData({
shareImage: res
})
})
},
// 获取语言包 保存全局
getDtailsLangs() {
miucms.request(`${app.globalData.baseURL}/tenement/v2/api/details/langs`).then(res => {

View File

@ -75,6 +75,8 @@
</view>
</view>
<!-- <image wx:if="{{ shareImage }}" class="shareImage" src="{{ shareImage }}"mode="widthFix"></image> -->
<!-- 房源详细信息 -->
<view class="HResource-detail">
<view class="HResource-header">
@ -241,7 +243,7 @@
<view class="other-information else-box" wx:if="{{ attestationElseResource.length != 0 }}">
<view class="other-information-name flexacenter">发布者的其他{{ info['intermediary'] == 1 ? '' : '认证' }}房源<view class="else-quantity">({{ attestationElseResource.length }})</view>
</view>
<block wx:for="{{ attestationElseResource }}">
<block wx:for="{{ attestationElseResource }}" wx:key="index">
<common-list item="{{ item }}"></common-list>
</block>
</view>

View File

@ -0,0 +1,4 @@
.shareImage {
width: 280px;
height: 224px;
}

View File

@ -0,0 +1,4 @@
.shareImage {
width: 280px;
height: 224px;
}

File diff suppressed because it is too large Load Diff