| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- <template>
- <view class="popup-container" v-if="value" @click="close">
- <view class="popup-wrapper">
- <view class="popup-title" v-if="showTitle">您已经成为亿职赞第{{ rank }}位金牌推荐官</view>
- <view class="poster-wrapper">
- <image :src="posterImageSrc" class="poster-img" mode="aspectFit"></image>
- </view>
- <view class="poster-tip" v-if="showTip">推荐者的个人信息不会公开展示,请放心推荐</view>
- <view class="button" @click.stop="handleSave">{{ saveBtnText }}</view>
- </view>
- </view>
- </template>
- <script>
- export default {
- props: {
- value: {
- type: Boolean,
- default: false
- },
- rank: {
- type: Number,
- default: 0
- },
- posterBase64Url: {
- type: String,
- default: ''
- },
- /** 纯 base64 时的图片类型,默认 png;若后端是 jpeg 可传 image/jpeg */
- posterMime: {
- type: String,
- default: 'image/png'
- },
- showTitle: {
- type: Boolean,
- default: true
- },
- showTip: {
- type: Boolean,
- default: true
- },
- saveBtnText: {
- type: String,
- default: '保存海报并成为推荐官'
- },
- },
- computed: {
- posterImageSrc() {
- const raw = this.posterBase64Url
- if (!raw || typeof raw !== 'string') return ''
- const t = raw.trim()
- if (
- t.startsWith('data:image') ||
- t.startsWith('http://') ||
- t.startsWith('https://') ||
- t.startsWith('/') ||
- t.startsWith('@/')
- ) {
- return t
- }
- // 去掉换行空格,拼成 data URL 供 image 显示
- const b64 = t.replace(/\s/g, '')
- return `data:${this.posterMime};base64,${b64}`
- }
- },
- data() {
- return {
- num: 990
- }
- },
- methods: {
- close() {
- this.$emit('close')
- },
-
- handleSave() {
- const src = this.posterImageSrc
- if (!src) {
- uni.showToast({ title: '暂无海报', icon: 'none' })
- return
- }
- // #ifdef H5
- this._savePosterH5(src)
- // #endif
- // #ifdef APP-PLUS
- this._savePosterApp(src)
- // #endif
- },
- // #ifdef H5
- _savePosterH5(src) {
- if (src.startsWith('data:image')) {
- try {
- const parts = src.split(',')
- const mimeMatch = parts[0].match(/:(.*?);/)
- const mime = mimeMatch ? mimeMatch[1] : 'image/png'
- const bstr = atob(parts[1])
- const n = bstr.length
- const u8arr = new Uint8Array(n)
- for (let i = 0; i < n; i++) u8arr[i] = bstr.charCodeAt(i)
- const blob = new Blob([u8arr], { type: mime })
- const url = URL.createObjectURL(blob)
- const a = document.createElement('a')
- a.href = url
- a.download = 'poster.' + (mime.indexOf('jpeg') !== -1 ? 'jpg' : 'png')
- document.body.appendChild(a)
- a.click()
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- uni.showToast({ title: '已开始下载' })
- this.$emit('saved')
- } catch (e) {
- uni.showToast({ title: '保存失败', icon: 'none' })
- }
- return
- }
- if (src.startsWith('http://') || src.startsWith('https://')) {
- uni.showLoading({ title: '保存中...', mask: true })
- fetch(src, { mode: 'cors' })
- .then((res) => {
- if (!res.ok) throw new Error('fetch fail')
- return res.blob()
- })
- .then((blob) => {
- uni.hideLoading()
- const url = URL.createObjectURL(blob)
- const a = document.createElement('a')
- a.href = url
- a.download = 'poster.png'
- document.body.appendChild(a)
- a.click()
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- uni.showToast({ title: '已开始下载' })
- this.$emit('saved')
- })
- .catch(() => {
- uni.hideLoading()
- uni.showToast({
- title: '无法下载,请长按图片保存',
- icon: 'none'
- })
- })
- return
- }
- uni.showToast({ title: '请使用网络图或 base64 图片', icon: 'none' })
- },
- // #endif
- // #ifdef APP-PLUS
- /**
- * App 保存难在:① 相册权限各版本 Android 规则不同,误检会拦死;② saveImageToPhotosAlbum 多数机型要传 _doc/相对路径而非乱转 file://。
- * 这里按 DCloud 文档最小路径:Bitmap → _doc/xxx.png → uni.saveImageToPhotosAlbum(先试相对路径)。
- */
- async _savePosterApp(src) {
- const allowed = await this._ensureAlbumPermissionForSave()
- if (!allowed) return
- uni.showLoading({ title: '保存中...', mask: true })
- const hide = () => {
- try {
- uni.hideLoading()
- } catch (e) {}
- }
- try {
- await this._withTimeout(this._savePosterAppCore(src), 50000, '保存超时,请重试')
- hide()
- uni.showToast({ title: '已保存到相册', icon: 'success' })
- this.$emit('saved')
- } catch (e) {
- hide()
- const msg = String((e && (e.errMsg || e.message)) || '保存失败')
- if (/auth|permission|denied|拒绝|authorize/i.test(msg)) {
- this._openAlbumSettingsModal()
- } else {
- uni.showToast({ title: msg.length > 36 ? '保存失败' : msg, icon: 'none' })
- }
- }
- },
- _withTimeout(p, ms, errMsg) {
- return Promise.race([
- Promise.resolve(p),
- new Promise((_, reject) =>
- setTimeout(() => reject(new Error(errMsg || '操作超时')), ms)
- )
- ])
- },
- async _savePosterAppCore(src) {
- let local = ''
- if (src.startsWith('data:image')) {
- local = await this._bitmapDataUrlToDocRel(src)
- } else if (src.startsWith('http://') || src.startsWith('https://')) {
- local = await new Promise((resolve, reject) => {
- uni.downloadFile({
- url: src,
- success: (r) => {
- if (r.statusCode === 200 && r.tempFilePath) resolve(r.tempFilePath)
- else reject(new Error('图片下载失败'))
- },
- fail: () => reject(new Error('图片下载失败'))
- })
- })
- } else {
- local = src
- }
- await this._saveImageToAlbumTryPaths(this._albumPathCandidates(local))
- },
- /** 官方示例:loadBase64Data → save 到 _doc,返回相对路径 */
- _bitmapDataUrlToDocRel(dataUrl) {
- return new Promise((resolve, reject) => {
- const name = 'poster_' + Date.now() + '.png'
- const rel = '_doc/' + name
- const id = 'posterBitmap' + Date.now()
- const bm = new plus.nativeObj.Bitmap(id)
- bm.loadBase64Data(
- dataUrl,
- () => {
- bm.save(
- rel,
- { overwrite: true },
- () => {
- try {
- bm.clear()
- } catch (e) {}
- resolve(rel)
- },
- (err) => {
- try {
- bm.clear()
- } catch (e) {}
- reject(err || new Error('生成临时图失败'))
- }
- )
- },
- (err) => {
- try {
- bm.clear()
- } catch (e) {}
- reject(err || new Error('图片无法解析'))
- }
- )
- })
- },
- _albumPathCandidates(p) {
- const list = []
- const add = (x) => {
- if (x && list.indexOf(x) === -1) list.push(x)
- }
- add(p)
- if (p && p.startsWith('file://')) {
- add(p.replace(/^file:\/\//, ''))
- }
- if (typeof plus !== 'undefined' && plus.io && p) {
- try {
- add(plus.io.convertLocalFileSystemURL(p))
- } catch (e) {}
- }
- return list
- },
- _saveImageToAlbumTryPaths(paths) {
- if (!paths.length) {
- return Promise.reject(new Error('没有可保存的路径'))
- }
- return paths.reduce(
- (acc, fp) =>
- acc.catch(() =>
- new Promise((resolve, reject) => {
- uni.saveImageToPhotosAlbum({
- filePath: fp,
- success: () => resolve(),
- fail: (err) => reject(err || new Error('保存失败'))
- })
- })
- ),
- Promise.reject(new Error('_'))
- )
- },
- _openAlbumSettingsModal() {
- uni.showModal({
- title: '需要相册权限',
- content: '保存图片到相册需要相册/存储权限,请在系统设置中开启后重试。',
- confirmText: '去设置',
- cancelText: '取消',
- success: (r) => {
- if (r.confirm && typeof uni.openAppAuthorizeSetting === 'function') {
- uni.openAppAuthorizeSetting({})
- }
- }
- })
- },
- /** 是否具备保存到相册所需权限;否:已弹窗引导,返回 false */
- _ensureAlbumPermissionForSave() {
- return new Promise((resolve) => {
- if (typeof plus === 'undefined') {
- resolve(true)
- return
- }
- const os = plus.os.name
- if (os === 'iOS') {
- resolve(this._ensureIosAlbumPermissionForSave())
- return
- }
- if (os === 'Android') {
- this._ensureAndroidAlbumPermissionForSave().then(resolve)
- return
- }
- resolve(true)
- })
- },
- _ensureIosAlbumPermissionForSave() {
- try {
- if (typeof uni.getAppAuthorizeSetting !== 'function') {
- return true
- }
- const s = uni.getAppAuthorizeSetting() || {}
- const a = s.albumAuthorized
- if (a === 'authorized' || a === 'limited') {
- return true
- }
- if (a === 'denied') {
- this._openAlbumSettingsModal()
- return false
- }
- // not determined / 未返回:首次保存时由系统弹窗授权
- return true
- } catch (e) {
- return true
- }
- },
- _androidCheckPermissions(permissions) {
- try {
- const main = plus.android.runtimeMainActivity()
- const PM = plus.android.importClass('android.content.pm.PackageManager')
- const granted = PM.PERMISSION_GRANTED
- for (let i = 0; i < permissions.length; i++) {
- if (main.checkSelfPermission(permissions[i]) !== granted) {
- return false
- }
- }
- return true
- } catch (e) {
- return false
- }
- },
- /**
- * Android 10+ 分区存储下,写入相册多走系统 MediaStore,预检 READ_MEDIA_IMAGES 常误判且与「保存」无关,会导致永远进不了保存。
- * 仅对 Android 9 及以下尝试申请 WRITE_EXTERNAL_STORAGE;其余交给 saveImageToPhotosAlbum 与系统弹窗。
- */
- _ensureAndroidAlbumPermissionForSave() {
- return new Promise((resolve) => {
- if (!plus.android) {
- resolve(true)
- return
- }
- try {
- const Build = plus.android.importClass('android.os.Build')
- if (Build.VERSION.SDK_INT >= 29) {
- resolve(true)
- return
- }
- if (Build.VERSION.SDK_INT < 23) {
- resolve(true)
- return
- }
- } catch (e) {
- resolve(true)
- return
- }
- if (typeof plus.android.requestPermissions !== 'function') {
- resolve(true)
- return
- }
- const perms = ['android.permission.WRITE_EXTERNAL_STORAGE']
- if (this._androidCheckPermissions(perms)) {
- resolve(true)
- return
- }
- plus.android.requestPermissions(
- perms,
- () => {
- if (this._androidCheckPermissions(perms)) {
- resolve(true)
- } else {
- this._openAlbumSettingsModal()
- resolve(false)
- }
- },
- () => {
- this._openAlbumSettingsModal()
- resolve(false)
- }
- )
- })
- },
- // #endif
- }
- }
- </script>
- <style lang="scss" scoped>
- .popup-container {
- position: fixed;
- left: 0;
- top: 0;
- width: 100vw;
- height: 100vh;
- background: rgba(0, 0, 0, 0.6);
- display: flex;
- align-items: center;
- justify-content: center;
- .popup-wrapper {
- .popup-title {
- color: rgba(255, 255, 255, 1);
- font-style: Bold;
- font-size: 40rpx;
- font-weight: 700;
- line-height: 1.6;
- }
- .poster-wrapper {
- width: 488rpx;
- height: 1028rpx;
- border-radius: 20rpx;
- margin: 64rpx auto 32rpx;
- .poster-img {
- width: 100%;
- height: 100%;
- }
- }
- .poster-tip {
- color: rgba(255, 255, 255, 1);
- font-size: 24rpx;
- font-weight: 400;
- line-height: 40rpx;
- text-align: center;
- }
- .button {
- border-radius: 126px;
- background: rgba(255, 255, 255, 1);
- margin: 24rpx 30rpx;
- text-align: center;
- line-height: 90rpx;
- color: rgba(0, 0, 0, 1);
- font-size: 32rpx;
- font-weight: 400;
- }
- }
- }
- </style>
|