feat: initialize merchant payment H5
This commit is contained in:
commit
8327e335df
44 files changed
+17171
No files matched your search
@@ -0,0 +1,11 @@
|
||||
# 后端请求前缀,开发环境通过 Vite 同源代理。
|
||||
VITE_API_URL=/api
|
||||
|
||||
# 是否为微服务;false 对接 Boot 单体服务,移除接口的 /third 前缀。
|
||||
VITE_IS_MICRO=true
|
||||
|
||||
# 开发环境后端地址。
|
||||
VITE_APP_DEV_SERVICE=https://dzc-test-1.fy.takin.cc
|
||||
|
||||
# 生产环境后端地址;配置为空时由线上部署提供同源接口。
|
||||
VITE_APP_PROD_SERVICE=https://www.zianxin.com/api
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
unpackage/
|
||||
.DS_Store
|
||||
*.log
|
||||
*.local
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"trailingComma": "all",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"printWidth": 140
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# 商户付款 H5
|
||||
|
||||
uni-app Vue 3 + TypeScript + UnoCSS + tm-ui 的独立 H5 项目。工程与蓝色主题参考 `../biz_zax_app_cloud`,只注册一个付款页面,默认中文、人民币、浅色主题。
|
||||
|
||||
## 本地开发
|
||||
|
||||
要求 Node.js 20.19+、pnpm 10.13.0。
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev:h5
|
||||
```
|
||||
|
||||
浏览器访问 <http://localhost:8890/>。同一局域网内可使用本机 IP 和 8890 端口进行手机预览。
|
||||
|
||||
```bash
|
||||
pnpm type-check
|
||||
pnpm build:h5
|
||||
```
|
||||
|
||||
`type-check` 检查业务代码与工程配置。tm-ui 采用按需引入的原始源码,保留第三方类型与样式;不对上游组件做单页定制。按团队规则,生产构建和完整测试在明确的集中验证指令后执行。
|
||||
|
||||
## 目录与调整入口
|
||||
|
||||
```text
|
||||
src/
|
||||
├── pages/pay/index.vue # 唯一页面:商户、金额与付款交互
|
||||
├── sheep/
|
||||
│ ├── components/payment-keyboard.vue # 四列常驻键盘
|
||||
│ ├── config/merchant.ts # 仅供设计参考的示例数据,真实页面不引用
|
||||
│ ├── config/theme.ts # UnoCSS、CSS 与 tm-ui 共享主题
|
||||
│ ├── hooks/use-payment-amount.ts # 金额字符串输入规则
|
||||
│ ├── scss/global.scss # 全局字体与 H5 视口适配
|
||||
│ └── stores/index.ts # tm-ui 主题使用的 Pinia
|
||||
└── uni_modules/tm-ui/ # 参考项目同版 3.2.0 组件及必要依赖
|
||||
```
|
||||
|
||||
- 商户名称与标识通过匿名商户接口读取;`src/sheep/config/merchant.ts` 中的示例仅保留作设计参考,真实页面不引用。标识缺省或加载失败时展示统一店铺图形。
|
||||
- 修改主题:编辑 `src/sheep/config/theme.ts`。页面基础色在 `pages.json`、`uni.scss` 和 HTML 的 theme-color 中保留一致的框架配置。
|
||||
- 页面采用 Hash 路由,唯一页面为 `/pages/pay/index`。部署子路径统一在 `src/manifest.json` 的 `h5.router.base` 中修改,默认 `/`,避免在 Vite 中重复设置路径前缀。
|
||||
- 接入 third 服务的匿名商户查询、线下收款、OAuth 回跳和真实支付。无签名入口或短期收银票据时展示错误,不回退到示例商户。
|
||||
|
||||
## 金额与 H5 交互
|
||||
|
||||
- 使用字符串保存元单位金额,保留 `0.` 和 `1.20` 等输入过程,不进行浮点金额运算;支持两位小数、前导零处理和逐位删除。
|
||||
- 付款键对空金额、零金额和超过 9,999.99 元的金额禁用;提交前保存稳定请求号,刷新或网络失败后复用原请求。
|
||||
- 金额框为可聚焦的只读展示,避免系统软键盘和常驻键盘重叠。物理键盘支持数字、小数点、退格和 Enter;Tab、组合快捷键及数字按钮自身的键盘激活不被拦截。
|
||||
- 内容最大宽度 480px,rpx 同步限制放大;使用 `100dvh` 并提供 `100vh` 回退,键盘位于独立的底部区域。页面直接展示商户和金额,无顶部标题。
|
||||
- 整页锁定在可视高度,常见竖屏不产生纵向滚动;短屏压缩卡片间距,极短横屏保留内容区内部滑动。所有内部滚动条均隐藏,长金额仍可横向查看。
|
||||
- 支持安全区、长商户名称、长金额横向滚动、可见焦点与减少动态效果偏好;不禁止浏览器缩放。
|
||||
|
||||
## 第三方来源
|
||||
|
||||
tm-ui `3.2.0` 从参考项目的 `src/uni_modules/tm-ui` 原样引入 `tm-sheet`、`tm-icon` 及其主题、工具、类型、CSS 和图标资源,保留上游说明与版本信息。数字键盘使用 uni-app 自带按钮,维持明确的禁用状态和 H5 键盘语义。
|
||||
|
||||
依赖基于参考项目的 pnpm 锁文件解析版本;只保留本项目需要的 H5 依赖,不包含 App 原生配置、服务地址或应用凭据。
|
||||
|
||||
## 接口与部署
|
||||
|
||||
项目只使用根目录一个 `.env`,变量命名与参考 App 一致:`VITE_API_URL` 为开发请求前缀,`VITE_APP_DEV_SERVICE` 为开发后端地址,`VITE_APP_PROD_SERVICE` 为生产后端地址,`VITE_IS_MICRO` 控制是否保留微服务前缀。修改后重启 `pnpm dev:h5`。这里不配置登录 Token、签名密钥或支付密钥。
|
||||
|
||||
开发模式下,浏览器请求 `VITE_API_URL`(默认 `/api`),由 `vite.config.ts` 转发到 `VITE_APP_DEV_SERVICE`,避免跨域。例如开发地址为 `https://dzc-test-1.fy.takin.cc` 且 `VITE_IS_MICRO=true` 时,`http://localhost:8890/api/third/aggregate-merchant-pay/merchant` 转发到 `https://dzc-test-1.fy.takin.cc/third/aggregate-merchant-pay/merchant`。目标地址自带 `/api` 等基础路径时会保留,不要在请求层再次添加。
|
||||
|
||||
`VITE_IS_MICRO=true` 保留 `/third`;`false` 对接单体服务时移除 `/third`,代理规则共用。生产构建直接读取同一 `.env` 的 `VITE_APP_PROD_SERVICE`;若线上反向代理已提供同源接口,可将其设为空。Vite 开发代理不随生产构建发布。
|
||||
|
||||
- 商户码入口:`/#/pages/pay/index?id=...&tenantId=...&v=1&sign=...`,商户和租户 ID 按字符串处理。
|
||||
- OAuth 回跳:`/#/pages/pay/index?tenantCode=...&ticket=...`,页面通过接口读取真实场景、固定金额和渠道参数。
|
||||
- `GET /third/aggregate-merchant-pay/merchant` 读取公开商户资料及 `supportedPlatforms`(10 微信、20 支付宝);商户可以只开通其中一个渠道。
|
||||
- `POST /third/aggregate-merchant-pay/create` 提交整数分金额及稳定 `requestId`;不在前端指定业务类型或渠道商户。
|
||||
- `GET /third/aggregate-order-pay/context/{tenantCode}/{ticket}` 读取收银上下文;现有 `/status/{tenantCode}/{ticket}` 确认结果。
|
||||
- 后端把本 H5 来源加入精确 CORS 允许列表;跨域接口必须使用 HTTPS。OAuth 回调仍由后端接收,平台密钥和 OAuth 身份不进入前端。
|
||||
|
||||
线下收款在提交前保留数字键盘;订单聚合及已提交的收款金额锁定。OAuth 回跳只自动调起一次,取消后可手动继续,刷新不会自动重复弹出。JSBridge 返回成功后仍查询服务端状态;未确认原单成功或关闭之前不释放金额创建新请求。
|
||||
|
||||
扫码客户端未在商户已开通渠道中时,在原单页展示“商户暂不支持微信/支付宝支付”及换用已开通 App 重新扫码的指引,隐藏金额与键盘。普通浏览器提示使用已支持 App 扫码;全部渠道停用时提示联系商户。可重新检查开通情况,不在当前客户端强制唤起另一个 App,也不会据此清除待确认的原付款请求。
|
||||
|
||||
完整后端配置、迁移、平台域名设置及发布顺序见 `../biz-api-cloud-zax/pjcloud-thirdparty/docs/aggregate-h5.md`。更换二维码使用的域名或签名密钥后需重新生成商户 URL 收款码,原 App Base64 收款码保持兼容。
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#ecf4f9" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<title>商户收款</title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "biz-pay-h5",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "基于 uni-app、UnoCSS 与 tm-ui 的商户付款 H5",
|
||||
"packageManager": "pnpm@10.13.0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:h5": "uni",
|
||||
"build:h5": "uni build",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-5000720260410001",
|
||||
"@dcloudio/uni-components": "3.0.0-5000720260410001",
|
||||
"@dcloudio/uni-h5": "3.0.0-5000720260410001",
|
||||
"pinia": "3.0.4",
|
||||
"vue": "3.5.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "3.4.28",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-5000720260410001",
|
||||
"@types/node": "20.19.30",
|
||||
"sass": "1.71.1",
|
||||
"typescript": "4.9.5",
|
||||
"unocss": "0.58.9",
|
||||
"unocss-preset-weapp": "0.58.9",
|
||||
"vite": "5.2.8",
|
||||
"vue-tsc": "1.8.27"
|
||||
}
|
||||
}
|
||||
Generated
+5563
File diff suppressed because it is too large.
Load diff
+10
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/** 单页付款工程的应用根节点;页面与生命周期由 uni-app 管理 */
|
||||
defineOptions({ name: "MerchantPaymentApp" })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import url("@/uni_modules/tm-ui/css/remixicon.min.css");
|
||||
@import url("@/uni_modules/tm-ui/css/tmui.css");
|
||||
@import "@/sheep/scss/global.scss";
|
||||
</style>
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="@dcloudio/types" />
|
||||
|
||||
// tm-ui 的全局 TM 类型来自随库保留的原始声明,不在业务侧重复定义
|
||||
import "./uni_modules/tm-ui/interface"
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue"
|
||||
/** uni-app 页面及 Easycom 组件的模块声明,具体模板类型由 vue-tsc 推导 */
|
||||
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, any>
|
||||
export default component
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { createSSRApp } from "vue"
|
||||
import "uno.css"
|
||||
import App from "./App.vue"
|
||||
import tmUi from "./uni_modules/tm-ui"
|
||||
import { colors } from "./uni_modules/tm-ui/libs/colors"
|
||||
import { useTmConfig } from "./uni_modules/tm-ui/libs/config"
|
||||
import pinia from "@@/stores"
|
||||
import { themeColors } from "@@/config/theme"
|
||||
|
||||
/** 创建 uni-app 应用,并在页面挂载前将 tm-ui 和 UnoCSS 对齐到参考项目的主题蓝 */
|
||||
export function createApp() {
|
||||
/** 当前应用实例;每次框架创建应用时独立注册插件 */
|
||||
const app = createSSRApp(App)
|
||||
app.use(pinia)
|
||||
app.use(tmUi)
|
||||
colors.set("primary", themeColors.primary)
|
||||
/** tm-ui 的主题状态依赖 Pinia,必须在注册 Pinia 后访问 */
|
||||
const themeStore = useTmConfig(pinia)
|
||||
themeStore.config.mode = "light"
|
||||
themeStore.config.color = themeColors.primary
|
||||
return { app, pinia }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "商户付款",
|
||||
"appid": "",
|
||||
"description": "商户信息展示与付款金额输入 H5",
|
||||
"versionName": "0.1.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"vueVersion": "3",
|
||||
"h5": {
|
||||
"title": "商户收款",
|
||||
"router": {
|
||||
"mode": "hash",
|
||||
"base": "/"
|
||||
}
|
||||
},
|
||||
"uniStatistics": { "enable": false }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^tm-(.*)": "@/uni_modules/tm-ui/components/tm-$1/tm-$1.vue"
|
||||
}
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/pay/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "商户收款",
|
||||
"disableScroll": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationStyle": "custom",
|
||||
"backgroundColor": "#ecf4f9",
|
||||
"rpxCalcMaxDeviceWidth": 480,
|
||||
"rpxCalcBaseDeviceWidth": 480
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<view class="payment-shell flex flex-col w-full max-w-480px mx-auto overflow-hidden" @keydown="handleKeyboard">
|
||||
<view class="payment-content flex-1 min-h-0 overflow-y-auto">
|
||||
<view class="px-32rpx pt-24rpx pb-24rpx">
|
||||
<tm-sheet :margin="[0]" :padding="[32]" :round="32" color="white" class="amount-card">
|
||||
<view class="flex items-center gap-24rpx min-w-0">
|
||||
<view class="merchant-logo flex-row-center size-96rpx shrink-0 rounded-28rpx bg-soft overflow-hidden" aria-hidden="true">
|
||||
<image v-if="merchant?.logoPic && !merchantLogoFailed" :src="merchant.logoPic" class="size-full" mode="aspectFill" @error="handleLogoError" />
|
||||
<tm-icon v-else name="store-2-line" :size="52" :color="themeColors.primary" />
|
||||
</view>
|
||||
<view class="flex-1 min-w-0">
|
||||
<text class="block text-24rpx text-muted mb-8rpx">收款商户</text>
|
||||
<text class="merchant-name block text-36rpx font-600 text-text">{{ merchant?.name || (loading ? '正在加载商户' : '收款信息不可用') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<template v-if="merchant && !unavailable">
|
||||
<view class="card-divider h-1px bg-border my-40rpx" aria-hidden="true" />
|
||||
<text class="block text-28rpx font-500 text-secondary">付款金额</text>
|
||||
<view id="payment-amount" class="amount-field flex items-baseline gap-12rpx mt-24rpx pb-24rpx"
|
||||
role="textbox" :tabindex="editable ? 0 : -1" aria-readonly="true"
|
||||
:aria-label="`付款金额,${amount || '0'} 元${editable ? ',使用下方数字键盘输入' : ',金额已锁定'}`">
|
||||
<text class="text-52rpx font-500 text-text shrink-0" aria-hidden="true">¥</text>
|
||||
<view id="payment-amount-scroll" class="amount-scroll flex-1 min-w-0 overflow-x-auto" aria-hidden="true">
|
||||
<view class="amount-digits inline-flex items-baseline" :class="amountSizeClass">
|
||||
<text class="font-600" :class="amount ? 'text-text' : 'text-placeholder'">{{ amount || '0.00' }}</text>
|
||||
<view v-if="editable" class="payment-cursor bg-primary ml-8rpx shrink-0" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="amount-hint block text-24rpx text-muted mt-24rpx">{{ editable ? '请与商户确认付款金额,单笔最高 9,999.99 元' : success ? '付款结果已由服务端确认' : '金额已确认,请勿重复付款' }}</text>
|
||||
</template>
|
||||
<template v-if="unavailable">
|
||||
<view class="card-divider h-1px bg-border my-32rpx" aria-hidden="true" />
|
||||
<payment-unavailable :notice="unavailable" />
|
||||
</template>
|
||||
<view v-if="error" class="mt-32rpx text-28rpx text-secondary leading-relaxed" role="alert">{{ error }}</view>
|
||||
<view v-else-if="feedback || loading" class="mt-32rpx text-28rpx leading-relaxed" :class="success ? 'text-primary' : 'text-secondary'" role="status" aria-live="polite">{{ feedback || '正在读取收款信息…' }}</view>
|
||||
</tm-sheet>
|
||||
</view>
|
||||
</view>
|
||||
<payment-keyboard v-if="showKeyboard && !error && !loading" class="shrink-0" :disabled="!canPay" @input="append" @delete="remove" @pay="pay" />
|
||||
<view v-else class="payment-actions shrink-0 bg-surface px-32rpx pt-24rpx">
|
||||
<button v-if="unavailable" class="payment-action w-full rounded-24rpx" :class="loading || busy ? 'bg-disabled text-secondary' : 'bg-primary text-white'" :disabled="loading || busy" :aria-disabled="loading || busy" role="button" :tabindex="loading || busy ? -1 : 0" @click="retry" @keydown.enter.prevent="retry" @keydown.space.prevent="retry">{{ loading ? '正在检查' : '重新检查支付方式' }}</button>
|
||||
<button v-else-if="error" class="payment-action w-full bg-primary text-white rounded-24rpx" role="button" tabindex="0" @click="retry" @keydown.enter.prevent="retry" @keydown.space.prevent="retry">重新查询</button>
|
||||
<template v-else>
|
||||
<button class="payment-action w-full rounded-24rpx" :class="canPay ? 'bg-primary text-white' : 'bg-disabled text-secondary'" :disabled="!canPay" :aria-disabled="!canPay" role="button" :tabindex="canPay ? 0 : -1" @click="pay" @keydown.enter.prevent="pay" @keydown.space.prevent="pay">
|
||||
{{ success ? '付款成功' : loading ? '加载中' : busy ? '正在发起付款' : confirming ? '付款结果确认中' : terminal ? '本次收款已结束' : '继续付款' }}
|
||||
</button>
|
||||
<button v-if="canRestart" class="payment-action mt-16rpx w-full bg-surface text-primary rounded-24rpx" role="button" tabindex="0" @click="restart" @keydown.enter.prevent="restart" @keydown.space.prevent="restart">{{ success ? '再付一笔' : '重新输入金额' }}</button>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from "vue"
|
||||
import PaymentKeyboard from "@@/components/payment-keyboard.vue"
|
||||
import PaymentUnavailable from "@@/components/payment-unavailable.vue"
|
||||
import { useMerchantPayment } from "@@/hooks/use-merchant-payment"
|
||||
import { themeColors } from "@@/config/theme"
|
||||
|
||||
/** 页面与数字键盘共享真实收银状态,任何付款入口都经过同一个请求和金额守卫。 */
|
||||
const { merchant, amount, loading, busy, error, feedback, editable, canPay, showKeyboard, canRestart, confirming, success, terminal, unavailable,
|
||||
pay, restart, retry, append, remove } = useMerchantPayment()
|
||||
/** 商户图片失败只切换占位标识,不影响真实商户名称和付款对象。 */
|
||||
const merchantLogoFailed = ref(false)
|
||||
/** 长金额按展示长度缩小字号,始终保留完整金额。 */
|
||||
const amountSizeClass = computed(() => amount.value.length > 10 ? "text-56rpx" : amount.value.length > 7 ? "text-72rpx" : "text-104rpx")
|
||||
|
||||
/** 标识失败时使用 tm-ui 店铺图形。 */
|
||||
function handleLogoError() { merchantLogoFailed.value = true }
|
||||
|
||||
/** 保留 Tab 和按钮原生键盘语义;锁定金额时 Hook 会拒绝数字输入。 */
|
||||
function handleKeyboard(event: KeyboardEvent) {
|
||||
if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey || event.isComposing) return
|
||||
if (/^[0-9.]$/.test(event.key)) { event.preventDefault(); append(event.key) }
|
||||
else if (event.key === "Backspace" || event.key === "Delete") { event.preventDefault(); remove() }
|
||||
else if (event.key === "Enter" && event.target instanceof Element && !event.target.closest("button, uni-button")) {
|
||||
event.preventDefault(); void pay()
|
||||
}
|
||||
}
|
||||
|
||||
/** 不创建可编辑 input,输入后仅滚动金额内部容器,避免唤起系统键盘或移动整页。 */
|
||||
watch(amount, async () => {
|
||||
await nextTick()
|
||||
const viewport = document.getElementById("payment-amount-scroll")
|
||||
if (viewport) viewport.scrollLeft = viewport.scrollWidth
|
||||
})
|
||||
/** 真实商户加载完成后才把焦点放入只读金额框。 */
|
||||
watch(editable, async value => {
|
||||
if (!value) return
|
||||
await nextTick()
|
||||
document.getElementById("payment-amount")?.focus({ preventScroll: true })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-shell {
|
||||
background: linear-gradient(180deg, var(--pay-tint) 0, var(--pay-background) 360px);
|
||||
}
|
||||
|
||||
.payment-content {
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
padding-right: env(safe-area-inset-right, 0px);
|
||||
padding-left: env(safe-area-inset-left, 0px);
|
||||
overscroll-behavior: contain;
|
||||
scroll-padding-block: 16px;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-actions {
|
||||
padding-bottom: max(20px, env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.payment-action {
|
||||
min-height: 52px;
|
||||
padding: 14px 16px;
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
border: 0;
|
||||
&::after { border: 0; }
|
||||
&:focus-visible { outline: 2px solid var(--pay-strong); outline-offset: 2px; }
|
||||
&:active { opacity: 0.85; }
|
||||
}
|
||||
|
||||
.amount-card {
|
||||
box-shadow: 0 8px 28px rgba(44, 83, 118, 0.035);
|
||||
}
|
||||
|
||||
.merchant-name {
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.amount-field {
|
||||
min-height: 84px;
|
||||
border-bottom: 1px solid var(--pay-border);
|
||||
outline: none;
|
||||
cursor: text;
|
||||
|
||||
&:focus-visible {
|
||||
border-bottom-color: var(--pay-primary);
|
||||
box-shadow: 0 1px 0 var(--pay-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.amount-scroll {
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-digits {
|
||||
min-height: 1.2em;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.payment-cursor {
|
||||
width: 2px;
|
||||
height: 0.85em;
|
||||
align-self: center;
|
||||
border-radius: 1px;
|
||||
animation: cursor-blink 1.2s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes cursor-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
// 短屏优先压缩装饰留白,保留完整金额和至少 44px 的键盘触控区域
|
||||
@media (max-height: 620px) {
|
||||
.merchant-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.merchant-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
margin-block: 12px;
|
||||
}
|
||||
|
||||
.amount-field {
|
||||
min-height: 64px;
|
||||
margin-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.amount-hint {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
import { request } from "@@/utils/request"
|
||||
|
||||
/** 商户签名入口,长整数始终以字符串保存,由后端绑定商户和租户。 */
|
||||
export interface MerchantEntry {
|
||||
/** 收款商户主键。 */
|
||||
id: string
|
||||
/** 签名对应的租户主键。 */
|
||||
tenantId: string
|
||||
/** 当前协议版本,仅支持 1。 */
|
||||
v: string
|
||||
/** 绑定线下收款用途的服务端签名。 */
|
||||
sign: string
|
||||
}
|
||||
|
||||
/** 与后端渠道编码一致:10 微信、20 支付宝。 */
|
||||
export type PaymentPlatform = "10" | "20"
|
||||
|
||||
/** 匿名可见的商户资料。 */
|
||||
export interface MerchantInfo {
|
||||
/** 商户主键,使用字符串避免精度损失。 */
|
||||
id: string
|
||||
/** 服务端返回的商户名称。 */
|
||||
name: string
|
||||
/** 商户标识,缺省时显示统一店铺图形。 */
|
||||
logoPic?: string | null
|
||||
/** 商户已启用的渠道,不包含商户号或配置;空列表表示暂无线上支付渠道。 */
|
||||
supportedPlatforms: PaymentPlatform[]
|
||||
}
|
||||
|
||||
/** OAuth 后的短期收银凭据,不是平台授权码。 */
|
||||
export interface CashierTicket {
|
||||
/** 服务端租户唯一编码。 */
|
||||
tenantCode: string
|
||||
/** 五分钟有效的不透明票据。 */
|
||||
ticket: string
|
||||
}
|
||||
|
||||
/** 聚合会话协议状态,沿用 Java 枚举的持久化编码。 */
|
||||
export type PaymentStatus = "10" | "15" | "20" | "30" | "40" | "50" | "60"
|
||||
|
||||
/** 收银状态仅以服务端查单或验签回调确认。 */
|
||||
export interface PaymentResult {
|
||||
/** 聚合会话协议状态。 */
|
||||
status: PaymentStatus
|
||||
/** 已由服务端确认渠道付款成功。 */
|
||||
paySuccess: boolean
|
||||
/** 现有第三方商户订单号。 */
|
||||
payNo?: string
|
||||
}
|
||||
|
||||
/** H5 票据上下文,金额和场景均不可由 URL 覆盖。 */
|
||||
export interface CashierContext extends PaymentResult {
|
||||
/** ORDER 为已有订单,OFFLINE 为付款人输入金额的线下收款。 */
|
||||
aggregateScene: "ORDER" | "OFFLINE"
|
||||
/** 最小公开商户信息。 */
|
||||
merchant: MerchantInfo
|
||||
/** 以整数分保存的已锁定金额。 */
|
||||
amount: number
|
||||
/** 10 为微信,20 为支付宝。 */
|
||||
selectedPlatform?: "10" | "20" | null
|
||||
/** 尚可付款时提供的渠道签名参数,终态时缺省。 */
|
||||
launchParams?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** 创建结果中的跳转地址由后端固定配置产生。 */
|
||||
export interface CreatedPayment {
|
||||
/** 服务端聚合会话编号。 */
|
||||
aggregateNo: string
|
||||
/** 带随机访问令牌的既有聚合 OAuth 入口。 */
|
||||
cashierUrl: string
|
||||
/** 服务端支付截止时间,仅供提示,前端不能据此自行确认关单。 */
|
||||
expireTime: string
|
||||
}
|
||||
|
||||
/** 查询签名入口对应的最小商户信息。 */
|
||||
export function getMerchant(entry: MerchantEntry) {
|
||||
return request<MerchantInfo>("/third/aggregate-merchant-pay/merchant", "GET", { ...entry }, entry.tenantId)
|
||||
}
|
||||
|
||||
/** 创建或恢复同一次收款意图,重试必须复用 requestId 和整数分金额。 */
|
||||
export function createPayment(entry: MerchantEntry, amount: number, requestId: string) {
|
||||
return request<CreatedPayment>("/third/aggregate-merchant-pay/create", "POST", { ...entry, amount, requestId }, entry.tenantId)
|
||||
}
|
||||
|
||||
/** 读取票据绑定的商户、固定金额和渠道参数。 */
|
||||
export function getCashierContext(value: CashierTicket) {
|
||||
return request<CashierContext>(`/third/aggregate-order-pay/context/${encodeURIComponent(value.tenantCode)}/${encodeURIComponent(value.ticket)}`, "GET")
|
||||
}
|
||||
|
||||
/** 读取原聚合状态接口,不能通过前端事件把流水标记为成功。 */
|
||||
export function getPaymentStatus(value: CashierTicket) {
|
||||
return request<PaymentResult>(`/third/aggregate-order-pay/status/${encodeURIComponent(value.tenantCode)}/${encodeURIComponent(value.ticket)}`, "GET")
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<view class="payment-keyboard bg-surface" role="group" aria-label="付款金额数字键盘">
|
||||
<view class="keypad-grid">
|
||||
<button
|
||||
v-for="key in numberKeys"
|
||||
:key="key"
|
||||
class="keypad-key bg-key text-text rounded-20rpx text-48rpx font-500"
|
||||
:aria-label="key"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:hover-class="'keypad-key--pressed'"
|
||||
:hover-stay-time="100"
|
||||
@click="emit('input', key)"
|
||||
@keydown.enter.prevent="emit('input', key)"
|
||||
@keydown.space.prevent="emit('input', key)"
|
||||
>
|
||||
{{ key }}
|
||||
</button>
|
||||
<button
|
||||
class="keypad-key keypad-delete bg-key text-secondary rounded-20rpx"
|
||||
aria-label="删除最后一位"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
hover-class="keypad-key--pressed"
|
||||
:hover-stay-time="100"
|
||||
@click="emit('delete')"
|
||||
@keydown.enter.prevent="emit('delete')"
|
||||
@keydown.space.prevent="emit('delete')"
|
||||
>
|
||||
<view class="flex-row-center" aria-hidden="true">
|
||||
<tm-icon name="delete-back-2-line" :size="48" :color="themeColors.secondary" />
|
||||
</view>
|
||||
</button>
|
||||
<button
|
||||
class="keypad-key keypad-zero bg-key text-text rounded-20rpx text-48rpx font-500"
|
||||
aria-label="0"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
hover-class="keypad-key--pressed"
|
||||
:hover-stay-time="100"
|
||||
@click="emit('input', '0')"
|
||||
@keydown.enter.prevent="emit('input', '0')"
|
||||
@keydown.space.prevent="emit('input', '0')"
|
||||
>
|
||||
0
|
||||
</button>
|
||||
<button
|
||||
class="keypad-key keypad-decimal bg-key text-text rounded-20rpx text-48rpx font-500"
|
||||
aria-label="小数点"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
hover-class="keypad-key--pressed"
|
||||
:hover-stay-time="100"
|
||||
@click="emit('input', '.')"
|
||||
@keydown.enter.prevent="emit('input', '.')"
|
||||
@keydown.space.prevent="emit('input', '.')"
|
||||
>
|
||||
.
|
||||
</button>
|
||||
<button
|
||||
class="keypad-key keypad-pay rounded-20rpx"
|
||||
:class="disabled ? 'bg-disabled text-secondary' : 'bg-primary text-white'"
|
||||
:disabled="disabled"
|
||||
:aria-disabled="disabled"
|
||||
aria-label="付款"
|
||||
role="button"
|
||||
:tabindex="disabled ? -1 : 0"
|
||||
hover-class="keypad-pay--pressed"
|
||||
:hover-stay-time="100"
|
||||
@click="handlePay"
|
||||
@keydown.enter.prevent="handlePay"
|
||||
@keydown.space.prevent="handlePay"
|
||||
>
|
||||
<view class="flex-col-center gap-12rpx pointer-events-none" aria-hidden="true">
|
||||
<text class="text-40rpx font-700">付款</text>
|
||||
<tm-icon name="arrow-right-line" :size="40" :color="disabled ? themeColors.secondary : 'white'" />
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { themeColors } from "@@/config/theme"
|
||||
|
||||
/** 键盘不持有金额,也不触发请求,金额规则与付款状态由所属页面负责 */
|
||||
const props = defineProps<{
|
||||
/** 金额无效或提交期间禁用付款,数字输入另外由页面的金额锁定状态保护 */
|
||||
disabled: boolean
|
||||
}>()
|
||||
|
||||
/** 页面监听输入、退格和付款事件,触屏与物理键盘复用页面金额 Hook */
|
||||
const emit = defineEmits<{
|
||||
/** 追加一个数字或小数点,由页面校验格式 */
|
||||
(event: "input", key: string): void
|
||||
/** 删除最后一个字符 */
|
||||
(event: "delete"): void
|
||||
/** 发起一次付款意图,由页面完成金额校验与幂等请求 */
|
||||
(event: "pay"): void
|
||||
}>()
|
||||
|
||||
/** 数字区按阅读顺序排列,0 与小数点在底行单独占位 */
|
||||
const numberKeys = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
|
||||
/** 在组件边界保护禁用状态,避免第三方或程序化点击绕过视觉状态 */
|
||||
function handlePay() {
|
||||
if (!props.disabled) emit("pay")
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-keyboard {
|
||||
padding: 16px 12px;
|
||||
padding-right: max(12px, env(safe-area-inset-right, 0px));
|
||||
padding-bottom: max(16px, env(safe-area-inset-bottom, 0px));
|
||||
padding-left: max(12px, env(safe-area-inset-left, 0px));
|
||||
border-top: 1px solid rgba(230, 237, 244, 0.85);
|
||||
border-radius: 24px 24px 0 0;
|
||||
box-shadow: 0 -8px 32px rgba(44, 83, 118, 0.035);
|
||||
}
|
||||
|
||||
// 收银键盘的非等长按键需要明确的网格占位,避免 flex 换行改变排列
|
||||
.keypad-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(4, 60px);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.keypad-key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: background-color 120ms ease, color 120ms ease;
|
||||
|
||||
&::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--pay-strong);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&--pressed {
|
||||
background: var(--pay-soft);
|
||||
}
|
||||
}
|
||||
|
||||
.keypad-delete {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.keypad-zero {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 4;
|
||||
}
|
||||
|
||||
.keypad-decimal {
|
||||
grid-column: 3;
|
||||
grid-row: 4;
|
||||
}
|
||||
|
||||
.keypad-pay {
|
||||
grid-column: 4;
|
||||
grid-row: 2 / 5;
|
||||
|
||||
&:active,
|
||||
&--pressed {
|
||||
background: var(--pay-strong);
|
||||
}
|
||||
|
||||
&[aria-disabled="true"] {
|
||||
color: var(--pay-secondary);
|
||||
background: var(--pay-disabled);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
.keypad-grid {
|
||||
grid-template-rows: repeat(4, clamp(48px, 7.5dvh, 64px));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<view class="flex-col-center pt-40rpx pb-24rpx text-center" role="status" aria-live="polite">
|
||||
<view class="flex-row-center size-128rpx rounded-full bg-soft" aria-hidden="true">
|
||||
<tm-icon name="bank-card-line" :size="64" :color="themeColors.primary" />
|
||||
</view>
|
||||
<text class="block mt-32rpx text-38rpx font-600 text-text leading-normal">{{ notice.title }}</text>
|
||||
<text class="block mt-20rpx text-28rpx text-secondary leading-relaxed">{{ notice.description }}</text>
|
||||
<view class="flex items-start gap-16rpx w-full mt-40rpx p-24rpx rounded-24rpx bg-soft text-left">
|
||||
<view class="shrink-0" aria-hidden="true">
|
||||
<tm-icon name="qr-scan-2-line" :size="36" :color="themeColors.strong" />
|
||||
</view>
|
||||
<text class="text-28rpx text-strong leading-relaxed">{{ notice.instruction }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { themeColors } from "@@/config/theme"
|
||||
import type { PaymentUnavailable } from "@@/utils/payment-platform"
|
||||
|
||||
/** 在原付款页内展示不可用原因和换用已支持 App 的指引,不创建新的支付意图。 */
|
||||
defineProps<{
|
||||
/** 由当前 App、商户渠道和已绑定订单共同确定的展示状态。 */
|
||||
notice: PaymentUnavailable
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
/** 静态页商户展示数据;仅用于展示,不代表后续支付接口的字段契约 */
|
||||
export interface MerchantInfo {
|
||||
/** 展示的商户名称,使用本地示例,可在此替换 */
|
||||
name: string
|
||||
/** 可选的商户标识地址;缺省或加载失败时展示店铺占位图形 */
|
||||
logoUrl?: string
|
||||
}
|
||||
|
||||
/** 首版商户示例,后续接入接口时由页面统一替换数据来源 */
|
||||
export const mockMerchant: MerchantInfo = {
|
||||
name: "资安信示例商户",
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/** 付款页语义颜色;参考 App 的主题蓝与浅蓝背景作为视觉基线 */
|
||||
export const themeColors = {
|
||||
/** 品牌主色,用于付款键、光标和商户标识 */
|
||||
primary: "#0094ff",
|
||||
/** 同色系深蓝,用于浅色背景上的小字号文本 */
|
||||
strong: "#006ec9",
|
||||
/** 页面底色,与参考项目 globalStyle 保持一致 */
|
||||
background: "#ecf4f9",
|
||||
/** 页面上方的柔和蓝色渐变起点,仅承担背景装饰 */
|
||||
tint: "#deeffc",
|
||||
/** 商户和金额卡片的白色表面 */
|
||||
surface: "#ffffff",
|
||||
/** 商户名称、金额及数字按键的主要文字 */
|
||||
text: "#263446",
|
||||
/** 标签及辅助说明,确保白色表面上的可读性 */
|
||||
secondary: "#536579",
|
||||
/** 次要说明文字,避免参考页面的浅灰在小字号下对比不足 */
|
||||
muted: "#64748b",
|
||||
/** 金额占位符使用大字号,区别于已经输入的金额 */
|
||||
placeholder: "#8192a6",
|
||||
/** 卡片分隔线和按键边界 */
|
||||
border: "#e6edf4",
|
||||
/** 商户标识与键盘按压反馈的浅蓝色 */
|
||||
soft: "#e6f3ff",
|
||||
/** 数字键的浅灰表面,和白色键盘容器形成层次 */
|
||||
key: "#f3f6fa",
|
||||
/** 无效金额时付款键的填充色 */
|
||||
disabled: "#d6e9f8",
|
||||
} as const
|
||||
@@ -0,0 +1,309 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue"
|
||||
import { createPayment, getCashierContext, getMerchant, getPaymentStatus } from "@@/api/payment"
|
||||
import type { CashierContext, CashierTicket, MerchantEntry, MerchantInfo, PaymentResult } from "@@/api/payment"
|
||||
import { amountToFen, formatFen, newPaymentRequestId, parsePaymentEntry } from "@@/utils/payment-entry"
|
||||
import { invokePayment } from "@@/utils/payment-bridge"
|
||||
import { paymentUnavailable, resolvePaymentPlatform } from "@@/utils/payment-platform"
|
||||
import { usePaymentAmount } from "@@/hooks/use-payment-amount"
|
||||
|
||||
/** 本标签页的一次线下付款意图,只保存公开入口和恢复信息,不保存平台授权身份。 */
|
||||
interface PendingPayment {
|
||||
/** 用户扫描的签名商户入口。 */
|
||||
entry: MerchantEntry
|
||||
/** 已确认的整数分,提交后不能就地修改。 */
|
||||
amount: number
|
||||
/** 请求超时和刷新时复用的 UUID。 */
|
||||
requestId: string
|
||||
/** 创建成功后可继续 OAuth 的固定服务端地址。 */
|
||||
cashierUrl?: string
|
||||
}
|
||||
|
||||
/** 标签页级恢复键,不跨浏览器账号共享,也不保存商户密钥。 */
|
||||
const PENDING_KEY = "merchant-pay:pending"
|
||||
/** 串行轮询间隔为 4 秒,避免请求堆积及同 IP 多端触发限流。 */
|
||||
const POLL_INTERVAL_MS = 4000
|
||||
/** 渠道已确认终态,只有这些状态可以结束当前付款意图。 */
|
||||
const TERMINAL_STATUSES = ["30", "40", "50", "60"]
|
||||
|
||||
/** 管理两种收银场景的授权、金额锁定、继续付款及服务端结果确认。 */
|
||||
export function useMerchantPayment() {
|
||||
/** 手动输入的人民币元字符串,只在未提交线下收款时可修改。 */
|
||||
const input = usePaymentAmount()
|
||||
/** 当前入口的真实商户,失败时不回退到示例数据。 */
|
||||
const merchant = ref<MerchantInfo>()
|
||||
/** 已通过格式校验的静态签名入口,真实有效性仍由后端判断。 */
|
||||
const entry = ref<MerchantEntry>()
|
||||
/** OAuth 回跳的短期收银票据。 */
|
||||
const ticket = ref<CashierTicket>()
|
||||
/** 后端固定的金额、渠道和状态。 */
|
||||
const context = ref<CashierContext>()
|
||||
/** 标签页内待恢复的付款意图,金额在发请求之前即锁定。 */
|
||||
const pending = ref<PendingPayment>()
|
||||
/** 页面首次查询或手动重试期间为 true。 */
|
||||
const loading = ref(false)
|
||||
/** 支付创建或桥调用期间为 true,防止重复提交。 */
|
||||
const busy = ref(false)
|
||||
/** 收银结果确认期间为 true,不允许重复调用支付组件。 */
|
||||
const confirming = ref(false)
|
||||
/** 当前加载错误,屏幕阅读器与可视提示共享文案。 */
|
||||
const error = ref("")
|
||||
/** 平台取消、查单和终态提示,不包含内部异常。 */
|
||||
const feedback = ref("")
|
||||
/** 当前扫码客户端;不可用页面只引导用户重新扫码,不尝试跨 App 强制唤起。 */
|
||||
const clientPlatform = resolvePaymentPlatform(navigator.userAgent)
|
||||
/** 在金额输入、会话创建及支付组件调起前统一拦截不受支持的客户端。 */
|
||||
const unavailable = computed(() => {
|
||||
if (!merchant.value || success.value || terminal.value) return undefined
|
||||
const notice = paymentUnavailable(merchant.value.supportedPlatforms, clientPlatform, context.value?.selectedPlatform)
|
||||
if (notice) return notice
|
||||
// 未开通渠道的订单扫码只签发只读票据;重新开通后需再扫码进入 OAuth,不能停在无调起参数的页面。
|
||||
if (context.value?.status === "10") return { title: "请重新扫码付款", description: "当前支付方式已可用,请重新扫码进入收银台。", instruction: "请使用当前 App 的“扫一扫”,重新扫描商户收款码。" }
|
||||
return undefined
|
||||
})
|
||||
/** 串行轮询计时器,页面隐藏或销毁时清除。 */
|
||||
let pollTimer: ReturnType<typeof setTimeout> | undefined
|
||||
/** 取消过期查询结果,避免页面离开或重试后旧请求覆盖新状态。 */
|
||||
let revision = 0
|
||||
/** 防止轮询请求与页面恢复请求并行。 */
|
||||
let checking = false
|
||||
/** 本页面是否已经销毁。 */
|
||||
let disposed = false
|
||||
/** 仅未提交的签名商户入口允许数字输入。 */
|
||||
const editable = computed(() => !!entry.value && !!merchant.value && !context.value && !pending.value && !loading.value && !error.value && !unavailable.value)
|
||||
/** 已提交或已有订单都显示服务端/冻结金额,不能使用 URL 金额。 */
|
||||
const amount = computed(() => context.value ? formatFen(context.value.amount) : pending.value ? formatFen(pending.value.amount) : input.amount.value)
|
||||
/** 成功仅依据后端确认值。 */
|
||||
const success = computed(() => context.value?.paySuccess === true)
|
||||
/** 当前会话已结束,不再提供支付入口。 */
|
||||
const terminal = computed(() => !!context.value && TERMINAL_STATUSES.includes(context.value.status))
|
||||
/** 金额校验、平台就绪及请求状态共同决定按钮是否可操作。 */
|
||||
const canPay = computed(() => !loading.value && !busy.value && !confirming.value && !error.value && !unavailable.value && !!merchant.value
|
||||
&& !terminal.value && (context.value ? context.value.status === "20" && !!context.value.launchParams : !!pending.value || amountToFen(input.amount.value) !== undefined))
|
||||
/** 已创建的线下付款和订单收银只保留底部操作区。 */
|
||||
const showKeyboard = computed(() => !!entry.value && !context.value && !pending.value && !unavailable.value)
|
||||
/** 成功或已确认关闭后可以重新输入;创建失败仍须重新扫码核对,避免未知渠道结果下新建支付。 */
|
||||
const canRestart = computed(() => !!entry.value && !!context.value && ["30", "40", "50"].includes(context.value.status))
|
||||
|
||||
/** 保存付款意图失败时停止提交,避免跳转后丢失幂等请求号。 */
|
||||
function savePending(value: PendingPayment) {
|
||||
sessionStorage.setItem(PENDING_KEY, JSON.stringify(value))
|
||||
pending.value = value
|
||||
}
|
||||
|
||||
/** 只恢复符合格式和金额约束的本标签页数据,付款状态仍需后端确认。 */
|
||||
function readPending(): PendingPayment | undefined {
|
||||
try {
|
||||
const stored = JSON.parse(sessionStorage.getItem(PENDING_KEY) || "null") as PendingPayment | null
|
||||
if (!stored || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(stored.requestId) || amountToFen(formatFen(stored.amount)) !== stored.amount) return
|
||||
const query = new URLSearchParams({ ...stored.entry }).toString()
|
||||
if (parsePaymentEntry(`https://entry.invalid/#/pages/pay/index?${query}`).kind !== "merchant") return
|
||||
if (stored.cashierUrl && new URL(stored.cashierUrl).protocol !== "https:") return
|
||||
return stored
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
/** 停止下次轮询;已经发出的请求通过 revision 取消写入。 */
|
||||
function stopPolling() {
|
||||
if (pollTimer) clearTimeout(pollTimer)
|
||||
pollTimer = undefined
|
||||
}
|
||||
|
||||
/** 按服务端终态更新提示,不以本机时间或 JSBridge 回调自行判定付款成功。 */
|
||||
function applyStatus(result: PaymentResult) {
|
||||
if (!context.value) return
|
||||
context.value = { ...context.value, ...result }
|
||||
if (result.paySuccess) feedback.value = "付款成功"
|
||||
else if (result.status === "40") feedback.value = "交易已关闭,未完成付款"
|
||||
else if (result.status === "50") feedback.value = "收款请求已过期"
|
||||
else if (result.status === "60") feedback.value = "付款创建失败,请重新扫码核对"
|
||||
if (TERMINAL_STATUSES.includes(result.status)) {
|
||||
confirming.value = false
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
|
||||
/** 排队下一次状态查询,前一个请求结束后才会启动。 */
|
||||
function queuePolling() {
|
||||
stopPolling()
|
||||
if (!disposed && ticket.value && !terminal.value && !document.hidden && !error.value && !unavailable.value) {
|
||||
pollTimer = setTimeout(() => { void checkStatus() }, POLL_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
/** 失败时保留原请求与金额,用户可手动重试状态查询,不能直接再建一笔交易。 */
|
||||
async function checkStatus() {
|
||||
if (!ticket.value || checking || disposed) return
|
||||
checking = true
|
||||
const current = revision
|
||||
try {
|
||||
const result = await getPaymentStatus(ticket.value)
|
||||
if (current !== revision || disposed) return
|
||||
applyStatus(result)
|
||||
error.value = ""
|
||||
} catch (cause) {
|
||||
if (current === revision && !disposed) error.value = cause instanceof Error ? cause.message : "付款结果暂不可用,请稍后查询"
|
||||
} finally {
|
||||
checking = false
|
||||
if (current === revision) queuePolling()
|
||||
}
|
||||
}
|
||||
|
||||
/** 调起当前已绑定的平台订单;取消后保持原单号,继续付款仍复用本单。 */
|
||||
async function launch() {
|
||||
const data = context.value
|
||||
if (!data?.launchParams || !data.selectedPlatform || data.status !== "20" || busy.value || confirming.value || unavailable.value) return
|
||||
busy.value = true
|
||||
const current = revision
|
||||
try {
|
||||
const result = await invokePayment(data.selectedPlatform, data.launchParams)
|
||||
if (disposed || current !== revision) return
|
||||
confirming.value = result === "confirming"
|
||||
feedback.value = result === "cancelled" ? "已取消付款,可继续付款" : result === "failed" ? "未完成付款,可继续付款" : "付款结果确认中,请勿重复付款"
|
||||
await checkStatus()
|
||||
} catch (cause) {
|
||||
if (!disposed && current === revision) feedback.value = cause instanceof Error ? cause.message : "未能调起付款,请稍后重试"
|
||||
} finally {
|
||||
if (!disposed && current === revision) { busy.value = false; queuePolling() }
|
||||
}
|
||||
}
|
||||
|
||||
/** 入口加载失败始终显示错误,只有真实商户加载成功后才开放输入或支付。 */
|
||||
async function load() {
|
||||
const current = ++revision
|
||||
stopPolling()
|
||||
loading.value = true
|
||||
error.value = ""
|
||||
try {
|
||||
const parsed = parsePaymentEntry(window.location.href)
|
||||
if (parsed.kind === "merchant") {
|
||||
entry.value = parsed.entry
|
||||
const data = await getMerchant(parsed.entry)
|
||||
if (disposed || current !== revision) return
|
||||
merchant.value = data
|
||||
const stored = readPending()
|
||||
if (stored?.entry.id === parsed.entry.id && stored.entry.tenantId === parsed.entry.tenantId && stored.entry.sign === parsed.entry.sign) pending.value = stored
|
||||
} else {
|
||||
ticket.value = parsed.ticket
|
||||
const data = await getCashierContext(parsed.ticket)
|
||||
if (disposed || current !== revision) return
|
||||
context.value = data
|
||||
merchant.value = data.merchant
|
||||
const stored = readPending()
|
||||
if (data.aggregateScene === "OFFLINE" && stored?.entry.id === data.merchant.id) {
|
||||
entry.value = stored.entry
|
||||
pending.value = stored
|
||||
}
|
||||
applyStatus(data)
|
||||
// 只在新的 OAuth 票据第一次到达时自动调起,刷新不会重复自动弹出收银台。
|
||||
const marker = `merchant-pay:launched:${parsed.ticket.ticket}`
|
||||
if (!unavailable.value && !sessionStorage.getItem(marker) && data.status === "20" && data.launchParams) {
|
||||
sessionStorage.setItem(marker, "1")
|
||||
void launch()
|
||||
}
|
||||
queuePolling()
|
||||
}
|
||||
} catch (cause) {
|
||||
if (!disposed && current === revision) error.value = cause instanceof Error ? cause.message : "收款信息暂不可用"
|
||||
} finally {
|
||||
if (current === revision) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 先保存稳定请求号再创建会话,任何网络重试都保留原金额与 requestId。 */
|
||||
async function pay() {
|
||||
if (!canPay.value) return
|
||||
if (context.value) { await launch(); return }
|
||||
if (!entry.value) return
|
||||
busy.value = true
|
||||
const current = revision
|
||||
try {
|
||||
if (!pending.value) {
|
||||
const value = amountToFen(input.amount.value)
|
||||
if (value === undefined) return
|
||||
savePending({ entry: entry.value, amount: value, requestId: newPaymentRequestId() })
|
||||
}
|
||||
const attempt = pending.value!
|
||||
let url = attempt.cashierUrl
|
||||
if (!url) {
|
||||
const result = await createPayment(attempt.entry, attempt.amount, attempt.requestId)
|
||||
if (disposed || current !== revision) return
|
||||
url = result.cashierUrl
|
||||
if (new URL(url).protocol !== "https:") throw new Error("付款入口不可用,请联系商户")
|
||||
savePending({ ...attempt, cashierUrl: url })
|
||||
}
|
||||
window.location.assign(url)
|
||||
} catch (cause) {
|
||||
if (!disposed && current === revision) {
|
||||
feedback.value = cause instanceof Error ? cause.message : "付款暂未能发起,请稍后重试"
|
||||
// 商户可能在页面停留期间停用渠道;重新读取开通情况,保留原金额和请求号。
|
||||
try {
|
||||
const data = await getMerchant(entry.value!)
|
||||
if (!disposed && current === revision) merchant.value = data
|
||||
} catch { /* 查询失败时保留原请求与错误提示,不能据此释放未知支付意图。 */ }
|
||||
}
|
||||
} finally { if (!disposed && current === revision) busy.value = false }
|
||||
}
|
||||
|
||||
/** 渠道提示页重新读取商户/票据上下文;已有正常付款仅查询结果,避免重新创建会话。 */
|
||||
async function retry() {
|
||||
if (loading.value || busy.value) return
|
||||
if (ticket.value && context.value && !unavailable.value) await checkStatus()
|
||||
else await load()
|
||||
}
|
||||
|
||||
/** 只有服务端已确认成功或关闭后,才释放旧意图并返回签名商户入口。 */
|
||||
function restart() {
|
||||
if (!canRestart.value || !entry.value) return
|
||||
sessionStorage.removeItem(PENDING_KEY)
|
||||
const query = new URLSearchParams({ ...entry.value }).toString()
|
||||
window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}#/pages/pay/index?${query}`)
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
/** 输入长度限制同时覆盖四位整数和两位小数,不改变冻结金额。 */
|
||||
function append(key: string) {
|
||||
if (!editable.value) return
|
||||
const next = input.amount.value
|
||||
if (/^\d$/.test(key) && !next.includes(".") && next !== "0" && next.length >= 4) return
|
||||
input.append(key)
|
||||
}
|
||||
|
||||
/** 只有未提交的线下金额允许删除。 */
|
||||
function remove() { if (editable.value) input.remove() }
|
||||
|
||||
/** 后台恢复时先查真实状态,页面隐藏期间停止轮询。 */
|
||||
function handleVisibility() {
|
||||
if (document.hidden) stopPolling()
|
||||
else if (ticket.value && !terminal.value) void checkStatus()
|
||||
}
|
||||
|
||||
/** 单页 Hash 入口变化也重新校验,避免浏览器复用同一页面时沿用上一商户或金额。 */
|
||||
function handleHashChange() {
|
||||
busy.value = false
|
||||
context.value = undefined
|
||||
ticket.value = undefined
|
||||
entry.value = undefined
|
||||
merchant.value = undefined
|
||||
pending.value = undefined
|
||||
feedback.value = ""
|
||||
confirming.value = false
|
||||
input.clear()
|
||||
void load()
|
||||
}
|
||||
|
||||
/** 页面挂载后读取真实入口,不从示例商户配置加载任何收款对象。 */
|
||||
onMounted(() => {
|
||||
document.addEventListener("visibilitychange", handleVisibility)
|
||||
window.addEventListener("hashchange", handleHashChange)
|
||||
void load()
|
||||
})
|
||||
/** 释放轮询和监听,未返回的请求不能再修改页面状态。 */
|
||||
onUnmounted(() => {
|
||||
disposed = true; revision++; stopPolling()
|
||||
document.removeEventListener("visibilitychange", handleVisibility)
|
||||
window.removeEventListener("hashchange", handleHashChange)
|
||||
})
|
||||
|
||||
return { merchant, amount, loading, busy, error, feedback, editable, canPay, showKeyboard, canRestart, confirming, success, terminal, unavailable,
|
||||
pay, restart, retry, append, remove }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { computed, readonly, ref } from "vue"
|
||||
|
||||
/** 人民币金额最多保留两位小数;输入过程使用字符串保留末尾的小数点与零 */
|
||||
const DECIMAL_PLACES = 2
|
||||
|
||||
/**
|
||||
* 将一个数字或小数点追加到金额尾部,无效输入返回原值
|
||||
* 不进行浮点运算,也不将展示长度限制当作真实支付限额
|
||||
*/
|
||||
export function appendAmount(current: string, key: string): string {
|
||||
if (!/^[0-9.]$/.test(key)) return current
|
||||
if (key === ".") return current.includes(".") ? current : `${current || "0"}.`
|
||||
if (current.includes(".") && current.split(".")[1].length >= DECIMAL_PLACES) return current
|
||||
return current === "0" ? key : current + key
|
||||
}
|
||||
|
||||
/** 页面金额状态的唯一写入入口,自定义键盘和物理键盘共享相同的输入规则 */
|
||||
export function usePaymentAmount() {
|
||||
/** 当前输入的金额,单位为元;空字符串表示尚未输入,生命周期与页面一致 */
|
||||
const amount = ref("")
|
||||
/** 输入由 appendAmount 保证格式,只需判断是否包含非零数字即可避免浮点精度问题 */
|
||||
const canPay = computed(() => /[1-9]/.test(amount.value))
|
||||
|
||||
/** 追加一个合法字符,保留正在输入的小数状态 */
|
||||
function append(key: string) {
|
||||
amount.value = appendAmount(amount.value, key)
|
||||
}
|
||||
|
||||
/** 删除最后一个字符,空金额继续保持为空 */
|
||||
function remove() {
|
||||
amount.value = amount.value.slice(0, -1)
|
||||
}
|
||||
|
||||
/** 切换到另一收款入口时清空未提交金额,不影响已持久化的付款请求 */
|
||||
function clear() { amount.value = "" }
|
||||
|
||||
return { amount: readonly(amount), canPay, append, remove, clear }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
page {
|
||||
height: 100%;
|
||||
color: var(--pay-text);
|
||||
background: var(--pay-background);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
view,
|
||||
text,
|
||||
button,
|
||||
image {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
html,
|
||||
body,
|
||||
#app,
|
||||
uni-app,
|
||||
uni-page,
|
||||
uni-page-wrapper,
|
||||
uni-page-body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: var(--pay-background);
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
// 整页锁在浏览器可视区域,极短屏只允许卡片所在区域内部滑动且不显示滚动条
|
||||
uni-app,
|
||||
uni-page,
|
||||
uni-page-wrapper,
|
||||
uni-page-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
// 使用动态视口适应移动浏览器地址栏,旧浏览器保留 vh 回退
|
||||
.payment-shell {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
box-shadow: 0 0 80px rgba(44, 83, 118, 0.07);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 仅对自绘输入光标关闭动画,按键状态反馈仍保留
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.payment-cursor {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createPinia } from "pinia"
|
||||
|
||||
/** 仅承载 tm-ui 的主题状态,付款金额保留在当前页面内,不持久化 */
|
||||
const pinia = createPinia()
|
||||
|
||||
export default pinia
|
||||
@@ -0,0 +1,77 @@
|
||||
/** 客户端回调只代表调起结果,最终付款状态必须再查询后端。 */
|
||||
export type BridgeResult = "confirming" | "cancelled" | "failed"
|
||||
|
||||
/** 两种客户端桥的最小接口,不引入 App SDK 或其他平台能力。 */
|
||||
interface PaymentBridge {
|
||||
/** 支付宝 tradePay 接口。 */
|
||||
call?: (name: string, params: Record<string, unknown>, callback: (result: Record<string, unknown>) => void) => void
|
||||
/** 微信 getBrandWCPayRequest 接口。 */
|
||||
invoke?: (name: string, params: Record<string, unknown>, callback: (result: Record<string, unknown>) => void) => void
|
||||
}
|
||||
|
||||
/** 当前客户端注入的全局桥对象,仅在 H5 运行期访问。 */
|
||||
type BridgeWindow = Window & { AlipayJSBridge?: PaymentBridge; WeixinJSBridge?: PaymentBridge }
|
||||
|
||||
/** 把平台回调归一化;9000/ok 仍进入服务端结果确认,不直接展示支付成功。 */
|
||||
export function bridgeResult(platform: "10" | "20", result: Record<string, unknown>): BridgeResult {
|
||||
if (platform === "20") {
|
||||
const code = String(result.resultCode || "")
|
||||
if (["9000", "8000", "6004"].includes(code)) return "confirming"
|
||||
return code === "6001" ? "cancelled" : "failed"
|
||||
}
|
||||
const message = String(result.err_msg || "").toLowerCase()
|
||||
return message.endsWith(":ok") ? "confirming" : message.endsWith(":cancel") ? "cancelled" : "failed"
|
||||
}
|
||||
|
||||
/** 等待客户端桥就绪并调起一次;取消或失败不自动重复唤起。 */
|
||||
export function invokePayment(platform: "10" | "20", params: Record<string, unknown>): Promise<BridgeResult> {
|
||||
const client = navigator.userAgent.toLowerCase()
|
||||
if ((platform === "10" && !client.includes("micromessenger")) || (platform === "20" && !client.includes("alipayclient"))) {
|
||||
return Promise.reject(new Error("请在原微信或支付宝客户端中继续付款"))
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
/** 当前桥等待和回调仅属于本次用户调起操作。 */
|
||||
const readyEvent = platform === "20" ? "AlipayJSBridgeReady" : "WeixinJSBridgeReady"
|
||||
let invoked = false
|
||||
let settled = false
|
||||
let timer: ReturnType<typeof setTimeout>
|
||||
/** 释放就绪监听和超时,避免页面反复授权后残留调起。 */
|
||||
function finish(result?: BridgeResult, error?: Error) {
|
||||
if (settled) return
|
||||
settled = true
|
||||
document.removeEventListener(readyEvent, launch)
|
||||
clearTimeout(timer)
|
||||
if (error) reject(error)
|
||||
else resolve(result || "confirming")
|
||||
}
|
||||
/** 只接受当前平台的最小参数,不把后端提供的对象转换成可执行脚本。 */
|
||||
function launch() {
|
||||
if (invoked || settled) return
|
||||
const host = window as BridgeWindow
|
||||
const bridge = platform === "20" ? host.AlipayJSBridge : host.WeixinJSBridge
|
||||
if (!bridge || (platform === "20" ? !bridge.call : !bridge.invoke)) return
|
||||
if (
|
||||
(platform === "20" && typeof params.tradeNO !== "string") ||
|
||||
(platform === "10" &&
|
||||
!["appId", "timeStamp", "nonceStr", "package", "signType", "paySign"].every((key) => typeof params[key] === "string"))
|
||||
) {
|
||||
finish(undefined, new Error("付款参数不可用,请重新扫码"))
|
||||
return
|
||||
}
|
||||
invoked = true
|
||||
clearTimeout(timer)
|
||||
// 用户在平台收银台停留较久时只转入查单,不据超时认定失败或再发起一笔支付。
|
||||
timer = setTimeout(() => finish("confirming"), 30000)
|
||||
try {
|
||||
const callback = (result: Record<string, unknown>) => finish(bridgeResult(platform, result))
|
||||
if (platform === "20") bridge.call!("tradePay", { tradeNO: params.tradeNO }, callback)
|
||||
else bridge.invoke!("getBrandWCPayRequest", params, callback)
|
||||
} catch {
|
||||
finish(undefined, new Error("支付组件调用失败,请稍后继续付款"))
|
||||
}
|
||||
}
|
||||
timer = setTimeout(() => finish(undefined, new Error("支付组件尚未就绪,请点击继续付款")), 5000)
|
||||
document.addEventListener(readyEvent, launch)
|
||||
launch()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { CashierTicket, MerchantEntry } from "@@/api/payment"
|
||||
|
||||
/** 初版单笔上限为 9,999.99 元,与后端配置同步,单位为分。 */
|
||||
export const MAX_OFFLINE_AMOUNT = 999999
|
||||
|
||||
/** 二维码只能是签名商户入口或短期收银票据两种形式。 */
|
||||
export type PaymentEntry = { kind: "merchant"; entry: MerchantEntry } | { kind: "cashier"; ticket: CashierTicket }
|
||||
|
||||
/** 解析 Hash 路由参数;拒绝重复参数及混用入口,避免跳转时悄悄切换收款对象。 */
|
||||
export function parsePaymentEntry(href: string): PaymentEntry {
|
||||
const url = new URL(href)
|
||||
const params = new URLSearchParams(url.hash.includes("?") ? url.hash.slice(url.hash.indexOf("?") + 1) : url.search)
|
||||
if (Array.from(params.keys()).some(key => params.getAll(key).length !== 1)) throw new Error("收款链接无效,请重新扫码")
|
||||
if (params.has("ticket") || params.has("tenantCode")) {
|
||||
if (params.has("id") || params.has("sign") || params.has("tenantId")) throw new Error("收银凭据无效,请重新扫码")
|
||||
const ticket = params.get("ticket") || ""
|
||||
const tenantCode = params.get("tenantCode") || ""
|
||||
if (!/^[A-Za-z0-9_-]{32}$/.test(ticket) || !tenantCode || tenantCode.length > 128) throw new Error("收银凭据无效,请重新扫码")
|
||||
return { kind: "cashier", ticket: { ticket, tenantCode } }
|
||||
}
|
||||
const entry = { id: params.get("id") || "", tenantId: params.get("tenantId") || "", v: params.get("v") || "", sign: params.get("sign") || "" }
|
||||
if (!/^[1-9]\d{0,18}$/.test(entry.id) || !/^[1-9]\d{0,18}$/.test(entry.tenantId)
|
||||
|| entry.v !== "1" || !/^[0-9a-f]{64}$/.test(entry.sign)) throw new Error("收款链接无效,请重新扫码")
|
||||
return { kind: "merchant", entry }
|
||||
}
|
||||
|
||||
/** 字符串元精确转换为整数分,拒绝零、科学计数、超过两位小数及超限输入。 */
|
||||
export function amountToFen(value: string): number | undefined {
|
||||
if (!/^\d{1,4}(\.\d{0,2})?$/.test(value)) return undefined
|
||||
const [whole, fraction = ""] = value.split(".")
|
||||
const result = Number(whole) * 100 + Number(fraction.padEnd(2, "0"))
|
||||
return Number.isSafeInteger(result) && result > 0 && result <= MAX_OFFLINE_AMOUNT ? result : undefined
|
||||
}
|
||||
|
||||
/** 固定金额从整数分格式化,不进行浮点金额运算。 */
|
||||
export function formatFen(value: number): string {
|
||||
return `${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`
|
||||
}
|
||||
|
||||
/** 浏览器为一次付款意图生成 UUID;网络重试复用该值。 */
|
||||
export function newPaymentRequestId(): string {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(16))
|
||||
bytes[6] = (bytes[6] & 15) | 64
|
||||
bytes[8] = (bytes[8] & 63) | 128
|
||||
const hex = Array.from(bytes, value => value.toString(16).padStart(2, "0")).join("")
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { PaymentPlatform } from "@@/api/payment"
|
||||
|
||||
/** 渠道名称只用于展示,后端始终依据已启用的商户配置校验实际付款。 */
|
||||
const PLATFORM_NAMES: Record<PaymentPlatform, string> = { "10": "微信", "20": "支付宝" }
|
||||
|
||||
/** 当前客户端无法付款时展示的原因和恢复指引。 */
|
||||
export interface PaymentUnavailable {
|
||||
/** 说明商户未开通当前渠道,或当前浏览器无法调起支付。 */
|
||||
title: string
|
||||
/** 已开通的替代方式或现有付款的绑定渠道。 */
|
||||
description: string
|
||||
/** 用户可以实际完成的下一步操作。 */
|
||||
instruction: string
|
||||
}
|
||||
|
||||
/** 仅识别两种受支持的扫码客户端,与后端的识别顺序保持一致。 */
|
||||
export function resolvePaymentPlatform(userAgent: string): PaymentPlatform | undefined {
|
||||
const client = userAgent.toLowerCase()
|
||||
if (client.includes("micromessenger")) return "10"
|
||||
if (client.includes("alipayclient")) return "20"
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 进入输入框前检查渠道;已有子订单按原渠道恢复,避免商户配置变化掩盖待确认付款。 */
|
||||
export function paymentUnavailable(supportedPlatforms: PaymentPlatform[] | undefined, client: PaymentPlatform | undefined, selected?: PaymentPlatform | null): PaymentUnavailable | undefined {
|
||||
if (selected) {
|
||||
if (client === selected) return undefined
|
||||
const name = PLATFORM_NAMES[selected]
|
||||
return { title: `请使用${name}继续付款`, description: `本次付款已在${name}中发起,请继续原来的付款。`, instruction: `请回到${name}完成付款,避免重复支付。` }
|
||||
}
|
||||
const platforms = Array.isArray(supportedPlatforms) ? [...new Set(supportedPlatforms.filter(value => value === "10" || value === "20"))] : []
|
||||
if (!platforms.length) {
|
||||
return { title: "商户暂未开通线上支付", description: "当前暂无可用的微信或支付宝支付方式。", instruction: "请联系商户确认其他收款方式。" }
|
||||
}
|
||||
const names = platforms.map(value => PLATFORM_NAMES[value]).join("或")
|
||||
if (!client) {
|
||||
return { title: `请使用${names}扫码`, description: `该商户支持${names}支付。`, instruction: `请打开${names},使用“扫一扫”扫描商户收款码。` }
|
||||
}
|
||||
if (!platforms.includes(client)) {
|
||||
return { title: `商户暂不支持${PLATFORM_NAMES[client]}支付`, description: `该商户当前支持${names}支付。`, instruction: `请打开${names},使用“扫一扫”重新扫描商户收款码。` }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/** 后端统一响应,仅公开收银接口使用,不携带登录 Token 或商户密钥。 */
|
||||
interface ApiResponse<T> {
|
||||
/** 0 表示业务成功,非零表示业务失败。 */
|
||||
code: number | string
|
||||
/** 经过后端处理的用户提示。 */
|
||||
msg?: string
|
||||
/** 具体接口的最小公开响应。 */
|
||||
data: T
|
||||
}
|
||||
|
||||
/** 沿用参考项目变量:开发走 VITE_API_URL 本地代理,生产读取 VITE_APP_PROD_SERVICE。 */
|
||||
const apiBase = (import.meta.env.DEV ? import.meta.env.VITE_API_URL || "/api" : import.meta.env.VITE_APP_PROD_SERVICE || "").trim().replace(/\/+$/, "")
|
||||
|
||||
/** 统一封装匿名请求与微服务/Boot 路径,不自动重试支付创建请求。 */
|
||||
export function request<T>(path: string, method: "GET" | "POST", data?: Record<string, unknown>, tenantId?: string): Promise<T> {
|
||||
/** Boot 聚合服务不使用网关 third 前缀,其他部署保留前缀。 */
|
||||
const endpoint = import.meta.env.VITE_IS_MICRO === "false" ? path.replace(/^\/third(?=\/)/, "") : path
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: apiBase + endpoint,
|
||||
method,
|
||||
data,
|
||||
timeout: 15000,
|
||||
withCredentials: false,
|
||||
header: { "Content-Type": "application/json", ...(tenantId ? { "TENANT-ID": tenantId } : {}) },
|
||||
success(response) {
|
||||
const body = response.data as ApiResponse<T> | undefined
|
||||
if (response.statusCode >= 200 && response.statusCode < 300 && body && String(body.code) === "0") {
|
||||
resolve(body.data)
|
||||
} else {
|
||||
reject(new Error(response.statusCode === 429 ? "请求较频繁,请稍后重试" : body?.msg || "服务暂不可用,请稍后重试"))
|
||||
}
|
||||
},
|
||||
fail() { reject(new Error("网络连接失败,请检查网络后重试")) },
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// 与参考 App 的主题保持一致,供 uni-app 内置组件使用
|
||||
$uni-color-primary: #0094ff;
|
||||
$uni-text-color: #263446;
|
||||
$uni-text-color-grey: #64748b;
|
||||
$uni-bg-color: #ecf4f9;
|
||||
$uni-border-color: #e6edf4;
|
||||
@@ -0,0 +1,13 @@
|
||||
## 3.2.0(2024-10-11)
|
||||
* [注意]本版本不向下兼容,可以直接通过市场导入或者在插件目录右键升级到项目中
|
||||
* [3.2.x文档链接](https://tmui.design/)
|
||||
* 1. 本版本不向下兼容!!!!!
|
||||
* 2. 不再兼容nvue平台,请谨慎升级
|
||||
* 3. 本版本不是之前的3.1.x版本的升级版本,无法平移过度到本版本
|
||||
* 4. 本版本为全新的tmui4.0x版本的小程序版本,风格,标准,细节与其一样,属性也大多相同,但内部是本质的不同.不可以4.0x混用
|
||||
* 5. 本版本以webview app + 小程序为准兼容,并且不提供任何原生插件,如果需要原生请使用tmui4.0x来支持我.
|
||||
* 6. 本版本一样是免费的,可以自由的使用,商用,任意修改不用担心版权问题.但请保留版权信息.但不得发布为公共的竞品版本,发现立即追究版权.
|
||||
* 7. 本版本更新迭代比较慢,力求后续兼容鸿蒙系统及所有小程序,因此会逐步减少组件数量,但会保证组件质量,保留最基本的组件库功能.
|
||||
* 8. 如果需要赞助或者捐赠,可以通过官网付款码赞助支持我的开发或者购买tmui4.0x以支持.
|
||||
* 发布第一个3.2.0版本为公测版本
|
||||
* 如果有遇到问题请提交bug至[Gitee](https://gitee.com/LYTB/tm-vuetify-for-vue3)
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<text
|
||||
@tap="onclick"
|
||||
v-if="!isImg"
|
||||
:class="['tmIcon', `${prefix}${iconName}`, isSpin?'tmIconSpin':'',_class]"
|
||||
:style="[
|
||||
{
|
||||
fontSize: iconSize,
|
||||
color: fontColor,
|
||||
fontFamily: fontFamily,
|
||||
fontStyle: 'normal',
|
||||
},
|
||||
rotateDeg ? { transform: `rotate(${rotateDeg})` } : {},
|
||||
_style
|
||||
]"
|
||||
></text>
|
||||
<image
|
||||
v-if="isImg"
|
||||
@tap="onclick"
|
||||
:src="iconName"
|
||||
:class="['tmIcon', isSpin?'tmIconSpin':'',_class]"
|
||||
:style="[
|
||||
{ width: iconSize, height: iconSize },
|
||||
rotateDeg ? { transform: `rotate(${rotateDeg})` } : {},
|
||||
_style
|
||||
]"
|
||||
></image>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { covetUniNumber } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor } from "../../libs/colors";
|
||||
|
||||
/**
|
||||
* @displayName 图标
|
||||
* @exportName tm-icon
|
||||
* @category 常用组件
|
||||
* @description 图标,可以是图片地址或者字体名称图标.也可以是你自定义的图标.
|
||||
* 如何自定义图标呢:你可以在iconfont网站勾选图标->添加购物车->添加项目->项目设置,勾选woff2,勾选base64->下载本地->解压得到iconfont.css
|
||||
* ->复制到你的app.vue全局中即可.然后使用图标时,font-family是你的图标名称比如myicon,prefix是图标前缀比如myicon-
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmIcon" });
|
||||
const { config } = useTmConfig();
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
"click",
|
||||
]);
|
||||
const attrs = defineProps({
|
||||
_style:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
_class:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
/**
|
||||
* 图标名称,可以是图片地址
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* 图标大小,默认32rpx
|
||||
*/
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: "32",
|
||||
},
|
||||
/**
|
||||
* 图标颜色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "#333333",
|
||||
},
|
||||
/**
|
||||
* 暗黑时的图标颜色
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
/**
|
||||
* 图标是否旋转
|
||||
*/
|
||||
spin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
rotate: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**字体图标名称,如果你要显示你自己的名称时,需要改成你自己的图标名称. */
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: "remixicon",
|
||||
},
|
||||
/**图标名称前缀,默认为ri-,没有你可以为空 */
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "ri-",
|
||||
},
|
||||
});
|
||||
|
||||
const iconName = computed(() => attrs.name);
|
||||
const iconSize = computed(() => covetUniNumber(attrs.size, "rpx"));
|
||||
const isImg = computed(() => iconName.value.includes("."));
|
||||
const isSpin = computed(() => attrs.spin);
|
||||
const rotateDeg = computed(() => attrs.rotate);
|
||||
const _style = computed(() => attrs._style);
|
||||
const _class = computed(() => attrs._class);
|
||||
const fontColor = computed(() => {
|
||||
let color = attrs.color;
|
||||
if (config.mode == "dark" && attrs.darkColor) {
|
||||
color = attrs.darkColor;
|
||||
}
|
||||
return getDefaultColor(color);
|
||||
});
|
||||
const onclick = () => emits("click");
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tmIcon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
transition: transform 0.3s linear;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.tmIconSpin {
|
||||
animation: tmIconAniSPin 1s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes tmIconAniSPin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<view
|
||||
@click="OnClick"
|
||||
class="tmSheet"
|
||||
@touchstart="touchStart"
|
||||
@touchcancel="touchEnd"
|
||||
@touchend="touchEnd"
|
||||
@mousedown="touchStart"
|
||||
@mouseup="touchEnd"
|
||||
@mouseleave="touchEnd"
|
||||
:style="[
|
||||
{
|
||||
width: _width,
|
||||
height: _height,
|
||||
},
|
||||
buttonStyle,
|
||||
]"
|
||||
>
|
||||
<!-- @slot 默认插槽 -->
|
||||
<slot></slot>
|
||||
<view
|
||||
v-if="_attrs.loading"
|
||||
class="tmSheetLoading"
|
||||
:style="{ borderRadius: buttonStyle.borderRadius }"
|
||||
>
|
||||
<tm-icon name="loader-line" size="50" spin color="primary"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, covetUniNumber, linearValid } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
|
||||
/**
|
||||
* @displayName 容器
|
||||
* @exportName tm-sheet
|
||||
* @category 常用组件
|
||||
* @description 像一张纸张一样,用于包裹内容区域,可塑性高,可以快速用来布局.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmSheet' });
|
||||
|
||||
const { config } = useTmConfig()
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
* @property {MouseEvent} evt 事件参数
|
||||
* */
|
||||
'click',
|
||||
])
|
||||
type PropsKeyType = keyof typeof attrs;
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 遵循规则:
|
||||
* string或者[x]:全部边线
|
||||
* [x,x]左右边线,上下边线
|
||||
* [x,x,x] 左,上,右
|
||||
* [x,x,x,x] 左,上,右,下
|
||||
*/
|
||||
borderColor: {
|
||||
type: [String, Array<string>],
|
||||
default: ""
|
||||
},
|
||||
darkBorderColor: {
|
||||
type: [String, Array<string>],
|
||||
default: "transparent"
|
||||
},
|
||||
/** 规则同borderColor */
|
||||
borderWidth: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: 0
|
||||
},
|
||||
/** 规则同borderColor */
|
||||
borderStyle: {
|
||||
type: [String, Array<string>],
|
||||
default: "solid"
|
||||
},
|
||||
/** 规则同borderColor
|
||||
* 左上,右上,右下,左下
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 遵循规则:填写0关闭,填写空值取全局
|
||||
* string,number,或者[x]:全部
|
||||
* [x,x]左右,上下
|
||||
* [x,x,x] 左,上,右
|
||||
* [x,x,x,x] 左,上,右,下
|
||||
*/
|
||||
margin: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 遵循规则:填写0关闭,填写空值取全局
|
||||
* string,number,或者[x]:全部
|
||||
* [x,x]左右,上下
|
||||
* [x,x,x] 左,上,右
|
||||
* [x,x,x,x] 左,上,右,下
|
||||
*/
|
||||
padding: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否跟随主题背景色
|
||||
* 下方的color失效
|
||||
*/
|
||||
flowTheme: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 背景主题
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
/**
|
||||
* 自定义暗黑背景
|
||||
* 空值时取全局配置
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 渐变,前面的color,bgColor,darkBgColor失效 */
|
||||
linear: {
|
||||
type: [Array<string>],
|
||||
default: () => [],
|
||||
validator: (val: string[]) => {
|
||||
if (val.length == 0) return true;
|
||||
let pass = val.length == 3;
|
||||
if (!pass) {
|
||||
console.error("tmui提醒:linear 参数格式错误,请检查,必须长度为3");
|
||||
}
|
||||
return pass
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 数字或者字符串时自动计算投影,它会投影大小
|
||||
* 数组时(必须为4),第一个x,第二为y,第三个为大小 ,第四个为投影颜色
|
||||
* 空字符串时,取全局配置,如果不想投影设置为none即可.
|
||||
*/
|
||||
shadow: {
|
||||
type: [String, Number, Array<string>],
|
||||
default: '',
|
||||
validator: (val: string | number | string[]) => {
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length != 4) {
|
||||
console.error("tmui提醒:shadow 第一个x,第二为y,第三个为大小 ,第四个为投影颜色");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
})
|
||||
const _attrs = computed(() => attrs);
|
||||
const OnClick = (evt: MouseEvent) => {
|
||||
if (_attrs.value.loading) return;
|
||||
emits('click', evt)
|
||||
}
|
||||
const _width = computed(() => {
|
||||
return covetUniNumber(attrs.width || 'auto')
|
||||
})
|
||||
const _height = computed(() => covetUniNumber(attrs.height || 'auto'))
|
||||
|
||||
const isHover = ref(false);
|
||||
const touchStart = () => {
|
||||
isHover.value = true;
|
||||
}
|
||||
const touchEnd = () => {
|
||||
isHover.value = false;
|
||||
}
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
let style = {
|
||||
borderColor: ``,
|
||||
borderWidth: ``,
|
||||
borderStyle: ``,
|
||||
background: ``,
|
||||
color: ``,
|
||||
borderRadius: ``,
|
||||
boxShadow: `none`,
|
||||
margin: '0px',
|
||||
padding: '0px'
|
||||
}
|
||||
let isDark = config.mode == 'dark';
|
||||
|
||||
let background = attrs.color
|
||||
let borderColor = arrayNumberValidByStyleBorderColor(attrs.borderColor)
|
||||
let borderWidth = arrayNumberValidByStyleMP(attrs.borderWidth)
|
||||
let borderStyle = arrayNumberValidByStyleBorderStyle(attrs.borderStyle)
|
||||
let borderRadius = arrayNumberValidByStyleMP(attrs.round || config.sheetRadius)
|
||||
let margin = arrayNumberValidByStyleMP(attrs.margin === '' ? config.sheetMargin : attrs.margin)
|
||||
let padding = arrayNumberValidByStyleMP(attrs.padding === '' ? config.sheetPadding : attrs.padding)
|
||||
let linear = linearValid(attrs.linear)
|
||||
|
||||
if (attrs.flowTheme) {
|
||||
background = config.color
|
||||
}
|
||||
if (isDark) {
|
||||
background = attrs.darkBgColor || config.sheetDarkColor
|
||||
}
|
||||
|
||||
let shadow = 'none'
|
||||
if (Array.isArray(attrs.shadow)) {
|
||||
shadow = attrs.shadow.join(' ')
|
||||
} else if (attrs.shadow !== '' && attrs.shadow != 'none') {
|
||||
let obj = getDefaultColorObj(attrs.color, attrs.color)
|
||||
shadow = `0px 0px ${covetUniNumber(attrs.shadow)} ${obj.default.shadow}`
|
||||
} else if (attrs.shadow === '') {
|
||||
if (Array.isArray(config.sheetShadow)) {
|
||||
shadow = config.sheetShadow.join(' ')
|
||||
} else if (config.sheetShadow && config.sheetShadow != 'none') {
|
||||
let obj = getDefaultColorObj(attrs.color, attrs.color)
|
||||
shadow = `0 4px ${covetUniNumber(config.sheetShadow)} ${obj.default.shadow}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
style.background = linear || getDefaultColor(background)
|
||||
style.borderColor = borderColor.join(' ')
|
||||
style.borderWidth = borderWidth.join(' ')
|
||||
style.borderStyle = borderStyle.join(' ')
|
||||
style.borderRadius = borderRadius.join(' ')
|
||||
style.margin = margin.join(' ')
|
||||
style.padding = padding.join(' ')
|
||||
style.boxShadow = shadow
|
||||
return style;
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tmSheet {
|
||||
position: relative;
|
||||
|
||||
.tmSheetLoading {
|
||||
display: flex;
|
||||
border: 0px solid transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
backdrop-filter: blur(3px);
|
||||
background-color: rgba(125, 125, 125, 0.2);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+1
@@ -0,0 +1 @@
|
||||
@font-face{font-family:remixicon;src:url(data:application/font-woff2;base64,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 truncated
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["tmui.scss","tmui.css"],"names":[],"mappings":"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XE;EAME,yCAAA;ACuXJ;;AD7XE;EAME,yCAAA;AC2XJ;;ADjYE;EAME,yCAAA;AC+XJ;;ADrYE;EAME,yCAAA;ACmYJ;;ADzYE;EAME,yCAAA;ACuYJ;;AD7YE;EAME,yCAAA;AC2YJ;;ADjZE;EAME,yCAAA;AC+YJ;;ADrZE;EAME,yCAAA;ACmZJ;;ADzZE;EAME,yCAAA;ACuZJ;;AD7ZE;EAME,yCAAA;AC2ZJ;;ADjaE;EAME,yCAAA;AC+ZJ;;ADraE;EAME,yCAAA;ACmaJ;;ADzaE;EAME,yCAAA;ACuaJ;;AD7aE;EAME,yCAAA;AC2aJ;;ADjbE;EAME,yCAAA;AC+aJ;;ADrbE;EAME,yCAAA;ACmbJ;;ADzbE;EAME,yCAAA;ACubJ;;AD7bE;EAME,yCAAA;AC2bJ;;ADjcE;EAME,yCAAA;AC+bJ;;ADrcE;EAME,0CAAA;ACmcJ;;ADzcE;EAUE,0CAAA;ACmcJ;;AD7cE;EAUE,0CAAA;ACucJ;;ADjdE;EAUE,0CAAA;AC2cJ;;ADrdE;EAUE,2CAAA;AC+cJ;;ADzdE;EAUE,2CAAA;ACmdJ;;AD7dE;EAUE,2CAAA;ACudJ;;ADjeE;EAUE,2CAAA;AC2dJ;;ADreE;EAUE,2CAAA;AC+dJ;;ADzeE;EAUE,2CAAA;ACmeJ;;AD7eE;EAUE,2CAAA;ACueJ;;ADjfE;EAUE,2CAAA;AC2eJ;;ADrfE;EAUE,2CAAA;AC+eJ;;ADzfE;EAUE,2CAAA;ACmfJ;;AD7fE;EAUE,2CAAA;ACufJ;;ADjgBE;EAUE,2CAAA;AC2fJ;;ADrgBE;EAUE,2CAAA;AC+fJ;;ADzgBE;EAUE,2CAAA;ACmgBJ;;AD7gBE;EAUE,2CAAA;ACugBJ;;ADjhBE;EAUE,2CAAA;AC2gBJ;;ADrhBE;EAUE,2CAAA;AC+gBJ;;ADzhBE;EAUE,2CAAA;ACmhBJ;;AD7hBE;EAUE,2CAAA;ACuhBJ;;ADjiBE;EAUE,2CAAA;AC2hBJ;;ADriBE;EAUE,2CAAA;AC+hBJ;;ADziBE;EAUE,2CAAA;ACmiBJ;;AD7iBE;EAUE,4CAAA;ACuiBJ;;ADjjBE;EAcE,2CAAA;ACuiBJ;;ADrjBE;EAcE,2CAAA;AC2iBJ;;ADzjBE;EAcE,2CAAA;AC+iBJ;;AD7jBE;EAcE,4CAAA;ACmjBJ;;ADjkBE;EAcE,4CAAA;ACujBJ;;ADrkBE;EAcE,4CAAA;AC2jBJ;;ADzkBE;EAcE,4CAAA;AC+jBJ;;AD7kBE;EAcE,4CAAA;ACmkBLine truncated
|
||||
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
/**
|
||||
* tmui3.2.x
|
||||
* @param app Vue
|
||||
* @param options tmui3.0配置
|
||||
*/
|
||||
// @ts-ignore
|
||||
install: (app: App, options: TM.THEME_CONFIG | null = null) => {
|
||||
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 上传的文件状态
|
||||
* 0未上传,1上传中,2上传成功,3上传失败(网络错误),4已取消(终止,上到一半取消了),5超过大小
|
||||
* 0 Not uploaded, 1 Uploading, 2 Upload successful, 3 Upload failed (network error), 4 Cancelled (terminated, canceled halfway), 5 Exceeds size limit,
|
||||
*/
|
||||
export enum TMUPLOAD_PHOTO_STATUS {
|
||||
NOT = 0,
|
||||
UPLOADING = 1,
|
||||
UPLOAD_SUCCESS = 2,
|
||||
UPLOAD_FAILED = 3,
|
||||
CANCELLED = 4,
|
||||
EXCEEDS_SIZE_LIMIT = 5,
|
||||
}
|
||||
declare global {
|
||||
/**
|
||||
* TMUI3.2.x类型命名空间
|
||||
* @author tmzdy
|
||||
* @date 2024-8-1
|
||||
*/
|
||||
namespace TM {
|
||||
/** 按钮尺寸大小 */
|
||||
export type BUTTON_SIZE = "xs" | "s" | "m" | "n" | "g"
|
||||
/** 按钮或者标签的主题样式 */
|
||||
export type BUTTON_THEME = "thin" | "outlined" | "text" | "normal" | "dashed"
|
||||
/** 主题状态 */
|
||||
export type THEME_STATUS = "warn" | "success" | "info" | "error" | "error"
|
||||
/** 主题样式 */
|
||||
export type THEME_MODE = "auto" | "dark" | "light"
|
||||
|
||||
export interface THEME_CONFIG {
|
||||
/** 主题模式 */
|
||||
mode: THEME_MODE,
|
||||
color: string,
|
||||
unit: 'rpx' | 'px',
|
||||
// 默认相对字号
|
||||
fontSizeBase: string | number,
|
||||
// 默认相对字号fontSizeBase倍数
|
||||
fontSizeScale: 1,
|
||||
buttonRadius: number | string,
|
||||
/** button默认的投影大小,none全局投影取消,但不影响局部值 */
|
||||
buttonShadow: number | string | Array<string>,
|
||||
|
||||
sheetRadius: number | string | Array<string | number>,
|
||||
sheetShadow: number | string | Array<string>,
|
||||
sheetMargin: number | string | Array<string | number>,
|
||||
sheetPadding: number | string | Array<string | number>,
|
||||
|
||||
//列表默认的圆角。
|
||||
cellRadius: number | string | Array<string | number>,
|
||||
|
||||
|
||||
/** 一般容器sheet的暗黑背景 */
|
||||
sheetDarkColor: string,
|
||||
/** 一般容器边线颜色如果你布局自己写了默认的边线色,但忘记填写组件上的暗黑线时,会取这里的值 */
|
||||
sheetDarkBorderColor: string[],
|
||||
|
||||
/** 输入框,表单,常规灰背景待统一的二级暗黑背景 */
|
||||
inputDarkColor: string,
|
||||
/**输入框默认全局的亮系背景. */
|
||||
inputLightColor: string,
|
||||
/**输入框全局圆角 */
|
||||
inputRadius: number | string | Array<string | number>,
|
||||
/** 一般的表单,列表底的暗黑统一边线颜色 */
|
||||
borderDarkColor: string,
|
||||
/**对话框圆角 */
|
||||
modalRadius: number | string,
|
||||
/**形状圆角 */
|
||||
switchRadius: number | string,
|
||||
/**抽屉圆角 */
|
||||
drawerRadius: number | string,
|
||||
unRadioAndCheckBoxColor: string,
|
||||
|
||||
/**部分组件统一的动画差值。 */
|
||||
animation: string,
|
||||
}
|
||||
export type tmDateDayInfoType = {
|
||||
/** 年 */
|
||||
year: number,
|
||||
/** 月0-11 */
|
||||
month: number,
|
||||
/** 天 */
|
||||
day: number,
|
||||
/** 小时0-23 */
|
||||
hours: number,
|
||||
/** 分钟0-59 */
|
||||
minutes: number,
|
||||
/** 秒0-59 */
|
||||
seconds: number,
|
||||
/** 本年的第几周 */
|
||||
weeks: number,
|
||||
/** 本周星期几 */
|
||||
week: number,
|
||||
/** 本周星期几,中文名称 */
|
||||
weekCn: string,
|
||||
/** 格式化的日期字符串 */
|
||||
date: string,
|
||||
/** 是否渲染显示 */
|
||||
isRender?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传的文件类型
|
||||
*/
|
||||
export type TMUPLOAD_PHOTO_INFO = {
|
||||
id: string,
|
||||
/** 文件大小 */
|
||||
size?: number,
|
||||
/** 文件路径 */
|
||||
path: string,
|
||||
progress: number,
|
||||
statusText: string,
|
||||
status: TMUPLOAD_PHOTO_STATUS,
|
||||
response: any
|
||||
}
|
||||
export type FORM_RULE_TYPE = {
|
||||
/** 校验正则,如果同时提供了valid,以valid为准,默认为null */
|
||||
rule: RegExp|null,
|
||||
/** 校验失败提示,默认提示词:请正在选择/填写 */
|
||||
message: string,
|
||||
/**校验函数,如果不提供,默认以min,max来判断. */
|
||||
validator: null|((val: any) => boolean),
|
||||
/**
|
||||
* 如果校验的值类型是数字是起作用,默认为1,至少要大于0
|
||||
* 如果预期是数组,那长度至少要大于0
|
||||
* 如果是字符串,长度要大于0
|
||||
*/
|
||||
min: number,
|
||||
//默认为-1最长/大值不限制
|
||||
max: number,
|
||||
//预期值的类型,如果不提供自动推断,不提供默认是auto
|
||||
type: "string" | "number" | "array" | "boolean" | "date" | "email" | "phone"|"auto",
|
||||
//必填项,是否校验本值
|
||||
required: boolean
|
||||
}
|
||||
export type FORM_RULE = { [Property in keyof FORM_RULE_TYPE]?: FORM_RULE_TYPE[Property] } & { required: boolean }
|
||||
export type FORM_RESULT_TYPE = { key: string, isPass: boolean, message: string, data: any }
|
||||
export type FORM_SUBMIT_RESULT = {
|
||||
/**是否校验通过 */
|
||||
isPass: boolean,
|
||||
/**校验不通过的第一条信息 */
|
||||
firstValid: FORM_RESULT_TYPE | null,
|
||||
/**校验结果集 */
|
||||
result: FORM_RESULT_TYPE[]
|
||||
}
|
||||
}
|
||||
}
|
||||
export { }
|
||||
|
||||
@@ -0,0 +1,684 @@
|
||||
type rgb = {
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a: number
|
||||
}
|
||||
type hsla = {
|
||||
h: number,
|
||||
s: number,
|
||||
l: number,
|
||||
a: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色值列表
|
||||
*/
|
||||
export const colors = new Map<string, string>([
|
||||
['primary', '#0579FF'],
|
||||
['success', '#07d73d'],
|
||||
['danger', '#ff6411'],
|
||||
['warn', '#ffa61e'],
|
||||
['error', '#f10226'],
|
||||
['info', '#F2F2F2'],
|
||||
|
||||
['kleinblue', '#002FA7'], // 克莱因蓝
|
||||
['chinesered', '#FF0000'], // 中国红
|
||||
['internationalorange', '#FF4F00'], // 国际橙
|
||||
['egyptianvlue', '#1034A6'], // 埃及艳蓝
|
||||
['parisviolet', '#6C3082'], // 巴黎紫
|
||||
['moroccanblue', '#1256A7'], // 摩洛哥蓝
|
||||
['brazilgreen', '#009B3A'], // 巴西绿
|
||||
['britishracinggreen', '#004225'], // 英国赛车绿
|
||||
['indianyellow', '#E3A857'], // 印度黄
|
||||
['australiangold', '#FFDF00'], // 澳大利亚金
|
||||
['venetianred', '#C80815'], // 威尼斯红
|
||||
['majorelleblue', '#6050DC'], // 马若雷蓝
|
||||
['tuscanred', '#7C3030'], // 托斯卡红
|
||||
['naplesyellow', '#FADA5E'], // 那不勒斯黄
|
||||
['capumortuum', '#592720'], // 死者之首
|
||||
['mayablue', '#73C2FB'], // 玛雅蓝
|
||||
['persianrose', '#FE28A2'], // 波斯玫瑰
|
||||
['tyrianpurple', '#66023C'], // 泰尔紫
|
||||
['saharasand', '#F1E788'], // 撒哈拉沙
|
||||
['burmeseruby', '#B00A0A'], // 缅甸红宝石
|
||||
|
||||
['transparent', 'rgba(0,0,0,0)'],
|
||||
['aliceblue', '#F0F8FF'],
|
||||
['antiquewhite', '#FAEBD7'],
|
||||
['aqua', '#00FFFF'],
|
||||
['aquamarine', '#7FFFD4'],
|
||||
['azure', '#F0FFFF'],
|
||||
['beige', '#F5F5DC'],
|
||||
['bisque', '#FFE4C4'],
|
||||
['black', '#000000'],
|
||||
['blanchedalmond', '#FFEBCD'],
|
||||
['blue', '#0000FF'],
|
||||
['blueviolet', '#8A2BE2'],
|
||||
['brown', '#A52A2A'],
|
||||
['burlywood', '#DEB887'],
|
||||
['cadetblue', '#5F9EA0'],
|
||||
['chartreuse', '#7FFF00'],
|
||||
['chocolate', '#D2691E'],
|
||||
['coral', '#FF7F50'],
|
||||
['cornflowerblue', '#6495ED'],
|
||||
['cornsilk', '#FFF8DC'],
|
||||
['crimson', '#DC143C'],
|
||||
['cyan', '#00FFFF'],
|
||||
['darkblue', '#00008B'],
|
||||
['darkcyan', '#008B8B'],
|
||||
['darkgoldenrod', '#B8860B'],
|
||||
['darkgray', '#A9A9A9'],
|
||||
['darkgreen', '#006400'],
|
||||
['darkkhaki', '#BDB76B'],
|
||||
['darkmagenta', '#8B008B'],
|
||||
['darkolivegreen', '#556B2F'],
|
||||
['darkorange', '#FF8C00'],
|
||||
['darkorchid', '#9932CC'],
|
||||
['darkred', '#8B0000'],
|
||||
['darksalmon', '#E9967A'],
|
||||
['darkseagreen', '#8FBC8F'],
|
||||
['darkslateblue', '#483D8B'],
|
||||
['darkslategray', '#2F4F4F'],
|
||||
['darkturquoise', '#00CED1'],
|
||||
['darkviolet', '#9400D3'],
|
||||
['deeppink', '#FF1493'],
|
||||
['deepskyblue', '#00BFFF'],
|
||||
['dimgray', '#696969'],
|
||||
['dodgerblue', '#1E90FF'],
|
||||
['firebrick', '#B22222'],
|
||||
['floralwhite', '#FFFAF0'],
|
||||
['forestgreen', '#228B22'],
|
||||
['fuchsia', '#FF00FF'],
|
||||
['gainsboro', '#DCDCDC'],
|
||||
['ghostwhite', '#F8F8FF'],
|
||||
['gold', '#FFD700'],
|
||||
['goldenrod', '#DAA520'],
|
||||
['gray', '#808080'],
|
||||
['green', '#008000'],
|
||||
['greenyellow', '#ADFF2F'],
|
||||
['honeydew', '#F0FFF0'],
|
||||
['hotpink', '#FF69B4'],
|
||||
['indianred', '#CD5C5C'],
|
||||
['indigo', '#4B0082'],
|
||||
['ivory', '#FFFFF0'],
|
||||
['khaki', '#F0E68C'],
|
||||
['lavender', '#E6E6FA'],
|
||||
['lavenderblush', '#FFF0F5'],
|
||||
['lawngreen', '#7CFC00'],
|
||||
['lemonchiffon', '#FFFACD'],
|
||||
['lightblue', '#ADD8E6'],
|
||||
['lightcoral', '#F08080'],
|
||||
['lightcyan', '#E0FFFF'],
|
||||
['lightgoldenrodyellow', '#FAFAD2'],
|
||||
['lightgray', '#D3D3D3'],
|
||||
['lightgreen', '#90EE90'],
|
||||
['lightpink', '#FFB6C1'],
|
||||
['lightsalmon', '#FFA07A'],
|
||||
['lightseagreen', '#20B2AA'],
|
||||
['lightskyblue', '#87CEFA'],
|
||||
['lightslategray', '#778899'],
|
||||
['lightsteelblue', '#B0C4DE'],
|
||||
['lightyellow', '#FFFFE0'],
|
||||
['lime', '#00FF00'],
|
||||
['limegreen', '#32CD32'],
|
||||
['linen', '#FAF0E6'],
|
||||
['magenta', '#FF00FF'],
|
||||
['maroon', '#800000'],
|
||||
['mediumaquamarine', '#66CDAA'],
|
||||
['mediumblue', '#0000CD'],
|
||||
['mediumorchid', '#BA55D3'],
|
||||
['mediumpurple', '#9370DB'],
|
||||
['mediumseagreen', '#3CB371'],
|
||||
['mediumslateblue', '#7B68EE'],
|
||||
['mediumspringgreen', '#00FA9A'],
|
||||
['mediumturquoise', '#48D1CC'],
|
||||
['mediumvioletred', '#C71585'],
|
||||
['midnightblue', '#191970'],
|
||||
['mintcream', '#F5FFFA'],
|
||||
['mistyrose', '#FFE4E1'],
|
||||
['moccasin', '#FFE4B5'],
|
||||
['navajowhite', '#FFDEAD'],
|
||||
['navy', '#000080'],
|
||||
['oldlace', '#FDF5E6'],
|
||||
['olive', '#808000'],
|
||||
['olivedrab', '#6B8E23'],
|
||||
['orange', '#FFA500'],
|
||||
['orangered', '#FF4500'],
|
||||
['orchid', '#DA70D6'],
|
||||
['palegoldenrod', '#EEE8AA'],
|
||||
['palegreen', '#98FB98'],
|
||||
['paleturquoise', '#AFEEEE'],
|
||||
['palevioletred', '#DB7093'],
|
||||
['papayawhip', '#FFEFD5'],
|
||||
['peachpuff', '#FFDAB9'],
|
||||
['peru', '#CD853F'],
|
||||
['pink', '#FFC0CB'],
|
||||
['plum', '#DDA0DD'],
|
||||
['powderblue', '#B0E0E6'],
|
||||
['purple', '#800080'],
|
||||
['rebeccapurple', '#663399'],
|
||||
['red', '#FF0000'],
|
||||
['rosybrown', '#BC8F8F'],
|
||||
['royalblue', '#4169E1'],
|
||||
['saddlebrown', '#8B4513'],
|
||||
['salmon', '#FA8072'],
|
||||
['sandybrown', '#F4A460'],
|
||||
['seagreen', '#2E8B57'],
|
||||
['seashell', '#FFF5EE'],
|
||||
['sienna', '#A0522D'],
|
||||
['silver', '#C0C0C0'],
|
||||
['skyblue', '#87CEEB'],
|
||||
['slateblue', '#6A5ACD'],
|
||||
['slategray', '#708090'],
|
||||
['snow', '#FFFAFA'],
|
||||
['springgreen', '#00FF7F'],
|
||||
['steelblue', '#4682B4'],
|
||||
['tan', '#D2B48C'],
|
||||
['teal', '#008080'],
|
||||
['thistle', '#D8BFD8'],
|
||||
['tomato', '#FF6347'],
|
||||
['turquoise', '#40E0D0'],
|
||||
['violet', '#EE82EE'],
|
||||
['wheat', '#F5DEB3'],
|
||||
['white', '#FFFFFF'],
|
||||
['whitesmoke', '#F5F5F5'],
|
||||
['yellow', '#FFFF00'],
|
||||
['yellowgreen', '#9ACD32']
|
||||
|
||||
])
|
||||
|
||||
|
||||
export const toHex = (n: number): string => {
|
||||
let hex = n.toString(16);
|
||||
if (hex.length == 1) {
|
||||
return '0' + hex;
|
||||
} else {
|
||||
return hex;
|
||||
}
|
||||
}
|
||||
export function isValidColor(color: string): boolean {
|
||||
// return chroma.valid(color);
|
||||
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8})$/;
|
||||
const rgbRegex = /^rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)$/;
|
||||
const rgbaRegex = /^rgba\((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*((1(\.0{1,2})?)|(0(\.\d{1,2})?))\)$/;
|
||||
const hslRegex = /^hsl\((\d{1,3}), (\d{1,3})%, (\d{1,3})%\)$/;
|
||||
const hslaRegex = /^hsla\(\s*((\d{1,2}|[1-2]\d{2}|3[0-5]\d)(\.\d+)?|\d+(\.\d+)?)\s*,\s*((\d{1,2}|\d{0,1}\d{1}\d{1}|[1-2]\d{2}|3[0-5]\d)(\.\d+)?|\d+(\.\d+)?)%\s*,\s*((\d{1,2}|\d{0,1}\d{1}\d{1}|[1-2]\d{2}|3[0-5]\d)(\.\d+)?|\d+(\.\d+)?)%\s*,\s*((1|0(\.\d{1,2})?|(\.\d{1,2})))\)$/i;
|
||||
|
||||
|
||||
if (color == '') {
|
||||
return false; // 颜色值为空
|
||||
} else if (color === 'inherit' || color === 'transparent') {
|
||||
return false; // 特殊颜色值
|
||||
} else if (color === 'currentColor') {
|
||||
return false; // currentColor 不是有效的颜色值
|
||||
} else if (hexRegex.test(color) || rgbRegex.test(color) || rgbaRegex.test(color) || hslRegex.test(color) || hslaRegex.test(color)) {
|
||||
return true; // 符合颜色值的格式
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 获取默认的颜色值
|
||||
* @param [string] 颜色名称或者16进制颜色值
|
||||
*/
|
||||
export function getDefaultColor(sColor: string): string {
|
||||
if (sColor == "") return ""
|
||||
let sc = sColor.toLocaleLowerCase().trim().replace(" ", "");
|
||||
|
||||
if (isValidColor(sc)) {
|
||||
// 如果符合所有颜色值,进行转换为16进制。css方便统一解析。
|
||||
return sc;
|
||||
}
|
||||
// 检测是否是颜色名称。
|
||||
let sco = colors.get(sc);
|
||||
if (typeof sco == 'string') return sco as string;
|
||||
return colors.get("primary")!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色转rgb
|
||||
*/
|
||||
export function hexToRgb(sColors: string) {
|
||||
if (sColors == "") {
|
||||
return { r: 0, g: 0, b: 0, a: 0 }
|
||||
}
|
||||
let reg = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8})$/;
|
||||
let sColor: string = sColors.toLowerCase();
|
||||
|
||||
if (sColor != '' && reg.test(sColor)) {
|
||||
|
||||
if (sColor.length == 4) {
|
||||
let sColorNew = "#";
|
||||
for (let i = 1; i < 4; i += 1) {
|
||||
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
|
||||
}
|
||||
sColor = sColorNew;
|
||||
}
|
||||
|
||||
//处理六位的颜色值
|
||||
let sColorChange: number[] = [];
|
||||
sColorChange.push(parseInt(sColor.substring(1, 3), 16));
|
||||
sColorChange.push(parseInt(sColor.substring(3, 5), 16));
|
||||
sColorChange.push(parseInt(sColor.substring(5, 7), 16));
|
||||
if (sColor.length == 9) {
|
||||
sColorChange.push(parseInt(sColor.substring(7, 9), 16) / 255);
|
||||
}
|
||||
return {
|
||||
r: sColorChange[0],
|
||||
g: sColorChange[1],
|
||||
b: sColorChange[2],
|
||||
a: sColorChange.length == 4 ? sColorChange[3] : 1
|
||||
}
|
||||
} else if (/^(rgb|RGB|rgba|RGBA)/.test(sColor)) {
|
||||
let arr: string[] = sColor.replace(/(?:\(|\)|rgba|rgb|RGB|RGBA)*/g, "").split(",")
|
||||
let p: number[] = arr.map((val: string): number => parseInt(val));
|
||||
if (p.length < 3) {
|
||||
return {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 1
|
||||
}
|
||||
}
|
||||
if (p.length == 3) {
|
||||
p.push(1)
|
||||
}
|
||||
return {
|
||||
r: p[0],
|
||||
g: p[1],
|
||||
b: p[2],
|
||||
a: arr.length == 3 ? p[3] : parseFloat(arr[3])
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* rgb转hsl
|
||||
*/
|
||||
export function rgbToHsl(rgb: rgb): hsla {
|
||||
let r = rgb.r / 255
|
||||
let g = rgb.g / 255
|
||||
let b = rgb.b / 255
|
||||
let a = rgb.a
|
||||
|
||||
var max = Math.max(r, g, b);
|
||||
var min = Math.min(r, g, b);
|
||||
let h = 60 * (4 + (r - g) / (max - min));
|
||||
let sdy = Math.max(2 - max - min,1)
|
||||
let s=(max - min) / sdy;
|
||||
let l = (max + min) / 2;
|
||||
|
||||
if (max === r) {
|
||||
h = (60 * (g - b)) / (max - min);
|
||||
} else if (max === g) {
|
||||
h = 60 * (2 + (b - r) / (max - min));
|
||||
}
|
||||
|
||||
if (h < 0) {
|
||||
h += 360;
|
||||
}
|
||||
|
||||
|
||||
if (max === min) {
|
||||
s = 0;
|
||||
} else if (l < 0.5) {
|
||||
s = (max - min) / (max + min);
|
||||
}
|
||||
|
||||
return { h: h, s: s * 100, l: l * 100, a: a };
|
||||
}
|
||||
|
||||
/**
|
||||
* hsl转rgb
|
||||
*/
|
||||
export function hslToRgb(hsl: hsla): rgb {
|
||||
let h = hsl.h
|
||||
let s = hsl.s
|
||||
let l = hsl.l
|
||||
let a = hsl.a
|
||||
|
||||
h = h / 360;
|
||||
s = s / 100;
|
||||
l = l / 100;
|
||||
let r = l;
|
||||
let g = l;
|
||||
let b = l;
|
||||
|
||||
function hue2rgb(pxx: number, q: number, txs: number): number {
|
||||
let t = txs;
|
||||
let p = pxx;
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (s > 0) {
|
||||
let q = l * (1 + s);
|
||||
if (l >= 0.5) {
|
||||
q = l + s - l * s;
|
||||
}
|
||||
var p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
|
||||
}
|
||||
|
||||
r = Math.round(r * 255);
|
||||
g = Math.round(g * 255);
|
||||
b = Math.round(b * 255);
|
||||
return { r, g, b, a };
|
||||
}
|
||||
|
||||
/**
|
||||
* rgb转16进制
|
||||
*/
|
||||
export function rgbToHex(rgb: rgb): string {
|
||||
let r = rgb.r
|
||||
let g = rgb.g
|
||||
let b = rgb.b
|
||||
let a = rgb.a
|
||||
return "#" + toHex(r) + toHex(g) + toHex(b) + toHex(a * 255)
|
||||
}
|
||||
export function hslaToCss(hsl: hsla): string {
|
||||
|
||||
let rgb = hslToRgb(hsl)
|
||||
|
||||
return rgbToHex(rgb)
|
||||
}
|
||||
|
||||
export function hslaToRgbCss(hsl: hsla): string {
|
||||
|
||||
let rgb = hslToRgb(hsl)
|
||||
let r = rgb.r
|
||||
let g = rgb.g
|
||||
let b = rgb.b
|
||||
let a = rgb.a
|
||||
return `rgba(${r},${g},${b},${a})`
|
||||
}
|
||||
|
||||
// 根据 HSLA 颜色计算亮度
|
||||
export function getLuminance(color: rgb | hsla, type = "rgba"): number {
|
||||
|
||||
|
||||
let colordefault: rgb = (type == 'rgba' ? color : hslToRgb(color as hsla)) as rgb;
|
||||
const r = colordefault.r
|
||||
const g = colordefault.g
|
||||
const b = colordefault.b
|
||||
const rs = r / 255
|
||||
const gs = g / 255
|
||||
const bs = b / 255
|
||||
const rl = Math.pow(rs <= 0.03928 ? rs / 12.92 : ((rs + 0.055) / 1.055), 2.4);
|
||||
const gl = Math.pow(gs <= 0.03928 ? gs / 12.92 : ((gs + 0.055) / 1.055), 2.4);
|
||||
const bl = Math.pow(bs <= 0.03928 ? bs / 12.92 : ((bs + 0.055) / 1.055), 2.4);
|
||||
|
||||
return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对颜色加深
|
||||
*/
|
||||
export function colorAddDeepen(sColor: string): string {
|
||||
|
||||
let rgb = hexToRgb(getDefaultColor(sColor));
|
||||
|
||||
let hsl = rgbToHsl(rgb);
|
||||
|
||||
let l = hsl.l - 5;
|
||||
l = Math.max(0, Math.min(l, 100))
|
||||
|
||||
return hslaToRgbCss({ h: hsl.h, s: hsl.s, l, a: hsl.a })
|
||||
}
|
||||
/**
|
||||
* 对颜色提亮
|
||||
*/
|
||||
export function colorAddBrighten(sColor: string): string {
|
||||
let rgb = hexToRgb(getDefaultColor(sColor));
|
||||
let hsl = rgbToHsl(rgb);
|
||||
let l = hsl.l + 10;
|
||||
l = Math.max(0, Math.min(l, 100))
|
||||
return rgbToHex(hslToRgb({ h: hsl.h, s: hsl.s, l, a: hsl.a }))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取浅色系主题,根据颜色值计算。返回正常值和hover值。
|
||||
*/
|
||||
export function colorGetThint(sColor: string) {
|
||||
let rgb = hexToRgb(getDefaultColor(sColor));
|
||||
let hsl = rgbToHsl(rgb);
|
||||
let s = hsl.s;
|
||||
// let l = hsl.getNumber("l");
|
||||
if (Math.ceil(s) > 0) {
|
||||
s = 45
|
||||
}
|
||||
return {
|
||||
default: rgbToHex(hslToRgb({ h: hsl.h, s, l: 93, a: hsl.a })),
|
||||
hover: rgbToHex(hslToRgb({ h: hsl.h, s, l: 85, a: hsl.a }))
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取正常主题,根据颜色值计算。返回正常值和hover值。
|
||||
*/
|
||||
export function colorGetHover(sColor: string) {
|
||||
|
||||
return {
|
||||
default: sColor,
|
||||
hover: colorAddDeepen(sColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理相主题色。
|
||||
* @param color 需要处理的颜色值
|
||||
* @param hoverColor 对应按下去的颜色默认与color相等。
|
||||
* @returns UTSJSONObject
|
||||
*/
|
||||
export function getDefaultColorObj(color: string, hoverColor: string) {
|
||||
let hsla = rgbToHsl(hexToRgb(getDefaultColor(color)));
|
||||
let hoverHsla = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
|
||||
let shadow = hslaToRgbCss({ h: hsla.h, s: (hsla.s) == 0 ? 0 : 100, l: 50, a: 0.2 });
|
||||
let shadowHOver = hslaToRgbCss({ h: hoverHsla.h, s: (hoverHsla.s) == 0 ? 0 : 70, l: 50, a: 0.15 });
|
||||
if ((hsla.h) == 0 && (hsla.s) == 0) {
|
||||
shadow = "transparent"
|
||||
}
|
||||
if ((hoverHsla.h) == 0 && (hoverHsla.s) == 0) {
|
||||
shadowHOver = "transparent"
|
||||
}
|
||||
let lightnum = getLuminance(hoverHsla, 'hsla');
|
||||
let o = {
|
||||
default: {
|
||||
background: hslaToRgbCss(hsla),
|
||||
borderColor: hslaToRgbCss({ h: hsla.h, s: hsla.s, a: hsla.a, l: Math.min(Math.max(0, hsla.l - 5), hsla.l) }),
|
||||
fontColor: lightnum < 0.6 && hsla.a > 0.1 ? '#ffffff' : '#000000',
|
||||
shadow: shadow
|
||||
},
|
||||
active: {
|
||||
background: hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: hoverHsla.a, l: Math.min(Math.max(0, hoverHsla.l - 10), hoverHsla.l) }),
|
||||
borderColor: hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: hoverHsla.a, l: Math.max(Math.max(0, hoverHsla.l - 10), hoverHsla.l) }),
|
||||
fontColor: lightnum < 0.6 && hoverHsla.a > 0.1 ? '#ffffff' : '#000000',
|
||||
shadow: shadowHOver
|
||||
}
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相主题色。
|
||||
* @param color 需要处理的颜色值
|
||||
* @param hoverColor 对应按下去的颜色默认与color相等。
|
||||
* @returns UTSJSONObject
|
||||
*/
|
||||
export function getTextColorObj(color: string, hoverColor: string, isCoverDark?: boolean) {
|
||||
let dark = isCoverDark == null ? false : isCoverDark
|
||||
// #ifdef WEB
|
||||
dark = isCoverDark == undefined ? false : dark
|
||||
// #endif
|
||||
let hsla = rgbToHsl(hexToRgb(getDefaultColor(color)));
|
||||
let hsla2 = rgbToHsl(hexToRgb(getDefaultColor(color)));
|
||||
let hoverHsla = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
|
||||
let bgcolor = hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: hoverHsla.a, l: dark == true ? 20 : 95 })
|
||||
if (dark) {
|
||||
let p = hsla2;
|
||||
p.l = 20
|
||||
p.s = (p.s) != 0 ? 0 : 20
|
||||
bgcolor = hslaToRgbCss(p)
|
||||
}
|
||||
let o = {
|
||||
default: {
|
||||
background: "transparent",
|
||||
borderColor: "transparent",
|
||||
fontColor: getLuminance(hsla, 'hsla') < 0.3 && hsla.a > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.h, s: hsla.s, a: hsla.a, l: 30 }),
|
||||
shadow: "transparent",
|
||||
},
|
||||
active: {
|
||||
background: bgcolor,
|
||||
borderColor: "transparent",
|
||||
fontColor: getLuminance(hoverHsla, 'hsla') < 0.3 && hoverHsla.a > 0.1 ? hslaToRgbCss(hoverHsla) : hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: hoverHsla.a, l: 30 }),
|
||||
shadow: "transparent",
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相主题色。
|
||||
* @param color 需要处理的颜色值
|
||||
* @param hoverColor 对应按下去的颜色默认与color相等。
|
||||
* @returns UTSJSONObject
|
||||
*/
|
||||
export function getThinColorObj(color: string, hoverColor: string, isCoverDark?: boolean) {
|
||||
let dark = isCoverDark == null ? false : isCoverDark
|
||||
// #ifdef WEB
|
||||
dark = isCoverDark == undefined ? false : dark
|
||||
// #endif
|
||||
|
||||
let hsla = rgbToHsl(hexToRgb(getDefaultColor(color)));
|
||||
let hoverHsla = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
|
||||
// tmxColor.hslaToCss({ ...hsla}) : tmxColor.hslaToCss({ ...hsla,l:30})
|
||||
let fontcolor = getLuminance(hsla, 'hsla') < 0.3 && hsla.a > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.h, s: hsla.s, a: hsla.a, l: 30 })
|
||||
let bordercolor = hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: hoverHsla.a, l: dark == true ? 21 : 92 })
|
||||
let bgcolor = hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: hoverHsla.a, l: dark == true ? 20 : 95 })
|
||||
|
||||
if (dark) {
|
||||
let p = hsla;
|
||||
p.l = 98
|
||||
fontcolor = hslaToRgbCss(p)
|
||||
p.l = 20
|
||||
p.s = (p.s) != 0 ? 5 : 20
|
||||
bgcolor = hslaToRgbCss(p)
|
||||
p.l = 22
|
||||
bordercolor = hslaToRgbCss(p)
|
||||
}
|
||||
let o = {
|
||||
default: {
|
||||
background: bgcolor,
|
||||
borderColor: bordercolor,
|
||||
fontColor: fontcolor,
|
||||
shadow: "transparent",
|
||||
},
|
||||
active: {
|
||||
background: hslaToRgbCss({ h: hoverHsla.h, s: dark ? 10 : 50, a: hoverHsla.a, l: dark == true ? 15 : 88 }),
|
||||
borderColor: hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: 0, l: dark == true ? 18 : 80 }),
|
||||
fontColor: getLuminance(hoverHsla, 'hsla') < 0.3 && hoverHsla.a > 0.1 ? hslaToRgbCss(hoverHsla) : hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: hoverHsla.a, l: 64 }),
|
||||
shadow: "transparent",
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相主题色。
|
||||
* @param color 需要处理的颜色值
|
||||
* @param hoverColor 对应按下去的颜色默认与color相等。
|
||||
* @returns UTSJSONObject
|
||||
*/
|
||||
export function getOutlineColorObj(color: string, hoverColor: string) {
|
||||
let hsla = rgbToHsl(hexToRgb(getDefaultColor(color)));
|
||||
let hoverHsla = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
|
||||
// tmxColor.hslaToCss({ ...hsla}) : tmxColor.hslaToCss({ ...hsla,l:30})
|
||||
let lightnum = getLuminance(hoverHsla, 'hsla');
|
||||
|
||||
let o = {
|
||||
default: {
|
||||
background: 'transparent',
|
||||
borderColor: hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: 1, l: 75 }),
|
||||
fontColor: lightnum < 0.6 && hsla.a > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.h, s: hsla.s, a: hsla.a, l: 30 }),
|
||||
shadow: "transparent",
|
||||
},
|
||||
|
||||
active: {
|
||||
background: 'transparent',
|
||||
borderColor: hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: 1, l: 70 }),
|
||||
fontColor: lightnum < 0.6 && hoverHsla.a > 0.1 ? hslaToRgbCss(hoverHsla) : hslaToRgbCss({ h: hoverHsla.h, s: hoverHsla.s, a: hoverHsla.a, l: 30 }),
|
||||
shadow: "transparent",
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置颜色的亮度值。29
|
||||
* isCoverDark:是根据亮度值,是否要设置n的值,如果n超过了
|
||||
* 指定的30就不操作。
|
||||
*/
|
||||
export function setTextColorLightByDark(color: string): string {
|
||||
let realColor = getDefaultColor(color);
|
||||
let hsla = rgbToHsl(hexToRgb(realColor))
|
||||
let l = hsla.l
|
||||
let s = hsla.s
|
||||
if (l < 30) {
|
||||
if (s > 0) {
|
||||
hsla.l = 50
|
||||
} else {
|
||||
hsla.l = 100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return hslaToRgbCss(hsla)
|
||||
}
|
||||
/**
|
||||
* 变动的颜色是否需要变深。如果不需要返回原始颜色
|
||||
* 如果果需要自动加深。
|
||||
*/
|
||||
export function setBgColorLightByDark(color: string): string {
|
||||
let realColor = getDefaultColor(color);
|
||||
let hsla = rgbToHsl(hexToRgb(realColor))
|
||||
let l = hsla.l
|
||||
let s = hsla.s
|
||||
if (s == 0 && l > 50) {
|
||||
hsla.l = 100 - l
|
||||
}
|
||||
if (s > 0) {
|
||||
if (l > 50) {
|
||||
hsla.l = 100 - l
|
||||
}
|
||||
}
|
||||
return hslaToRgbCss(hsla)
|
||||
}
|
||||
|
||||
export function isBlackAndWhite(color: string): boolean {
|
||||
let realColor = getDefaultColor(color);
|
||||
let hsla = rgbToHsl(hexToRgb(realColor))
|
||||
let s = hsla.s
|
||||
return s == 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
|
||||
export const useTmConfig = defineStore('tmConfig', {
|
||||
state: () => {
|
||||
return {
|
||||
config: {
|
||||
mode: 'light',
|
||||
unit: 'rpx',
|
||||
fontSizeBase: '28',
|
||||
fontSizeScale: 1,
|
||||
buttonRadius: 24,
|
||||
buttonShadow: 'none',
|
||||
color: "primary",
|
||||
|
||||
sheetRadius: 24,
|
||||
sheetMargin: [20, 0, 20, 20],
|
||||
sheetPadding: [24],
|
||||
sheetShadow: 'none',
|
||||
|
||||
cellRadius: 24,
|
||||
|
||||
sheetDarkColor: '#141414',
|
||||
sheetDarkBorderColor: ["#232323"],
|
||||
|
||||
/** 输入框暗黑背景 */
|
||||
inputDarkColor: '#272727',
|
||||
inputLightColor: '#f5f5f5',
|
||||
inputRadius: 24,
|
||||
borderDarkColor: '#1c1c1c',
|
||||
unRadioAndCheckBoxColor: "#cfcfcf",
|
||||
|
||||
switchRadius:64,
|
||||
|
||||
modalRadius: 32,
|
||||
drawerRadius:32,
|
||||
animation: 'cubic-bezier(.42,.38,.15,.93)'
|
||||
|
||||
} as TM.THEME_CONFIG
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { getDefaultColor } from "./colors";
|
||||
|
||||
export const covetUniNumber = (n: number | string, defaultUnit = 'rpx'): string => {
|
||||
if (typeof n === 'number') {
|
||||
return `${n}${defaultUnit}`
|
||||
}
|
||||
let unit = getUnit(n, defaultUnit);
|
||||
let zhi = n.match(/\d+(\.\d+)?/g);
|
||||
let real = '0'
|
||||
if (zhi) {
|
||||
real = zhi[0]
|
||||
}
|
||||
if (unit === 'rpx' && defaultUnit == 'px') {
|
||||
return uni.upx2px(Number(real)).toString() + unit;
|
||||
}
|
||||
if (unit == 'auto') return unit;
|
||||
return real + unit;
|
||||
}
|
||||
export const getUnit = (n: string | number, defaultUnit = 'rpx'): string => {
|
||||
if (typeof n === 'number') {
|
||||
return defaultUnit;
|
||||
}
|
||||
const unit = n.match(/[a-zA-Z|%|auto]+/g);
|
||||
if (!unit || !Array.isArray(unit) || unit?.length === 0) return defaultUnit;
|
||||
return unit[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机一个uid
|
||||
* @param rdix 随机因子
|
||||
* @param length 取的长度
|
||||
* @param isAddStr 是否限制随机结果中的长度,不允许输出长度
|
||||
* @returns String
|
||||
*/
|
||||
export function getUid(rdix = 1, length = 12): string {
|
||||
let ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length);
|
||||
return ix;
|
||||
}
|
||||
|
||||
export const linearValid = (linear: string[]): string => {
|
||||
if (linear.length < 3) return '';
|
||||
return `linear-gradient(${linear.join(',')})`
|
||||
}
|
||||
export const arrayNumberValid = (border: string | number | Array<string | number>, defaultValue = '0px'): string[] => {
|
||||
let zhi: string[] = Array.isArray(border) ? border.map((item: string | number): string => covetUniNumber(item)) : [covetUniNumber(border || defaultValue)];
|
||||
if (zhi.length == 0) {
|
||||
zhi = [covetUniNumber(defaultValue)]
|
||||
} else {
|
||||
let diff = 4 - zhi.length;
|
||||
zhi = [...zhi, ...Array(diff).fill(zhi[0])]
|
||||
}
|
||||
return zhi;
|
||||
}
|
||||
export const arrayNumber = (border: string | number | Array<string | number>, defaultValue = '0px', len = 2): string[] => {
|
||||
let zhi: string[] = Array.isArray(border) ? border.map((item: string | number): string => covetUniNumber(item)) : [covetUniNumber(border || defaultValue)];
|
||||
if (zhi.length == 0) {
|
||||
zhi = Array(len).fill(covetUniNumber(defaultValue))
|
||||
} else {
|
||||
let diff = len - zhi.length;
|
||||
if (diff < 0) {
|
||||
zhi = zhi.slice(0, len)
|
||||
diff = 0
|
||||
}
|
||||
zhi = [...zhi, ...Array(diff).fill(zhi[zhi.length - 1])]
|
||||
}
|
||||
return zhi;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于margin,padding,border-width的校验
|
||||
*/
|
||||
export const arrayNumberValidByStyleMP = (border: string | number | Array<string | number>, defaultValue = '0px'): string[] => {
|
||||
let zhi: string[] = Array.isArray(border) ? border.map((item: string | number): string => covetUniNumber(item)) : [covetUniNumber(border || defaultValue)];
|
||||
if (zhi.length == 0) {
|
||||
zhi = [covetUniNumber(defaultValue)]
|
||||
} else if (zhi.length == 2) {
|
||||
zhi = [zhi[1], zhi[0]]
|
||||
} else if (zhi.length == 3) {
|
||||
// 左,上,右
|
||||
// [0,1,2]
|
||||
// 上 右 下 左
|
||||
zhi = [zhi[1], zhi[2], covetUniNumber(defaultValue), zhi[0]]
|
||||
} else if (zhi.length == 4) {
|
||||
zhi = [zhi[1], zhi[2], zhi[3], zhi[0]]
|
||||
}
|
||||
return zhi;
|
||||
}
|
||||
|
||||
export const arrayNumberValidByStyleBorderColor = (border: string | Array<string>, defaultValue = 'transparent'): string[] => {
|
||||
let zhi: string[] = Array.isArray(border) ? border.map((item: string): string => getDefaultColor(item)) : [getDefaultColor(border || defaultValue)];
|
||||
if (zhi.length == 0) {
|
||||
zhi = [getDefaultColor(defaultValue)]
|
||||
} else if (zhi.length == 2) {
|
||||
zhi = [zhi[1], zhi[0]]
|
||||
} else if (zhi.length == 3) {
|
||||
// 左,上,右
|
||||
// [0,1,2]
|
||||
// 上 右 下 左
|
||||
zhi = [zhi[1], zhi[2], getDefaultColor(defaultValue), zhi[0]]
|
||||
} else if (zhi.length == 4) {
|
||||
zhi = [zhi[1], zhi[2], zhi[3], zhi[0]]
|
||||
}
|
||||
return zhi;
|
||||
}
|
||||
export const arrayNumberValidByStyleBorderStyle = (border: string | Array<string>, defaultValue = 'none'): string[] => {
|
||||
let zhi: string[] = Array.isArray(border) ? border : [border || defaultValue];
|
||||
if (zhi.length == 0) {
|
||||
zhi = [defaultValue]
|
||||
} else if (zhi.length == 2) {
|
||||
zhi = [zhi[1], zhi[0]]
|
||||
} else if (zhi.length == 3) {
|
||||
// 左,上,右
|
||||
// [0,1,2]
|
||||
// 上 右 下 左
|
||||
zhi = [zhi[1], zhi[2], defaultValue, zhi[0]]
|
||||
} else if (zhi.length == 4) {
|
||||
zhi = [zhi[1], zhi[2], zhi[3], zhi[0]]
|
||||
}
|
||||
return zhi;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度克隆
|
||||
* @param {T} data 待大克隆复制的数据
|
||||
* @return {T} any
|
||||
*/
|
||||
export function deepClone<T>(data: T): T {
|
||||
// 对常见的“非”值,直接返回原来值
|
||||
if (data === null || typeof data !== "object") {
|
||||
return data;
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
const clone: any[] = [];
|
||||
for (const item of data) {
|
||||
clone.push(deepClone(item));
|
||||
}
|
||||
return clone as any;
|
||||
}
|
||||
if (data instanceof Date) {
|
||||
return new Date(data.getTime()) as unknown as T;
|
||||
}
|
||||
if (data instanceof RegExp) {
|
||||
const flags = data.flags;
|
||||
return new RegExp(data.source, flags) as unknown as T;
|
||||
}
|
||||
if (typeof data === "function") {
|
||||
return data as unknown as T;
|
||||
}
|
||||
const clone = {} as T;
|
||||
for (const key in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
clone[key] = deepClone(data[key]);
|
||||
}
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"id": "tm-ui",
|
||||
"displayName": "tmui3.2.0 vue3 typeScript nvue全平台ui组件库 ",
|
||||
"version": "3.2.0",
|
||||
"description": "tmui3.0 高性能、高颜值、多主题、暗黑模式vue3 ui组件库,没有比它更完美!",
|
||||
"keywords": [
|
||||
"ui",
|
||||
"vue3",
|
||||
"typeScript"
|
||||
],
|
||||
"repository": "https://gitee.com/LYTB/tmui-design",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.6.18"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "https://www.npmjs.com/package/tmui-cli",
|
||||
"type": "component-vue"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "n"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "n",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "n"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": {
|
||||
"minVersion": "10"
|
||||
},
|
||||
"Android Browser": {
|
||||
"minVersion": "6"
|
||||
},
|
||||
"微信浏览器(Android)": {
|
||||
"minVersion": "8"
|
||||
},
|
||||
"QQ浏览器(Android)": {
|
||||
"minVersion": "10"
|
||||
}
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "n",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"钉钉": "u",
|
||||
"快手": "u",
|
||||
"飞书": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# TMUI 3.0 | TMUI4.0
|
||||
|
||||

|
||||
|
||||
[](https://gitee.com/LYTB/tmui-design/stargazers) [](https://gitee.com/LYTB/tmui-design/members)
|
||||
|
||||
### 声明
|
||||
|
||||
**免费开源贡献**是2.0和3.0版本的初衷。
|
||||
|
||||
**4.0x我是收费的原生uniappx开发套伯按大量原生插件** 已经有一定的销量,有动力支撑3.0的维护。
|
||||
|
||||
从tmui2.0[不维护],3.1.x[不维护],tmui3.2[维护],tmui4.0x[维护]已经和大家度过了5年,感谢大家的支持。实属不容易请大家努力提交3.2.x PR功能让我合并及支持4.0x的维护。
|
||||
|
||||
### 各文档链接
|
||||
|
||||
| 2.0x vue2 js版本 | 3.0x vue3 ts版本 | 4.0x Uts版本 |
|
||||
| --- | --- | --- |
|
||||
| [2.0插件](https://ext.dcloud.net.cn/plugin?id=5949) | [3.0](https://ext.dcloud.net.cn/plugin?id=8372) | [4.0插件](https://ext.dcloud.net.cn/plugin?id=16369) |
|
||||
|
||||
### [tmui3.0开发文档](https://tmui.design/)
|
||||
|
||||
TMUI3.2.0继承tmui4.0x的规范及标准。目前已经发布3.2.0预览版本组件有点少,但够用。后续补齐。
|
||||
|
||||
#### 如果你是之前的3.1.x用户请注意:
|
||||
|
||||
* 1. 本版本不向下兼容!!!!!
|
||||
* 2. 不再兼容nvue平台,请谨慎升级
|
||||
* 3. 3.1.x及2.0.x版本已经不维护,不需要提bug到仓库,当前仅接受3.2.x的bug和维护及pr
|
||||
|
||||
#### 如果你是tmui2.0用户请注意:
|
||||
这是vue3 setup ts版本不是vue2 js版本。请先学习相关知识。
|
||||
|
||||
#### 如果你是TMUI4.0x VIP/赞助用户请注意:
|
||||
这是app vue,小程序版本,并非uts,它是ts写的。语法与uts是有差异的。但相对来说tmui3.2.x是继承tmui4.0x的
|
||||
因此属性规范和标准是一样的(很少不一样,我尽量控制一样)。因为appvue,小程序是无dom的因此有些动画,手势没有x流畅
|
||||
请自行选择用哪个版本写app。你可以用4.0x写原生app然后混合开发webview非必要页面嵌入到你的原生应用中大大提升你的开发效率,并且风格一样,可以对比大厂app做法。
|
||||
|
||||
作者发布了tmui-cli工具方便大家更新创建tmui项目。<br>
|
||||
2022.9.7 更新了cli工具
|
||||
2024.10.10 更新了 cli工具,如果想创建tmui3.0.x请务必按下面执行。
|
||||
|
||||
```npm
|
||||
npm -g install tmui-cli
|
||||
安装成功后输入:tmui 按回车
|
||||
查看所有可用命令
|
||||
```
|
||||
|
||||
[](https://gitee.com/LYTB/tmui-design)
|
||||
|
||||
|
||||
### [注意事项](https://ext.dcloud.net.cn/plugin?name=tm-ui)
|
||||
|
||||
- 仅接受tmui3.2.x的bug和维护及pr
|
||||
- 提出建议和bug,请访问码云代码库:[点击打开](https://gitee.com/LYTB/tmui-design)
|
||||
,并反馈相关bug或者建议.
|
||||
|
||||
---
|
||||
### [贡献者](https://tmui.design/doc/start/%E8%B4%A1%E7%8C%AE%E8%80%85.html#%E8%B4%A1%E7%8C%AE%E4%BA%BA%E5%91%98)
|
||||
在伟大的无私贡献人员帮助下,使得此开源项目的可持续性发展更加的持久。他/她们理应得到展示,并为大家所知。不管是如何的贡献,哪怕很小的一部分也值得称赞!!<br>
|
||||
点击图片查看更多
|
||||
|
||||
[](https://tmui.design/doc/start/%E8%B4%A1%E7%8C%AE%E8%80%85.html)
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
### 赞助项目,以持续更新
|
||||
|
||||
如果帮到了你,适当的赞助有助力作者成果上的回报,报以项目的持续更新。
|
||||
|
||||

|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"useDefineForClassFields": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@@/*": ["src/sheep/*"]
|
||||
},
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"types": ["@dcloudio/types", "node", "vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "vite.config.ts", "unocss.config.ts"],
|
||||
"exclude": ["node_modules", "dist", "src/uni_modules"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import presetWeapp from "unocss-preset-weapp"
|
||||
import { extractorAttributify, transformerClass } from "unocss-preset-weapp/transformer"
|
||||
import { themeColors } from "./src/sheep/config/theme"
|
||||
|
||||
/** 沿用参考项目的 rpx 原子类与属性转换,布局尺寸统一按 750rpx 设计宽度换算 */
|
||||
const { presetWeappAttributify, transformerAttributify } = extractorAttributify()
|
||||
|
||||
export default {
|
||||
presets: [presetWeapp({ whRpx: true }), presetWeappAttributify()],
|
||||
transformers: [transformerAttributify(), transformerClass()],
|
||||
theme: { colors: themeColors },
|
||||
shortcuts: {
|
||||
"flex-row-center": "flex items-center justify-center",
|
||||
"flex-col-center": "flex flex-col items-center justify-center",
|
||||
"focus-ring": "outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
|
||||
},
|
||||
preflights: [
|
||||
{
|
||||
/** CSS 与 UnoCSS 从同一份颜色表取值,避免卡片、键盘与 tm-ui 主题偏离 */
|
||||
getCSS: () => `:root, page { ${Object.entries(themeColors).map(([key, color]) => `--pay-${key}: ${color}`).join("; ")} }`,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import path from "node:path"
|
||||
import { defineConfig, loadEnv } from "vite"
|
||||
import uni from "@dcloudio/vite-plugin-uni"
|
||||
import UnoCSS from "unocss/vite"
|
||||
|
||||
/** 按当前模式读取后端地址;保留目标自带的 /api 等路径,只移除浏览器请求的代理前缀。 */
|
||||
function createApiProxy(mode: string) {
|
||||
/** 沿用参考项目的 .env 变量,由 Vite 按运行模式加载,开发服务器只转发到开发后端。 */
|
||||
const env = loadEnv(mode, __dirname, "VITE_")
|
||||
const apiTarget = (env.VITE_APP_DEV_SERVICE || "").trim().replace(/\/+$/, "")
|
||||
/** 与请求层共用 .env 中的本地接口前缀,默认 /api。 */
|
||||
const apiPrefix = (env.VITE_API_URL || "/api").trim().replace(/\/+$/, "")
|
||||
return apiTarget ? {
|
||||
[apiPrefix]: {
|
||||
target: apiTarget,
|
||||
changeOrigin: true,
|
||||
/** 微服务的 /third 与单体服务的无前缀路径由请求层决定,代理只移除本地前缀。 */
|
||||
rewrite: (requestPath: string) => requestPath.slice(apiPrefix.length),
|
||||
},
|
||||
} : undefined
|
||||
}
|
||||
|
||||
/** H5 独立开发端口避开参考 App 的 8889,别名沿用其业务目录约定 */
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [uni(), UnoCSS()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
"@@": path.resolve(__dirname, "src/sheep"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 8890,
|
||||
strictPort: true,
|
||||
proxy: createApiProxy(mode),
|
||||
},
|
||||
build: {
|
||||
target: "es2018",
|
||||
sourcemap: false,
|
||||
},
|
||||
}))
|
||||
Reference in new issue
Block a user