Button.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. <template>
  2. <Touchable
  3. :innerStyle="{
  4. ...currentStyle.style,
  5. ...innerStyle,
  6. }"
  7. :innerClass="['nana-button', props.block ? 'nana-button-block' : 'nana-button-auto']"
  8. center
  9. direction="row"
  10. v-bind="viewProps"
  11. :pressedColor="finalPressedColor"
  12. :touchable="touchable && !loading"
  13. :gap="iconMargin"
  14. @state="(v) => state = v"
  15. @click="(e) => emit('click', e)"
  16. >
  17. <slot name="leftIcon">
  18. <ActivityIndicator
  19. v-if="loading"
  20. :size="selectStyleType(size, 'medium', FonstSizes)"
  21. :color="themeContext.resolveThemeColor(loadingColor) || currentStyle.color"
  22. />
  23. <Icon
  24. v-else-if="icon"
  25. :icon="icon"
  26. :size="selectStyleType(size, 'medium', FonstSizes)"
  27. :color="currentStyle.color"
  28. v-bind="iconProps"
  29. />
  30. </slot>
  31. <slot>
  32. <Text
  33. :color="textColorFinal"
  34. :fontSize="selectStyleType(size, 'medium', FonstSizes)"
  35. :fontWeight="type === 'text' ? 'bold' : undefined"
  36. :innerStyle="textStyle"
  37. :text="currentText"
  38. v-bind="textProps"
  39. />
  40. </slot>
  41. <slot name="rightIcon">
  42. <Icon
  43. v-if="rightIcon"
  44. :icon="rightIcon"
  45. :size="selectStyleType(size, 'medium', FonstSizes)"
  46. :color="currentStyle.color"
  47. v-bind="rightIconProps"
  48. />
  49. </slot>
  50. </Touchable>
  51. </template>
  52. <script setup lang="ts">
  53. import { computed, inject, ref } from 'vue';
  54. import { propGetThemeVar, useTheme, type ViewStyle } from '../theme/ThemeDefine';
  55. import { configPadding, DynamicColor, DynamicSize, selectStyleType } from '../theme/ThemeTools';
  56. import type { IconProps } from './Icon.vue';
  57. import type { FlexProps } from '../layout/FlexView.vue';
  58. import Text from './Text.vue';
  59. import ActivityIndicator from './ActivityIndicator.vue';
  60. import Icon from './Icon.vue';
  61. import Touchable from '../feedback/Touchable.vue';
  62. import type { ButtonGroupContext } from './ButtonGroup.vue';
  63. import { useChildLinkChild } from '../composeabe/ChildItem';
  64. import type { TextProps } from './Text';
  65. export type ButtonType = 'default'|'primary'|'success'|'warning'|'danger'|'custom'|'text';
  66. export type ButtomSizeType = 'small'|'medium'|'large'|'larger'|'mini';
  67. export interface ButtonProp {
  68. /**
  69. * 按钮文字
  70. */
  71. text?: string,
  72. /**
  73. * 按钮文字的额外属性
  74. */
  75. textProps?: TextProps,
  76. /**
  77. * 按钮支持 default、primary、success、warning、danger、custom 自定义 六种类型
  78. * @default 'default'
  79. */
  80. type?: ButtonType,
  81. /**
  82. * 占满父级主轴
  83. * @default false
  84. */
  85. block?: boolean,
  86. /**
  87. * * plain 将按钮设置为朴素按钮,朴素按钮的文字为按钮颜色,背景为白色。
  88. * * light 将按钮设置为浅色按钮,浅色按钮的文字为按钮颜色,背景为主色调浅色。
  89. * @default 'default'
  90. */
  91. scheme?: 'default'|'plain'|'light',
  92. /**
  93. * 通过 loading 属性设置按钮为加载状态,加载状态下默认会隐藏按钮文字,可以通过 loadingText 设置加载状态下的文字。
  94. * @default false
  95. */
  96. loading?: boolean,
  97. /**
  98. * 加载状态下的文字。
  99. * @default false
  100. */
  101. loadingText?: string,
  102. /**
  103. * 加载状态圆圈颜色
  104. */
  105. loadingColor?: string,
  106. /**
  107. * 按钮形状 通过 square 设置方形按钮,通过 round 设置圆形按钮。
  108. * @default 'round'
  109. */
  110. shape?: 'square'|'round',
  111. /**
  112. * 左侧图标。支持 Icon 组件里的所有图标,也可以传入图标的图片 URL(http/https)。
  113. */
  114. icon?: string,
  115. /**
  116. * 当使用图标时,左侧图标的附加属性
  117. */
  118. iconProps?: IconProps;
  119. /**
  120. * 图标与文字之间的间距
  121. * @default 10
  122. */
  123. iconMargin?: number|string;
  124. /**
  125. * 右侧图标。支持 Icon 组件里的所有图标,也可以传入图标的图片 URL(http/https)。
  126. */
  127. rightIcon?: string,
  128. /**
  129. * 当使用图标时,右侧图标的附加属性
  130. */
  131. rightIconProps?: IconProps;
  132. /**
  133. * 是否可以点击
  134. * @default true
  135. */
  136. touchable?: boolean,
  137. /**
  138. * 当按扭为round圆形按扭时的圆角大小。
  139. * @default 5
  140. */
  141. radius?: number|string,
  142. /**
  143. * 按钮尺寸. 支持 large、medium、small、mini 四种尺寸。
  144. * @default 'medium'
  145. */
  146. size?: ButtomSizeType,
  147. /**
  148. * 通过 color 属性可以自定义按钮的背景颜色,仅在 type 为 `custom` 时有效
  149. * @default grey
  150. */
  151. color?: string;
  152. /**
  153. * 按钮文字的颜色。
  154. */
  155. textColor?: string;
  156. /**
  157. * 按下时按钮文字的颜色。
  158. */
  159. pressedTextColor?: string;
  160. /**
  161. * 按钮文字的样式。
  162. */
  163. textStyle?: object;
  164. /**
  165. * 按下时的颜色,仅在 type 为 `custom` 时有效
  166. * @default PressedColor(primary)
  167. */
  168. pressedColor?: string;
  169. /**
  170. * 禁用时的颜色,仅在 type 为 `custom` 时有效
  171. * @default grey
  172. */
  173. disabledColor?: string;
  174. /**
  175. * 自定义样式
  176. */
  177. innerStyle?: object,
  178. /**
  179. * 按扭的文字,等同于 text 属性
  180. */
  181. children?: string;
  182. /**
  183. * 强制控制按钮的边距
  184. * * 如果是数字,则设置所有方向边距
  185. * * 两位数组 [vetical,horizontal]
  186. * * 四位数组 [top,right,down,left]
  187. */
  188. padding?: number|number[],
  189. /**
  190. * 外层容器参数
  191. */
  192. viewProps?: FlexProps,
  193. formType?: string;
  194. openType?: string;
  195. appParameter?: string;
  196. }
  197. defineOptions({
  198. options: {
  199. styleIsolation: "shared",
  200. virtualHost: true,
  201. }
  202. })
  203. const emit = defineEmits([
  204. 'click',
  205. ])
  206. const themeContext = useTheme();
  207. const props = withDefaults(defineProps<ButtonProp>(), {
  208. touchable: true,
  209. loading: false,
  210. color: () => propGetThemeVar('ButtonColor', 'primary'),
  211. pressedColor: () => propGetThemeVar('ButtonPressedColor', 'pressed.primary'),
  212. disabledColor: () => propGetThemeVar('ButtonDisabledColor', 'grey'),
  213. type: () => propGetThemeVar('ButtonType', 'default'),
  214. size: () => propGetThemeVar('ButtonSize', 'medium'),
  215. block: () => propGetThemeVar('ButtonBlock', false),
  216. radius: () => propGetThemeVar('ButtonRadius', 16),
  217. iconMargin: () => propGetThemeVar('ButtonIconMargin', 10),
  218. shape: () => propGetThemeVar('ButtonShape', "round"),
  219. });
  220. const FonstSizes = computed(() => ({
  221. mini: themeContext.resolveThemeSize('ButtonMiniFonstSize', 'fontSize.sm'),
  222. small: themeContext.resolveThemeSize('ButtonSmallFonstSize', 'fontSize.sm'),
  223. medium: themeContext.resolveThemeSize('ButtonMediumFonstSize', 'fontSize.md'),
  224. large: themeContext.resolveThemeSize('ButtonLargeFonstSize', 'fontSize.lg'),
  225. larger: themeContext.resolveThemeSize('ButtonLargerFonstSize', 'fontSize.xl'),
  226. }));
  227. const themeVars = themeContext.getVars({
  228. ButtonBorderWidth: 1.5,
  229. ButtonDisableOpacity: 0.5,
  230. });
  231. const themeStyles = themeContext.useThemeStyles({
  232. plainButtonDefault: {
  233. borderStyle: 'solid',
  234. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  235. borderColor: DynamicColor('ButtonPlainDefaultBorderColor', 'border.default'),
  236. color: DynamicColor('ButtonPlainDefaultColor', 'text.content'),
  237. },
  238. plainButtonPrimary: {
  239. borderStyle: 'solid',
  240. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  241. borderColor: DynamicColor('ButtonPlainPrimaryBorderColor', 'primary'),
  242. color: DynamicColor('ButtonPlainPrimaryColor', 'primary'),
  243. },
  244. plainButtonSuccess: {
  245. borderStyle: 'solid',
  246. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  247. borderColor: DynamicColor('ButtonPlainSuccessBorderColor', 'success'),
  248. color: DynamicColor('ButtonPlainSuccessColor', 'success'),
  249. },
  250. plainButtonWarning: {
  251. borderStyle: 'solid',
  252. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  253. borderColor: DynamicColor('ButtonPlainWarningBorderColor', 'warning'),
  254. color: DynamicColor('ButtonPlainWarningColor', 'warning'),
  255. },
  256. plainButtonDanger: {
  257. borderStyle: 'solid',
  258. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  259. borderColor: DynamicColor('ButtonPlainDangerBorderColor', 'danger'),
  260. color: DynamicColor('ButtonPlainDangerColor', 'danger'),
  261. },
  262. lightButtonDefault: {
  263. backgroundColor: DynamicColor('ButtonLightDefaultBackgroundColor', 'background.button'),
  264. color: DynamicColor('ButtonLightDefaultColor', 'text.content'),
  265. },
  266. lightButtonPrimary: {
  267. backgroundColor: DynamicColor('ButtonLightPrimaryBackgroundColor', 'background.primary'),
  268. color: DynamicColor('ButtonLightPrimaryColor', 'text.primary'),
  269. },
  270. lightButtonSuccess: {
  271. backgroundColor: DynamicColor('ButtonLightSuccessBackgroundColor', 'background.success'),
  272. color: DynamicColor('ButtonLightSuccessColor', 'text.success'),
  273. },
  274. lightButtonWarning: {
  275. backgroundColor: DynamicColor('ButtonLightWarningBackgroundColor', 'background.warning'),
  276. color: DynamicColor('ButtonLightWarningColor', 'text.warning'),
  277. },
  278. lightButtonDanger: {
  279. backgroundColor: DynamicColor('ButtonLightDangerBackgroundColor', 'background.danger'),
  280. color: DynamicColor('ButtonLightDangerColor', 'text.danger'),
  281. },
  282. buttonSizeLarger: {
  283. paddingVertical: DynamicSize('ButtonPaddingVerticalLarger', 25),
  284. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalLarger', 30),
  285. },
  286. buttonSizeLarge: {
  287. paddingVertical: DynamicSize('ButtonPaddingVerticalLarge', 20),
  288. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalLarge', 25),
  289. },
  290. buttonSizeMedium: {
  291. paddingVertical: DynamicSize('ButtonPaddingVerticalMedium', 15),
  292. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalMedium', 20),
  293. },
  294. buttonSizeSmall: {
  295. paddingVertical: DynamicSize('ButtonPaddingVerticalSmall', 10),
  296. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalSmall', 15),
  297. },
  298. buttonSizeMini: {
  299. paddingVertical: DynamicSize('ButtonPaddingVerticalMini', 5),
  300. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalMini', 6),
  301. },
  302. buttonDefault: {
  303. backgroundColor: DynamicColor('ButtonDefaultBackgroundColor', 'button'),
  304. color: DynamicColor('ButtonDefaultColor', 'black'),
  305. },
  306. buttonPrimary: {
  307. backgroundColor: DynamicColor('ButtonPrimaryBackgroundColor', 'primary'),
  308. color: DynamicColor('ButtonPrimaryColor', 'white'),
  309. },
  310. buttonSuccess: {
  311. backgroundColor: DynamicColor('ButtonSuccessBackgroundColor', 'success'),
  312. color: DynamicColor('ButtonSuccessColor', 'white'),
  313. },
  314. buttonWarning: {
  315. backgroundColor: DynamicColor('ButtonWarningBackgroundColor', 'warning'),
  316. color: DynamicColor('ButtonWarningColor', 'white'),
  317. },
  318. buttonDanger: {
  319. backgroundColor: DynamicColor('ButtonDangerBackgroundColor', 'danger'),
  320. color: DynamicColor('ButtonDangerColor', 'white'),
  321. },
  322. });
  323. //按钮样式生成
  324. const currentStyle = computed(() => {
  325. const colorStyle = selectStyleType<ViewStyle, ButtonType>(props.type, 'default',
  326. selectStyleType(props.scheme, 'default', {
  327. default: {
  328. default: themeStyles.buttonDefault.value,
  329. primary: themeStyles.buttonPrimary.value,
  330. success: themeStyles.buttonSuccess.value,
  331. warning: themeStyles.buttonWarning.value,
  332. danger: themeStyles.buttonDanger.value,
  333. custom: {
  334. backgroundColor: themeContext.resolveThemeColor(touchable.value ? props.color : props.disabledColor),
  335. color: themeContext.resolveThemeColor(props.textColor),
  336. },
  337. text: {
  338. color: themeContext.resolveThemeColor(props.textColor),
  339. },
  340. },
  341. plain: {
  342. default: themeStyles.plainButtonDefault.value,
  343. primary: themeStyles.plainButtonPrimary.value,
  344. success: themeStyles.plainButtonSuccess.value,
  345. warning: themeStyles.plainButtonWarning.value,
  346. danger: themeStyles.plainButtonDanger.value,
  347. custom: {
  348. borderStyle: 'solid',
  349. borderWidth: themeContext.resolveThemeSize(themeVars.ButtonBorderWidth),
  350. borderColor: themeContext.resolveThemeColor(props.color),
  351. color: themeContext.resolveThemeColor(props.color),
  352. },
  353. text: {
  354. color: themeContext.resolveThemeColor(props.color),
  355. },
  356. },
  357. light: {
  358. default: themeStyles.lightButtonDefault.value,
  359. primary: themeStyles.lightButtonPrimary.value,
  360. success: themeStyles.lightButtonSuccess.value,
  361. warning: themeStyles.lightButtonWarning.value,
  362. danger: themeStyles.lightButtonDanger.value,
  363. custom: {
  364. backgroundColor: themeContext.resolveThemeColor(touchable.value ? props.color : props.disabledColor),
  365. color: themeContext.resolveThemeColor(props.textColor),
  366. },
  367. text: {
  368. color: themeContext.resolveThemeColor(props.color),
  369. },
  370. },
  371. })
  372. );
  373. const speicalStyle : ViewStyle = {
  374. opacity: touchable.value ? 1 : themeVars.ButtonDisableOpacity,
  375. borderRadius: props.shape === 'round' ? themeContext.resolveThemeSize(props.radius) : 0,
  376. };
  377. if (props.shape === 'round') {
  378. if (noLeftRadius.value) {
  379. speicalStyle.borderTopLeftRadius = 0;
  380. speicalStyle.borderBottomLeftRadius = 0;
  381. }
  382. if (noRightRadius.value) {
  383. speicalStyle.borderTopRightRadius = 0;
  384. speicalStyle.borderBottomRightRadius = 0;
  385. }
  386. }
  387. //自定义状态下的禁用颜色
  388. if (props.disabledColor && !touchable.value && props.type === 'custom')
  389. speicalStyle.backgroundColor = themeContext.resolveThemeColor(props.disabledColor);
  390. const sizeStyle = selectStyleType<ViewStyle, ButtomSizeType>(props.size, 'medium', {
  391. large: themeStyles.buttonSizeLarge.value,
  392. larger: themeStyles.buttonSizeLarger.value,
  393. medium: themeStyles.buttonSizeMedium.value,
  394. small: themeStyles.buttonSizeSmall.value,
  395. mini: themeStyles.buttonSizeMini.value,
  396. });
  397. //if (props.shape === 'round')
  398. // sizeStyle.paddingHorizontal = `calc(${sizeStyle.paddingHorizontal} + ${themeContext.resolveSize(props.radius / 4)})`;
  399. //内边距样式的强制设置
  400. configPadding(speicalStyle, themeContext, props.padding);
  401. return {
  402. color: (colorStyle).color,
  403. style: {
  404. ...colorStyle,
  405. ...sizeStyle,
  406. ...speicalStyle,
  407. },
  408. };
  409. });
  410. const state = ref('');
  411. const currentText = computed(() => (props.loading ? (props.loadingText || props.text) : props.text));
  412. const textColorFinal = computed(() => (
  413. state.value === 'active' ?
  414. themeContext.resolveThemeColor(props.pressedTextColor) :
  415. themeContext.resolveThemeColor(props.textColor)
  416. ) || currentStyle.value.color);
  417. const finalPressedColor = computed(() => {
  418. if (props.type === 'custom')
  419. return themeContext.resolveThemeColor(props.pressedColor);
  420. return (themeContext.resolveThemeColor((
  421. props.scheme === 'plain'
  422. || props.scheme === 'light'
  423. || props.type === 'text'
  424. ) ?
  425. 'pressed.notice' :
  426. 'pressed.' + props.type))
  427. });
  428. //按钮组的处理
  429. const buttonGroupContext = inject<ButtonGroupContext>('buttonGroupContext', undefined as any);
  430. const { position } = useChildLinkChild(() => buttonGroupContext?.getPosition());
  431. const noRightRadius = computed(() => buttonGroupContext?.mergeRadius.value && position.value !== buttonGroupContext.length.value - 1);
  432. const noLeftRadius = computed(() => buttonGroupContext?.mergeRadius.value && position.value !== 0);
  433. const touchable = computed(() => props.touchable !== false && !buttonGroupContext?.groupDisabled.value);
  434. </script>
  435. <style>
  436. .nana-button {
  437. width: auto;
  438. user-select: none;
  439. cursor: pointer;
  440. appearance: none;
  441. }
  442. .nana-button-inner {
  443. display: flex;
  444. flex-direction: row;
  445. justify-content: center;
  446. align-items: center;
  447. background-color: transparent;
  448. border: none;
  449. outline: none;
  450. line-height: auto;
  451. padding: 0;
  452. margin: 0;
  453. min-height: 0;
  454. }
  455. .nana-button-inner::after {
  456. display: none;
  457. }
  458. .nana-button-auto {
  459. flex-shrink: 0;
  460. flex-grow: 0;
  461. flex-basis: auto;
  462. }
  463. .nana-button-block {
  464. align-self: stretch;
  465. max-width: 100%;
  466. }
  467. </style>